From f2759cb65a72377e092d6569797fa8487875a8e4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 21 Feb 2026 03:44:28 +1300 Subject: [PATCH] fix: remove file caching, disable --functional for Databases/Functions/Realtime Services that depend on shared static state between test methods (Databases, Functions, Realtime) now run without --functional flag, so test methods execute sequentially within each class while classes still run in parallel. All other services keep --functional mode. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/tests.yml | 20 +- tests/e2e/Scopes/Scope.php | 68 -- .../e2e/Services/Databases/DatabasesBase.php | 1080 ++++++++--------- .../TransactionPermissionsBase.php | 50 +- .../Transactions/TransactionsBase.php | 114 +- .../e2e/Services/Functions/FunctionsBase.php | 19 +- 6 files changed, 575 insertions(+), 776 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f752128234..9f7143b98b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -273,7 +273,13 @@ jobs: echo "Using project tables" SERVICE_PATH="/usr/src/code/tests/e2e/Services/${{ matrix.service }}" - echo "Running with paratest (parallel) for: ${{ matrix.service }}" + # Services that rely on sequential test method execution (shared static state) + FUNCTIONAL_FLAG="--functional" + case "${{ matrix.service }}" in + Databases|Functions|Realtime) FUNCTIONAL_FLAG="" ;; + esac + + echo "Running with paratest (parallel) for: ${{ matrix.service }} ${FUNCTIONAL_FLAG:+(functional)}" docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES="" \ -e _APP_DATABASE_SHARED_TABLES_V1="" \ @@ -282,7 +288,7 @@ jobs: -e _APP_DB_PORT="${{ env._APP_DB_PORT }}" \ -e _APP_DB_SCHEMA=appwrite \ -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ - appwrite vendor/bin/paratest --processes $(nproc) --functional "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml + appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml - name: Failure Logs if: failure() @@ -375,12 +381,18 @@ jobs: fi SERVICE_PATH="/usr/src/code/tests/e2e/Services/${{ matrix.service }}" - + + # Services that rely on sequential test method execution (shared static state) + FUNCTIONAL_FLAG="--functional" + case "${{ matrix.service }}" in + Databases|Functions|Realtime) FUNCTIONAL_FLAG="" ;; + esac + docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ - appwrite vendor/bin/paratest --processes $(nproc) --functional "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml + appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml - name: Failure Logs if: failure() diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php index 2940596e38..4f9efa129d 100644 --- a/tests/e2e/Scopes/Scope.php +++ b/tests/e2e/Scopes/Scope.php @@ -43,74 +43,6 @@ abstract class Scope extends TestCase $this->client = null; } - /** - * File-based cache directory for cross-process caching in ParaTest --functional mode. - * Each test method runs in its own process so static variables are not shared. - * File-based caching lets methods within the same test class share expensive resources. - */ - private const FILE_CACHE_DIR = '/tmp/appwrite-tests/'; - - /** - * Read data from file-based cross-process cache. - */ - protected static function readFileCache(string $key): ?array - { - $path = self::FILE_CACHE_DIR . md5($key) . '.json'; - if (!file_exists($path)) { - return null; - } - $data = @file_get_contents($path); - if ($data === false) { - return null; - } - return json_decode($data, true) ?: null; - } - - /** - * Write data to file-based cross-process cache. - */ - protected static function writeFileCache(string $key, array $data): void - { - if (!is_dir(self::FILE_CACHE_DIR)) { - @mkdir(self::FILE_CACHE_DIR, 0777, true); - } - file_put_contents(self::FILE_CACHE_DIR . md5($key) . '.json', json_encode($data), LOCK_EX); - } - - /** - * Execute callback with file-based locking, returning cached data if available. - * Ensures only one process creates a shared resource; others wait and read from cache. - */ - protected function withFileCache(string $key, callable $callback): array - { - $cached = self::readFileCache($key); - if ($cached !== null) { - return $cached; - } - - if (!is_dir(self::FILE_CACHE_DIR)) { - @mkdir(self::FILE_CACHE_DIR, 0777, true); - } - - $lockPath = self::FILE_CACHE_DIR . md5($key) . '.lock'; - $lockHandle = fopen($lockPath, 'c'); - flock($lockHandle, LOCK_EX); - - try { - $cached = self::readFileCache($key); - if ($cached !== null) { - return $cached; - } - - $result = $callback(); - self::writeFileCache($key, $result); - return $result; - } finally { - flock($lockHandle, LOCK_UN); - fclose($lockHandle); - } - } - /** * @var array|null Cached console variables */ diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index afc5eb42c3..1adb1c1e5f 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -33,37 +33,6 @@ trait DatabasesBase private static array $oneToManyCache = []; private static array $fulltextDocsCache = []; - /** - * Ensure all Database test methods share the same project and user via file cache. - * In ParaTest --functional mode, each test method runs in its own process. - * Without this, each process would create a new project, and the file-cached - * database/collection/attribute IDs would be invalid for the new project. - * The user is also cached because collection permissions reference the user's ID. - */ - protected function ensureSharedProject(): void - { - // If we already have a project in static cache, use it - if (!empty(self::$project)) { - return; - } - - // File-cache the project + user so all methods in this test class share them - $cached = $this->withFileCache('db_project_' . static::class, function () { - $project = $this->createNewProject(); - // Temporarily set the project so getUser() can create a user in this project - self::$project = $project; - $user = $this->getUser(); - return [ - '_project' => $project, - '_user' => $user, - ]; - }); - - self::$project = $cached['_project']; - $projectId = self::$project['$id']; - self::$user[$projectId] = $cached['_user']; - } - /** * Get cache key for current test instance (based on project ID) */ @@ -78,29 +47,24 @@ trait DatabasesBase */ protected function setupDatabase(): array { - $this->ensureSharedProject(); - $cacheKey = $this->getCacheKey(); if (!empty(self::$databaseCache[$cacheKey])) { return self::$databaseCache[$cacheKey]; } - self::$databaseCache[$cacheKey] = $this->withFileCache('database_' . static::class, function () { - $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ], [ - 'databaseId' => ID::unique(), - 'name' => 'Test Database' - ]); + $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Test Database' + ]); - $this->assertNotEmpty($database['body']['$id']); - $this->assertEquals(201, $database['headers']['status-code']); - - return ['databaseId' => $database['body']['$id']]; - }); + $this->assertNotEmpty($database['body']['$id']); + $this->assertEquals(201, $database['headers']['status-code']); + self::$databaseCache[$cacheKey] = ['databaseId' => $database['body']['$id']]; return self::$databaseCache[$cacheKey]; } @@ -110,69 +74,65 @@ trait DatabasesBase */ protected function setupCollection(): array { - $this->ensureSharedProject(); $cacheKey = $this->getCacheKey(); if (!empty(self::$collectionCache[$cacheKey])) { return self::$collectionCache[$cacheKey]; } - self::$collectionCache[$cacheKey] = $this->withFileCache('collection_' . static::class, function () { - $data = $this->setupDatabase(); - $databaseId = $data['databaseId']; + $data = $this->setupDatabase(); + $databaseId = $data['databaseId']; - $movies = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - $this->getContainerIdParam() => ID::unique(), - 'name' => 'Movies', - $this->getSecurityParam() => true, - 'permissions' => [ - Permission::create(Role::user($this->getUser()['$id'])), - ], - ]); + $movies = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + $this->getContainerIdParam() => ID::unique(), + 'name' => 'Movies', + $this->getSecurityParam() => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + ]); - $this->assertEquals(201, $movies['headers']['status-code']); + $this->assertEquals(201, $movies['headers']['status-code']); - $actors = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - $this->getContainerIdParam() => ID::unique(), - 'name' => 'Actors', - $this->getSecurityParam() => true, - 'permissions' => [ - Permission::create(Role::user($this->getUser()['$id'])), - ], - ]); + $actors = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + $this->getContainerIdParam() => ID::unique(), + 'name' => 'Actors', + $this->getSecurityParam() => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + ]); - $this->assertEquals(201, $actors['headers']['status-code']); + $this->assertEquals(201, $actors['headers']['status-code']); - $books = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - $this->getContainerIdParam() => ID::unique(), - 'name' => 'Books', - $this->getSecurityParam() => true, - 'permissions' => [ - Permission::create(Role::user($this->getUser()['$id'])), - ], - ]); + $books = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + $this->getContainerIdParam() => ID::unique(), + 'name' => 'Books', + $this->getSecurityParam() => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + ]); - $this->assertEquals(201, $books['headers']['status-code']); - - return [ - 'databaseId' => $databaseId, - 'moviesId' => $movies['body']['$id'], - 'actorsId' => $actors['body']['$id'], - 'booksId' => $books['body']['$id'], - ]; - }); + $this->assertEquals(201, $books['headers']['status-code']); + self::$collectionCache[$cacheKey] = [ + 'databaseId' => $databaseId, + 'moviesId' => $movies['body']['$id'], + 'actorsId' => $actors['body']['$id'], + 'booksId' => $books['body']['$id'], + ]; return self::$collectionCache[$cacheKey]; } @@ -182,154 +142,150 @@ trait DatabasesBase */ protected function setupAttributes(): array { - $this->ensureSharedProject(); $cacheKey = $this->getCacheKey(); if (!empty(self::$attributesCache[$cacheKey])) { return self::$attributesCache[$cacheKey]; } - self::$attributesCache[$cacheKey] = $this->withFileCache('attributes_' . static::class, function () { - $data = $this->setupCollection(); - $databaseId = $data['databaseId']; + $data = $this->setupCollection(); + $databaseId = $data['databaseId']; - $title = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'title', - 'size' => 256, - 'required' => true, - ]); + $title = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 256, + 'required' => true, + ]); - $description = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'description', - 'size' => 512, - 'required' => false, - 'default' => '', - ]); + $description = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'description', + 'size' => 512, + 'required' => false, + 'default' => '', + ]); - $tagline = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'tagline', - 'size' => 512, - 'required' => false, - 'default' => '', - ]); + $tagline = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'tagline', + 'size' => 512, + 'required' => false, + 'default' => '', + ]); - $releaseYear = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/integer', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'releaseYear', - 'required' => true, - 'min' => 1900, - 'max' => 2200, - ]); + $releaseYear = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/integer', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'releaseYear', + 'required' => true, + 'min' => 1900, + 'max' => 2200, + ]); - $duration = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/integer', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'duration', - 'required' => false, - 'min' => 60, - ]); + $duration = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/integer', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'duration', + 'required' => false, + 'min' => 60, + ]); - $actors = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'actors', - 'size' => 256, - 'required' => false, - 'array' => true, - ]); + $actors = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'actors', + 'size' => 256, + 'required' => false, + 'array' => true, + ]); - $datetime = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/datetime', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'birthDay', - 'required' => false, - ]); + $datetime = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/datetime', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'birthDay', + 'required' => false, + ]); - $relationship = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/relationship', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - $this->getRelatedIdParam() => $data['actorsId'], - 'type' => 'oneToMany', - 'twoWay' => true, - 'key' => 'starringActors', - 'twoWayKey' => 'movie' - ]); + $relationship = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + $this->getRelatedIdParam() => $data['actorsId'], + 'type' => 'oneToMany', + 'twoWay' => true, + 'key' => 'starringActors', + 'twoWayKey' => 'movie' + ]); - $integers = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/integer', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'integers', - 'required' => false, - 'array' => true, - 'min' => 10, - 'max' => 99, - ]); + $integers = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/integer', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'integers', + 'required' => false, + 'array' => true, + 'min' => 10, + 'max' => 99, + ]); - $this->assertEquals(202, $title['headers']['status-code']); - $this->assertEquals(202, $description['headers']['status-code']); - $this->assertEquals(202, $tagline['headers']['status-code']); - $this->assertEquals(202, $releaseYear['headers']['status-code']); - $this->assertEquals(202, $duration['headers']['status-code']); - $this->assertEquals(202, $actors['headers']['status-code']); - $this->assertEquals(202, $datetime['headers']['status-code']); - $this->assertEquals(202, $relationship['headers']['status-code']); - $this->assertEquals(202, $integers['headers']['status-code']); + $this->assertEquals(202, $title['headers']['status-code']); + $this->assertEquals(202, $description['headers']['status-code']); + $this->assertEquals(202, $tagline['headers']['status-code']); + $this->assertEquals(202, $releaseYear['headers']['status-code']); + $this->assertEquals(202, $duration['headers']['status-code']); + $this->assertEquals(202, $actors['headers']['status-code']); + $this->assertEquals(202, $datetime['headers']['status-code']); + $this->assertEquals(202, $relationship['headers']['status-code']); + $this->assertEquals(202, $integers['headers']['status-code']); - // Books collection attributes (for fulltext search tests) - $bookTitle = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['booksId']) . '/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'title', - 'size' => 256, - 'required' => true, - ]); + // Books collection attributes (for fulltext search tests) + $bookTitle = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['booksId']) . '/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 256, + 'required' => true, + ]); - $bookDescription = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['booksId']) . '/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'description', - 'size' => 2048, - 'required' => true, - ]); + $bookDescription = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['booksId']) . '/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'description', + 'size' => 2048, + 'required' => true, + ]); - $this->assertEquals(202, $bookTitle['headers']['status-code']); - $this->assertEquals(202, $bookDescription['headers']['status-code']); + $this->assertEquals(202, $bookTitle['headers']['status-code']); + $this->assertEquals(202, $bookDescription['headers']['status-code']); - // wait for database worker to create attributes - $this->waitForAllAttributes($databaseId, $data['moviesId']); - $this->waitForAllAttributes($databaseId, $data['booksId']); - - return $data; - }); + // wait for database worker to create attributes + $this->waitForAllAttributes($databaseId, $data['moviesId']); + $this->waitForAllAttributes($databaseId, $data['booksId']); + self::$attributesCache[$cacheKey] = $data; return self::$attributesCache[$cacheKey]; } @@ -339,79 +295,75 @@ trait DatabasesBase */ protected function setupIndexes(): array { - $this->ensureSharedProject(); $cacheKey = $this->getCacheKey(); if (!empty(self::$indexesCache[$cacheKey])) { return self::$indexesCache[$cacheKey]; } - self::$indexesCache[$cacheKey] = $this->withFileCache('indexes_' . static::class, function () { - $data = $this->setupAttributes(); - $databaseId = $data['databaseId']; + $data = $this->setupAttributes(); + $databaseId = $data['databaseId']; - $titleIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'titleIndex', - 'type' => 'fulltext', - $this->getIndexAttributesParam() => ['title'], - ]); + $titleIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'titleIndex', + 'type' => 'fulltext', + $this->getIndexAttributesParam() => ['title'], + ]); - $releaseYearIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'releaseYear', - 'type' => 'key', - $this->getIndexAttributesParam() => ['releaseYear'], - ]); + $releaseYearIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'releaseYear', + 'type' => 'key', + $this->getIndexAttributesParam() => ['releaseYear'], + ]); - $releaseWithDate1 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'releaseYearDated', - 'type' => 'key', - $this->getIndexAttributesParam() => ['releaseYear', '$createdAt', '$updatedAt'], - ]); + $releaseWithDate1 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'releaseYearDated', + 'type' => 'key', + $this->getIndexAttributesParam() => ['releaseYear', '$createdAt', '$updatedAt'], + ]); - $releaseWithDate2 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'birthDay', - 'type' => 'key', - $this->getIndexAttributesParam() => ['birthDay'], - ]); + $releaseWithDate2 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['moviesId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'birthDay', + 'type' => 'key', + $this->getIndexAttributesParam() => ['birthDay'], + ]); - // Fulltext index on Books.description (for testNotSearch) - $booksFtsIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['booksId']), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'fts_description', - 'type' => Database::INDEX_FULLTEXT, - $this->getIndexAttributesParam() => ['description'], - ]); + // Fulltext index on Books.description (for testNotSearch) + $booksFtsIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $data['booksId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'fts_description', + 'type' => Database::INDEX_FULLTEXT, + $this->getIndexAttributesParam() => ['description'], + ]); - $this->assertEquals(202, $titleIndex['headers']['status-code']); - $this->assertEquals(202, $releaseYearIndex['headers']['status-code']); - $this->assertEquals(202, $releaseWithDate1['headers']['status-code']); - $this->assertEquals(202, $releaseWithDate2['headers']['status-code']); - $this->assertEquals(202, $booksFtsIndex['headers']['status-code']); + $this->assertEquals(202, $titleIndex['headers']['status-code']); + $this->assertEquals(202, $releaseYearIndex['headers']['status-code']); + $this->assertEquals(202, $releaseWithDate1['headers']['status-code']); + $this->assertEquals(202, $releaseWithDate2['headers']['status-code']); + $this->assertEquals(202, $booksFtsIndex['headers']['status-code']); - $this->waitForAllIndexes($databaseId, $data['moviesId']); - $this->waitForAllIndexes($databaseId, $data['booksId']); - - return $data; - }); + $this->waitForAllIndexes($databaseId, $data['moviesId']); + $this->waitForAllIndexes($databaseId, $data['booksId']); + self::$indexesCache[$cacheKey] = $data; return self::$indexesCache[$cacheKey]; } @@ -421,96 +373,92 @@ trait DatabasesBase */ protected function setupDocuments(): array { - $this->ensureSharedProject(); $cacheKey = $this->getCacheKey(); if (!empty(self::$documentsCache[$cacheKey])) { return self::$documentsCache[$cacheKey]; } - self::$documentsCache[$cacheKey] = $this->withFileCache('documents_' . static::class, function () { - $data = $this->setupIndexes(); - $databaseId = $data['databaseId']; + $data = $this->setupIndexes(); + $databaseId = $data['databaseId']; - $document1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - $this->getRecordIdParam() => ID::unique(), - 'data' => [ - 'title' => 'Captain America', - 'releaseYear' => 1944, - 'birthDay' => '1975-06-12 14:12:55+02:00', - 'actors' => [ - 'Chris Evans', - 'Samuel Jackson', - ] - ], - 'permissions' => [ - Permission::read(Role::user($this->getUser()['$id'])), - Permission::update(Role::user($this->getUser()['$id'])), - Permission::delete(Role::user($this->getUser()['$id'])), + $document1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + $this->getRecordIdParam() => ID::unique(), + 'data' => [ + 'title' => 'Captain America', + 'releaseYear' => 1944, + 'birthDay' => '1975-06-12 14:12:55+02:00', + 'actors' => [ + 'Chris Evans', + 'Samuel Jackson', ] - ]); + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); - $document2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - $this->getRecordIdParam() => ID::unique(), - 'data' => [ - 'title' => 'Spider-Man: Far From Home', - 'releaseYear' => 2019, - 'birthDay' => null, - 'actors' => [ - 'Tom Holland', - 'Zendaya Maree Stoermer', - 'Samuel Jackson', - ], - 'integers' => [50, 60] + $document2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + $this->getRecordIdParam() => ID::unique(), + 'data' => [ + 'title' => 'Spider-Man: Far From Home', + 'releaseYear' => 2019, + 'birthDay' => null, + 'actors' => [ + 'Tom Holland', + 'Zendaya Maree Stoermer', + 'Samuel Jackson', ], - 'permissions' => [ - Permission::read(Role::user($this->getUser()['$id'])), - Permission::update(Role::user($this->getUser()['$id'])), - Permission::delete(Role::user($this->getUser()['$id'])), - ] - ]); + 'integers' => [50, 60] + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); - $document3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - $this->getRecordIdParam() => ID::unique(), - 'data' => [ - 'title' => 'Spider-Man: Homecoming', - 'releaseYear' => 2017, - 'birthDay' => '1975-06-12 14:12:55 America/New_York', - 'duration' => 65, - 'actors' => [ - 'Tom Holland', - 'Zendaya Maree Stoermer', - ], - 'integers' => [50] + $document3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + $this->getRecordIdParam() => ID::unique(), + 'data' => [ + 'title' => 'Spider-Man: Homecoming', + 'releaseYear' => 2017, + 'birthDay' => '1975-06-12 14:12:55 America/New_York', + 'duration' => 65, + 'actors' => [ + 'Tom Holland', + 'Zendaya Maree Stoermer', ], - 'permissions' => [ - Permission::read(Role::user($this->getUser()['$id'])), - Permission::update(Role::user($this->getUser()['$id'])), - Permission::delete(Role::user($this->getUser()['$id'])), - ] - ]); + 'integers' => [50] + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); - $this->assertEquals(201, $document1['headers']['status-code']); - $this->assertEquals(201, $document2['headers']['status-code']); - $this->assertEquals(201, $document3['headers']['status-code']); + $this->assertEquals(201, $document1['headers']['status-code']); + $this->assertEquals(201, $document2['headers']['status-code']); + $this->assertEquals(201, $document3['headers']['status-code']); - $data['documentIds'] = [ - $document1['body']['$id'], - $document2['body']['$id'], - $document3['body']['$id'], - ]; - - return $data; - }); + $data['documentIds'] = [ + $document1['body']['$id'], + $document2['body']['$id'], + $document3['body']['$id'], + ]; + self::$documentsCache[$cacheKey] = $data; return self::$documentsCache[$cacheKey]; } @@ -520,83 +468,79 @@ trait DatabasesBase */ protected function setupOneToOneRelationship(): array { - $this->ensureSharedProject(); $cacheKey = $this->getCacheKey(); if (!empty(self::$oneToOneCache[$cacheKey])) { return self::$oneToOneCache[$cacheKey]; } - self::$oneToOneCache[$cacheKey] = $this->withFileCache('onetoone_' . static::class, function () { - $data = $this->setupDatabase(); - $databaseId = $data['databaseId']; + $data = $this->setupDatabase(); + $databaseId = $data['databaseId']; - $person = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - $this->getContainerIdParam() => ID::unique(), - 'name' => 'person', - 'permissions' => [ - Permission::read(Role::user($this->getUser()['$id'])), - Permission::update(Role::user($this->getUser()['$id'])), - Permission::delete(Role::user($this->getUser()['$id'])), - Permission::create(Role::user($this->getUser()['$id'])), - ], - $this->getSecurityParam() => true, - ]); + $person = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + $this->getContainerIdParam() => ID::unique(), + 'name' => 'person', + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + Permission::create(Role::user($this->getUser()['$id'])), + ], + $this->getSecurityParam() => true, + ]); - $this->assertEquals(201, $person['headers']['status-code']); + $this->assertEquals(201, $person['headers']['status-code']); - $library = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - $this->getContainerIdParam() => ID::unique(), - 'name' => 'library', - 'permissions' => [ - Permission::read(Role::user($this->getUser()['$id'])), - Permission::update(Role::user($this->getUser()['$id'])), - Permission::create(Role::user($this->getUser()['$id'])), - ], - $this->getSecurityParam() => true, - ]); + $library = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + $this->getContainerIdParam() => ID::unique(), + 'name' => 'library', + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::create(Role::user($this->getUser()['$id'])), + ], + $this->getSecurityParam() => true, + ]); - $this->assertEquals(201, $library['headers']['status-code']); + $this->assertEquals(201, $library['headers']['status-code']); - $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $person['body']['$id']) . '/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'fullName', - 'size' => 255, - 'required' => false, - ]); + $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $person['body']['$id']) . '/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'fullName', + 'size' => 255, + 'required' => false, + ]); - $this->waitForAttribute($databaseId, $person['body']['$id'], 'fullName'); + $this->waitForAttribute($databaseId, $person['body']['$id'], 'fullName'); - $libraryName = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $library['body']['$id']) . '/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'libraryName', - 'size' => 255, - 'required' => true, - ]); + $libraryName = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $library['body']['$id']) . '/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'libraryName', + 'size' => 255, + 'required' => true, + ]); - $this->waitForAttribute($databaseId, $library['body']['$id'], 'libraryName'); - $this->assertEquals(202, $libraryName['headers']['status-code']); - - return [ - 'databaseId' => $databaseId, - 'personCollection' => $person['body']['$id'], - 'libraryCollection' => $library['body']['$id'], - ]; - }); + $this->waitForAttribute($databaseId, $library['body']['$id'], 'libraryName'); + $this->assertEquals(202, $libraryName['headers']['status-code']); + self::$oneToOneCache[$cacheKey] = [ + 'databaseId' => $databaseId, + 'personCollection' => $person['body']['$id'], + 'libraryCollection' => $library['body']['$id'], + ]; return self::$oneToOneCache[$cacheKey]; } @@ -606,118 +550,114 @@ trait DatabasesBase */ protected function setupOneToManyRelationship(): array { - $this->ensureSharedProject(); $cacheKey = $this->getCacheKey(); if (!empty(self::$oneToManyCache[$cacheKey])) { return self::$oneToManyCache[$cacheKey]; } - self::$oneToManyCache[$cacheKey] = $this->withFileCache('onetomany_' . static::class, function () { - $data = $this->setupOneToOneRelationship(); - $databaseId = $data['databaseId']; - $personCollection = $data['personCollection']; - $libraryCollection = $data['libraryCollection']; + $data = $this->setupOneToOneRelationship(); + $databaseId = $data['databaseId']; + $personCollection = $data['personCollection']; + $libraryCollection = $data['libraryCollection']; - // One person can own several libraries - $relation = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $personCollection) . '/relationship', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - $this->getRelatedIdParam() => $libraryCollection, - 'type' => Database::RELATION_ONE_TO_MANY, - 'twoWay' => true, - 'key' => 'libraries', - 'twoWayKey' => 'person_one_to_many', - ]); + // One person can own several libraries + $relation = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $personCollection) . '/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + $this->getRelatedIdParam() => $libraryCollection, + 'type' => Database::RELATION_ONE_TO_MANY, + 'twoWay' => true, + 'key' => 'libraries', + 'twoWayKey' => 'person_one_to_many', + ]); - // Handle 409 if relationship already exists (possible race condition in parallel mode) - if ($relation['headers']['status-code'] === 409) { - // Relationship already exists, just wait for it to be available - } else { - $this->assertEquals(202, $relation['headers']['status-code'], 'Relationship creation failed: ' . \json_encode($relation['body'] ?? 'no body')); - } + // Handle 409 if relationship already exists (possible race condition in parallel mode) + if ($relation['headers']['status-code'] === 409) { + // Relationship already exists, just wait for it to be available + } else { + $this->assertEquals(202, $relation['headers']['status-code'], 'Relationship creation failed: ' . \json_encode($relation['body'] ?? 'no body')); + } - // Wait for both the relationship attribute and its twoWayKey to be available - $this->waitForAttribute($databaseId, $personCollection, 'libraries'); - $this->waitForAttribute($databaseId, $libraryCollection, 'person_one_to_many'); + // Wait for both the relationship attribute and its twoWayKey to be available + $this->waitForAttribute($databaseId, $personCollection, 'libraries'); + $this->waitForAttribute($databaseId, $libraryCollection, 'person_one_to_many'); - $serverHeaders = [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]; + $serverHeaders = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]; - // Create a person with libraries - $person = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $personCollection), $serverHeaders, [ - $this->getRecordIdParam() => ID::unique(), - 'data' => [ - 'fullName' => 'Stevie Wonder', - 'libraries' => [ - [ - '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'libraryName' => 'Library 10', + // Create a person with libraries + $person = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $personCollection), $serverHeaders, [ + $this->getRecordIdParam() => ID::unique(), + 'data' => [ + 'fullName' => 'Stevie Wonder', + 'libraries' => [ + [ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), ], - [ - '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'libraryName' => 'Library 11', - ] + 'libraryName' => 'Library 10', ], + [ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'libraryName' => 'Library 11', + ] ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ] - ]); + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ] + ]); - $this->assertEquals(201, $person['headers']['status-code'], 'Person with libraries creation failed: ' . \json_encode($person['body'] ?? 'no body')); + $this->assertEquals(201, $person['headers']['status-code'], 'Person with libraries creation failed: ' . \json_encode($person['body'] ?? 'no body')); - // Create two person documents with null fullName for isNull query testing - $nullPerson1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $personCollection), $serverHeaders, [ - $this->getRecordIdParam() => ID::unique(), - 'data' => [ - 'fullName' => null, - ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ] - ]); - $this->assertEquals(201, $nullPerson1['headers']['status-code'], 'Null person 1 creation failed: ' . \json_encode($nullPerson1['body'] ?? 'no body')); + // Create two person documents with null fullName for isNull query testing + $nullPerson1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $personCollection), $serverHeaders, [ + $this->getRecordIdParam() => ID::unique(), + 'data' => [ + 'fullName' => null, + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ] + ]); + $this->assertEquals(201, $nullPerson1['headers']['status-code'], 'Null person 1 creation failed: ' . \json_encode($nullPerson1['body'] ?? 'no body')); - $nullPerson2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $personCollection), $serverHeaders, [ - $this->getRecordIdParam() => ID::unique(), - 'data' => [ - 'fullName' => null, - ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ] - ]); - $this->assertEquals(201, $nullPerson2['headers']['status-code'], 'Null person 2 creation failed: ' . \json_encode($nullPerson2['body'] ?? 'no body')); + $nullPerson2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $personCollection), $serverHeaders, [ + $this->getRecordIdParam() => ID::unique(), + 'data' => [ + 'fullName' => null, + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ] + ]); + $this->assertEquals(201, $nullPerson2['headers']['status-code'], 'Null person 2 creation failed: ' . \json_encode($nullPerson2['body'] ?? 'no body')); - // Update onDelete to cascade - $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($databaseId, $personCollection, 'relationship', 'libraries'), $serverHeaders, [ - 'onDelete' => Database::RELATION_MUTATE_CASCADE, - ]); - - return ['databaseId' => $databaseId, 'personCollection' => $personCollection, 'libraryCollection' => $libraryCollection]; - }); + // Update onDelete to cascade + $this->client->call(Client::METHOD_PATCH, $this->getSchemaUrl($databaseId, $personCollection, 'relationship', 'libraries'), $serverHeaders, [ + 'onDelete' => Database::RELATION_MUTATE_CASCADE, + ]); + self::$oneToManyCache[$cacheKey] = ['databaseId' => $databaseId, 'personCollection' => $personCollection, 'libraryCollection' => $libraryCollection]; return self::$oneToManyCache[$cacheKey]; } @@ -728,68 +668,64 @@ trait DatabasesBase */ protected function setupFulltextSearchDocuments(): array { - $this->ensureSharedProject(); $cacheKey = $this->getCacheKey(); if (!empty(self::$fulltextDocsCache[$cacheKey])) { return self::$fulltextDocsCache[$cacheKey]; } - self::$fulltextDocsCache[$cacheKey] = $this->withFileCache('fulltextdocs_' . static::class, function () { - $data = $this->setupIndexes(); - $databaseId = $data['databaseId']; - $booksId = $data['booksId']; + $data = $this->setupIndexes(); + $databaseId = $data['databaseId']; + $booksId = $data['booksId']; - $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $booksId), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - $this->getRecordIdParam() => ID::unique(), - 'data' => [ - 'title' => 'Science Fiction Adventures', - 'description' => 'A thrilling journey through space and time', - ], - 'permissions' => [ - Permission::read(Role::any()), - ] - ]); - $this->assertEquals(201, $row1['headers']['status-code']); + $row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $booksId), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + $this->getRecordIdParam() => ID::unique(), + 'data' => [ + 'title' => 'Science Fiction Adventures', + 'description' => 'A thrilling journey through space and time', + ], + 'permissions' => [ + Permission::read(Role::any()), + ] + ]); + $this->assertEquals(201, $row1['headers']['status-code']); - $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $booksId), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - $this->getRecordIdParam() => ID::unique(), - 'data' => [ - 'title' => 'Romance Novel', - 'description' => 'A love story set in modern times', - ], - 'permissions' => [ - Permission::read(Role::any()), - ] - ]); - $this->assertEquals(201, $row2['headers']['status-code']); + $row2 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $booksId), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + $this->getRecordIdParam() => ID::unique(), + 'data' => [ + 'title' => 'Romance Novel', + 'description' => 'A love story set in modern times', + ], + 'permissions' => [ + Permission::read(Role::any()), + ] + ]); + $this->assertEquals(201, $row2['headers']['status-code']); - $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $booksId), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - $this->getRecordIdParam() => ID::unique(), - 'data' => [ - 'title' => 'Mystery Thriller', - 'description' => 'A detective solves complex crimes', - ], - 'permissions' => [ - Permission::read(Role::any()), - ] - ]); - $this->assertEquals(201, $row3['headers']['status-code']); - - return $data; - }); + $row3 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $booksId), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + $this->getRecordIdParam() => ID::unique(), + 'data' => [ + 'title' => 'Mystery Thriller', + 'description' => 'A detective solves complex crimes', + ], + 'permissions' => [ + Permission::read(Role::any()), + ] + ]); + $this->assertEquals(201, $row3['headers']['status-code']); + self::$fulltextDocsCache[$cacheKey] = $data; return self::$fulltextDocsCache[$cacheKey]; } diff --git a/tests/e2e/Services/Databases/Transactions/TransactionPermissionsBase.php b/tests/e2e/Services/Databases/Transactions/TransactionPermissionsBase.php index ba9aa131b8..733fbbbb5c 100644 --- a/tests/e2e/Services/Databases/Transactions/TransactionPermissionsBase.php +++ b/tests/e2e/Services/Databases/Transactions/TransactionPermissionsBase.php @@ -24,56 +24,26 @@ trait TransactionPermissionsBase parent::setUpBeforeClass(); } - /** - * Ensure all Transaction permission test methods share the same project and user via file cache. - */ - protected function ensureSharedProject(): void - { - if (!empty(self::$project)) { - return; - } - - $cached = $this->withFileCache('db_project_' . static::class, function () { - $project = $this->createNewProject(); - self::$project = $project; - $user = $this->getUser(); - return [ - '_project' => $project, - '_user' => $user, - ]; - }); - - self::$project = $cached['_project']; - $projectId = self::$project['$id']; - self::$user[$projectId] = $cached['_user']; - } - /** * Initialize the permissions database if not already done */ protected function ensurePermissionsDatabase(): void { - $this->ensureSharedProject(); - if (!empty(self::$permissionsDatabase)) { return; } - $cached = $this->withFileCache('txn_perms_db_' . static::class, function () { - $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'databaseId' => ID::unique(), - 'name' => 'PermissionsTestDB' - ]); + $database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'PermissionsTestDB' + ]); - $this->assertEquals(201, $database['headers']['status-code']); - return ['databaseId' => $database['body']['$id']]; - }); - - self::$permissionsDatabase = $cached['databaseId']; + $this->assertEquals(201, $database['headers']['status-code']); + self::$permissionsDatabase = $database['body']['$id']; } /** diff --git a/tests/e2e/Services/Databases/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/Transactions/TransactionsBase.php index 7ce2e9da33..479e4d5c68 100644 --- a/tests/e2e/Services/Databases/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/Transactions/TransactionsBase.php @@ -18,56 +18,26 @@ trait TransactionsBase protected static string $sharedCollectionId = ''; protected static bool $sharedSetupDone = false; - /** - * Ensure all Transaction test methods share the same project and user via file cache. - */ - protected function ensureSharedProject(): void - { - if (!empty(self::$project)) { - return; - } - - $cached = $this->withFileCache('db_project_' . static::class, function () { - $project = $this->createNewProject(); - self::$project = $project; - $user = $this->getUser(); - return [ - '_project' => $project, - '_user' => $user, - ]; - }); - - self::$project = $cached['_project']; - $projectId = self::$project['$id']; - self::$user[$projectId] = $cached['_user']; - } - /** * Get or create a shared database for tests that don't need isolation */ protected function getSharedDatabase(): string { - $this->ensureSharedProject(); - if (!empty(self::$sharedDatabaseId)) { return self::$sharedDatabaseId; } - $cached = $this->withFileCache('txn_database_' . static::class, function () { - $database = $this->client->call(Client::METHOD_POST, $this->getDatabaseUrl(), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'databaseId' => ID::unique(), - 'name' => 'SharedTransactionTestDB' - ]); + $database = $this->client->call(Client::METHOD_POST, $this->getDatabaseUrl(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'SharedTransactionTestDB' + ]); - $this->assertEquals(201, $database['headers']['status-code']); - return ['databaseId' => $database['body']['$id']]; - }); - - self::$sharedDatabaseId = $cached['databaseId']; + $this->assertEquals(201, $database['headers']['status-code']); + self::$sharedDatabaseId = $database['body']['$id']; return self::$sharedDatabaseId; } @@ -80,46 +50,40 @@ trait TransactionsBase return self::$sharedCollectionId; } - $cached = $this->withFileCache('txn_collection_' . static::class, function () { - $databaseId = $this->getSharedDatabase(); + $databaseId = $this->getSharedDatabase(); - $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - $this->getContainerIdParam() => ID::unique(), - 'name' => 'SharedTestCollection', - 'permissions' => [ - Permission::create(Role::any()), - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); + $collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + $this->getContainerIdParam() => ID::unique(), + 'name' => 'SharedTestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); - $this->assertEquals(201, $collection['headers']['status-code']); + $this->assertEquals(201, $collection['headers']['status-code']); + self::$sharedCollectionId = $collection['body']['$id']; - $collectionId = $collection['body']['$id']; + // Create a standard 'name' attribute + $nameAttr = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, self::$sharedCollectionId, "string", null), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + $this->assertEquals(202, $nameAttr['headers']['status-code']); - // Create a standard 'name' attribute - $nameAttr = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'name', - 'size' => 256, - 'required' => true, - ]); - $this->assertEquals(202, $nameAttr['headers']['status-code']); + $this->waitForAllAttributes($databaseId, self::$sharedCollectionId); - $this->waitForAllAttributes($databaseId, $collectionId); - - return ['collectionId' => $collectionId]; - }); - - self::$sharedCollectionId = $cached['collectionId']; return self::$sharedCollectionId; } diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index d90a09c026..7cf9894509 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -19,16 +19,14 @@ trait FunctionsBase protected string $stderr = ''; /** - * Retry an API call on transient 401 auth errors and connection failures. + * Retry an API call on transient 401 auth errors. * CI can intermittently fail API key lookups under load, * especially on MongoDB when the database is recovering. - * Also retries on TCP connection errors (EOF, timeout) common with multipart uploads. */ protected function callWithAuthRetry(string $method, string $path, array $headers, mixed $params = []): array { $maxRetries = 10; $response = null; - $lastException = null; for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { // Refresh project credentials after several failed attempts @@ -38,16 +36,7 @@ trait FunctionsBase $headers['x-appwrite-key'] = $project['apiKey']; } - try { - $response = $this->client->call($method, $path, array_merge($headers), $params); - $lastException = null; - } catch (\Exception $e) { - $lastException = $e; - if ($attempt < $maxRetries) { - \sleep(\min($attempt * 2, 10)); - } - continue; - } + $response = $this->client->call($method, $path, array_merge($headers), $params); if ($response['headers']['status-code'] !== 401) { return $response; @@ -58,10 +47,6 @@ trait FunctionsBase } } - if ($lastException !== null) { - throw $lastException; - } - return $response; }