From 496da61cd9616ff363989022e25c441d55bca079 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 6 Mar 2023 19:54:02 +0530 Subject: [PATCH] feat: team preferences --- app/config/collections.php | 11 +++ app/config/events.php | 3 + app/controllers/api/teams.php | 74 +++++++++++++++++-- composer.lock | 51 ++++++------- docs/references/teams/get-team-prefs.md | 1 + docs/references/teams/update-team-name.md | 1 + docs/references/teams/update-team-prefs.md | 1 + docs/references/teams/update-team.md | 1 - src/Appwrite/Utopia/Response/Model/Team.php | 6 ++ .../Realtime/RealtimeCustomClientTest.php | 37 ++++++++++ tests/e2e/Services/Teams/TeamsBase.php | 66 +++++++++++++++++ tests/e2e/Services/Webhooks/WebhooksBase.php | 47 ++++++++++++ 12 files changed, 268 insertions(+), 31 deletions(-) create mode 100644 docs/references/teams/get-team-prefs.md create mode 100644 docs/references/teams/update-team-name.md create mode 100644 docs/references/teams/update-team-prefs.md delete mode 100644 docs/references/teams/update-team.md diff --git a/app/config/collections.php b/app/config/collections.php index f8254bf889..8425702d34 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1865,6 +1865,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('prefs'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 65535, + 'signed' => true, + 'required' => false, + 'default' => new \stdClass(), + 'array' => false, + 'filters' => ['json'], + ], ], 'indexes' => [ [ diff --git a/app/config/events.php b/app/config/events.php index 6fe030d4bf..c4051f0515 100644 --- a/app/config/events.php +++ b/app/config/events.php @@ -190,6 +190,9 @@ return [ ], 'update' => [ '$description' => 'This event triggers when a bucket is updated.', + 'prefs' => [ + '$description' => 'This event triggers when a team\'s preferences is updated.', + ], ] ], 'functions' => [ diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index f95c779838..0f143b8f54 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -36,10 +36,9 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Locale\Locale; -use Utopia\Validator\Text; -use Utopia\Validator\Range; use Utopia\Validator\ArrayList; -use Utopia\Validator\WhiteList; +use Utopia\Validator\Assoc; +use Utopia\Validator\Text; use Appwrite\Event\Phone as EventPhone; App::post('/v1/teams') @@ -78,6 +77,7 @@ App::post('/v1/teams') ], 'name' => $name, 'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1, + 'prefs' => new \stdClass(), 'search' => implode(' ', [$teamId, $name]), ]))); @@ -201,8 +201,36 @@ App::get('/v1/teams/:teamId') $response->dynamic($team, Response::MODEL_TEAM); }); +App::get('/v1/teams/:teamId/prefs') + ->desc('Get Team Preferences') + ->groups(['api', 'teams']) + ->label('scope', 'teams.read') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'teams') + ->label('sdk.method', 'getPrefs') + ->label('sdk.description', '/docs/references/teams/get-team-prefs.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PREFERENCES) + ->label('sdk.offline.model', '/teams/{teamId}/prefs') + ->param('teamId', '', new UID(), 'Team ID.') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $teamId, Response $response, Database $dbForProject) { + + $team = $dbForProject->getDocument('teams', $teamId); + + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + + $prefs = $team->getAttribute('prefs', new \stdClass()); + + $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); + }); + App::put('/v1/teams/:teamId') - ->desc('Update Team') + ->desc('Update Name') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].update') ->label('scope', 'teams.write') @@ -211,7 +239,7 @@ App::put('/v1/teams/:teamId') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'teams') ->label('sdk.method', 'update') - ->label('sdk.description', '/docs/references/teams/update-team.md') + ->label('sdk.description', '/docs/references/teams/update-team-name.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_TEAM) @@ -239,6 +267,42 @@ App::put('/v1/teams/:teamId') $response->dynamic($team, Response::MODEL_TEAM); }); +App::patch('/v1/teams/:teamId/prefs') + ->desc('Update Preferences') + ->groups(['api', 'teams']) + ->label('event', 'teams.[teamId].update.prefs') + ->label('scope', 'teams.write') + ->label('audits.event', 'team.update') + ->label('audits.resource', 'team/{response.$id}') + ->label('audits.userId', '{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'teams') + ->label('sdk.method', 'updatePrefs') + ->label('sdk.description', '/docs/references/teams/update-team-prefs.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PREFERENCES) + ->label('sdk.offline.model', '/teams/{teamId}') + ->param('teamId', '', new UID(), 'Team ID.') + ->param('prefs', '', new Assoc(), 'Prefs key-value JSON object.') + ->inject('response') + ->inject('dbForProject') + ->inject('events') + ->action(function (string $teamId, array $prefs, Response $response, Database $dbForProject, Event $events) { + + $team = $dbForProject->getDocument('teams', $teamId); + + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + + $team = $dbForProject->updateDocument('teams', $team->getId(), $team->setAttribute('prefs', $prefs)); + + $events->setParam('teamId', $team->getId()); + + $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); + }); + App::delete('/v1/teams/:teamId') ->desc('Delete Team') ->groups(['api', 'teams']) diff --git a/composer.lock b/composer.lock index 82a9de301f..c6355fb527 100644 --- a/composer.lock +++ b/composer.lock @@ -1658,16 +1658,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.2.0", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3" + "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/1ee04c65529dea5d8744774d474e7cbd2f1206d3", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", "shasum": "" }, "require": { @@ -1705,7 +1705,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.1" }, "funding": [ { @@ -1721,7 +1721,7 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-03-01T10:25:55+00:00" }, { "name": "symfony/polyfill-php80", @@ -3342,16 +3342,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.3", + "version": "v4.15.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", "shasum": "" }, "require": { @@ -3392,9 +3392,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" }, - "time": "2023-01-16T22:05:37+00:00" + "time": "2023-03-05T19:49:14+00:00" }, { "name": "phar-io/manifest", @@ -3742,23 +3742,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.24", + "version": "9.2.25", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed" + "reference": "0e2b40518197a8c0d4b08bc34dfff1c99c508954" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2cf940ebc6355a9d430462811b5aaa308b174bed", - "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0e2b40518197a8c0d4b08bc34dfff1c99c508954", + "reference": "0e2b40518197a8c0d4b08bc34dfff1c99c508954", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.14", + "nikic/php-parser": "^4.15", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -3807,7 +3807,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.24" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.25" }, "funding": [ { @@ -3815,7 +3815,7 @@ "type": "github" } ], - "time": "2023-01-26T08:26:55+00:00" + "time": "2023-02-25T05:32:00+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5127,16 +5127,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.1", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "shasum": "" }, "require": { @@ -5172,14 +5172,15 @@ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2022-06-18T07:21:10+00:00" + "time": "2023-02-22T23:07:41+00:00" }, { "name": "swoole/ide-helper", diff --git a/docs/references/teams/get-team-prefs.md b/docs/references/teams/get-team-prefs.md new file mode 100644 index 0000000000..a3f963fbc4 --- /dev/null +++ b/docs/references/teams/get-team-prefs.md @@ -0,0 +1 @@ +Get the team preferences by its unique ID. \ No newline at end of file diff --git a/docs/references/teams/update-team-name.md b/docs/references/teams/update-team-name.md new file mode 100644 index 0000000000..fc39de08a1 --- /dev/null +++ b/docs/references/teams/update-team-name.md @@ -0,0 +1 @@ +Update the team name by its unique ID. \ No newline at end of file diff --git a/docs/references/teams/update-team-prefs.md b/docs/references/teams/update-team-prefs.md new file mode 100644 index 0000000000..afe9e78aaf --- /dev/null +++ b/docs/references/teams/update-team-prefs.md @@ -0,0 +1 @@ +Update the team preferences by its unique ID. The object you pass is stored as is, and replaces any previous value. The maximum allowed prefs size is 64kB and throws error if exceeded. \ No newline at end of file diff --git a/docs/references/teams/update-team.md b/docs/references/teams/update-team.md deleted file mode 100644 index 1f646bad57..0000000000 --- a/docs/references/teams/update-team.md +++ /dev/null @@ -1 +0,0 @@ -Update a team using its ID. Only members with the owner role can update the team. \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/Team.php b/src/Appwrite/Utopia/Response/Model/Team.php index f4580729c7..62df089438 100644 --- a/src/Appwrite/Utopia/Response/Model/Team.php +++ b/src/Appwrite/Utopia/Response/Model/Team.php @@ -40,6 +40,12 @@ class Team extends Model 'default' => 0, 'example' => 7, ]) + ->addRule('prefs', [ + 'type' => Response::MODEL_PREFERENCES, + 'description' => 'Team preferences as a key-value object', + 'default' => new \stdClass(), + 'example' => ['theme' => 'pink', 'timezone' => 'UTC'], + ]) ; } diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 5910c9975a..6da70b038a 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -1458,6 +1458,43 @@ class RealtimeCustomClientTest extends Scope $this->assertContains("teams.*", $response['data']['events']); $this->assertNotEmpty($response['data']['payload']); + /** + * Test Team Update Prefs + */ + $team = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamId . '/prefs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), [ + 'prefs' => [ + 'funcKey1' => 'funcValue1', + 'funcKey2' => 'funcValue2', + ] + ]); + + $this->assertEquals($team['headers']['status-code'], 200); + $this->assertEquals($team['body']['funcKey1'], 'funcValue1'); + $this->assertEquals($team['body']['funcKey2'], 'funcValue2'); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('type', $response); + $this->assertArrayHasKey('data', $response); + $this->assertEquals('event', $response['type']); + $this->assertNotEmpty($response['data']); + $this->assertArrayHasKey('timestamp', $response['data']); + $this->assertCount(2, $response['data']['channels']); + $this->assertContains('teams', $response['data']['channels']); + $this->assertContains("teams.{$teamId}", $response['data']['channels']); + $this->assertContains("teams.{$teamId}.update", $response['data']['events']); + $this->assertContains("teams.{$teamId}.update.prefs", $response['data']['events']); + $this->assertContains("teams.{$teamId}", $response['data']['events']); + $this->assertContains("teams.*.update.prefs", $response['data']['events']); + $this->assertContains("teams.*.update", $response['data']['events']); + $this->assertContains("teams.*", $response['data']['events']); + $this->assertNotEmpty($response['data']['payload']); + $this->assertEquals($response['data']['payload']['funcKey1'], 'funcValue1'); + $this->assertEquals($response['data']['payload']['funcKey2'], 'funcValue2'); + $client->close(); return ['teamId' => $teamId]; diff --git a/tests/e2e/Services/Teams/TeamsBase.php b/tests/e2e/Services/Teams/TeamsBase.php index daaf65494d..3db6437bda 100644 --- a/tests/e2e/Services/Teams/TeamsBase.php +++ b/tests/e2e/Services/Teams/TeamsBase.php @@ -27,6 +27,8 @@ trait TeamsBase $this->assertEquals('Arsenal', $response1['body']['name']); $this->assertGreaterThan(-1, $response1['body']['total']); $this->assertIsInt($response1['body']['total']); + $this->assertArrayHasKey('prefs', $response1['body']); + $dateValidator = new DatetimeValidator(); $this->assertEquals(true, $dateValidator->isValid($response1['body']['$createdAt'])); @@ -48,6 +50,7 @@ trait TeamsBase $this->assertEquals('Manchester United', $response2['body']['name']); $this->assertGreaterThan(-1, $response2['body']['total']); $this->assertIsInt($response2['body']['total']); + $this->assertArrayHasKey('prefs', $response2['body']); $this->assertEquals(true, $dateValidator->isValid($response2['body']['$createdAt'])); $response3 = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ @@ -64,6 +67,7 @@ trait TeamsBase $this->assertGreaterThan(-1, $response3['body']['total']); $this->assertIsInt($response3['body']['total']); $this->assertEquals(true, $dateValidator->isValid($response3['body']['$createdAt'])); + /** * Test for FAILURE */ @@ -98,6 +102,7 @@ trait TeamsBase $this->assertEquals('Arsenal', $response['body']['name']); $this->assertGreaterThan(-1, $response['body']['total']); $this->assertIsInt($response['body']['total']); + $this->assertArrayHasKey('prefs', $response['body']); $dateValidator = new DatetimeValidator(); $this->assertEquals(true, $dateValidator->isValid($response['body']['$createdAt'])); @@ -292,6 +297,7 @@ trait TeamsBase $this->assertEquals('Demo New', $response['body']['name']); $this->assertGreaterThan(-1, $response['body']['total']); $this->assertIsInt($response['body']['total']); + $this->assertArrayHasKey('prefs', $response['body']); $this->assertEquals(true, $dateValidator->isValid($response['body']['$createdAt'])); /** @@ -328,6 +334,7 @@ trait TeamsBase $this->assertEquals('Demo', $response['body']['name']); $this->assertGreaterThan(-1, $response['body']['total']); $this->assertIsInt($response['body']['total']); + $this->assertArrayHasKey('prefs', $response['body']); $dateValidator = new DatetimeValidator(); $this->assertEquals(true, $dateValidator->isValid($response['body']['$createdAt'])); @@ -351,4 +358,63 @@ trait TeamsBase return []; } + + /** + * @depends testCreateTeam + */ + public function testUpdateAndGetUserPrefs(array $data): void + { + $id = $data['teamUid'] ?? ''; + + /** + * Test for SUCCESS + */ + $team = $this->client->call(Client::METHOD_PATCH, '/teams/' . $id . '/prefs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'prefs' => [ + 'funcKey1' => 'funcValue1', + 'funcKey2' => 'funcValue2', + ], + ]); + + $this->assertEquals($team['headers']['status-code'], 200); + $this->assertEquals($team['body']['funcKey1'], 'funcValue1'); + $this->assertEquals($team['body']['funcKey2'], 'funcValue2'); + + $team = $this->client->call(Client::METHOD_GET, '/teams/' . $id . '/prefs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($team['headers']['status-code'], 200); + $this->assertEquals($team['body'], [ + 'funcKey1' => 'funcValue1', + 'funcKey2' => 'funcValue2', + ]); + + $team = $this->client->call(Client::METHOD_GET, '/teams/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($team['headers']['status-code'], 200); + $this->assertEquals($team['body']['prefs'], [ + 'funcKey1' => 'funcValue1', + 'funcKey2' => 'funcValue2', + ]); + + /** + * Test for FAILURE + */ + $user = $this->client->call(Client::METHOD_PATCH, '/teams/' . $id . '/prefs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'prefs' => 'bad-string', + ]); + + $this->assertEquals($user['headers']['status-code'], 400); + } } diff --git a/tests/e2e/Services/Webhooks/WebhooksBase.php b/tests/e2e/Services/Webhooks/WebhooksBase.php index 1afa9cdb52..4d6d39d1ff 100644 --- a/tests/e2e/Services/Webhooks/WebhooksBase.php +++ b/tests/e2e/Services/Webhooks/WebhooksBase.php @@ -777,6 +777,53 @@ trait WebhooksBase return ['teamId' => $team['body']['$id']]; } + /** + * @depends testCreateTeam + */ + public function testUpdateTeamPrefs(array $data): array + { + $id = $data['teamId'] ?? ''; + + $team = $this->client->call(Client::METHOD_PATCH, '/teams/' . $id . '/prefs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'prefs' => [ + 'prefKey1' => 'prefValue1', + 'prefKey2' => 'prefValue2', + ] + ]); + + $this->assertEquals($team['headers']['status-code'], 200); + $this->assertIsArray($team['body']); + + $webhook = $this->getLastRequest(); + $signatureKey = $this->getProject()['signatureKey']; + $payload = json_encode($webhook['data']); + $url = $webhook['url']; + $signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true)); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertStringContainsString('teams.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('teams.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('teams.*.update.prefs', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$id}.update.prefs", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide())); + $this->assertEquals($webhook['data'], [ + 'prefKey1' => 'prefValue1', + 'prefKey2' => 'prefValue2', + ]); + + return $data; + } + public function testDeleteTeam(): array { /**