added vectordb tests

This commit is contained in:
ArnabChatterjee20k
2026-02-27 19:27:24 +05:30
parent c78d19de0d
commit 9a32c59340
16 changed files with 2791 additions and 181 deletions
@@ -0,0 +1,281 @@
<?php
namespace Tests\E2E\Services\Databases\VectorDB;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Services\Databases\VectorDB\Permissions\DatabasesPermissionsScope;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
class VectorDBPermissionsGuestTest extends Scope
{
use ProjectCustom;
use SideClient;
use DatabasesPermissionsScope;
private $authorization;
public function getAuthorization(): Authorization
{
if (isset($this->authorization)) {
return $this->authorization;
}
$this->authorization = new Authorization();
return $this->authorization;
}
public function createCollection(): array
{
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'VectorGuestDB',
]);
$this->assertEquals(201, $database['headers']['status-code']);
$this->assertEquals('VectorGuestDB', $database['body']['name']);
$databaseId = $database['body']['$id'];
$publicMovies = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => ID::unique(),
'name' => 'Movies',
'dimension' => 3,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$privateMovies = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => ID::unique(),
'name' => 'Movies',
'dimension' => 3,
'permissions' => [],
'documentSecurity' => true,
]);
$publicCollection = ['id' => $publicMovies['body']['$id']];
$privateCollection = ['id' => $privateMovies['body']['$id']];
return [
'databaseId' => $databaseId,
'publicCollectionId' => $publicCollection['id'],
'privateCollectionId' => $privateCollection['id'],
];
}
public static function permissionsProvider(): array
{
return [
[[Permission::read(Role::any())]],
[[Permission::read(Role::users())]],
[[Permission::update(Role::any()), Permission::delete(Role::any())]],
[[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]],
[[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())]],
[[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())]],
];
}
#[DataProvider('permissionsProvider')]
public function testReadDocuments($permissions)
{
$data = $this->createCollection();
$publicCollectionId = $data['publicCollectionId'];
$privateCollectionId = $data['privateCollectionId'];
$databaseId = $data['databaseId'];
$publicResponse = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', $this->getServerHeader(), [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [1.0, 0.0, 0.0],
'metadata' => ['title' => 'Lorem'],
],
'permissions' => $permissions,
]);
$privateResponse = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', $this->getServerHeader(), [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [0.0, 1.0, 0.0],
'metadata' => ['title' => 'Lorem'],
],
'permissions' => $permissions,
]);
$this->assertEquals(201, $publicResponse['headers']['status-code']);
$this->assertEquals(201, $privateResponse['headers']['status-code']);
$roles = $this->getAuthorization()->getRoles();
$this->getAuthorization()->cleanRoles();
$publicDocuments = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$privateDocuments = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertEquals(1, $publicDocuments['body']['total']);
$this->assertEquals($permissions, $publicDocuments['body']['documents'][0]['$permissions']);
if (\in_array(Permission::read(Role::any()), $permissions)) {
$this->assertEquals(1, $privateDocuments['body']['total']);
$this->assertEquals($permissions, $privateDocuments['body']['documents'][0]['$permissions']);
} else {
$this->assertEquals(0, $privateDocuments['body']['total']);
}
foreach ($roles as $role) {
$this->getAuthorization()->addRole($role);
}
}
public function testWriteDocument()
{
$data = $this->createCollection();
$publicCollectionId = $data['publicCollectionId'];
$privateCollectionId = $data['privateCollectionId'];
$databaseId = $data['databaseId'];
$roles = $this->getAuthorization()->getRoles();
$this->getAuthorization()->cleanRoles();
$publicResponse = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [1.0, 0.0, 0.0],
'metadata' => ['title' => 'Lorem'],
]
]);
$publicDocumentId = $publicResponse['body']['$id'];
$this->assertEquals(201, $publicResponse['headers']['status-code']);
$privateResponse = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [0.0, 1.0, 0.0],
'metadata' => ['title' => 'Lorem'],
],
]);
$this->assertEquals(401, $privateResponse['headers']['status-code']);
// Create a document in private collection with API key so we can test that update and delete are also not allowed
$privateResponse = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', $this->getServerHeader(), [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [0.0, 0.0, 1.0],
'metadata' => ['title' => 'Lorem'],
],
]);
$this->assertEquals(201, $privateResponse['headers']['status-code']);
$privateDocumentId = $privateResponse['body']['$id'];
$publicDocument = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents/' . $publicDocumentId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'data' => [
'embeddings' => [0.5, 0.5, 0.0],
'metadata' => ['title' => 'Thor: Ragnarok'],
],
]);
$this->assertEquals(200, $publicDocument['headers']['status-code']);
$this->assertEquals('Thor: Ragnarok', $publicDocument['body']['metadata']['title']);
$privateDocument = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents/' . $privateDocumentId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'data' => [
'embeddings' => [0.2, 0.3, 0.5],
'metadata' => ['title' => 'Thor: Ragnarok'],
],
]);
$this->assertEquals(401, $privateDocument['headers']['status-code']);
$publicDocument = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents/' . $publicDocumentId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertEquals(204, $publicDocument['headers']['status-code']);
$privateDocument = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents/' . $privateDocumentId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertEquals(401, $privateDocument['headers']['status-code']);
foreach ($roles as $role) {
$this->getAuthorization()->addRole($role);
}
}
public function testWriteDocumentWithPermissions()
{
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'VectorGuestPermsWrite',
]);
$this->assertEquals(201, $database['headers']['status-code']);
$this->assertEquals('VectorGuestPermsWrite', $database['body']['name']);
$databaseId = $database['body']['$id'];
$movies = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => ID::unique(),
'name' => 'Movies',
'dimension' => 3,
'permissions' => [
Permission::create(Role::any()),
],
'documentSecurity' => true
]);
$moviesId = $movies['body']['$id'];
$document = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $moviesId . '/documents', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [1.0, 0.0, 0.0],
'metadata' => ['title' => 'Thor: Ragnarok'],
],
'permissions' => [
Permission::read(Role::any()),
]
]);
$this->assertEquals(201, $document['headers']['status-code']);
$this->assertEquals('Thor: Ragnarok', $document['body']['metadata']['title']);
}
}
@@ -0,0 +1,197 @@
<?php
namespace Tests\E2E\Services\Databases\VectorDB;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Services\Databases\VectorDB\Permissions\DatabasesPermissionsScope;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class VectorDBPermissionsMemberTest extends Scope
{
use ProjectCustom;
use SideClient;
use DatabasesPermissionsScope;
public array $collections = [];
public function createUsers(): array
{
return [
'user1' => $this->createUser('user1', 'lorem@ipsum.com'),
'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
];
}
public static function permissionsProvider(): array
{
return [
[[Permission::read(Role::any())], 1, 1, 1],
[[Permission::read(Role::users())], 2, 2, 2],
[[Permission::read(Role::user(ID::custom('random')))], 3, 3, 2],
[[Permission::read(Role::user(ID::custom('lorem'))), Permission::update(Role::user('lorem')), Permission::delete(Role::user('lorem'))], 4, 4, 2],
[[Permission::read(Role::user(ID::custom('dolor'))), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], 5, 5, 2],
[[Permission::read(Role::user(ID::custom('dolor'))), Permission::read(Role::user('lorem')), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], 6, 6, 2],
[[Permission::update(Role::any()), Permission::delete(Role::any())], 7, 7, 2],
[[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())], 8, 8, 3],
[[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())], 9, 9, 4],
[[Permission::read(Role::user(ID::custom('user1')))], 10, 10, 5],
[[Permission::read(Role::user(ID::custom('user1'))), Permission::read(Role::user(ID::custom('user1')))], 11, 11, 6],
[[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())], 12, 12, 7],
];
}
/**
* Setup database
*
* Data providers lose object state so explicitly pass [$users, $collections] to each iteration
*
* @return array
* @throws \Exception
*/
public function testSetupDatabase(): array
{
$this->createUsers();
$db = $this->client->call(Client::METHOD_POST, '/vectordb', $this->getServerHeader(), [
'databaseId' => ID::unique(),
'name' => 'Test Database',
]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$public = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => ID::unique(),
'name' => 'Movies',
'dimension' => 3,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$this->assertEquals(201, $public['headers']['status-code']);
$this->collections = ['public' => $public['body']['$id']];
$private = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => ID::unique(),
'name' => 'Private Movies',
'dimension' => 3,
'permissions' => [
Permission::read(Role::users()),
Permission::create(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
'documentSecurity' => true,
]);
$this->assertEquals(201, $private['headers']['status-code']);
$this->collections['private'] = $private['body']['$id'];
$doconly = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => ID::unique(),
'name' => 'Document Only Movies',
'dimension' => 3,
'permissions' => [],
'documentSecurity' => true,
]);
$this->assertEquals(201, $private['headers']['status-code']);
$this->collections['doconly'] = $doconly['body']['$id'];
return [
'users' => $this->users,
'collections' => $this->collections,
'databaseId' => $databaseId
];
}
/**
* Data provider params are passed before test dependencies.
*/
#[DataProvider('permissionsProvider')]
#[Depends('testSetupDatabase')]
public function testReadDocuments($permissions, $anyCount, $usersCount, $docOnlyCount, $data)
{
$users = $data['users'];
$collections = $data['collections'];
$databaseId = $data['databaseId'];
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $collections['public'] . '/documents', $this->getServerHeader(), [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [1.0, 0.0, 0.0],
'metadata' => ['title' => 'Lorem'],
],
'permissions' => $permissions
]);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $collections['private'] . '/documents', $this->getServerHeader(), [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [0.0, 1.0, 0.0],
'metadata' => ['title' => 'Lorem'],
],
'permissions' => $permissions
]);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $collections['doconly'] . '/documents', $this->getServerHeader(), [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [0.0, 0.0, 1.0],
'metadata' => ['title' => 'Lorem'],
],
'permissions' => $permissions
]);
$this->assertEquals(201, $response['headers']['status-code']);
/**
* Check "any" permission collection
*/
$documents = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $collections['public'] . '/documents', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
$this->assertEquals($anyCount, $documents['body']['total']);
/**
* Check "users" permission collection
*/
$documents = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $collections['private'] . '/documents', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
$this->assertEquals($usersCount, $documents['body']['total']);
/**
* Check "user:user1" document only permission collection
*/
$documents = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $collections['doconly'] . '/documents', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
]);
$this->assertEquals(200, $documents['headers']['status-code']);
$this->assertEquals($docOnlyCount, $documents['body']['total']);
}
}
@@ -0,0 +1,200 @@
<?php
namespace Tests\E2E\Services\Databases\VectorDB;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Services\Databases\VectorDB\Permissions\DatabasesPermissionsScope;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class VectorDBPermissionsTeamTest extends Scope
{
use ProjectCustom;
use SideClient;
use DatabasesPermissionsScope;
public array $collections = [];
public string $databaseId = 'testpermissiondb';
public function createTeams(): array
{
return [
'team1' => $this->createTeam('team1', 'Team 1'),
'team2' => $this->createTeam('team2', 'Team 2'),
];
}
public function createUsers(): array
{
return [
'user1' => $this->createUser('user1', 'lorem@ipsum.com'),
'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
'user3' => $this->createUser('user3', 'sit@ipsum.com'),
];
}
public function createCollections($teams)
{
$db = $this->client->call(Client::METHOD_POST, '/vectordb', $this->getServerHeader(), [
'databaseId' => $this->databaseId,
'name' => 'Test Database',
]);
$this->assertEquals(201, $db['headers']['status-code']);
$collection1 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $this->databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => ID::custom('collection1'),
'name' => 'Collection 1',
'dimension' => 3,
'permissions' => [
Permission::read(Role::team($teams['team1']['$id'])),
Permission::create(Role::team($teams['team1']['$id'], 'admin')),
Permission::update(Role::team($teams['team1']['$id'], 'admin')),
Permission::delete(Role::team($teams['team1']['$id'], 'admin')),
],
]);
$this->collections['collection1'] = $collection1['body']['$id'];
$collection2 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $this->databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => ID::custom('collection2'),
'name' => 'Collection 2',
'dimension' => 3,
'permissions' => [
Permission::read(Role::team($teams['team2']['$id'])),
Permission::create(Role::team($teams['team2']['$id'], 'owner')),
Permission::update(Role::team($teams['team2']['$id'], 'owner')),
Permission::delete(Role::team($teams['team2']['$id'], 'owner')),
]
]);
$this->collections['collection2'] = $collection2['body']['$id'];
return $this->collections;
}
/*
* $success = can $user read from $collection
* [$user, $collection, $success]
*/
public static function readDocumentsProvider(): array
{
return [
['user1', 'collection1', true],
['user2', 'collection1', false],
['user3', 'collection1', true],
['user1', 'collection2', false],
['user2', 'collection2', true],
['user3', 'collection2', true],
];
}
/*
* $success = can $user write to $collection
* [$user, $collection, $success]
*/
public static function writeDocumentsProvider(): array
{
return [
['user1', 'collection1', true],
['user2', 'collection1', false],
['user3', 'collection1', false],
['user1', 'collection2', false],
['user2', 'collection2', true],
['user3', 'collection2', false],
];
}
/**
* Setup database
*
* Data providers lose object state
* so explicitly pass $users to each iteration
* @return array $users
*/
public function testSetupDatabase(): array
{
$this->createUsers();
$this->createTeams();
$this->addToTeam('user1', 'team1', ['admin']);
$this->addToTeam('user2', 'team2', ['owner']);
// user3 in both teams but with no roles
$this->addToTeam('user3', 'team1');
$this->addToTeam('user3', 'team2');
$this->createCollections($this->teams);
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $this->databaseId . '/collections/' . $this->collections['collection1'] . '/documents', $this->getServerHeader(), [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [1.0, 0.0, 0.0],
'metadata' => ['title' => 'Lorem'],
],
]);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $this->databaseId . '/collections/' . $this->collections['collection2'] . '/documents', $this->getServerHeader(), [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [0.0, 1.0, 0.0],
'metadata' => ['title' => 'Ipsum'],
],
]);
$this->assertEquals(201, $response['headers']['status-code']);
return $this->users;
}
/**
* Data provider params are passed before test dependencies.
*/
#[Depends('testSetupDatabase')]
#[DataProvider('readDocumentsProvider')]
public function testReadDocuments($user, $collection, $success, $users)
{
$documents = $this->client->call(Client::METHOD_GET, '/vectordb/' . $this->databaseId . '/collections/' . $collection . '/documents', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
]);
if ($success) {
$this->assertCount(1, $documents['body']['documents']);
} else {
$this->assertEquals(401, $documents['headers']['status-code']);
}
}
#[Depends('testSetupDatabase')]
#[DataProvider('writeDocumentsProvider')]
public function testWriteDocuments($user, $collection, $success, $users)
{
$documents = $this->client->call(Client::METHOD_POST, '/vectordb/' . $this->databaseId . '/collections/' . $collection . '/documents', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
], [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [0.2, 0.3, 0.5],
'metadata' => ['title' => 'Ipsum'],
],
]);
if ($success) {
$this->assertEquals(201, $documents['headers']['status-code']);
} else {
// 401 if user is a part of team, 404 otherwise
$this->assertContains($documents['headers']['status-code'], [401, 404]);
}
}
}
@@ -0,0 +1,528 @@
<?php
namespace Tests\E2E\Services\Databases\Transactions;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class VectorDBACIDTest extends Scope
{
use ProjectCustom;
use SideClient;
private function generateEmbeddings(int $dimensions = 3, float $value = 0.1): array
{
$vector = array_fill(0, $dimensions, $value);
$vector[0] = 1.0;
return $vector;
}
/**
* Test atomicity - all operations succeed or all fail
*/
public function testAtomicity(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'AtomicityTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection for the test
$collection = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'AtomicityTest',
'dimension' => 3,
'documentSecurity' => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create a document outside the transaction
$existingDocumentId = 'existing_doc';
$doc1 = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => $existingDocumentId,
'data' => [
'embeddings' => $this->generateEmbeddings(3),
'metadata' => ['email' => 'existing@example.com'],
],
]);
$this->assertEquals(201, $doc1['headers']['status-code']);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, '/vectordb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed. Response: ' . json_encode($transaction));
$this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id. Response body: ' . json_encode($transaction['body']));
$transactionId = $transaction['body']['$id'];
// Add operations - second create reuses an existing documentId and should cause the commit to fail
$response = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documents' => [
[
'$id' => 'txn_doc_1',
'embeddings' => $this->generateEmbeddings(3, 0.2),
'metadata' => ['email' => 'newuser@example.com'],
],
[
'$id' => $existingDocumentId,
'embeddings' => $this->generateEmbeddings(3, 0.3),
'metadata' => ['email' => 'duplicate@example.com'],
],
[
'$id' => 'txn_doc_2',
'embeddings' => $this->generateEmbeddings(3, 0.4),
'metadata' => ['email' => 'should-not-exist@example.com'],
],
],
'transactionId' => $transactionId,
]);
$this->assertEquals(200, $response['headers']['status-code'], 'Adding documents via normal route should succeed. Response: ' . json_encode($response['body']));
// Attempt to commit - should fail due to duplicate document ID
$response = $this->client->call(Client::METHOD_PATCH, "/vectordb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertEquals(409, $response['headers']['status-code']);
// Verify NO new documents were created (atomicity)
$documents = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(1, $documents['body']['total']);
$this->assertEquals('existing@example.com', $documents['body']['documents'][0]['metadata']['email']);
}
/**
* Test consistency - schema validation and constraints
*/
public function testConsistency(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'ConsistencyTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection
$collection = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'ConsistencyTest',
'dimension' => 3,
'documentSecurity' => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, '/vectordb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$transactionId = $transaction['body']['$id'];
// Stage operations with valid and invalid data (embedding length mismatch)
$response = $this->client->call(Client::METHOD_POST, "/vectordb/transactions/{$transactionId}/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'create',
'documentId' => ID::unique(),
'data' => [
'embeddings' => $this->generateEmbeddings(3, 0.2),
'metadata' => ['name' => 'Valid User'],
],
],
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'create',
'documentId' => ID::unique(),
'data' => [
'embeddings' => $this->generateEmbeddings(2, 0.5), // Invalid dimensions
'metadata' => ['name' => 'Invalid User'],
],
],
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'create',
'documentId' => ID::unique(),
'data' => [
'embeddings' => $this->generateEmbeddings(3, 0.6),
'metadata' => ['name' => 'Should Not Persist'],
],
],
],
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Attempt to commit - should fail due to invalid embeddings
$response = $this->client->call(Client::METHOD_PATCH, "/vectordb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertContains($response['headers']['status-code'], [400, 409, 500], 'Transaction commit should fail due to validation. Response: ' . json_encode($response['body']));
// Verify no documents were created
$documents = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(0, $documents['body']['total']);
}
/**
* Test isolation - concurrent transactions on same data
*/
public function testIsolation(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'IsolationTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection
$collection = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'IsolationTest',
'dimension' => 3,
'documentSecurity' => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
],
]);
$collectionId = $collection['body']['$id'];
// Create initial document with status metadata
$doc = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'shared_doc',
'data' => [
'embeddings' => $this->generateEmbeddings(3),
'metadata' => ['status' => 'pending'],
],
]);
$this->assertEquals(201, $doc['headers']['status-code']);
// Create first transaction
$transaction1 = $this->client->call(Client::METHOD_POST, '/vectordb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction1['headers']['status-code'], 'Transaction 1 creation should succeed');
$this->assertArrayHasKey('$id', $transaction1['body'], 'Transaction 1 response should have $id');
$transactionId1 = $transaction1['body']['$id'];
// Transaction 1: update status to approved
$this->client->call(Client::METHOD_POST, "/vectordb/transactions/{$transactionId1}/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'update',
'documentId' => 'shared_doc',
'data' => [
'metadata' => ['status' => 'approved'],
],
],
],
]);
// Commit first transaction
$response1 = $this->client->call(Client::METHOD_PATCH, "/vectordb/transactions/{$transactionId1}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertEquals(200, $response1['headers']['status-code']);
// Document should reflect the first transaction's update
$document = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/shared_doc", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals('approved', $document['body']['metadata']['status']);
// Create second transaction after first commit
$transaction2 = $this->client->call(Client::METHOD_POST, '/vectordb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction2['headers']['status-code'], 'Transaction 2 creation should succeed');
$this->assertArrayHasKey('$id', $transaction2['body'], 'Transaction 2 response should have $id');
$transactionId2 = $transaction2['body']['$id'];
// Transaction 2: update status to declined
$this->client->call(Client::METHOD_POST, "/vectordb/transactions/{$transactionId2}/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'update',
'documentId' => 'shared_doc',
'data' => [
'metadata' => ['status' => 'declined'],
],
],
],
]);
// Commit second transaction and ensure isolation guarantees
$response2 = $this->client->call(Client::METHOD_PATCH, "/vectordb/transactions/{$transactionId2}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertEquals(200, $response2['headers']['status-code']);
// Final document should reflect the second transaction's update
$document = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/shared_doc", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals('declined', $document['body']['metadata']['status']);
}
/**
* Test durability - committed data persists
*/
public function testDurability(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'DurabilityTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection
$collection = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'DurabilityTest',
'dimension' => 3,
'documentSecurity' => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create transaction with multiple operations
$transaction = $this->client->call(Client::METHOD_POST, '/vectordb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed');
$this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id');
$transactionId = $transaction['body']['$id'];
// Create two documents via normal route inside transaction
$this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documents' => [
[
'$id' => 'durable_doc_1',
'embeddings' => $this->generateEmbeddings(3, 0.3),
'metadata' => ['data' => 'Important data 1'],
],
[
'$id' => 'durable_doc_2',
'embeddings' => $this->generateEmbeddings(3, 0.5),
'metadata' => ['data' => 'Important data 2'],
],
],
'transactionId' => $transactionId,
]);
// Update first document inside the same transaction
$this->client->call(Client::METHOD_PATCH, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'metadata' => ['data' => 'Updated important data 1'],
],
'transactionId' => $transactionId,
]);
// Commit transaction
$response = $this->client->call(Client::METHOD_PATCH, "/vectordb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertEquals(200, $response['headers']['status-code'], 'Commit should succeed. Response: ' . json_encode($response['body']));
$this->assertEquals('committed', $response['body']['status']);
// Verify documents exist and have correct data
$document1 = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $document1['headers']['status-code']);
$this->assertEquals('Updated important data 1', $document1['body']['metadata']['data']);
$document2 = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_2", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $document2['headers']['status-code']);
$this->assertEquals('Important data 2', $document2['body']['metadata']['data']);
// Further update outside transaction to ensure persistence
$update = $this->client->call(Client::METHOD_PATCH, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'metadata' => ['data' => 'Modified outside transaction'],
],
]);
$this->assertEquals(200, $update['headers']['status-code']);
// Verify the update persisted
$document1 = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals('Modified outside transaction', $document1['body']['metadata']['data']);
// List all documents to verify total count
$documents = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(2, $documents['body']['total']);
}
}
@@ -0,0 +1,15 @@
<?php
namespace Tests\E2E\Services\Databases\Transactions;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
use Tests\E2E\Services\Databases\VectorDB\Transactions\TransactionsBase;
class VectorDBTransactionsConsoleClientTest extends Scope
{
use TransactionsBase;
use ProjectCustom;
use SideConsole;
}
@@ -0,0 +1,15 @@
<?php
namespace Tests\E2E\Services\Databases\Transactions;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Services\Databases\VectorDB\Transactions\TransactionsBase;
class VectorDBTransactionsCustomClientTest extends Scope
{
use TransactionsBase;
use ProjectCustom;
use SideClient;
}
@@ -0,0 +1,15 @@
<?php
namespace Tests\E2E\Services\Databases\Transactions;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Tests\E2E\Services\Databases\VectorDB\Transactions\TransactionsBase;
class VectorDBTransactionsCustomServerTest extends Scope
{
use TransactionsBase;
use ProjectCustom;
use SideServer;
}
@@ -2,6 +2,7 @@
namespace Tests\E2E\Services\Databases\VectorDB;
use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
@@ -33,9 +34,7 @@ trait DatabasesBase
return ['databaseId' => $database['body']['$id']];
}
/**
* @depends testCreateCollectionSample
*/
#[Depends('testCreateCollectionSample')]
public function testCreateDocument(array $data): array
{
$databaseId = $data['databaseId'];
@@ -131,9 +130,7 @@ trait DatabasesBase
];
}
/**
* @depends testCreateDocument
*/
#[Depends('testCreateDocument')]
public function testGetDocument(array $data): array
{
$databaseId = $data['databaseId'];
@@ -160,9 +157,7 @@ trait DatabasesBase
return $data;
}
/**
* @depends testCreateDocument
*/
#[Depends('testCreateDocument')]
public function testListDocuments(array $data): array
{
$databaseId = $data['databaseId'];
@@ -211,9 +206,7 @@ trait DatabasesBase
return $data;
}
/**
* @depends testCreateDocument
*/
#[Depends('testCreateDocument')]
public function testUpsertDocument(array $data): array
{
$databaseId = $data['databaseId'];
@@ -250,9 +243,7 @@ trait DatabasesBase
return $data;
}
/**
* @depends testUpsertDocument
*/
#[Depends('testUpsertDocument')]
public function testUpdateDocument(array $data): array
{
// Upsert is used for update semantics
@@ -303,9 +294,7 @@ trait DatabasesBase
return $data;
}
/**
* @depends testUpdateDocument
*/
#[Depends('testUpdateDocument')]
public function testDocumentsVectorQueries(array $data): array
{
$databaseId = $data['databaseId'];
@@ -409,9 +398,7 @@ trait DatabasesBase
return $data;
}
/**
* @depends testDocumentsVectorQueries
*/
#[Depends('testDocumentsVectorQueries')]
public function testDeleteDocument(array $data): void
{
$databaseId = $data['databaseId'];
@@ -444,9 +431,7 @@ trait DatabasesBase
$this->assertEquals(200, $list['headers']['status-code']);
}
/**
* @depends testCreateCollectionSample
*/
#[Depends('testCreateCollectionSample')]
public function testDocumentPermissions(array $data): void
{
$databaseId = $data['databaseId'];
@@ -487,9 +472,7 @@ trait DatabasesBase
$this->assertEquals(200, $withKey['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
#[Depends('testCreateDatabase')]
public function testCreateCollection(array $data): array
{
$databaseId = $data['databaseId'];
@@ -556,9 +539,7 @@ trait DatabasesBase
return ['databaseId' => $database['body']['$id']];
}
/**
* @depends testCreateDatabaseSample
*/
#[Depends('testCreateDatabaseSample')]
public function testCreateCollectionSample(array $data): array
{
$databaseId = $data['databaseId'];
@@ -972,9 +953,7 @@ trait DatabasesBase
$this->assertEquals(0, $res['body']['total']);
}
/**
* @depends testCreateCollection
*/
#[Depends('testCreateCollection')]
public function testCreateIndexes(array $data): array
{
$databaseId = $data['databaseId'];
@@ -1023,9 +1002,7 @@ trait DatabasesBase
];
}
/**
* @depends testCreateIndexes
*/
#[Depends('testCreateIndexes')]
public function testListIndexes(array $data): void
{
$databaseId = $data['databaseId'];
@@ -1044,9 +1021,7 @@ trait DatabasesBase
}
}
/**
* @depends testCreateIndexes
*/
#[Depends('testCreateIndexes')]
public function testGetIndexByKey(array $data): void
{
$databaseId = $data['databaseId'];
@@ -2,6 +2,7 @@
namespace Tests\E2E\Services\Databases\VectorDB;
use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
@@ -99,11 +100,7 @@ class DatabasesConsoleClientTest extends Scope
return ['moviesId' => $movies['body']['$id'], 'databaseId' => $databaseId, 'tvShowsId' => $tvShows['body']['$id']];
}
/**
* @depends testCreateCollection
* @param array $data
* @throws \Exception
*/
#[Depends('testCreateCollection')]
public function testListCollection(array $data)
{
/**
@@ -120,11 +117,7 @@ class DatabasesConsoleClientTest extends Scope
$this->assertEquals(2, $collections['body']['total']);
}
/**
* @depends testCreateCollection
* @param array $data
* @throws \Exception
*/
#[Depends('testCreateCollection')]
public function testGetCollection(array $data)
{
$databaseId = $data['databaseId'];
@@ -144,12 +137,7 @@ class DatabasesConsoleClientTest extends Scope
$this->assertFalse($collection['body']['enabled']);
}
/**
* @depends testCreateCollection
* @param array $data
* @throws \Exception
* @throws \Exception
*/
#[Depends('testCreateCollection')]
public function testUpdateCollection(array $data)
{
$databaseId = $data['databaseId'];
@@ -172,12 +160,7 @@ class DatabasesConsoleClientTest extends Scope
$this->assertFalse($collection['body']['enabled']);
}
/**
* @depends testCreateCollection
* @param array $data
* @throws \Exception
* @throws \Exception
*/
#[Depends('testCreateCollection')]
public function testDeleteCollection(array $data)
{
$databaseId = $data['databaseId'];
@@ -195,9 +178,7 @@ class DatabasesConsoleClientTest extends Scope
$this->assertEquals($response['body'], "");
}
/**
* @depends testCreateCollection
*/
#[Depends('testCreateCollection')]
public function testGetDatabaseUsage(array $data)
{
$databaseId = $data['databaseId'];
@@ -235,9 +216,7 @@ class DatabasesConsoleClientTest extends Scope
}
/**
* @depends testCreateCollection
*/
#[Depends('testCreateCollection')]
public function testGetCollectionUsage(array $data)
{
$databaseId = $data['databaseId'];
@@ -279,10 +258,7 @@ class DatabasesConsoleClientTest extends Scope
$this->assertIsArray($response['body']['documents']);
}
/**
* @depends testCreateCollection
* @throws \Utopia\Database\Exception\Query
*/
#[Depends('testCreateCollection')]
public function testGetCollectionLogs(array $data)
{
$databaseId = $data['databaseId'];
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests\E2E\Services\Databases\VectorDB;
use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
@@ -66,9 +67,7 @@ class DatabasesCustomServerTest extends Scope
return ['databaseId' => $db1['body']['$id']];
}
/**
* @depends testListDatabases
*/
#[Depends('testListDatabases')]
public function testGetDatabase(array $data): array
{
$databaseId = $data['databaseId'];
@@ -84,9 +83,7 @@ class DatabasesCustomServerTest extends Scope
return ['databaseId' => $databaseId];
}
/**
* @depends testListDatabases
*/
#[Depends('testListDatabases')]
public function testUpdateDatabase(array $data): array
{
$databaseId = $data['databaseId'];
@@ -103,9 +100,7 @@ class DatabasesCustomServerTest extends Scope
return ['databaseId' => $databaseId];
}
/**
* @depends testListDatabases
*/
#[Depends('testListDatabases')]
public function testDeleteDatabase(array $data): void
{
$databaseId = $data['databaseId'];
@@ -238,9 +233,7 @@ class DatabasesCustomServerTest extends Scope
];
}
/**
* @depends testCollectionsCRUD
*/
#[Depends('testCollectionsCRUD')]
public function testUpdateCollectionMore(array $data): array
{
$databaseId = $data['databaseId'];
@@ -272,9 +265,7 @@ class DatabasesCustomServerTest extends Scope
return $data;
}
/**
* @depends testCollectionsCRUD
*/
#[Depends('testCollectionsCRUD')]
public function testUpdateCollectionEnabledFlag(array $data): array
{
$databaseId = $data['databaseId'];
@@ -364,9 +355,7 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals(204, $del['headers']['status-code']);
}
/**
* @depends testCollectionsCRUD
*/
#[Depends('testCollectionsCRUD')]
public function testRecreateIndex(array $data): void
{
$databaseId = $data['databaseId'];
@@ -402,9 +391,7 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals(204, $del['headers']['status-code']);
}
/**
* @depends testCollectionsCRUD
*/
#[Depends('testCollectionsCRUD')]
public function testIndexesCRUD(array $data): void
{
$databaseId = $data['databaseId'];
@@ -2,6 +2,7 @@
namespace Tests\E2E\Services\Databases\VectorDB\Permissions;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
@@ -73,7 +74,7 @@ class DatabasesPermissionsGuestTest extends Scope
];
}
public function permissionsProvider(): array
public static function permissionsProvider(): array
{
return [
[[Permission::read(Role::any())]],
@@ -85,9 +86,7 @@ class DatabasesPermissionsGuestTest extends Scope
];
}
/**
* @dataProvider permissionsProvider
*/
#[DataProvider('permissionsProvider')]
public function testReadDocuments($permissions)
{
$data = $this->createCollection();
@@ -2,6 +2,8 @@
namespace Tests\E2E\Services\Databases\VectorDB\Permissions;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
@@ -26,81 +28,21 @@ class DatabasesPermissionsMemberTest extends Scope
];
}
public function permissionsProvider(): array
public static function permissionsProvider(): array
{
return [
[
'permissions' => [Permission::read(Role::any())],
'any' => 1,
'users' => 1,
'doconly' => 1,
],
[
'permissions' => [Permission::read(Role::users())],
'any' => 2,
'users' => 2,
'doconly' => 2,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('random')))],
'any' => 3,
'users' => 3,
'doconly' => 2,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('lorem'))), Permission::update(Role::user('lorem')), Permission::delete(Role::user('lorem'))],
'any' => 4,
'users' => 4,
'doconly' => 2,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('dolor'))), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))],
'any' => 5,
'users' => 5,
'doconly' => 2,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('dolor'))), Permission::read(Role::user('lorem')), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))],
'any' => 6,
'users' => 6,
'doconly' => 2,
],
[
'permissions' => [Permission::update(Role::any()), Permission::delete(Role::any())],
'any' => 7,
'users' => 7,
'doconly' => 2,
],
[
'permissions' => [Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())],
'any' => 8,
'users' => 8,
'doconly' => 3,
],
[
'permissions' => [Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())],
'any' => 9,
'users' => 9,
'doconly' => 4,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('user1')))],
'any' => 10,
'users' => 10,
'doconly' => 5,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('user1'))), Permission::read(Role::user(ID::custom('user1')))],
'any' => 11,
'users' => 11,
'doconly' => 6,
],
[
'permissions' => [Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())],
'any' => 12,
'users' => 12,
'doconly' => 7,
],
[[Permission::read(Role::any())], 1, 1, 1],
[[Permission::read(Role::users())], 2, 2, 2],
[[Permission::read(Role::user(ID::custom('random')))], 3, 3, 2],
[[Permission::read(Role::user(ID::custom('lorem'))), Permission::update(Role::user('lorem')), Permission::delete(Role::user('lorem'))], 4, 4, 2],
[[Permission::read(Role::user(ID::custom('dolor'))), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], 5, 5, 2],
[[Permission::read(Role::user(ID::custom('dolor'))), Permission::read(Role::user('lorem')), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], 6, 6, 2],
[[Permission::update(Role::any()), Permission::delete(Role::any())], 7, 7, 2],
[[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())], 8, 8, 3],
[[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())], 9, 9, 4],
[[Permission::read(Role::user(ID::custom('user1')))], 10, 10, 5],
[[Permission::read(Role::user(ID::custom('user1'))), Permission::read(Role::user(ID::custom('user1')))], 11, 11, 6],
[[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())], 12, 12, 7],
];
}
@@ -172,10 +114,10 @@ class DatabasesPermissionsMemberTest extends Scope
}
/**
* Data provider params are passed before test dependencies
* @dataProvider permissionsProvider
* @depends testSetupDatabase
* Data provider params are passed before test dependencies.
*/
#[DataProvider('permissionsProvider')]
#[Depends('testSetupDatabase')]
public function testReadDocuments($permissions, $anyCount, $usersCount, $docOnlyCount, $data)
{
$users = $data['users'];
@@ -2,6 +2,8 @@
namespace Tests\E2E\Services\Databases\VectorDB\Permissions;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
@@ -79,7 +81,7 @@ class DatabasesPermissionsTeamTest extends Scope
* $success = can $user read from $collection
* [$user, $collection, $success]
*/
public function readDocumentsProvider(): array
public static function readDocumentsProvider(): array
{
return [
['user1', 'collection1', true],
@@ -95,7 +97,7 @@ class DatabasesPermissionsTeamTest extends Scope
* $success = can $user write to $collection
* [$user, $collection, $success]
*/
public function writeDocumentsProvider(): array
public static function writeDocumentsProvider(): array
{
return [
['user1', 'collection1', true],
@@ -150,10 +152,10 @@ class DatabasesPermissionsTeamTest extends Scope
}
/**
* Data provider params are passed before test dependencies
* @depends testSetupDatabase
* @dataProvider readDocumentsProvider
* Data provider params are passed before test dependencies.
*/
#[Depends('testSetupDatabase')]
#[DataProvider('readDocumentsProvider')]
public function testReadDocuments($user, $collection, $success, $users)
{
$documents = $this->client->call(Client::METHOD_GET, '/vectordb/' . $this->databaseId . '/collections/' . $collection . '/documents', [
@@ -170,10 +172,8 @@ class DatabasesPermissionsTeamTest extends Scope
}
}
/**
* @depends testSetupDatabase
* @dataProvider writeDocumentsProvider
*/
#[Depends('testSetupDatabase')]
#[DataProvider('writeDocumentsProvider')]
public function testWriteDocuments($user, $collection, $success, $users)
{
$documents = $this->client->call(Client::METHOD_POST, '/vectordb/' . $this->databaseId . '/collections/' . $collection . '/documents', [
@@ -0,0 +1,312 @@
<?php
namespace Tests\E2E\Services\Databases;
use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
class VectorDBConsoleClientTest extends Scope
{
use ProjectCustom;
use SideConsole;
public function testCreateCollection(): array
{
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'databaseId' => ID::unique(),
'name' => 'Vector Console DB',
]);
$this->assertEquals(201, $database['headers']['status-code']);
$this->assertEquals('Vector Console DB', $database['body']['name']);
$this->assertTrue($database['body']['enabled']);
$databaseId = $database['body']['$id'];
/**
* Test for SUCCESS
*/
$movies = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'collectionId' => ID::unique(),
'name' => 'Movies',
'dimension' => 3,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$this->assertEquals(201, $movies['headers']['status-code']);
$this->assertEquals($movies['body']['name'], 'Movies');
/**
* Test when database is disabled but can still create collections
*/
$database = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Vector Console DB Updated',
'enabled' => false,
]);
$this->assertFalse($database['body']['enabled']);
$tvShows = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'collectionId' => ID::unique(),
'name' => 'TvShows',
'dimension' => 3,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
/**
* Test when collection is disabled but can still modify collections
*/
$database = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $movies['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Movies',
'enabled' => false,
]);
$this->assertEquals(201, $tvShows['headers']['status-code']);
$this->assertEquals($tvShows['body']['name'], 'TvShows');
return ['moviesId' => $movies['body']['$id'], 'databaseId' => $databaseId, 'tvShowsId' => $tvShows['body']['$id']];
}
#[Depends('testCreateCollection')]
public function testListCollection(array $data)
{
/**
* Test when database is disabled but can still call list collections
*/
$databaseId = $data['databaseId'];
$collections = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()));
$this->assertEquals(200, $collections['headers']['status-code']);
$this->assertEquals(2, $collections['body']['total']);
}
#[Depends('testCreateCollection')]
public function testGetCollection(array $data)
{
$databaseId = $data['databaseId'];
$moviesCollectionId = $data['moviesId'];
/**
* Test when database and collection are disabled but can still call get collection
*/
$collection = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $moviesCollectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $collection['headers']['status-code']);
$this->assertEquals('Movies', $collection['body']['name']);
$this->assertEquals($moviesCollectionId, $collection['body']['$id']);
$this->assertFalse($collection['body']['enabled']);
}
#[Depends('testCreateCollection')]
public function testUpdateCollection(array $data)
{
$databaseId = $data['databaseId'];
$moviesCollectionId = $data['moviesId'];
/**
* Test When database and collection are disabled but can still call update collection
*/
$collection = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $moviesCollectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Movies Updated',
'enabled' => false
]);
$this->assertEquals(200, $collection['headers']['status-code']);
$this->assertEquals('Movies Updated', $collection['body']['name']);
$this->assertEquals($moviesCollectionId, $collection['body']['$id']);
$this->assertFalse($collection['body']['enabled']);
}
#[Depends('testCreateCollection')]
public function testDeleteCollection(array $data)
{
$databaseId = $data['databaseId'];
$tvShowsId = $data['tvShowsId'];
/**
* Test when database and collection are disabled but can still call delete collection
*/
$response = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId . '/collections/' . $tvShowsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEquals($response['body'], "");
}
#[Depends('testCreateCollection')]
public function testGetDatabaseUsage(array $data)
{
$databaseId = $data['databaseId'];
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '32h'
]);
$this->assertEquals(400, $response['headers']['status-code']);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(11, count($response['body']));
$this->assertEquals('24h', $response['body']['range']);
$this->assertIsNumeric($response['body']['documentsTotal']);
$this->assertIsNumeric($response['body']['collectionsTotal']);
$this->assertIsArray($response['body']['collections']);
$this->assertIsArray($response['body']['documents']);
}
#[Depends('testCreateCollection')]
public function testGetCollectionUsage(array $data)
{
$databaseId = $data['databaseId'];
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '32h'
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/randomCollectionId/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals(404, $response['headers']['status-code']);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(3, count($response['body']));
$this->assertEquals('24h', $response['body']['range']);
$this->assertIsNumeric($response['body']['documentsTotal']);
$this->assertIsArray($response['body']['documents']);
}
#[Depends('testCreateCollection')]
public function testGetCollectionLogs(array $data)
{
$databaseId = $data['databaseId'];
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::limit(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::offset(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::offset(1)->toString(), Query::limit(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
}
}
@@ -0,0 +1,205 @@
<?php
namespace Tests\E2E\Services\Databases;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Services\Databases\VectorDB\DatabasesBase;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class VectorDBCustomClientTest extends Scope
{
use DatabasesBase;
use ProjectCustom;
use SideClient;
public function testAllowedPermissions(): void
{
/**
* Test for SUCCESS
*/
$database = $this->client->call(Client::METHOD_POST, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'Test Database'
]);
$databaseId = $database['body']['$id'];
// Collection aliases write to create, update, delete
$movies = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'Movies',
'dimension' => 3,
'documentSecurity' => true,
'permissions' => [
Permission::write(Role::user($this->getUser()['$id'])),
],
]);
$moviesId = $movies['body']['$id'];
$this->assertContains(Permission::create(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
$this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
$this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
// VectorDB uses fixed schema (embeddings, metadata). No attribute creation needed.
// Document aliases write to update, delete
$document1 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [1.0, 0.0, 0.0],
'metadata' => ['k' => 'v'],
],
'permissions' => [
Permission::write(Role::user($this->getUser()['$id'])),
]
]);
$this->assertNotContains(Permission::create(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
$this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
$this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
/**
* Test for FAILURE
*/
// Document does not allow create permission
$document2 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [0.0, 1.0, 0.0],
'metadata' => ['k' => 'v'],
],
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
]
]);
$this->assertEquals(400, $document2['headers']['status-code']);
}
public function testUpdateWithoutPermission(): array
{
// As a part of preparation, we get ID of currently logged-in user
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$userId = $response['body']['$id'];
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::custom('permissionCheckDatabase'),
'name' => 'Test Database',
]);
$this->assertEquals(201, $database['headers']['status-code']);
$this->assertEquals('Test Database', $database['body']['name']);
$databaseId = $database['body']['$id'];
// Create collection
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::custom('permissionCheck'),
'name' => 'permissionCheck',
'dimension' => 3,
'permissions' => [],
'documentSecurity' => true,
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Creating document by server, give read permission to our user + some other user
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/permissionCheck/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => ID::custom('permissionCheckDocument'),
'data' => [
'embeddings' => [1.0, 0.0, 0.0],
'metadata' => ['name' => 'AppwriteBeginner'],
],
'permissions' => [
Permission::read(Role::user(ID::custom('user2'))),
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Update document
// This is the point of this test. We should be allowed to do this action, and it should not fail on permission check
$response = $this->client->call(Client::METHOD_PATCH, '/vectordb/' . $databaseId . '/collections/permissionCheck/documents/permissionCheckDocument', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'data' => [
'embeddings' => [0.0, 1.0, 0.0],
'metadata' => ['name' => 'AppwriteExpert'],
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Get name of the document, should be the new one
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/permissionCheck/documents/permissionCheckDocument', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals("AppwriteExpert", $response['body']['metadata']['name']);
// Cleanup to prevent collision with other tests
// Delete collection
$response = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId . '/collections/permissionCheck', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(204, $response['headers']['status-code']);
// Wait for database worker to finish deleting collection
sleep(2);
// Make sure collection has been deleted
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/permissionCheck', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(404, $response['headers']['status-code']);
return [];
}
}
@@ -0,0 +1,963 @@
<?php
namespace Tests\E2E\Services\Databases;
use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Tests\E2E\Services\Databases\VectorDB\DatabasesBase;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class VectorDBCustomServerTest extends Scope
{
use DatabasesBase;
use ProjectCustom;
use SideServer;
public function testListDatabases(): array
{
$db1 = $this->client->call(Client::METHOD_POST, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::custom('first'),
'name' => 'Test 1',
]);
$this->assertEquals(201, $db1['headers']['status-code']);
$this->assertEquals('Test 1', $db1['body']['name']);
$this->assertEquals('vectordb', $db1['body']['type']);
// Validate database response model fields on create
$this->assertArrayHasKey('$id', $db1['body']);
$this->assertArrayHasKey('$createdAt', $db1['body']);
$this->assertArrayHasKey('$updatedAt', $db1['body']);
$this->assertArrayHasKey('enabled', $db1['body']);
$db2 = $this->client->call(Client::METHOD_POST, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::custom('second'),
'name' => 'Test 2',
]);
$this->assertEquals(201, $db2['headers']['status-code']);
$this->assertEquals('Test 2', $db2['body']['name']);
$this->assertEquals('vectordb', $db2['body']['type']);
$list = $this->client->call(Client::METHOD_GET, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $list['headers']['status-code']);
$this->assertIsInt($list['body']['total']);
$this->assertGreaterThanOrEqual(2, $list['body']['total']);
$this->assertIsArray($list['body']['databases']);
$this->assertArrayHasKey('$id', $list['body']['databases'][0]);
$this->assertArrayHasKey('name', $list['body']['databases'][0]);
$this->assertArrayHasKey('type', $list['body']['databases'][0]);
return ['databaseId' => $db1['body']['$id']];
}
#[Depends('testListDatabases')]
public function testGetDatabase(array $data): array
{
$databaseId = $data['databaseId'];
$res = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $res['headers']['status-code']);
$this->assertEquals($databaseId, $res['body']['$id']);
$this->assertEquals('Test 1', $res['body']['name']);
$this->assertEquals('vectordb', $res['body']['type']);
return ['databaseId' => $databaseId];
}
#[Depends('testListDatabases')]
public function testUpdateDatabase(array $data): array
{
$databaseId = $data['databaseId'];
$res = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test 1 Updated',
]);
$this->assertEquals(200, $res['headers']['status-code']);
$this->assertEquals('Test 1 Updated', $res['body']['name']);
$this->assertEquals('vectordb', $res['body']['type']);
return ['databaseId' => $databaseId];
}
#[Depends('testListDatabases')]
public function testDeleteDatabase(array $data): void
{
$databaseId = $data['databaseId'];
$del = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $del['headers']['status-code']);
$this->assertEquals("", $del['body']);
$get = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(404, $get['headers']['status-code']);
}
public function testCollectionsCRUD(): array
{
// Create database for collections tests
$database = $this->client->call(Client::METHOD_POST, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'Collections DB',
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create two collections
$col1 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test 1',
'collectionId' => ID::custom('first'),
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
'dimension' => 3,
]);
$this->assertEquals(201, $col1['headers']['status-code']);
// Validate collection response model on create
$this->assertArrayHasKey('$id', $col1['body']);
$this->assertArrayHasKey('$createdAt', $col1['body']);
$this->assertArrayHasKey('$updatedAt', $col1['body']);
$this->assertArrayHasKey('enabled', $col1['body']);
$this->assertArrayHasKey('documentSecurity', $col1['body']);
$this->assertArrayHasKey('dimension', $col1['body']);
$col2 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test 2',
'collectionId' => ID::custom('second'),
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
'dimension' => 3,
]);
$this->assertEquals(201, $col2['headers']['status-code']);
$this->assertArrayHasKey('$id', $col2['body']);
$this->assertArrayHasKey('$createdAt', $col2['body']);
$this->assertArrayHasKey('$updatedAt', $col2['body']);
// List collections
$list = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $list['headers']['status-code']);
$this->assertIsInt($list['body']['total']);
$this->assertGreaterThanOrEqual(2, $list['body']['total']);
$this->assertIsArray($list['body']['collections']);
$this->assertArrayHasKey('$id', $list['body']['collections'][0]);
$this->assertArrayHasKey('name', $list['body']['collections'][0]);
$this->assertArrayHasKey('dimension', $list['body']['collections'][0]);
// Get collection
$get = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $col1['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals($col1['body']['$id'], $get['body']['$id']);
$this->assertEquals('Test 1', $get['body']['name']);
$this->assertEquals(3, $get['body']['dimension']);
// Update collection (name only)
$upd = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $col1['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test 1 Updated',
]);
$this->assertEquals(200, $upd['headers']['status-code']);
$this->assertEquals('Test 1 Updated', $upd['body']['name']);
$this->assertArrayHasKey('$updatedAt', $upd['body']);
// Delete collection
$del = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId . '/collections/' . $col2['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $del['headers']['status-code']);
$this->assertEquals("", $del['body']);
return [
'databaseId' => $databaseId,
'collectionId' => $col1['body']['$id'],
];
}
#[Depends('testCollectionsCRUD')]
public function testUpdateCollectionMore(array $data): array
{
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
// Update collection name and dimensions
$upd = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $collectionId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test 1 Renamed',
'dimension' => 4,
]);
$this->assertEquals(200, $upd['headers']['status-code']);
$this->assertEquals('Test 1 Renamed', $upd['body']['name']);
$this->assertEquals(4, $upd['body']['dimension']);
// Read back to confirm
$get = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $collectionId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals('Test 1 Renamed', $get['body']['name']);
$this->assertEquals(4, $get['body']['dimension']);
return $data;
}
#[Depends('testCollectionsCRUD')]
public function testUpdateCollectionEnabledFlag(array $data): array
{
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
// Disable collection
$disable = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $collectionId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Updated',
'enabled' => false,
]);
$this->assertEquals(200, $disable['headers']['status-code']);
$this->assertFalse($disable['body']['enabled']);
// Re-enable collection
$enable = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $collectionId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Updated',
'enabled' => true,
]);
$this->assertEquals(200, $enable['headers']['status-code']);
$this->assertTrue($enable['body']['enabled']);
return $data;
}
public function testUpdateDatabaseNameAndEnabled(): void
{
// Create isolated database for this test to avoid ordering conflicts
$create = $this->client->call(Client::METHOD_POST, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'Update DB',
]);
$this->assertEquals(201, $create['headers']['status-code']);
$databaseId = $create['body']['$id'];
// Update name
$rename = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test DB Renamed',
]);
$this->assertEquals(200, $rename['headers']['status-code']);
$this->assertEquals('Test DB Renamed', $rename['body']['name']);
// Toggle enabled off then on
$disable = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test DB Renamed',
'enabled' => false,
]);
$this->assertEquals(200, $disable['headers']['status-code']);
$this->assertFalse($disable['body']['enabled']);
$enable = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test DB Renamed',
'enabled' => true,
]);
$this->assertEquals(200, $enable['headers']['status-code']);
$this->assertTrue($enable['body']['enabled']);
// Cleanup
$del = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $del['headers']['status-code']);
}
#[Depends('testCollectionsCRUD')]
public function testRecreateIndex(array $data): void
{
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
// Create a new index variant
$create = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'embedding_euclidean_v2',
'type' => Database::INDEX_HNSW_EUCLIDEAN,
'attributes' => ['embeddings']
]);
$this->assertEquals(202, $create['headers']['status-code']);
// Ensure it exists
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean_v2", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals('embedding_euclidean_v2', $get['body']['key']);
// Delete it
$del = $this->client->call(Client::METHOD_DELETE, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean_v2", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $del['headers']['status-code']);
}
#[Depends('testCollectionsCRUD')]
public function testIndexesCRUD(array $data): void
{
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
// Create indexes
$eu = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'embedding_euclidean',
'type' => Database::INDEX_HNSW_EUCLIDEAN,
'attributes' => ['embeddings']
]);
$this->assertEquals(202, $eu['headers']['status-code']);
$dot = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'embedding_dot',
'type' => Database::INDEX_HNSW_DOT,
'attributes' => ['embeddings']
]);
$this->assertEquals(202, $dot['headers']['status-code']);
$cos = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'embedding_cosine',
'type' => Database::INDEX_HNSW_COSINE,
'attributes' => ['embeddings']
]);
$this->assertEquals(202, $cos['headers']['status-code']);
// List indexes
$list = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $list['headers']['status-code']);
$this->assertIsArray($list['body']['indexes']);
$keys = array_map(fn ($i) => $i['key'], $list['body']['indexes']);
$this->assertContains('embedding_euclidean', $keys);
$this->assertContains('embedding_dot', $keys);
$this->assertContains('embedding_cosine', $keys);
// Get index by key
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals('embedding_euclidean', $get['body']['key']);
$this->assertEquals(Database::INDEX_HNSW_EUCLIDEAN, $get['body']['type']);
// Delete index
$del = $this->client->call(Client::METHOD_DELETE, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes/embedding_dot", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $del['headers']['status-code']);
sleep(4);
// Ensure it's gone
$getMissing = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes/embedding_dot", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(404, $getMissing['headers']['status-code']);
}
public function testBulkCreate(): array
{
// Setup: create isolated database and collection
$db = $this->client->call(Client::METHOD_POST, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'BulkDBCreate'
]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'BulkColCreate',
'documentSecurity' => true,
'dimension' => 3,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
$docs = [
[
'embeddings' => [1.0, 0.0, 0.0],
'metadata' => ['group' => 'bulkA'],
'$permissions' => [Permission::read(Role::any())]
],
[
'embeddings' => [0.0, 1.0, 0.0],
'metadata' => ['group' => 'bulkB'],
'$permissions' => [Permission::read(Role::any())]
],
];
$res = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documents' => $docs
]);
$this->assertEquals(201, $res['headers']['status-code']);
$this->assertIsInt($res['body']['total'] ?? 0);
$this->assertGreaterThanOrEqual(2, $res['body']['total']);
$this->assertIsArray($res['body']['documents']);
$this->assertCount(2, $res['body']['documents']);
$ids = array_map(fn ($d) => $d['$id'], $res['body']['documents']);
$this->assertNotEmpty($ids[0]);
$this->assertNotEmpty($ids[1]);
// Fetch and validate persisted data via GET
foreach ($ids as $i => $id) {
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals($id, $get['body']['$id']);
$this->assertIsArray($get['body']['embeddings']);
$this->assertCount(3, $get['body']['embeddings']);
$this->assertArrayHasKey('group', $get['body']['metadata']);
}
return [ 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'bulkIds' => $ids ];
}
public function testCreateTextEmbeddingsSuccessAndErrors(): void
{
// Setup new database and collection
$db = $this->client->call(Client::METHOD_POST, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'EmbedDB',
]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'EmbedCol',
'documentSecurity' => true,
'dimension' => 3,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
// Success: two embeddings
$this->assertEventually(function () {
$ok = $this->client->call(Client::METHOD_POST, "/vectordb/embeddings/text", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'model' => 'embeddinggemma',
'texts' => [
'hello world',
'second sentence',
],
]);
$this->assertEquals(200, $ok['headers']['status-code']);
$this->assertIsInt($ok['body']['total'] ?? 0);
$this->assertEquals(2, $ok['body']['total']);
$this->assertIsArray($ok['body']['embeddings']);
$this->assertCount(2, $ok['body']['embeddings']);
foreach ($ok['body']['embeddings'] as $embed) {
$this->assertIsString($embed['model']);
$this->assertIsInt($embed['dimension']);
$this->assertIsArray($embed['embedding']);
$this->assertGreaterThan(0, count($embed['embedding']));
$this->assertArrayHasKey('error', $embed);
}
}, 3000, 100);
// Error: missing texts payload
$missingTexts = $this->client->call(Client::METHOD_POST, "/vectordb/embeddings/text", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], []);
$this->assertEquals(400, $missingTexts['headers']['status-code']);
// Error: invalid texts item type (must be strings)
$invalidItem = $this->client->call(Client::METHOD_POST, "/vectordb/embeddings/text", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'model' => 'embeddinggemma',
'texts' => [
'valid text',
123, // invalid, not a string
],
]);
$this->assertEquals(400, $invalidItem['headers']['status-code']);
// Error: unknown embedding model
$unknownModel = $this->client->call(Client::METHOD_POST, "/vectordb/embeddings/text", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'model' => 'nonexistent-model',
'texts' => ['hello'],
]);
$this->assertEquals(400, $unknownModel['headers']['status-code']);
}
public function testBulkUpsert(): void
{
// Setup fresh db/collection
$db = $this->client->call(Client::METHOD_POST, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBUpsert' ]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'BulkColUpsert',
'documentSecurity' => true,
'dimension' => 3,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
$docs = [
[
'embeddings' => [0.5, 0.5, 0.0],
'metadata' => ['group' => 'bulkA', 'updated' => true],
'$permissions' => [Permission::read(Role::any())]
],
[
'embeddings' => [0.2, 0.8, 0.0],
'metadata' => ['group' => 'bulkB', 'updated' => true],
'$permissions' => [Permission::read(Role::any())]
],
];
$res = $this->client->call(Client::METHOD_PUT, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documents' => $docs
]);
$this->assertEquals(200, $res['headers']['status-code']);
$this->assertIsArray($res['body']['documents']);
$this->assertCount(2, $res['body']['documents']);
$this->assertTrue($res['body']['documents'][0]['metadata']['updated']);
$this->assertTrue($res['body']['documents'][1]['metadata']['updated']);
// Fetch and validate updated content
$ids = array_map(fn ($d) => $d['$id'], $res['body']['documents']);
foreach ($ids as $id) {
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertTrue($get['body']['metadata']['updated']);
}
// Perform another bulk upsert to mutate the same documents
$docs2 = [
[ 'embeddings' => [0.6, 0.4, 0.0], 'metadata' => ['updatedAgain' => true] ],
[ 'embeddings' => [0.3, 0.7, 0.0], 'metadata' => ['updatedAgain' => true] ],
];
$res2 = $this->client->call(Client::METHOD_PUT, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documents' => $docs2
]);
$this->assertEquals(200, $res2['headers']['status-code']);
$this->assertIsArray($res2['body']['documents']);
$this->assertCount(2, $res2['body']['documents']);
// Fetch again and assert second update persisted
$ids2 = array_map(fn ($d) => $d['$id'], $res2['body']['documents']);
foreach ($ids2 as $id) {
$get2 = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get2['headers']['status-code']);
$this->assertTrue($get2['body']['metadata']['updatedAgain']);
}
}
public function testBulkUpdate(): void
{
// Setup: create db/collection and two docs
$db = $this->client->call(Client::METHOD_POST, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBUpdate' ]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'BulkColUpdate',
'documentSecurity' => true,
'dimension' => 3,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
$seed = $this->client->call(Client::METHOD_PUT, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documents' => [
['embeddings' => [1.0,0.0,0.0], 'metadata' => ['seed' => 1], '$permissions' => [Permission::read(Role::any())]],
['embeddings' => [0.0,1.0,0.0], 'metadata' => ['seed' => 2], '$permissions' => [Permission::read(Role::any())]]
]
]);
$this->assertEquals(200, $seed['headers']['status-code']);
$ids = array_map(fn ($d) => $d['$id'], $seed['body']['documents']);
$res = $this->client->call(Client::METHOD_PATCH, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'data' => [ 'metadata' => ['bulkUpdated' => true] ],
'queries' => [
\Utopia\Database\Query::equal('$id', $ids)->toString()
]
]);
$this->assertEquals(200, $res['headers']['status-code']);
$this->assertIsArray($res['body']['documents']);
$this->assertCount(2, $res['body']['documents']);
foreach ($res['body']['documents'] as $doc) {
$this->assertTrue($doc['metadata']['bulkUpdated']);
}
// Fetch by IDs and assert update persisted
foreach ($ids as $id) {
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertTrue($get['body']['metadata']['bulkUpdated']);
}
}
public function testBulkDelete(): void
{
// Setup: create db/collection and two docs
$db = $this->client->call(Client::METHOD_POST, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBDelete' ]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'BulkColDelete',
'documentSecurity' => true,
'dimension' => 3,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
$seed = $this->client->call(Client::METHOD_PUT, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documents' => [
['embeddings' => [1.0,0.0,0.0], 'metadata' => ['seed' => 1], '$permissions' => [Permission::read(Role::any())]],
['embeddings' => [0.0,1.0,0.0], 'metadata' => ['seed' => 2], '$permissions' => [Permission::read(Role::any())]]
]
]);
$this->assertEquals(200, $seed['headers']['status-code']);
$ids = array_map(fn ($d) => $d['$id'], $seed['body']['documents']);
$res = $this->client->call(Client::METHOD_DELETE, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'queries' => [
\Utopia\Database\Query::equal('$id', $ids)->toString()
]
]);
$this->assertEquals(200, $res['headers']['status-code']);
$this->assertIsInt($res['body']['total'] ?? 0);
$this->assertGreaterThanOrEqual(2, $res['body']['total']);
// Ensure they are deleted
foreach ($ids as $id) {
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(404, $get['headers']['status-code']);
}
}
public function testCustomTimestamps(): void
{
// Setup: create database and collection
$db = $this->client->call(Client::METHOD_POST, '/vectordb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'TimestampTestDB'
]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'TimestampTestCollection',
'documentSecurity' => true,
'dimension' => 1536,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
// Test: Create document with custom timestamps using PUT (upsert)
$customCreatedAt = '1970-01-01T00:00:00.000+00:00';
$customUpdatedAt = '1970-01-01T00:00:00.000+00:00';
$vector = array_fill(0, 1536, 0.0);
$vector[0] = 1.0;
$documentId = ID::unique();
$doc = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documentId' => $documentId,
'data' => [
'$createdAt' => $customCreatedAt,
'$updatedAt' => $customUpdatedAt,
'embeddings' => $vector,
'metadata' => ['test' => 'custom_timestamps']
]
]);
$this->assertEquals(201, $doc['headers']['status-code']);
$documentId = $doc['body']['$id'];
$this->assertNotEmpty($documentId);
// Verify timestamps were set correctly
$this->assertEquals($customCreatedAt, $doc['body']['$createdAt'], 'CreatedAt should match custom timestamp');
$this->assertEquals($customUpdatedAt, $doc['body']['$updatedAt'], 'UpdatedAt should match custom timestamp');
// Fetch document and verify timestamps persist
$fetched = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $fetched['headers']['status-code']);
$this->assertEquals($customCreatedAt, $fetched['body']['$createdAt'], 'CreatedAt should persist after fetch');
$this->assertEquals($customUpdatedAt, $fetched['body']['$updatedAt'], 'UpdatedAt should persist after fetch');
// Test: Update document with new custom timestamps
$newCustomUpdatedAt = '2000-01-01T12:00:00.000+00:00';
$vector2 = array_fill(0, 1536, 0.0);
$vector2[1] = 1.0;
$updated = $this->client->call(Client::METHOD_PUT, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'data' => [
'$createdAt' => $customCreatedAt, // Keep original createdAt
'$updatedAt' => $newCustomUpdatedAt, // Update updatedAt
'embeddings' => $vector2,
'metadata' => ['test' => 'updated_timestamps']
]
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals($customCreatedAt, $updated['body']['$createdAt'], 'CreatedAt should remain unchanged');
$this->assertEquals($newCustomUpdatedAt, $updated['body']['$updatedAt'], 'UpdatedAt should be updated to new custom timestamp');
// Final verification
$final = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $final['headers']['status-code']);
$this->assertEquals($customCreatedAt, $final['body']['$createdAt'], 'CreatedAt should persist through updates');
$this->assertEquals($newCustomUpdatedAt, $final['body']['$updatedAt'], 'UpdatedAt should reflect the latest custom timestamp');
}
}