From a0b8437f08720474ad05a2dfa669c2bd2335bf1c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 21 Feb 2026 02:46:56 +1300 Subject: [PATCH] fix: scope file caching to Database tests only, revert for non-Database tests The previous file caching approach cached getRoot(), getUser(), getProject(), and getConsoleVariables() globally. This caused all test methods in a class to share the same project, breaking non-Database tests that expect isolated state (Account 401s, Storage 500s, Users 404s, etc.). Now file caching is only applied in Database/Transaction test setup chains: - ensureSharedProject() in DatabasesBase, TransactionsBase, TransactionPermissionsBase creates and file-caches both the project AND user so all methods share consistent project + user state (needed for collection permissions) - Non-Database tests (Account, Storage, Users, etc.) create their own isolated projects per-process as before Co-Authored-By: Claude Opus 4.6 --- tests/e2e/Scopes/ProjectCustom.php | 4 +- tests/e2e/Scopes/Scope.php | 146 ++++++++---------- .../e2e/Services/Databases/DatabasesBase.php | 33 ++++ .../TransactionPermissionsBase.php | 26 ++++ .../Transactions/TransactionsBase.php | 26 ++++ 5 files changed, 153 insertions(+), 82 deletions(-) diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index e867d6da09..23b2eaeb1a 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -28,9 +28,7 @@ trait ProjectCustom return $this->createNewProject(); } - self::$project = $this->withFileCache('project_' . static::class, function () { - return $this->createNewProject(); - }); + self::$project = $this->createNewProject(); return self::$project; } diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php index 99a8f851ed..2940596e38 100644 --- a/tests/e2e/Scopes/Scope.php +++ b/tests/e2e/Scopes/Scope.php @@ -125,18 +125,16 @@ abstract class Scope extends TestCase return self::$consoleVariables; } - self::$consoleVariables = $this->withFileCache('console_variables', function () { - $root = $this->getRoot(); + $root = $this->getRoot(); - $response = $this->client->call(Client::METHOD_GET, '/console/variables', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => 'console', - 'cookie' => 'a_session_console=' . $root['session'], - ]); + $response = $this->client->call(Client::METHOD_GET, '/console/variables', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => 'a_session_console=' . $root['session'], + ]); - return $response['body'] ?? []; - }); + self::$consoleVariables = $response['body'] ?? []; return self::$consoleVariables; } @@ -468,41 +466,39 @@ abstract class Scope extends TestCase return self::$root; } - self::$root = $this->withFileCache('root_' . static::class, function () { - // Use more entropy to avoid collisions in parallel test execution - $email = uniqid('', true) . getmypid() . bin2hex(random_bytes(4)) . '@localhost.test'; - $password = 'password'; - $name = 'User Name'; + // Use more entropy to avoid collisions in parallel test execution + $email = uniqid('', true) . getmypid() . bin2hex(random_bytes(4)) . '@localhost.test'; + $password = 'password'; + $name = 'User Name'; - $root = $this->client->call(Client::METHOD_POST, '/account', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => 'console', - ], [ - 'userId' => ID::unique(), - 'email' => $email, - 'password' => $password, - 'name' => $name, - ]); + $root = $this->client->call(Client::METHOD_POST, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + ], [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => $name, + ]); - $this->assertEquals(201, $root['headers']['status-code']); + $this->assertEquals(201, $root['headers']['status-code']); - $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => 'console', - ], [ - 'email' => $email, - 'password' => $password, - ]); + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + ], [ + 'email' => $email, + 'password' => $password, + ]); - return [ - '$id' => ID::custom($root['body']['$id']), - 'name' => $root['body']['name'], - 'email' => $root['body']['email'], - 'session' => $session['cookies']['a_session_console'], - ]; - }); + self::$root = [ + '$id' => ID::custom($root['body']['$id']), + 'name' => $root['body']['name'], + 'email' => $root['body']['email'], + 'session' => $session['cookies']['a_session_console'], + ]; return self::$root; } @@ -523,48 +519,40 @@ abstract class Scope extends TestCase return self::$user[$projectId]; } - $createUser = function () use ($projectId) { - // Use more entropy to avoid collisions in parallel test execution - $email = uniqid('', true) . getmypid() . bin2hex(random_bytes(4)) . '@localhost.test'; - $password = 'password'; - $name = 'User Name'; + // Use more entropy to avoid collisions in parallel test execution + $email = uniqid('', true) . getmypid() . bin2hex(random_bytes(4)) . '@localhost.test'; + $password = 'password'; + $name = 'User Name'; - $user = $this->client->call(Client::METHOD_POST, '/account', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - ], [ - 'userId' => ID::unique(), - 'email' => $email, - 'password' => $password, - 'name' => $name, - ]); + $user = $this->client->call(Client::METHOD_POST, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => $name, + ]); - $this->assertEquals(201, $user['headers']['status-code']); + $this->assertEquals(201, $user['headers']['status-code']); - $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - ], [ - 'email' => $email, - 'password' => $password, - ]); + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], [ + 'email' => $email, + 'password' => $password, + ]); - return [ - '$id' => ID::custom($user['body']['$id']), - 'name' => $user['body']['name'], - 'email' => $user['body']['email'], - 'session' => $session['cookies']['a_session_' . $projectId], - 'sessionId' => $session['body']['$id'], - ]; - }; - - if ($fresh) { - self::$user[$projectId] = $createUser(); - } else { - self::$user[$projectId] = $this->withFileCache('user_' . static::class, $createUser); - } + self::$user[$projectId] = [ + '$id' => ID::custom($user['body']['$id']), + 'name' => $user['body']['name'], + 'email' => $user['body']['email'], + 'session' => $session['cookies']['a_session_' . $projectId], + 'sessionId' => $session['body']['$id'], + ]; return self::$user[$projectId]; } diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 91f6329577..fff1d86856 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -33,6 +33,37 @@ 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) */ @@ -47,6 +78,8 @@ trait DatabasesBase */ protected function setupDatabase(): array { + $this->ensureSharedProject(); + $cacheKey = $this->getCacheKey(); if (!empty(self::$databaseCache[$cacheKey])) { return self::$databaseCache[$cacheKey]; diff --git a/tests/e2e/Services/Databases/Transactions/TransactionPermissionsBase.php b/tests/e2e/Services/Databases/Transactions/TransactionPermissionsBase.php index f4352be6a5..ba9aa131b8 100644 --- a/tests/e2e/Services/Databases/Transactions/TransactionPermissionsBase.php +++ b/tests/e2e/Services/Databases/Transactions/TransactionPermissionsBase.php @@ -24,11 +24,37 @@ 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; } diff --git a/tests/e2e/Services/Databases/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/Transactions/TransactionsBase.php index ad00b04e97..7ce2e9da33 100644 --- a/tests/e2e/Services/Databases/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/Transactions/TransactionsBase.php @@ -18,11 +18,37 @@ 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; }