mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
1108 lines
40 KiB
PHP
1108 lines
40 KiB
PHP
<?php
|
|
|
|
namespace Tests\E2E\Services\Presences;
|
|
|
|
use Tests\E2E\Client;
|
|
use Utopia\Database\Helpers\ID;
|
|
use Utopia\Database\Helpers\Permission;
|
|
use Utopia\Database\Helpers\Role;
|
|
use Utopia\Database\Query;
|
|
|
|
trait PresenceBase
|
|
{
|
|
private static array $presenceCache = [];
|
|
private static array $presenceApiKeyCache = [];
|
|
|
|
protected function getPresenceApiKey(): string
|
|
{
|
|
$projectId = $this->getProject()['$id'];
|
|
|
|
if (!empty(self::$presenceApiKeyCache[$projectId])) {
|
|
return self::$presenceApiKeyCache[$projectId];
|
|
}
|
|
|
|
self::$presenceApiKeyCache[$projectId] = $this->getNewKey([
|
|
'presences.read',
|
|
'presences.write',
|
|
]);
|
|
|
|
return self::$presenceApiKeyCache[$projectId];
|
|
}
|
|
|
|
/**
|
|
* Server-side helper: ensure presences requests use a presence-scoped API key.
|
|
*/
|
|
protected function getPresenceServerHeaders(): array
|
|
{
|
|
$headers = $this->getHeaders(false);
|
|
|
|
// Override the project API key added by `SideServer` with a presence-scoped key.
|
|
$headers['x-appwrite-key'] = $this->getPresenceApiKey();
|
|
|
|
return $headers;
|
|
}
|
|
|
|
protected function setupPresence(array $overrides = []): array
|
|
{
|
|
$projectId = $this->getProject()['$id'];
|
|
$cacheKey = $projectId;
|
|
|
|
if (empty($overrides) && !empty(self::$presenceCache[$cacheKey])) {
|
|
return self::$presenceCache[$cacheKey];
|
|
}
|
|
|
|
$payload = \array_merge([
|
|
'userId' => $this->getUser()['$id'],
|
|
'status' => 'online',
|
|
'metadata' => [
|
|
'device' => 'web',
|
|
'setup' => true,
|
|
],
|
|
], $overrides);
|
|
|
|
$response = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
[
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
'x-appwrite-key' => $this->getPresenceApiKey(),
|
|
],
|
|
$payload
|
|
);
|
|
$this->assertEquals(200, $response['headers']['status-code']);
|
|
$this->assertNotEmpty($response['body']['$id']);
|
|
$this->assertArrayHasKey('userId', $response['body']);
|
|
$this->assertArrayHasKey('status', $response['body']);
|
|
$this->assertArrayHasKey('metadata', $response['body']);
|
|
|
|
$this->assertEquals($payload['userId'], $response['body']['userId']);
|
|
|
|
$canonicalPresence = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences',
|
|
[
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
'x-appwrite-key' => $this->getPresenceApiKey(),
|
|
],
|
|
[
|
|
'queries' => [
|
|
Query::equal('userId', [$payload['userId']])->toString(),
|
|
],
|
|
]
|
|
);
|
|
$this->assertEquals(200, $canonicalPresence['headers']['status-code']);
|
|
$this->assertGreaterThanOrEqual(1, $canonicalPresence['body']['total'] ?? 0);
|
|
$this->assertNotEmpty($canonicalPresence['body']['presences'][0] ?? []);
|
|
|
|
$presence = $canonicalPresence['body']['presences'][0];
|
|
|
|
if (empty($overrides)) {
|
|
self::$presenceCache[$cacheKey] = $presence;
|
|
}
|
|
|
|
return $presence;
|
|
}
|
|
|
|
protected function resolvePresenceForUser(string $userId, array $headers): array
|
|
{
|
|
$presence = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences',
|
|
$headers,
|
|
[
|
|
'queries' => [
|
|
Query::equal('userId', [$userId])->toString(),
|
|
],
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $presence['headers']['status-code']);
|
|
$this->assertGreaterThanOrEqual(1, $presence['body']['total'] ?? 0);
|
|
$this->assertNotEmpty($presence['body']['presences'][0] ?? []);
|
|
|
|
return $presence['body']['presences'][0];
|
|
}
|
|
|
|
public function testUpsertAndGetPresence(): void
|
|
{
|
|
if ($this->getSide() === 'client') {
|
|
$userId = $this->getUser()['$id'];
|
|
|
|
$upsert = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'status' => 'online',
|
|
'metadata' => ['device' => 'web'],
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $upsert['headers']['status-code']);
|
|
$this->assertNotEmpty($upsert['body']['$id']);
|
|
$this->assertEquals($userId, $upsert['body']['userId']);
|
|
|
|
$get = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences/' . $upsert['body']['$id'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false))
|
|
);
|
|
|
|
$this->assertEquals(200, $get['headers']['status-code']);
|
|
$this->assertEquals($upsert['body']['$id'], $get['body']['$id']);
|
|
$this->assertEquals($userId, $get['body']['userId']);
|
|
$this->assertArrayHasKey('expiresAt', $get['body']);
|
|
|
|
return;
|
|
}
|
|
|
|
$presence = $this->setupPresence();
|
|
|
|
$get = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences/' . $presence['$id'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getPresenceServerHeaders())
|
|
);
|
|
|
|
$this->assertEquals(200, $get['headers']['status-code']);
|
|
$this->assertEquals($presence['$id'], $get['body']['$id']);
|
|
$this->assertEquals($presence['userId'], $get['body']['userId']);
|
|
$this->assertArrayHasKey('expiresAt', $get['body']);
|
|
}
|
|
|
|
public function testListPresences(): void
|
|
{
|
|
if ($this->getSide() === 'client') {
|
|
$upsert = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'status' => 'online',
|
|
'metadata' => ['device' => 'web'],
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $upsert['headers']['status-code']);
|
|
$this->assertNotEmpty($upsert['body']['$id']);
|
|
$this->assertArrayHasKey('userId', $upsert['body']);
|
|
|
|
$list = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences',
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'queries' => [
|
|
Query::equal('userId', [$upsert['body']['userId']])->toString(),
|
|
],
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $list['headers']['status-code']);
|
|
$this->assertArrayHasKey('total', $list['body']);
|
|
$this->assertArrayHasKey('presences', $list['body']);
|
|
$this->assertIsArray($list['body']['presences']);
|
|
$this->assertGreaterThanOrEqual(1, $list['body']['total']);
|
|
|
|
// Client sessions must not be able to list presences belonging to a different user.
|
|
$projectId = $this->getProject()['$id'];
|
|
$originalUser = $this->getUser();
|
|
$otherUserId = $this->getUser(true)['$id'];
|
|
|
|
// Important: don't let `getUser(true)` overwrite the cached user/session for the rest
|
|
// of this test run. We only need the other user's ID.
|
|
self::$user[$projectId] = $originalUser;
|
|
|
|
// Seed another presence for the other user (setup via API key, not the client session).
|
|
$this->setupPresence([
|
|
'userId' => $otherUserId,
|
|
'status' => 'online',
|
|
'metadata' => ['device' => 'other-user'],
|
|
]);
|
|
|
|
$otherList = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences',
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'queries' => [
|
|
Query::equal('userId', [$otherUserId])->toString(),
|
|
],
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $otherList['headers']['status-code']);
|
|
$this->assertArrayHasKey('total', $otherList['body']);
|
|
$this->assertArrayHasKey('presences', $otherList['body']);
|
|
$this->assertSame([], $otherList['body']['presences']);
|
|
$this->assertEquals(0, $otherList['body']['total']);
|
|
return;
|
|
}
|
|
|
|
$presence = $this->setupPresence();
|
|
|
|
$list = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences',
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getPresenceServerHeaders()),
|
|
[
|
|
'queries' => [
|
|
Query::equal('userId', [$presence['userId']])->toString(),
|
|
],
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $list['headers']['status-code']);
|
|
$this->assertArrayHasKey('total', $list['body']);
|
|
$this->assertArrayHasKey('presences', $list['body']);
|
|
$this->assertIsArray($list['body']['presences']);
|
|
$this->assertGreaterThanOrEqual(1, $list['body']['total']);
|
|
}
|
|
|
|
public function testClientPresenceCustomPermissionsForOtherUser(): void
|
|
{
|
|
if ($this->getSide() !== 'client') {
|
|
$this->expectNotToPerformAssertions();
|
|
return;
|
|
}
|
|
|
|
$projectId = $this->getProject()['$id'];
|
|
$user1 = $this->getUser(true);
|
|
$user2 = $this->getUser(true);
|
|
$headersUser2 = [
|
|
'origin' => 'http://localhost',
|
|
'cookie' => 'a_session_' . $projectId . '=' . $user2['session'],
|
|
];
|
|
|
|
$permissionsForUser2 = [
|
|
Permission::read(Role::user($user2['$id'])),
|
|
Permission::update(Role::user($user2['$id'])),
|
|
Permission::delete(Role::user($user2['$id'])),
|
|
Permission::write(Role::user($user2['$id'])),
|
|
];
|
|
|
|
$permissionsForUser1 = [
|
|
Permission::read(Role::user($user1['$id'])),
|
|
Permission::update(Role::user($user1['$id'])),
|
|
Permission::delete(Role::user($user1['$id'])),
|
|
Permission::write(Role::user($user1['$id'])),
|
|
];
|
|
|
|
// Create a presence for user1 using a presence-scoped API key so we can set ACLs.
|
|
$presenceAllow = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
'x-appwrite-key' => $this->getPresenceApiKey(),
|
|
]),
|
|
[
|
|
'userId' => $user1['$id'],
|
|
'status' => 'online',
|
|
'metadata' => ['case' => 'allow'],
|
|
// Owner always retains full permissions; user2 additionally gets access.
|
|
'permissions' => \array_merge($permissionsForUser1, $permissionsForUser2),
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $presenceAllow['headers']['status-code']);
|
|
$presenceIdAllow = $presenceAllow['body']['$id'];
|
|
|
|
// user2 can read
|
|
$get = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences/' . $presenceIdAllow,
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser2)
|
|
);
|
|
$this->assertEquals(200, $get['headers']['status-code']);
|
|
|
|
// user2 can update
|
|
$patch = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/presences/' . $presenceIdAllow,
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser2),
|
|
[
|
|
'status' => 'busy',
|
|
'metadata' => ['case' => 'allow-update'],
|
|
]
|
|
);
|
|
$this->assertEquals(200, $patch['headers']['status-code']);
|
|
$this->assertEquals('busy', $patch['body']['status']);
|
|
|
|
// user2 can delete
|
|
$delete = $this->client->call(
|
|
Client::METHOD_DELETE,
|
|
'/presences/' . $presenceIdAllow,
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser2)
|
|
);
|
|
$this->assertEquals(204, $delete['headers']['status-code']);
|
|
|
|
// Create another presence for user1 without granting any special permissions to user2.
|
|
$presenceDeny = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
'x-appwrite-key' => $this->getPresenceApiKey(),
|
|
]),
|
|
[
|
|
'userId' => $user1['$id'],
|
|
'status' => 'online',
|
|
'metadata' => ['case' => 'deny'],
|
|
// Only the owner has permissions; user2 should not be able to access this document.
|
|
'permissions' => $permissionsForUser1,
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $presenceDeny['headers']['status-code']);
|
|
$presenceIdDeny = $presenceDeny['body']['$id'];
|
|
|
|
// user2 cannot read
|
|
$getDeny = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences/' . $presenceIdDeny,
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser2)
|
|
);
|
|
// When read permission is missing, the document should be treated as not found.
|
|
$this->assertEquals(404, $getDeny['headers']['status-code']);
|
|
|
|
// user2 cannot update
|
|
$patchDeny = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/presences/' . $presenceIdDeny,
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser2),
|
|
[
|
|
'status' => 'busy',
|
|
]
|
|
);
|
|
$this->assertEquals(404, $patchDeny['headers']['status-code']);
|
|
|
|
// user2 cannot delete
|
|
$deleteDeny = $this->client->call(
|
|
Client::METHOD_DELETE,
|
|
'/presences/' . $presenceIdDeny,
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser2)
|
|
);
|
|
$this->assertEquals(404, $deleteDeny['headers']['status-code']);
|
|
}
|
|
|
|
public function testUpdatePresenceSparseFields(): void
|
|
{
|
|
if ($this->getSide() === 'client') {
|
|
$upsert = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'status' => 'away',
|
|
'metadata' => ['source' => 'setup'],
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $upsert['headers']['status-code']);
|
|
$presence = $this->resolvePresenceForUser(
|
|
$upsert['body']['userId'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false))
|
|
);
|
|
$presenceId = $presence['$id'];
|
|
|
|
$update = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/presences/' . $presenceId,
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'status' => 'busy',
|
|
'metadata' => ['source' => 'update'],
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $update['headers']['status-code']);
|
|
$this->assertEquals('busy', $update['body']['status']);
|
|
$this->assertEquals(['source' => 'update'], $update['body']['metadata']);
|
|
|
|
return;
|
|
}
|
|
|
|
$presence = $this->setupPresence([
|
|
'status' => 'away',
|
|
'metadata' => ['source' => 'setup'],
|
|
]);
|
|
|
|
$payload = [
|
|
'status' => 'busy',
|
|
'metadata' => ['source' => 'update'],
|
|
];
|
|
|
|
if ($this->getSide() === 'server') {
|
|
$payload['userId'] = $presence['userId'];
|
|
}
|
|
|
|
$update = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/presences/' . $presence['$id'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getPresenceServerHeaders()),
|
|
$payload
|
|
);
|
|
|
|
$this->assertEquals(200, $update['headers']['status-code']);
|
|
$this->assertEquals('busy', $update['body']['status']);
|
|
$this->assertEquals(['source' => 'update'], $update['body']['metadata']);
|
|
}
|
|
|
|
public function testUpdatePresenceUserIdReassignsDefaultPermissions(): void
|
|
{
|
|
if ($this->getSide() !== 'server') {
|
|
$this->expectNotToPerformAssertions();
|
|
return;
|
|
}
|
|
|
|
$projectId = $this->getProject()['$id'];
|
|
$user1 = $this->getUser(true);
|
|
$user2 = $this->getUser(true);
|
|
|
|
$headersUser1 = [
|
|
'origin' => 'http://localhost',
|
|
'cookie' => 'a_session_' . $projectId . '=' . $user1['session'],
|
|
];
|
|
|
|
$headersUser2 = [
|
|
'origin' => 'http://localhost',
|
|
'cookie' => 'a_session_' . $projectId . '=' . $user2['session'],
|
|
];
|
|
|
|
$create = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser1),
|
|
[
|
|
'status' => 'online',
|
|
'metadata' => ['owner' => 'user1'],
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $create['headers']['status-code']);
|
|
$presence = $this->resolvePresenceForUser(
|
|
$user1['$id'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser1)
|
|
);
|
|
|
|
$reassign = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/presences/' . $presence['$id'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $this->getPresenceServerHeaders()),
|
|
[
|
|
'userId' => $user2['$id'],
|
|
'status' => 'busy',
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $reassign['headers']['status-code']);
|
|
$this->assertSame($user2['$id'], $reassign['body']['userId']);
|
|
|
|
$getOldOwner = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences/' . $presence['$id'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser1)
|
|
);
|
|
$this->assertEquals(404, $getOldOwner['headers']['status-code']);
|
|
|
|
$getNewOwner = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences/' . $presence['$id'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser2)
|
|
);
|
|
$this->assertEquals(200, $getNewOwner['headers']['status-code']);
|
|
$this->assertSame($user2['$id'], $getNewOwner['body']['userId']);
|
|
|
|
$patchOldOwner = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/presences/' . $presence['$id'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser1),
|
|
[
|
|
'status' => 'offline',
|
|
]
|
|
);
|
|
$this->assertEquals(404, $patchOldOwner['headers']['status-code']);
|
|
|
|
$patchNewOwner = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/presences/' . $presence['$id'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $headersUser2),
|
|
[
|
|
'status' => 'away',
|
|
]
|
|
);
|
|
$this->assertEquals(200, $patchNewOwner['headers']['status-code']);
|
|
$this->assertSame('away', $patchNewOwner['body']['status']);
|
|
}
|
|
|
|
public function testDeletePresence(): void
|
|
{
|
|
if ($this->getSide() === 'client') {
|
|
$upsert = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'status' => 'temp-delete',
|
|
'metadata' => ['cleanup' => true],
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $upsert['headers']['status-code']);
|
|
$presence = $this->resolvePresenceForUser(
|
|
$upsert['body']['userId'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false))
|
|
);
|
|
$presenceId = $presence['$id'];
|
|
|
|
$delete = $this->client->call(
|
|
Client::METHOD_DELETE,
|
|
'/presences/' . $presenceId,
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false))
|
|
);
|
|
|
|
$this->assertEquals(204, $delete['headers']['status-code']);
|
|
|
|
return;
|
|
}
|
|
|
|
$presence = $this->setupPresence([
|
|
'status' => 'temp-delete',
|
|
'metadata' => ['cleanup' => true],
|
|
]);
|
|
|
|
$delete = $this->client->call(
|
|
Client::METHOD_DELETE,
|
|
'/presences/' . $presence['$id'],
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getPresenceServerHeaders())
|
|
);
|
|
|
|
$this->assertEquals(204, $delete['headers']['status-code']);
|
|
}
|
|
|
|
public function testUpdatePresencePurgeListCache(): void
|
|
{
|
|
if ($this->getSide() === 'client') {
|
|
$upsert = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'status' => 'cache-update-setup',
|
|
'metadata' => ['cache' => 'update-setup'],
|
|
]
|
|
);
|
|
$this->assertEquals(200, $upsert['headers']['status-code']);
|
|
$headers = \array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false));
|
|
$presence = $this->resolvePresenceForUser($upsert['body']['userId'], $headers);
|
|
} else {
|
|
$presence = $this->setupPresence([
|
|
'status' => 'cache-update-setup',
|
|
'metadata' => ['cache' => 'update-setup'],
|
|
]);
|
|
$headers = \array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getPresenceServerHeaders());
|
|
}
|
|
|
|
$listPayload = [
|
|
'queries' => [
|
|
Query::equal('userId', [$presence['userId']])->toString(),
|
|
],
|
|
'ttl' => 60,
|
|
];
|
|
|
|
$list1 = $this->client->call(Client::METHOD_GET, '/presences', $headers, $listPayload);
|
|
$this->assertEquals(200, $list1['headers']['status-code']);
|
|
$this->assertArrayHasKey('x-appwrite-cache', $list1['headers']);
|
|
|
|
$list2 = $this->client->call(Client::METHOD_GET, '/presences', $headers, $listPayload);
|
|
$this->assertEquals(200, $list2['headers']['status-code']);
|
|
$this->assertArrayHasKey('x-appwrite-cache', $list2['headers']);
|
|
$this->assertEquals('hit', $list2['headers']['x-appwrite-cache']);
|
|
|
|
$updatePayload = [
|
|
'status' => 'cache-update-applied',
|
|
'purge' => true,
|
|
];
|
|
|
|
if ($this->getSide() !== 'client') {
|
|
$updatePayload['userId'] = $presence['userId'];
|
|
}
|
|
|
|
$update = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/presences/' . $presence['$id'],
|
|
$headers,
|
|
$updatePayload
|
|
);
|
|
$this->assertEquals(200, $update['headers']['status-code']);
|
|
$this->assertEquals('cache-update-applied', $update['body']['status']);
|
|
|
|
$list3 = $this->client->call(Client::METHOD_GET, '/presences', $headers, $listPayload);
|
|
$this->assertEquals(200, $list3['headers']['status-code']);
|
|
$this->assertArrayHasKey('x-appwrite-cache', $list3['headers']);
|
|
$this->assertEquals('miss', $list3['headers']['x-appwrite-cache']);
|
|
}
|
|
|
|
public function testUpdatePresencePurgeOnlyListCache(): void
|
|
{
|
|
if ($this->getSide() === 'client') {
|
|
$upsert = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'status' => 'cache-purge-only-setup',
|
|
'metadata' => ['cache' => 'purge-only-setup'],
|
|
]
|
|
);
|
|
$this->assertEquals(200, $upsert['headers']['status-code']);
|
|
$headers = \array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false));
|
|
$presence = $this->resolvePresenceForUser($upsert['body']['userId'], $headers);
|
|
} else {
|
|
$presence = $this->setupPresence([
|
|
'status' => 'cache-purge-only-setup',
|
|
'metadata' => ['cache' => 'purge-only-setup'],
|
|
]);
|
|
$headers = \array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getPresenceServerHeaders());
|
|
}
|
|
|
|
$listPayload = [
|
|
'queries' => [
|
|
Query::equal('userId', [$presence['userId']])->toString(),
|
|
],
|
|
'ttl' => 60,
|
|
];
|
|
|
|
$list1 = $this->client->call(Client::METHOD_GET, '/presences', $headers, $listPayload);
|
|
$this->assertEquals(200, $list1['headers']['status-code']);
|
|
$this->assertArrayHasKey('x-appwrite-cache', $list1['headers']);
|
|
|
|
$list2 = $this->client->call(Client::METHOD_GET, '/presences', $headers, $listPayload);
|
|
$this->assertEquals(200, $list2['headers']['status-code']);
|
|
$this->assertArrayHasKey('x-appwrite-cache', $list2['headers']);
|
|
$this->assertEquals('hit', $list2['headers']['x-appwrite-cache']);
|
|
|
|
$updatePayload = [
|
|
'purge' => true,
|
|
];
|
|
|
|
if ($this->getSide() !== 'client') {
|
|
$updatePayload['userId'] = $presence['userId'];
|
|
}
|
|
|
|
$update = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/presences/' . $presence['$id'],
|
|
$headers,
|
|
$updatePayload
|
|
);
|
|
$this->assertEquals(200, $update['headers']['status-code']);
|
|
$this->assertEquals($presence['$id'], $update['body']['$id']);
|
|
|
|
$list3 = $this->client->call(Client::METHOD_GET, '/presences', $headers, $listPayload);
|
|
$this->assertEquals(200, $list3['headers']['status-code']);
|
|
$this->assertArrayHasKey('x-appwrite-cache', $list3['headers']);
|
|
$this->assertEquals('miss', $list3['headers']['x-appwrite-cache']);
|
|
}
|
|
|
|
public function testDeletePresencePurgesListCache(): void
|
|
{
|
|
if ($this->getSide() === 'client') {
|
|
$upsert = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'status' => 'cache-delete-setup',
|
|
'metadata' => ['cache' => 'delete-setup'],
|
|
]
|
|
);
|
|
$this->assertEquals(200, $upsert['headers']['status-code']);
|
|
$headers = \array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false));
|
|
$presence = $this->resolvePresenceForUser($upsert['body']['userId'], $headers);
|
|
} else {
|
|
$presence = $this->setupPresence([
|
|
'status' => 'cache-delete-setup',
|
|
'metadata' => ['cache' => 'delete-setup'],
|
|
]);
|
|
$headers = \array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getPresenceServerHeaders());
|
|
}
|
|
|
|
$listPayload = [
|
|
'queries' => [
|
|
Query::equal('userId', [$presence['userId']])->toString(),
|
|
],
|
|
'ttl' => 60,
|
|
];
|
|
|
|
$list1 = $this->client->call(Client::METHOD_GET, '/presences', $headers, $listPayload);
|
|
$this->assertEquals(200, $list1['headers']['status-code']);
|
|
$this->assertArrayHasKey('x-appwrite-cache', $list1['headers']);
|
|
|
|
$list2 = $this->client->call(Client::METHOD_GET, '/presences', $headers, $listPayload);
|
|
$this->assertEquals(200, $list2['headers']['status-code']);
|
|
$this->assertArrayHasKey('x-appwrite-cache', $list2['headers']);
|
|
$this->assertEquals('hit', $list2['headers']['x-appwrite-cache']);
|
|
|
|
$delete = $this->client->call(
|
|
Client::METHOD_DELETE,
|
|
'/presences/' . $presence['$id'],
|
|
$headers
|
|
);
|
|
$this->assertEquals(204, $delete['headers']['status-code']);
|
|
|
|
$list3 = $this->client->call(Client::METHOD_GET, '/presences', $headers, $listPayload);
|
|
$this->assertEquals(200, $list3['headers']['status-code']);
|
|
$this->assertArrayHasKey('x-appwrite-cache', $list3['headers']);
|
|
$this->assertEquals('miss', $list3['headers']['x-appwrite-cache']);
|
|
}
|
|
|
|
public function testUpdateNotFound(): void
|
|
{
|
|
if ($this->getSide() === 'client') {
|
|
$response = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'status' => 'ghost',
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(404, $response['headers']['status-code']);
|
|
return;
|
|
}
|
|
|
|
$payload = [
|
|
'status' => 'ghost',
|
|
];
|
|
|
|
if ($this->getSide() === 'server') {
|
|
$payload['userId'] = $this->getUser()['$id'];
|
|
}
|
|
|
|
$response = $this->client->call(
|
|
Client::METHOD_PATCH,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getPresenceServerHeaders()),
|
|
$payload
|
|
);
|
|
|
|
$this->assertEquals(404, $response['headers']['status-code']);
|
|
}
|
|
|
|
public function testClientCannotPassUserId(): void
|
|
{
|
|
if ($this->getSide() === 'server') {
|
|
$this->expectNotToPerformAssertions();
|
|
return;
|
|
}
|
|
|
|
$response = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getHeaders(false)),
|
|
[
|
|
'userId' => ID::unique(),
|
|
'status' => 'online',
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(401, $response['headers']['status-code']);
|
|
}
|
|
|
|
public function testServerRequiresUserId(): void
|
|
{
|
|
if ($this->getSide() === 'client') {
|
|
$this->expectNotToPerformAssertions();
|
|
return;
|
|
}
|
|
|
|
$response = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
\array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
], $this->getPresenceServerHeaders()),
|
|
[
|
|
'status' => 'online',
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(400, $response['headers']['status-code']);
|
|
}
|
|
|
|
public function testUpsertSameUserMaintainsSinglePresence(): void
|
|
{
|
|
if ($this->getSide() === 'client') {
|
|
$this->expectNotToPerformAssertions();
|
|
return;
|
|
}
|
|
|
|
$projectId = $this->getProject()['$id'];
|
|
$userId = $this->getUser()['$id'];
|
|
$headers = \array_merge([
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
], $this->getPresenceServerHeaders());
|
|
|
|
$firstUpsert = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
$headers,
|
|
[
|
|
'userId' => $userId,
|
|
'status' => 'online',
|
|
'metadata' => ['source' => 'first-upsert'],
|
|
]
|
|
);
|
|
$this->assertEquals(200, $firstUpsert['headers']['status-code']);
|
|
|
|
$secondUpsert = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . ID::unique(),
|
|
$headers,
|
|
[
|
|
'userId' => $userId,
|
|
'status' => 'away',
|
|
'metadata' => ['source' => 'second-upsert'],
|
|
]
|
|
);
|
|
$this->assertEquals(200, $secondUpsert['headers']['status-code']);
|
|
|
|
$this->assertEquals('away', $secondUpsert['body']['status']);
|
|
$this->assertEquals(['source' => 'second-upsert'], $secondUpsert['body']['metadata']);
|
|
|
|
$list = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences',
|
|
$headers,
|
|
[
|
|
'queries' => [
|
|
Query::equal('userId', [$userId])->toString(),
|
|
],
|
|
]
|
|
);
|
|
|
|
$this->assertEquals(200, $list['headers']['status-code']);
|
|
$this->assertEquals(1, $list['body']['total']);
|
|
$this->assertCount(1, $list['body']['presences']);
|
|
$this->assertEquals($userId, $list['body']['presences'][0]['userId']);
|
|
$this->assertEquals('away', $list['body']['presences'][0]['status']);
|
|
}
|
|
|
|
/**
|
|
* Regression test for cross-user overwrite on the native-upsert path.
|
|
*
|
|
* Scenario:
|
|
* - User A has a presence row with $id = $sharedPresenceId.
|
|
* - User B (different userInternalId, no existing presence) issues an upsert that
|
|
* re-uses $sharedPresenceId.
|
|
*
|
|
* Without the ownership guard in State::upsertForUser, the second call would silently
|
|
* UPDATE A's row (because upsertDocument matches on the primary key) leaving B's data
|
|
* under A's $id. With the guard, the second call must fail with PRESENCE_ALREADY_EXISTS
|
|
* and A's row must be untouched.
|
|
*/
|
|
public function testCrossUserUpsertDoesNotOverwriteForeignPresence(): void
|
|
{
|
|
if ($this->getSide() !== 'client') {
|
|
$this->expectNotToPerformAssertions();
|
|
return;
|
|
}
|
|
|
|
$projectId = $this->getProject()['$id'];
|
|
$originalUser = $this->getUser();
|
|
|
|
$user1 = $this->getUser(true);
|
|
$user2 = $this->getUser(true);
|
|
|
|
// Preserve the cached session for the rest of the test run.
|
|
self::$user[$projectId] = $originalUser;
|
|
|
|
$headersUser1 = [
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
'origin' => 'http://localhost',
|
|
'cookie' => 'a_session_' . $projectId . '=' . $user1['session'],
|
|
];
|
|
$headersUser2 = [
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
'origin' => 'http://localhost',
|
|
'cookie' => 'a_session_' . $projectId . '=' . $user2['session'],
|
|
];
|
|
|
|
$sharedPresenceId = ID::unique();
|
|
|
|
$victim = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . $sharedPresenceId,
|
|
$headersUser1,
|
|
[
|
|
'status' => 'online',
|
|
'metadata' => ['owner' => 'user1'],
|
|
]
|
|
);
|
|
$this->assertEquals(200, $victim['headers']['status-code']);
|
|
$this->assertEquals($sharedPresenceId, $victim['body']['$id']);
|
|
$this->assertEquals($user1['$id'], $victim['body']['userId']);
|
|
|
|
$attack = $this->client->call(
|
|
Client::METHOD_PUT,
|
|
'/presences/' . $sharedPresenceId,
|
|
$headersUser2,
|
|
[
|
|
'status' => 'online',
|
|
'metadata' => ['owner' => 'user2'],
|
|
]
|
|
);
|
|
$this->assertNotEquals(
|
|
200,
|
|
$attack['headers']['status-code'],
|
|
'Cross-user upsert must not succeed silently. Got body: ' . \json_encode($attack['body'] ?? [])
|
|
);
|
|
|
|
// Verify User1's row is intact. Read via a presence-scoped API key to bypass
|
|
// any read-permission ambiguity and inspect the persisted state directly.
|
|
$check = $this->client->call(
|
|
Client::METHOD_GET,
|
|
'/presences/' . $sharedPresenceId,
|
|
[
|
|
'content-type' => 'application/json',
|
|
'x-appwrite-project' => $projectId,
|
|
'x-appwrite-key' => $this->getPresenceApiKey(),
|
|
]
|
|
);
|
|
$this->assertEquals(200, $check['headers']['status-code']);
|
|
$this->assertEquals($user1['$id'], $check['body']['userId']);
|
|
$this->assertEquals(['owner' => 'user1'], $check['body']['metadata']);
|
|
}
|
|
}
|