appwrite/tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
Jake Barnby daaf1d7f77 fix: revert test polling timeouts to original values
The root cause (PostgreSQL duplicate index race condition) has been
fixed in the database library, so the timeout workarounds are no
longer needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 21:00:36 +13:00

745 lines
27 KiB
PHP

<?php
namespace Tests\E2E\Services\GraphQL\Legacy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Services\GraphQL\Base;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class DatabaseClientTest extends Scope
{
use ProjectCustom;
use SideClient;
use Base;
/**
* Cached database data
*/
private static array $database = [];
/**
* Cached collection data (includes database)
*/
private static array $collection = [];
/**
* Cached document data (includes database, collection)
*/
private static array $document = [];
/**
* Cached bulk operations data
*/
private static array $bulkData = [];
/**
* Helper to set up database
*/
protected function setupDatabase(): array
{
$cacheKey = $this->getProject()['$id'] ?? 'default';
if (!empty(static::$database[$cacheKey])) {
return static::$database[$cacheKey];
}
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::CREATE_DATABASE);
$databaseId = ID::unique();
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $databaseId,
'name' => 'Actors',
]
];
$database = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
// Handle errors with better diagnostics
if (isset($database['body']['errors'])) {
$errorMessage = $database['body']['errors'][0]['message'] ?? 'Unknown error';
$this->fail('Failed to create database: ' . $errorMessage);
}
$this->assertIsArray($database['body']['data']);
static::$database[$cacheKey] = $database['body']['data']['databasesCreate'];
return static::$database[$cacheKey];
}
/**
* Helper to set up collection (includes database setup)
*/
protected function setupCollection(): array
{
$cacheKey = $this->getProject()['$id'] ?? 'default';
if (!empty(static::$collection[$cacheKey])) {
return static::$collection[$cacheKey];
}
$database = $this->setupDatabase();
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::CREATE_COLLECTION);
$collectionId = ID::unique();
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $database['_id'],
'collectionId' => $collectionId,
'name' => 'Actors',
'documentSecurity' => false,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]
];
$collection = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
// Handle errors with better diagnostics
if (isset($collection['body']['errors'])) {
$errorMessage = $collection['body']['errors'][0]['message'] ?? 'Unknown error';
$this->fail('Failed to create collection: ' . $errorMessage);
}
$this->assertIsArray($collection['body']['data']);
static::$collection[$cacheKey] = [
'database' => $database,
'collection' => $collection['body']['data']['databasesCreateCollection'],
];
return static::$collection[$cacheKey];
}
/**
* Helper to set up attributes (string and integer)
*/
protected function setupAttributes(): array
{
$data = $this->setupCollection();
// Use a static flag to track if attributes have been created, keyed by project
static $attributesCreated = [];
$cacheKey = $this->getProject()['$id'] ?? 'default';
if (!empty($attributesCreated[$cacheKey])) {
return $data;
}
$projectId = $this->getProject()['$id'];
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
];
// Create string attribute (may already exist from testCreateStringAttribute)
$query = $this->getQuery(self::CREATE_STRING_ATTRIBUTE);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection']['_id'],
'key' => 'name',
'size' => 256,
'required' => true,
]
];
$attribute = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $gqlPayload);
// Handle 409 conflict - attribute may already exist from individual test
if (isset($attribute['body']['errors'])) {
$errorMessage = $attribute['body']['errors'][0]['message'] ?? '';
if (strpos($errorMessage, 'already exists') === false && strpos($errorMessage, 'Document with the requested ID already exists') === false) {
$this->assertArrayNotHasKey('errors', $attribute['body']);
}
}
// Create integer attribute (may already exist from testCreateIntegerAttribute)
$query = $this->getQuery(self::CREATE_INTEGER_ATTRIBUTE);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection']['_id'],
'key' => 'age',
'min' => 18,
'max' => 150,
'required' => true,
]
];
$attribute = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $gqlPayload);
// Handle 409 conflict - attribute may already exist from individual test
if (isset($attribute['body']['errors'])) {
$errorMessage = $attribute['body']['errors'][0]['message'] ?? '';
if (strpos($errorMessage, 'already exists') === false && strpos($errorMessage, 'Document with the requested ID already exists') === false) {
$this->assertArrayNotHasKey('errors', $attribute['body']);
}
}
$attributesCreated[$cacheKey] = true;
return $data;
}
/**
* Helper to set up document (includes database, collection, and attributes setup)
*/
protected function setupDocument(): array
{
$cacheKey = $this->getProject()['$id'] ?? 'default';
if (!empty(static::$document[$cacheKey])) {
return static::$document[$cacheKey];
}
$data = $this->setupAttributes();
$this->assertEventually(function () use ($data) {
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $data['database']['_id'] . '/collections/' . $data['collection']['_id'] . '/attributes/name', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals('available', $response['body']['status']);
}, 30000, 250);
$this->assertEventually(function () use ($data) {
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $data['database']['_id'] . '/collections/' . $data['collection']['_id'] . '/attributes/age', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals('available', $response['body']['status']);
}, 30000, 250);
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::CREATE_DOCUMENT);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection']['_id'],
'documentId' => ID::unique(),
'data' => [
'name' => 'John Doe',
'age' => 35,
],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]
];
$document = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertArrayNotHasKey('errors', $document['body']);
$this->assertIsArray($document['body']['data']);
static::$document[$cacheKey] = [
'database' => $data['database'],
'collection' => $data['collection'],
'document' => $document['body']['data']['databasesCreateDocument'],
];
return static::$document[$cacheKey];
}
/**
* Helper to set up bulk operations data
*/
protected function setupBulkData(): array
{
$cacheKey = $this->getProject()['$id'] ?? 'default';
if (!empty(static::$bulkData[$cacheKey])) {
return static::$bulkData[$cacheKey];
}
$project = $this->getProject();
$projectId = $project['$id'];
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $project['apiKey'],
];
// Step 1: Create database
$query = $this->getQuery(self::CREATE_DATABASE);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => ID::unique(),
'name' => 'Bulk',
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$databaseId = $res['body']['data']['databasesCreate']['_id'];
// Step 2: Create collection
$query = $this->getQuery(self::CREATE_COLLECTION);
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'collectionId' => ID::unique(),
'name' => 'Operations',
'documentSecurity' => false,
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$collectionId = $res['body']['data']['databasesCreateCollection']['_id'];
// Step 3: Create attribute
$query = $this->getQuery(self::CREATE_STRING_ATTRIBUTE);
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'key' => 'name',
'size' => 256,
'required' => true,
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertEventually(function () use ($databaseId, $collectionId) {
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals('available', $response['body']['status']);
}, 30000, 250);
// Step 4: Create documents
$query = $this->getQuery(self::CREATE_DOCUMENTS);
$documents = [];
for ($i = 1; $i <= 10; $i++) {
$documents[] = ['$id' => ID::unique(), 'name' => 'Doc #' . $i];
}
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'documents' => $documents,
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(10, $res['body']['data']['databasesCreateDocuments']['documents']);
static::$bulkData[$cacheKey] = [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'projectId' => $projectId,
];
return static::$bulkData[$cacheKey];
}
/**
* Helper to update bulk documents
*/
protected function setupBulkUpdatedData(): array
{
$data = $this->setupBulkData();
static $bulkUpdated = [];
$cacheKey = $this->getProject()['$id'] ?? 'default';
if (!empty($bulkUpdated[$cacheKey])) {
return $data;
}
$userId = $this->getUser()['$id'];
$permissions = [
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
];
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
$query = $this->getQuery(self::UPDATE_DOCUMENTS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'collectionId' => $data['collectionId'],
'data' => [
'name' => 'Docs Updated',
'$permissions' => $permissions,
],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(10, $res['body']['data']['databasesUpdateDocuments']['documents']);
$bulkUpdated[$cacheKey] = true;
return $data;
}
/**
* Helper to upsert bulk documents
*/
protected function setupBulkUpsertedData(): array
{
$data = $this->setupBulkUpdatedData();
static $bulkUpserted = [];
$cacheKey = $this->getProject()['$id'] ?? 'default';
if (!empty($bulkUpserted[$cacheKey])) {
return $data;
}
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
// Upsert: Update one, insert one
$query = $this->getQuery(self::UPSERT_DOCUMENTS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'collectionId' => $data['collectionId'],
'documents' => [
['$id' => ID::unique(), 'name' => 'Doc #1000'],
['name' => 'Doc #11'],
],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(2, $res['body']['data']['databasesUpsertDocuments']['documents']);
$bulkUpserted[$cacheKey] = true;
return $data;
}
public function testCreateDatabase(): void
{
$database = $this->setupDatabase();
$this->assertEquals('Actors', $database['name']);
}
public function testCreateCollection(): void
{
$data = $this->setupCollection();
$this->assertEquals('Actors', $data['collection']['name']);
}
public function testCreateStringAttribute(): void
{
$data = $this->setupCollection();
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::CREATE_STRING_ATTRIBUTE);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection']['_id'],
'key' => 'name',
'size' => 256,
'required' => true,
]
];
$attribute = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
// Attribute may already exist from setupAttributes, so we check for either success or already exists error
if (isset($attribute['body']['errors'])) {
$this->assertStringContainsString('already', $attribute['body']['errors'][0]['message']);
} else {
$this->assertIsArray($attribute['body']['data']);
$this->assertIsArray($attribute['body']['data']['databasesCreateStringAttribute']);
}
}
public function testCreateIntegerAttribute(): void
{
$data = $this->setupCollection();
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::CREATE_INTEGER_ATTRIBUTE);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection']['_id'],
'key' => 'age',
'min' => 18,
'max' => 150,
'required' => true,
]
];
$attribute = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
// Attribute may already exist from setupAttributes, so we check for either success or already exists error
if (isset($attribute['body']['errors'])) {
$this->assertStringContainsString('already', $attribute['body']['errors'][0]['message']);
} else {
$this->assertIsArray($attribute['body']['data']);
$this->assertIsArray($attribute['body']['data']['databasesCreateIntegerAttribute']);
}
}
public function testCreateDocument(): void
{
$data = $this->setupDocument();
$this->assertIsArray($data['document']);
}
/**
* @throws \Exception
*/
public function testGetDocuments(): void
{
$data = $this->setupCollection();
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::GET_DOCUMENTS);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection']['_id'],
]
];
$documents = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertArrayNotHasKey('errors', $documents['body']);
$this->assertIsArray($documents['body']['data']);
$this->assertIsArray($documents['body']['data']['databasesListDocuments']);
}
/**
* @throws \Exception
*/
public function testGetDocument(): void
{
$data = $this->setupDocument();
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::GET_DOCUMENT);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection']['_id'],
'documentId' => $data['document']['_id'],
]
];
$document = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertArrayNotHasKey('errors', $document['body']);
$this->assertIsArray($document['body']['data']);
$this->assertIsArray($document['body']['data']['databasesGetDocument']);
}
/**
* @throws \Exception
*/
public function testUpdateDocument(): void
{
$data = $this->setupDocument();
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::UPDATE_DOCUMENT);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection']['_id'],
'documentId' => $data['document']['_id'],
'data' => [
'name' => 'New Document Name',
],
]
];
$document = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertArrayNotHasKey('errors', $document['body']);
$this->assertIsArray($document['body']['data']);
$document = $document['body']['data']['databasesUpdateDocument'];
$this->assertIsArray($document);
$this->assertStringContainsString('New Document Name', $document['data']);
}
/**
* @throws \Exception
*/
public function testDeleteDocument(): void
{
// Create a fresh document for deletion to avoid conflicts with other tests
$data = $this->setupAttributes();
$this->assertEventually(function () use ($data) {
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $data['database']['_id'] . '/collections/' . $data['collection']['_id'] . '/attributes/name', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals('available', $response['body']['status']);
}, 30000, 250);
$this->assertEventually(function () use ($data) {
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $data['database']['_id'] . '/collections/' . $data['collection']['_id'] . '/attributes/age', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals('available', $response['body']['status']);
}, 30000, 250);
$projectId = $this->getProject()['$id'];
// Create a document specifically for this delete test
$query = $this->getQuery(self::CREATE_DOCUMENT);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection']['_id'],
'documentId' => ID::unique(),
'data' => [
'name' => 'To Be Deleted',
'age' => 25,
],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]
];
$document = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertArrayNotHasKey('errors', $document['body']);
$documentId = $document['body']['data']['databasesCreateDocument']['_id'];
// Now delete it
$query = $this->getQuery(self::DELETE_DOCUMENT);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection']['_id'],
'documentId' => $documentId,
]
];
$document = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertIsNotArray($document['body']);
$this->assertEquals(204, $document['headers']['status-code']);
}
/**
* @throws \Exception
*/
public function testBulkCreateDocuments(): void
{
$data = $this->setupBulkData();
$this->assertNotEmpty($data['databaseId']);
$this->assertNotEmpty($data['collectionId']);
$this->assertNotEmpty($data['projectId']);
}
public function testBulkUpdateDocuments(): void
{
$data = $this->setupBulkUpdatedData();
$this->assertNotEmpty($data['databaseId']);
$this->assertNotEmpty($data['collectionId']);
}
public function testBulkUpsertDocuments(): void
{
$data = $this->setupBulkUpsertedData();
$this->assertNotEmpty($data['databaseId']);
$this->assertNotEmpty($data['collectionId']);
}
public function testBulkDeleteDocuments(): void
{
$data = $this->setupBulkUpsertedData();
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
$query = $this->getQuery(self::DELETE_DOCUMENTS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'collectionId' => $data['collectionId'],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(12, $res['body']['data']['databasesDeleteDocuments']['documents']);
}
}