Implement presence expiry management and enhance user ID handling in presence actions. Add automated tests for expired presence deletion during maintenance.

This commit is contained in:
ArnabChatterjee20k
2026-04-13 22:31:46 +05:30
parent f9d0cb7daf
commit 54fa813bc6
5 changed files with 144 additions and 11 deletions
@@ -80,10 +80,6 @@ class Update extends PresenceAction
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'userId is not allowed for non-API key and non-privileged users');
}
if (($isAPIKey || $isPrivilegedUser) && !$userId) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'userId is required for API key and privileged users');
}
$presence = $dbForProject->getDocument('presenceLogs', $presenceId);
if ($presence->isEmpty()) {
@@ -94,6 +90,8 @@ class Update extends PresenceAction
if ($userId !== null) {
$updateData['userId'] = $userId;
$userDoc = $dbForProject->getDocument('users', $userId);
$updateData['userInternalId'] = $userDoc->getSequence();
}
if ($status !== null) {
@@ -55,7 +55,7 @@ class Upsert extends PresenceAction
],
))
->param('presenceId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Presence unique ID.', false, ['dbForProject'])
->param('userId', '', new UID(), 'User ID.', false)
->param('userId', null, new Nullable(new UID()), 'User ID.', true)
->param('status', '', new Text(Database::LENGTH_KEY), 'Presence status.', false)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE])), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
// TODO: what shall be the min and max date here
@@ -92,16 +92,16 @@ class Upsert extends PresenceAction
$userInternalId = null;
$resolvedUserId = $userId;
if (!$isAPIKey && !$isPrivilegedUser) {
$userInternalId = $user->getId();
$userInternalId = $user->getSequence();
$resolvedUserId = $user->getId();
} else {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
$fetchedUser = $dbForProject->getDocument('users', $userId);
if ($fetchedUser->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND, params: [$userId]);
}
$userInternalId = $user->getId();
$resolvedUserId = $user->getId();
$userInternalId = $fetchedUser->getSequence();
$resolvedUserId = $fetchedUser->getId();
}
$presenceData = [
+13
View File
@@ -214,6 +214,7 @@ class Deletes extends Action
$this->deleteUsageStats($project, $getProjectDB, $getLogsDB, $hourlyUsageRetentionDatetime);
$this->deleteExpiredSessions($project, $getProjectDB);
$this->deleteExpiredTransactions($project, $getProjectDB);
$this->deleteExpiredPresences($project, $getProjectDB);
$this->deleteOldDeployments($queueForDeletes, $project, $getProjectDB);
break;
default:
@@ -1644,4 +1645,16 @@ class Deletes extends Action
// Swallow errors to avoid breaking the cleanup process
});
}
private function deleteExpiredPresences(Document $project, callable $getProjectDB): void
{
$dbForProject = $getProjectDB($project);
$dbForProject->deleteDocuments('presenceLogs', [
Query::isNotNull('expiry'),
Query::lessThan('expiry', DateTime::format(new \DateTime())),
], onError: function (Throwable $th) {
// Swallow errors to avoid breaking the cleanup process
});
}
}
@@ -294,6 +294,5 @@ trait PresenceBase
);
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertEquals('general_argument_invalid', $response['body']['type']);
}
}
@@ -0,0 +1,123 @@
<?php
namespace Tests\E2E\Services\Presence;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Console;
use Utopia\Database\Helpers\ID;
class PresenceExpiryTest extends Scope
{
use ProjectCustom;
use SideServer;
public function testExpiredPresenceDeletedByMaintenance(): void
{
$projectId = $this->getProject()['$id'];
$userId = $this->getUser()['$id'];
$expiredAt = \gmdate('Y-m-d\TH:i:s.v\Z', \time() - 120);
$createServer = $this->client->call(
Client::METHOD_PUT,
'/presences/' . ID::unique(),
[
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
],
[
'userId' => $userId,
'status' => 'online',
'metadata' => ['test' => 'presence-expiry'],
]
);
$this->assertEquals(200, $createServer['headers']['status-code']);
$presenceIdServer = $createServer['body']['$id'];
$expireServer = $this->client->call(
Client::METHOD_PATCH,
'/presences/' . $presenceIdServer,
[
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
],
[
'userId' => $userId,
'expiry' => $expiredAt,
]
);
$this->assertEquals(200, $expireServer['headers']['status-code']);
$this->assertEquals($expiredAt, $expireServer['body']['expiry']);
$createClient = $this->client->call(
Client::METHOD_PUT,
'/presences/' . ID::unique(),
[
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $this->getUser()['session'],
],
[
'status' => 'online',
'metadata' => ['test' => 'presence-expiry-client'],
]
);
$this->assertEquals(200, $createClient['headers']['status-code']);
$presenceIdClient = $createClient['body']['$id'];
$expireClient = $this->client->call(
Client::METHOD_PATCH,
'/presences/' . $presenceIdClient,
[
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $this->getUser()['session'],
],
[
'expiry' => $expiredAt,
]
);
$this->assertEquals(200, $expireClient['headers']['status-code']);
$this->assertEquals($expiredAt, $expireClient['body']['expiry']);
$stdout = '';
$stderr = '';
$code = Console::execute('docker exec appwrite maintenance --type=trigger', '', $stdout, $stderr);
$this->assertSame(0, $code, "Maintenance command failed with code $code: $stderr ($stdout)");
$this->assertEventually(function () use ($presenceIdServer, $presenceIdClient, $projectId) {
$getServer = $this->client->call(
Client::METHOD_GET,
'/presences/' . $presenceIdServer,
[
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
]
);
$getClient = $this->client->call(
Client::METHOD_GET,
'/presences/' . $presenceIdClient,
[
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
]
);
$this->assertEquals(404, $getServer['headers']['status-code']);
$this->assertEquals(404, $getClient['headers']['status-code']);
});
}
}