Skip to content

Commit 5010cde

Browse files
authored
Merge pull request #11903 from appwrite/fix/ci-mongodb-retry
2 parents 13413c0 + 8671533 commit 5010cde

File tree

3 files changed

+2
-175
lines changed

3 files changed

+2
-175
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ jobs:
512512
# Services that rely on sequential test method execution (shared static state)
513513
FUNCTIONAL_FLAG="--functional"
514514
case "${{ matrix.service }}" in
515-
Databases|TablesDB|Functions|Realtime|GraphQL) FUNCTIONAL_FLAG="" ;;
515+
Databases|TablesDB|Functions|Realtime|GraphQL|ProjectWebhooks) FUNCTIONAL_FLAG="" ;;
516516
esac
517517
518518
docker compose exec -T \

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,6 +1288,7 @@ services:
12881288
image: mongo:8.2.5
12891289
container_name: appwrite-mongodb
12901290
<<: *x-logging
1291+
restart: on-failure:3
12911292
networks:
12921293
- appwrite
12931294
volumes:

tests/e2e/Services/Account/AccountCustomClientTest.php

Lines changed: 0 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -4160,178 +4160,4 @@ public function testMFARecoveryCodeChallenge(): void
41604160

41614161
$this->assertEquals(401, $verification3['headers']['status-code']);
41624162
}
4163-
4164-
/**
4165-
* Test that a new email/password session is immediately usable even when
4166-
* a concurrent request re-populates the user cache between the cache purge
4167-
* and session creation.
4168-
*
4169-
* Regression test for: purging the user cache BEFORE persisting the session
4170-
* allows a concurrent request (from a different Swoole worker) to re-cache
4171-
* a stale user document that lacks the new session, causing sessionVerify
4172-
* to fail with 401 on subsequent requests using the new session.
4173-
*/
4174-
public function testEmailPasswordSessionNotCorruptedByConcurrentRequests(): void
4175-
{
4176-
$projectId = $this->getProject()['$id'];
4177-
$endpoint = $this->client->getEndpoint();
4178-
4179-
$email = uniqid('race_', true) . getmypid() . '@localhost.test';
4180-
$password = 'password123!';
4181-
4182-
// Create user
4183-
$response = $this->client->call(Client::METHOD_POST, '/account', [
4184-
'origin' => 'http://localhost',
4185-
'content-type' => 'application/json',
4186-
'x-appwrite-project' => $projectId,
4187-
], [
4188-
'userId' => ID::unique(),
4189-
'email' => $email,
4190-
'password' => $password,
4191-
'name' => 'Race Test User',
4192-
]);
4193-
$this->assertEquals(201, $response['headers']['status-code']);
4194-
4195-
// Login to get session A
4196-
$responseA = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
4197-
'origin' => 'http://localhost',
4198-
'content-type' => 'application/json',
4199-
'x-appwrite-project' => $projectId,
4200-
], [
4201-
'email' => $email,
4202-
'password' => $password,
4203-
]);
4204-
$this->assertEquals(201, $responseA['headers']['status-code']);
4205-
$sessionA = $responseA['cookies']['a_session_' . $projectId];
4206-
4207-
// Verify session A works
4208-
$verifyA = $this->client->call(Client::METHOD_GET, '/account', [
4209-
'origin' => 'http://localhost',
4210-
'content-type' => 'application/json',
4211-
'x-appwrite-project' => $projectId,
4212-
'cookie' => 'a_session_' . $projectId . '=' . $sessionA,
4213-
]);
4214-
$this->assertEquals(200, $verifyA['headers']['status-code']);
4215-
4216-
/**
4217-
* Race condition scenario:
4218-
* 1. Start login B via curl_multi (non-blocking)
4219-
* 2. Drive the transfer for ~150ms so login B reaches purgeCachedDocument
4220-
* (findOne ~15ms + Argon2 hash verify ~60ms + middleware overhead)
4221-
* 3. THEN add GET requests to curl_multi - these hit different workers and
4222-
* re-cache a stale user document (without session B) during the window
4223-
* between purgeCachedDocument and createDocument
4224-
* 4. After all complete, verify session B is usable
4225-
*/
4226-
for ($attempt = 0; $attempt < 5; $attempt++) {
4227-
$loginCookies = [];
4228-
4229-
$multi = curl_multi_init();
4230-
4231-
// Start login B first (alone)
4232-
$loginHandle = curl_init("{$endpoint}/account/sessions/email");
4233-
curl_setopt_array($loginHandle, [
4234-
CURLOPT_POST => true,
4235-
CURLOPT_RETURNTRANSFER => true,
4236-
CURLOPT_HTTPHEADER => [
4237-
'origin: http://localhost',
4238-
'content-type: application/json',
4239-
"x-appwrite-project: {$projectId}",
4240-
],
4241-
CURLOPT_POSTFIELDS => \json_encode([
4242-
'email' => $email,
4243-
'password' => $password,
4244-
]),
4245-
CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$loginCookies) {
4246-
if (\stripos($header, 'set-cookie:') === 0) {
4247-
$cookiePart = \trim(\substr($header, 11));
4248-
$eqPos = \strpos($cookiePart, '=');
4249-
if ($eqPos !== false) {
4250-
$name = \substr($cookiePart, 0, $eqPos);
4251-
$rest = \substr($cookiePart, $eqPos + 1);
4252-
$semiPos = \strpos($rest, ';');
4253-
$loginCookies[$name] = $semiPos !== false
4254-
? \substr($rest, 0, $semiPos)
4255-
: $rest;
4256-
}
4257-
}
4258-
return \strlen($header);
4259-
},
4260-
]);
4261-
curl_multi_add_handle($multi, $loginHandle);
4262-
4263-
// Drive the login transfer forward and wait for the server to start
4264-
// processing the login (past hash verification + cache purge).
4265-
$deadline = \microtime(true) + 0.15; // 150ms
4266-
do {
4267-
curl_multi_exec($multi, $active);
4268-
curl_multi_select($multi, 0.005);
4269-
} while (\microtime(true) < $deadline && $active);
4270-
4271-
// NOW add GET requests - they arrive after the cache purge
4272-
// but before session creation (which is delayed by the usleep or I/O).
4273-
$getHandles = [];
4274-
for ($i = 0; $i < 10; $i++) {
4275-
$gh = curl_init("{$endpoint}/account");
4276-
curl_setopt_array($gh, [
4277-
CURLOPT_RETURNTRANSFER => true,
4278-
CURLOPT_HTTPHEADER => [
4279-
'origin: http://localhost',
4280-
'content-type: application/json',
4281-
"x-appwrite-project: {$projectId}",
4282-
"cookie: a_session_{$projectId}={$sessionA}",
4283-
],
4284-
]);
4285-
curl_multi_add_handle($multi, $gh);
4286-
$getHandles[] = $gh;
4287-
}
4288-
4289-
// Drive all to completion
4290-
do {
4291-
$status = curl_multi_exec($multi, $active);
4292-
if ($active) {
4293-
curl_multi_select($multi, 0.05);
4294-
}
4295-
} while ($active && $status === CURLM_OK);
4296-
4297-
$loginStatus = curl_getinfo($loginHandle, CURLINFO_HTTP_CODE);
4298-
4299-
curl_multi_remove_handle($multi, $loginHandle);
4300-
curl_close($loginHandle);
4301-
foreach ($getHandles as $gh) {
4302-
curl_multi_remove_handle($multi, $gh);
4303-
curl_close($gh);
4304-
}
4305-
curl_multi_close($multi);
4306-
4307-
$this->assertEquals(201, $loginStatus, 'Login for session B should succeed');
4308-
4309-
$sessionBCookie = $loginCookies["a_session_{$projectId}"] ?? null;
4310-
$this->assertNotNull($sessionBCookie, 'Session B cookie should be set');
4311-
4312-
// THE CRITICAL CHECK: verify session B is usable immediately
4313-
$verifyB = $this->client->call(Client::METHOD_GET, '/account', [
4314-
'origin' => 'http://localhost',
4315-
'content-type' => 'application/json',
4316-
'x-appwrite-project' => $projectId,
4317-
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
4318-
]);
4319-
4320-
$this->assertEquals(
4321-
200,
4322-
$verifyB['headers']['status-code'],
4323-
'Session B must be immediately usable after login. '
4324-
. 'A 401 here means a stale user cache (without the new session) was served. '
4325-
. 'The fix is to create the session document BEFORE purging the user cache.'
4326-
);
4327-
4328-
// Clean up session B for next iteration
4329-
$this->client->call(Client::METHOD_DELETE, '/account/sessions/current', [
4330-
'origin' => 'http://localhost',
4331-
'content-type' => 'application/json',
4332-
'x-appwrite-project' => $projectId,
4333-
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
4334-
]);
4335-
}
4336-
}
43374163
}

0 commit comments

Comments
 (0)