Merge branch '1.9.x' into add-api-key-migration

This commit is contained in:
premtsd-code
2026-05-26 11:51:19 +01:00
committed by GitHub
6 changed files with 639 additions and 472 deletions
+142
View File
@@ -2755,4 +2755,146 @@ return [
]
]
],
// Naming it presenceLogs as later it might be only be used as a presence events table only and not for the actual presence
'presenceLogs' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('presenceLogs'),
'name' => 'Presence Logs',
'attributes' => [
[
'$id' => ID::custom('userInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('expiresAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('source'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('hostname'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('metadata'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => new \stdClass(),
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('permissionsHash'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 32,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_unique_userId'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['userId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_userInternal'),
'type' => Database::INDEX_KEY,
'attributes' => ['userInternalId'],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_expiresAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['expiresAt'],
'lengths' => [],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['status'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_source'),
'type' => Database::INDEX_KEY,
'attributes' => ['source'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_source_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['source', 'status']
],
[
'$id' => ID::custom('_key_permissionsHash'),
'type' => Database::INDEX_KEY,
'attributes' => ['permissionsHash']
]
]
],
];
-142
View File
@@ -2798,146 +2798,4 @@ return [
],
],
],
// Naming it presenceLogs as later it might be only be used as a presence events table only and not for the actual presence
'presenceLogs' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('presenceLogs'),
'name' => 'Presence Logs',
'attributes' => [
[
'$id' => ID::custom('userInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('expiresAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('source'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('hostname'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('metadata'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => new \stdClass(),
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('permissionsHash'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 32,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_unique_userId'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['userId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_userInternal'),
'type' => Database::INDEX_KEY,
'attributes' => ['userInternalId'],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_expiresAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['expiresAt'],
'lengths' => [],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['status'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_source'),
'type' => Database::INDEX_KEY,
'attributes' => ['source'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_source_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['source', 'status']
],
[
'$id' => ID::custom('_key_permissionsHash'),
'type' => Database::INDEX_KEY,
'attributes' => ['permissionsHash']
]
]
]
];
+1
View File
@@ -707,6 +707,7 @@ services:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_POOL_ADAPTER
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
File diff suppressed because it is too large Load Diff
+67 -21
View File
@@ -127,7 +127,7 @@ trait PresenceBase
public function testUpsertAndGetPresence(): void
{
if ($this->getSide() === 'client') {
if ($this->getSide() === 'client' || $this->getSide() === 'console') {
$userId = $this->getUser()['$id'];
$upsert = $this->client->call(
@@ -183,7 +183,7 @@ trait PresenceBase
public function testListPresences(): void
{
if ($this->getSide() === 'client') {
if ($this->getSide() === 'client' || $this->getSide() === 'console') {
$upsert = $this->client->call(
Client::METHOD_PUT,
'/presences/' . ID::unique(),
@@ -224,18 +224,36 @@ trait PresenceBase
// 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'];
$otherUser = $this->getUser(true);
$otherUserId = $otherUser['$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.
// of this test run.
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'],
]);
if ($projectId === 'console') {
// The console project has no API keys; seed via the other user's own session.
$this->client->call(
Client::METHOD_PUT,
'/presences/' . ID::unique(),
[
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $otherUser['session'],
],
[
'status' => 'online',
'metadata' => ['device' => 'other-user'],
]
);
} else {
// 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,
@@ -284,6 +302,8 @@ trait PresenceBase
public function testClientPresenceCustomPermissionsForOtherUser(): void
{
// Requires API key to create two concurrent presences for the same user with
// different ACLs. Server-only — also skipped on console (which has no API keys).
if ($this->getSide() !== 'client') {
$this->expectNotToPerformAssertions();
return;
@@ -431,7 +451,7 @@ trait PresenceBase
public function testUpdatePresenceSparseFields(): void
{
if ($this->getSide() === 'client') {
if ($this->getSide() === 'client' || $this->getSide() === 'console') {
$upsert = $this->client->call(
Client::METHOD_PUT,
'/presences/' . ID::unique(),
@@ -614,7 +634,7 @@ trait PresenceBase
public function testDeletePresence(): void
{
if ($this->getSide() === 'client') {
if ($this->getSide() === 'client' || $this->getSide() === 'console') {
$upsert = $this->client->call(
Client::METHOD_PUT,
'/presences/' . ID::unique(),
@@ -671,6 +691,14 @@ trait PresenceBase
public function testUpdatePresencePurgeListCache(): void
{
if ($this->getProject()['$id'] === 'console') {
// The console project shares dbForPlatform's cache with every other request,
// so parallel workers can wipe the list cache between calls and the hit/miss
// assertions become flaky. Skip on console.
$this->expectNotToPerformAssertions();
return;
}
if ($this->getSide() === 'client') {
$upsert = $this->client->call(
Client::METHOD_PUT,
@@ -743,6 +771,11 @@ trait PresenceBase
public function testUpdatePresencePurgeOnlyListCache(): void
{
if ($this->getProject()['$id'] === 'console') {
$this->expectNotToPerformAssertions();
return;
}
if ($this->getSide() === 'client') {
$upsert = $this->client->call(
Client::METHOD_PUT,
@@ -814,6 +847,11 @@ trait PresenceBase
public function testDeletePresencePurgesListCache(): void
{
if ($this->getProject()['$id'] === 'console') {
$this->expectNotToPerformAssertions();
return;
}
if ($this->getSide() === 'client') {
$upsert = $this->client->call(
Client::METHOD_PUT,
@@ -875,7 +913,7 @@ trait PresenceBase
public function testUpdateNotFound(): void
{
if ($this->getSide() === 'client') {
if ($this->getSide() === 'client' || $this->getSide() === 'console') {
$response = $this->client->call(
Client::METHOD_PATCH,
'/presences/' . ID::unique(),
@@ -938,7 +976,8 @@ trait PresenceBase
public function testServerRequiresUserId(): void
{
if ($this->getSide() === 'client') {
// Server-only behavior — also skipped on console (no API keys for the console project).
if ($this->getSide() === 'client' || $this->getSide() === 'console') {
$this->expectNotToPerformAssertions();
return;
}
@@ -960,7 +999,8 @@ trait PresenceBase
public function testUpsertSameUserMaintainsSinglePresence(): void
{
if ($this->getSide() === 'client') {
// Server-only behavior — also skipped on console (no API keys for the console project).
if ($this->getSide() === 'client' || $this->getSide() === 'console') {
$this->expectNotToPerformAssertions();
return;
}
@@ -1032,7 +1072,7 @@ trait PresenceBase
*/
public function testCrossUserUpsertDoesNotOverwriteForeignPresence(): void
{
if ($this->getSide() !== 'client') {
if ($this->getSide() !== 'client' && $this->getSide() !== 'console') {
$this->expectNotToPerformAssertions();
return;
}
@@ -1091,14 +1131,20 @@ trait PresenceBase
// 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,
[
// The console project has no API keys, so fall back to user1's own session —
// if the bug ever resurfaces and user2 overwrote the row, user1 would lose
// read permission and this GET would return 404, still surfacing the failure.
$checkHeaders = $projectId === 'console'
? $headersUser1
: [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getPresenceApiKey(),
]
];
$check = $this->client->call(
Client::METHOD_GET,
'/presences/' . $sharedPresenceId,
$checkHeaders
);
$this->assertEquals(200, $check['headers']['status-code']);
$this->assertEquals($user1['$id'], $check['body']['userId']);
@@ -9,15 +9,38 @@ use Tests\E2E\Scopes\SideConsole;
class PresenceConsoleClientTest extends Scope
{
use ProjectCustom;
use SideConsole;
use PresenceBase;
use ProjectCustom {
getProject as getCustomProject;
}
use SideConsole {
getHeaders as getAdminHeaders;
}
public function getProject(bool $fresh = false): array
{
return ['$id' => 'console'];
}
// `x-appwrite-mode: admin` is forbidden for the console project, so authenticate
// as a console session user instead — `getUser()` signs them up against project=console.
public function getHeaders(bool $devKey = true): array
{
return [
'origin' => 'http://localhost',
'cookie' => 'a_session_console=' . $this->getUser()['session'],
];
}
public function testGetPresenceUsage(): void
{
// Usage requires admin scope, which the console project rejects — run against a regular project.
$projectId = $this->getCustomProject()['$id'];
$response = $this->client->call(Client::METHOD_GET, '/presences/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'x-appwrite-project' => $projectId,
], $this->getAdminHeaders()), [
'range' => '32h',
]);
@@ -25,8 +48,8 @@ class PresenceConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/presences/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'x-appwrite-project' => $projectId,
], $this->getAdminHeaders()), [
'range' => '24h',
]);