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 <noreply@anthropic.com>
This commit is contained in:
Jake Barnby
2026-02-21 02:46:56 +13:00
parent 3ea1e8e59f
commit a0b8437f08
5 changed files with 153 additions and 82 deletions
+1 -3
View File
@@ -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;
}
+67 -79
View File
@@ -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];
}
@@ -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];
@@ -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;
}
@@ -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;
}