From 677bb048cc7014efd3775f43dab2e499e8eeef00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 17 Mar 2026 11:03:18 +0100 Subject: [PATCH] Introduce new webhooks API --- .github/workflows/ci.yml | 1 + app/config/scopes/project.php | 8 + src/Appwrite/Platform/Appwrite.php | 2 + .../Modules/Webhooks/Http/Webhooks/XList.php | 113 + .../Platform/Modules/Webhooks/Module.php | 14 + .../Modules/Webhooks/Services/Http.php | 17 + src/Appwrite/Platform/Workers/Migrations.php | 2 + .../Database/Validator/Queries/Webhooks.php | 26 + tests/e2e/Scopes/ProjectCustom.php | 2 + .../Services/ProjectWebhooks/WebhooksBase.php | 1834 +++++++++++++++++ .../WebhooksCustomClientTest.php | 2 +- .../WebhooksCustomServerTest.php | 952 +++++++++ tests/e2e/Services/Webhooks/WebhooksBase.php | 1827 +--------------- .../Webhooks/WebhooksConsoleClientTest.php | 14 + .../Webhooks/WebhooksCustomServerTest.php | 938 --------- 15 files changed, 3003 insertions(+), 2749 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/XList.php create mode 100644 src/Appwrite/Platform/Modules/Webhooks/Module.php create mode 100644 src/Appwrite/Platform/Modules/Webhooks/Services/Http.php create mode 100644 src/Appwrite/Utopia/Database/Validator/Queries/Webhooks.php create mode 100644 tests/e2e/Services/ProjectWebhooks/WebhooksBase.php rename tests/e2e/Services/{Webhooks => ProjectWebhooks}/WebhooksCustomClientTest.php (99%) create mode 100644 tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php create mode 100644 tests/e2e/Services/Webhooks/WebhooksConsoleClientTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bfe432c7e..fe2f61bfcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -392,6 +392,7 @@ jobs: Tokens, Teams, Users, + ProjectWebhooks, Webhooks, VCS, Messaging, diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php index 0a3feba9e7..1f318b0376 100644 --- a/app/config/scopes/project.php +++ b/app/config/scopes/project.php @@ -172,4 +172,12 @@ return [ // List of publicly visible scopes 'tokens.write' => [ 'description' => 'Access to create, update, and delete your project\'s tokens', ], + "webhooks.read" => [ + "description" => + "Access to read project\'s webhooks", + ], + "webhooks.write" => [ + "description" => + "Access to create, update, and delete project\'s webhooks", + ], ]; diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php index 681e1038c3..77b9c4d1dd 100644 --- a/src/Appwrite/Platform/Appwrite.php +++ b/src/Appwrite/Platform/Appwrite.php @@ -16,6 +16,7 @@ use Appwrite\Platform\Modules\Storage; use Appwrite\Platform\Modules\Teams; use Appwrite\Platform\Modules\Tokens; use Appwrite\Platform\Modules\VCS; +use Appwrite\Platform\Modules\Webhooks; use Utopia\Platform\Platform; class Appwrite extends Platform @@ -36,5 +37,6 @@ class Appwrite extends Platform $this->addModule(new Tokens\Module()); $this->addModule(new Storage\Module()); $this->addModule(new VCS\Module()); + $this->addModule(new Webhooks\Module()); } } diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/XList.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/XList.php new file mode 100644 index 0000000000..d12fee0789 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/XList.php @@ -0,0 +1,113 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/webhooks') + ->desc('List webhooks') + ->groups(['api', 'webhooks']) + ->label('scope', 'webhooks.read') + ->label('sdk', new Method( + namespace: 'webhooks', + group: null, + name: 'list', + description: <<param('queries', [], new Webhooks(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Webhooks::ALLOWED_ATTRIBUTES), true) + ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) + ->inject('project') + ->inject('response') + ->inject('dbForPlatform') + ->inject('authorization') + ->callback($this->action(...)); + } + + public function action(array $queries, bool $includeTotal, Document $project, Response $response, Database $dbForPlatform, Authorization $authorization) + { + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + $queries[] = Query::equal('projectInternalId', [$project->getSequence()]); + + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $webhookId = $cursor->getValue(); + $cursorDocument = $authorization->skip(fn () => $dbForPlatform->getDocument('webhooks', $webhookId, [ + Query::equal('projectInternalId', [$project->getSequence()]), + ])); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Webhook '{$webhookId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + + try { + $webhooks = $authorization->skip(fn () => $dbForPlatform->find('webhooks', $queries)); + $total = $includeTotal ? $authorization->skip(fn () => $dbForPlatform->count('webhooks', $filterQueries, APP_LIMIT_COUNT)) : 0; + } catch (OrderException $e) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } + + $response->dynamic(new Document([ + 'webhooks' => $webhooks, + 'total' => $total, + ]), Response::MODEL_WEBHOOK_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Webhooks/Module.php b/src/Appwrite/Platform/Modules/Webhooks/Module.php new file mode 100644 index 0000000000..66400ccd9a --- /dev/null +++ b/src/Appwrite/Platform/Modules/Webhooks/Module.php @@ -0,0 +1,14 @@ +addService('http', new Http()); + } +} diff --git a/src/Appwrite/Platform/Modules/Webhooks/Services/Http.php b/src/Appwrite/Platform/Modules/Webhooks/Services/Http.php new file mode 100644 index 0000000000..602ca02074 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Webhooks/Services/Http.php @@ -0,0 +1,17 @@ +type = Service::TYPE_HTTP; + + // Webhooks + $this->addAction(ListWebhooks::getName(), new ListWebhooks()); + } +} diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index ce20358626..25d5bfa027 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -332,6 +332,8 @@ class Migrations extends Action 'messages.write', 'targets.read', 'targets.write', + 'webhooks.read', + 'webhooks.write' ] ]); diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Webhooks.php b/src/Appwrite/Utopia/Database/Validator/Queries/Webhooks.php new file mode 100644 index 0000000000..fa20bf34ef --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Webhooks.php @@ -0,0 +1,26 @@ +assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body'])); + }, 120000, 500); + } + + /** + * Create a probe callback that filters webhooks by event pattern. + */ + private function webhookEventProbe(string $eventPattern): callable + { + return function (array $request) use ($eventPattern) { + $this->assertStringContainsString( + $eventPattern, + $request['headers']['X-Appwrite-Webhook-Events'] ?? '' + ); + }; + } + + public static function getWebhookSignature(array $webhook, string $signatureKey): string + { + $payload = json_encode($webhook['data']); + $url = $webhook['url']; + return base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true)); + } + + /** + * Creates a database and collection with proper attributes for document operations. + * + * @return array Array containing 'databaseId' and 'actorsId' + */ + protected function setupCollectionWithAttributes(): array + { + // Create database + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Actors DB', + ]); + + $databaseId = $database['body']['$id']; + + // Create collection + $actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Actors', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'documentSecurity' => true, + ]); + + $actorsId = $actors['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'firstName', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'lastName', + 'size' => 256, + 'required' => true, + ]); + + // Wait for attributes to be available + $this->assertEventually(function () use ($databaseId, $actorsId) { + $collection = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $actorsId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertCount(2, $collection['body']['attributes']); + $this->assertEquals('available', $collection['body']['attributes'][0]['status']); + $this->assertEquals('available', $collection['body']['attributes'][1]['status']); + }, 15000, 500); + + return ['databaseId' => $databaseId, 'actorsId' => $actorsId]; + } + + /** + * Creates a database and table with proper columns for row operations. + * + * @return array Array containing 'databaseId' and 'actorsId' + */ + protected function setupTableWithColumns(): array + { + // Create database + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Actors DB', + ]); + + $databaseId = $database['body']['$id']; + + // Create table + $actors = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Actors', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'rowSecurity' => true, + ]); + + $actorsId = $actors['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'firstName', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'lastName', + 'size' => 256, + 'required' => true, + ]); + + // Wait for columns to be available + $this->assertEventually(function () use ($databaseId, $actorsId) { + $table = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $actorsId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertCount(2, $table['body']['columns']); + $this->assertEquals('available', $table['body']['columns'][0]['status']); + $this->assertEquals('available', $table['body']['columns'][1]['status']); + }, 15000, 500); + + return ['databaseId' => $databaseId, 'actorsId' => $actorsId]; + } + + /** + * Creates an enabled storage bucket. + * + * @return array Array containing 'bucketId' + */ + protected function setupStorageBucket(): array + { + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'bucketId' => ID::unique(), + 'name' => 'Test Bucket', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'fileSecurity' => true, + 'enabled' => true, + ]); + + return ['bucketId' => $bucket['body']['$id']]; + } + + /** + * Creates a team and returns its ID. + * + * @param string $name Team name + * @return array Array containing 'teamId' + */ + protected function setupTeam(string $name = 'Arsenal'): array + { + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => ID::unique(), + 'name' => $name + ]); + + return ['teamId' => $team['body']['$id']]; + } + + /** + * Creates a team membership and returns membership details including secret. + * + * @param string $teamId The team ID + * @return array Array containing 'teamId', 'membershipId', 'userId', 'secret' + */ + protected function setupTeamMembership(string $teamId): array + { + $email = uniqid() . 'friend@localhost.test'; + + // Create user first to ensure team event is triggered after user event + $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => 'password', + 'name' => 'Friend User', + ]); + + // Create membership + $team = $this->client->call(Client::METHOD_POST, '/teams/' . $teamId . '/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'email' => $email, + 'roles' => ['admin', 'editor'], + 'url' => 'http://localhost:5000/join-us#title' + ]); + + $membershipId = $team['body']['$id']; + $userId = $team['body']['userId']; + + // Get the secret from email (use probe to match correct email by recipient address) + $lastEmail = $this->getLastEmail(1, function ($msg) use ($email) { + $this->assertEquals($email, $msg['to'][0]['address'] ?? ''); + }); + $tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html'] ?? ''); + $secret = $tokens['secret'] ?? ''; + + return [ + 'teamId' => $teamId, + 'membershipId' => $membershipId, + 'userId' => $userId, + 'secret' => $secret, + ]; + } + + /** + * Creates a document in a collection. + * + * @param string $databaseId Database ID + * @param string $collectionId Collection ID + * @return array Array containing document details including 'documentId' + */ + protected function setupDocument(string $databaseId, string $collectionId): array + { + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'firstName' => 'Chris', + 'lastName' => 'Evans', + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + return ['documentId' => $document['body']['$id']]; + } + + /** + * Creates a row in a table. + * + * @param string $databaseId Database ID + * @param string $tableId Table ID + * @return array Array containing row details including 'rowId' + */ + protected function setupRow(string $databaseId, string $tableId): array + { + $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'firstName' => 'Chris', + 'lastName' => 'Evans', + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + return ['rowId' => $row['body']['$id']]; + } + + /** + * Creates a file in a bucket. + * + * @param string $bucketId Bucket ID + * @return array Array containing file details including 'fileId' + */ + protected function setupBucketFile(string $bucketId): array + { + $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'), + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'folderId' => ID::custom('xyz'), + ]); + + return ['fileId' => $file['body']['$id']]; + } + + // Collection APIs + public function testCreateCollection(): void + { + /** + * Create database + */ + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Actors DB', + ]); + + $databaseId = $database['body']['$id']; + + /** + * Test for SUCCESS + */ + $actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Actors', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'documentSecurity' => true, + ]); + + $actorsId = $actors['body']['$id']; + + $this->assertEquals($actors['headers']['status-code'], 201); + $this->assertNotEmpty($actors['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.create", $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'] ?? ''), true); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['name'], 'Actors'); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(4, $webhook['data']['$permissions']); + } + + public function testCreateAttributes(): void + { + /** + * Create database + */ + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Actors DB', + ]); + + $databaseId = $database['body']['$id']; + + /** + * Create collection + */ + $actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Actors', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'documentSecurity' => true, + ]); + + $actorsId = $actors['body']['$id']; + + $firstName = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'firstName', + 'size' => 256, + 'required' => true, + ]); + + $lastName = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'lastName', + 'size' => 256, + 'required' => true, + ]); + + $extra = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'extra', + 'size' => 64, + 'required' => false, + ]); + + $attributeId = $extra['body']['key']; + + $this->assertEquals($firstName['headers']['status-code'], 202); + $this->assertEquals($firstName['body']['key'], 'firstName'); + $this->assertEquals($lastName['headers']['status-code'], 202); + $this->assertEquals($lastName['body']['key'], 'lastName'); + $this->assertEquals($extra['headers']['status-code'], 202); + $this->assertEquals($extra['body']['key'], 'extra'); + + // wait for database worker to kick in + $this->assertEventually(function () use ($databaseId, $actorsId) { + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.attributes.*.create")); + $this->assertNotEmpty($webhook); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.attributes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.attributes.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.attributes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.attributes.*.create", $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->assertNotEmpty($webhook['data']['key']); + $this->assertEquals($webhook['data']['key'], 'extra'); + }, 15000, 500); + + $removed = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/' . $extra['body']['key'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(204, $removed['headers']['status-code']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.attributes.*.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + // $this->assertEquals($webhook['method'], 'DELETE'); + $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.attributes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.attributes.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.attributes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.attributes.*.update", $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->assertNotEmpty($webhook['data']['key']); + $this->assertEquals($webhook['data']['key'], 'extra'); + } + + public function testCreateDocument(): void + { + // Set up collection with attributes + $data = $this->setupCollectionWithAttributes(); + $actorsId = $data['actorsId']; + $databaseId = $data['databaseId']; + + /** + * Test for SUCCESS + */ + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'firstName' => 'Chris', + 'lastName' => 'Evans', + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $documentId = $document['body']['$id']; + + $this->assertEquals($document['headers']['status-code'], 201); + $this->assertNotEmpty($document['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.create", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['firstName'], 'Chris'); + $this->assertEquals($webhook['data']['lastName'], 'Evans'); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(3, $webhook['data']['$permissions']); + } + + public function testUpdateDocument(): void + { + // Set up collection with attributes and create a document + $data = $this->setupCollectionWithAttributes(); + $actorsId = $data['actorsId']; + $databaseId = $data['databaseId']; + $documentData = $this->setupDocument($databaseId, $actorsId); + $documentId = $documentData['documentId']; + + /** + * Test for SUCCESS + */ + $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $actorsId . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'firstName' => 'Chris1', + 'lastName' => 'Evans2', + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $documentId = $document['body']['$id']; + + $this->assertEquals($document['headers']['status-code'], 200); + $this->assertNotEmpty($document['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.update", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['firstName'], 'Chris1'); + $this->assertEquals($webhook['data']['lastName'], 'Evans2'); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(3, $webhook['data']['$permissions']); + } + + #[Retry(count: 1)] + public function testDeleteDocument(): void + { + // Set up collection with attributes + $data = $this->setupCollectionWithAttributes(); + $actorsId = $data['actorsId']; + $databaseId = $data['databaseId']; + + /** + * Test for SUCCESS + */ + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'firstName' => 'Bradly', + 'lastName' => 'Cooper', + + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $documentId = $document['body']['$id']; + + $this->assertEquals($document['headers']['status-code'], 201); + $this->assertNotEmpty($document['body']['$id']); + + $document = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $actorsId . '/documents/' . $document['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($document['headers']['status-code'], 204); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.delete")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.delete", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['firstName'], 'Bradly'); + $this->assertEquals($webhook['data']['lastName'], 'Cooper'); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(3, $webhook['data']['$permissions']); + } + + // Table APIs + public function testCreateTable(): void + { + /** + * Create database + */ + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Actors DB', + ]); + + $databaseId = $database['body']['$id']; + + /** + * Test for SUCCESS + */ + $actors = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Actors', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'rowSecurity' => true, + ]); + + $actorsId = $actors['body']['$id']; + + $this->assertEquals($actors['headers']['status-code'], 201); + $this->assertNotEmpty($actors['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.create", $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'] ?? ''), true); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['name'], 'Actors'); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(4, $webhook['data']['$permissions']); + } + + public function testCreateColumns(): void + { + /** + * Create database + */ + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Actors DB', + ]); + + $databaseId = $database['body']['$id']; + + /** + * Create table + */ + $actors = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Actors', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'rowSecurity' => true, + ]); + + $actorsId = $actors['body']['$id']; + + $firstName = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'firstName', + 'size' => 256, + 'required' => true, + ]); + + $lastName = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'lastName', + 'size' => 256, + 'required' => true, + ]); + + $extra = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'extra', + 'size' => 64, + 'required' => false, + ]); + + $this->assertEquals($firstName['headers']['status-code'], 202); + $this->assertEquals($firstName['body']['key'], 'firstName'); + $this->assertEquals($lastName['headers']['status-code'], 202); + $this->assertEquals($lastName['body']['key'], 'lastName'); + $this->assertEquals($extra['headers']['status-code'], 202); + $this->assertEquals($extra['body']['key'], 'extra'); + + // wait for database worker to kick in + $this->assertEventually(function () use ($databaseId, $actorsId) { + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.columns.*.create")); + $this->assertNotEmpty($webhook); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*.create", $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->assertNotEmpty($webhook['data']['key']); + $this->assertEquals($webhook['data']['key'], 'extra'); + }, 15000, 500); + + $removed = $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/' . $extra['body']['key'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(204, $removed['headers']['status-code']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.columns.*.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + // $this->assertEquals($webhook['method'], 'DELETE'); + $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*.update", $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->assertNotEmpty($webhook['data']['key']); + $this->assertEquals($webhook['data']['key'], 'extra'); + } + + public function testCreateRow(): void + { + // Set up table with columns + $data = $this->setupTableWithColumns(); + $actorsId = $data['actorsId']; + $databaseId = $data['databaseId']; + + /** + * Test for SUCCESS + */ + $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'firstName' => 'Chris', + 'lastName' => 'Evans', + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $documentId = $row['body']['$id']; + + $this->assertEquals($row['headers']['status-code'], 201); + $this->assertNotEmpty($row['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.rows.{$documentId}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$documentId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$documentId}.create", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['firstName'], 'Chris'); + $this->assertEquals($webhook['data']['lastName'], 'Evans'); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(3, $webhook['data']['$permissions']); + } + + public function testUpdateRow(): void + { + // Set up table with columns and create a row + $data = $this->setupTableWithColumns(); + $actorsId = $data['actorsId']; + $databaseId = $data['databaseId']; + $rowData = $this->setupRow($databaseId, $actorsId); + $rowId = $rowData['rowId']; + + /** + * Test for SUCCESS + */ + $document = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows/' . $rowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'firstName' => 'Chris1', + 'lastName' => 'Evans2', + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $rowId = $document['body']['$id']; + + $this->assertEquals($document['headers']['status-code'], 200); + $this->assertNotEmpty($document['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.update", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['firstName'], 'Chris1'); + $this->assertEquals($webhook['data']['lastName'], 'Evans2'); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(3, $webhook['data']['$permissions']); + } + + #[Retry(count: 1)] + public function testDeleteRow(): void + { + // Set up table with columns + $data = $this->setupTableWithColumns(); + $actorsId = $data['actorsId']; + $databaseId = $data['databaseId']; + + /** + * Test for SUCCESS + */ + $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'firstName' => 'Bradly', + 'lastName' => 'Cooper', + + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $rowId = $row['body']['$id']; + + $this->assertEquals($row['headers']['status-code'], 201); + $this->assertNotEmpty($row['body']['$id']); + + $row = $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows/' . $row['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($row['headers']['status-code'], 204); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.delete")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.delete", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['firstName'], 'Bradly'); + $this->assertEquals($webhook['data']['lastName'], 'Cooper'); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(3, $webhook['data']['$permissions']); + } + + public function testCreateStorageBucket(): void + { + /** + * Test for SUCCESS + */ + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'bucketId' => ID::unique(), + 'name' => 'Test Bucket', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $bucketId = $bucket['body']['$id']; + + $this->assertEquals($bucket['headers']['status-code'], 201); + $this->assertNotEmpty($bucket['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('buckets.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.create", $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'] ?? ''), true); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals('Test Bucket', $webhook['data']['name']); + $this->assertEquals(true, $webhook['data']['enabled']); + $this->assertIsArray($webhook['data']['$permissions']); + } + + public function testUpdateStorageBucket(): void + { + // Set up a storage bucket + $data = $this->setupStorageBucket(); + $bucketId = $data['bucketId']; + + /** + * Test for SUCCESS + */ + $bucket = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $bucketId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'name' => 'Test Bucket Updated', + 'fileSecurity' => true, + 'enabled' => false, + ]); + + $this->assertEquals($bucket['headers']['status-code'], 200); + $this->assertNotEmpty($bucket['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('buckets.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.update", $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'] ?? ''), true); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals('Test Bucket Updated', $webhook['data']['name']); + $this->assertEquals(false, $webhook['data']['enabled']); + $this->assertIsArray($webhook['data']['$permissions']); + } + + public function testCreateBucketFile(): void + { + // Set up an enabled storage bucket + $data = $this->setupStorageBucket(); + $bucketId = $data['bucketId']; + + /** + * Test for SUCCESS + */ + $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'), + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'folderId' => ID::custom('xyz'), + ]); + + $fileId = $file['body']['$id']; + + $this->assertEquals($file['headers']['status-code'], 201); + $this->assertNotEmpty($file['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.files.{$fileId}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('buckets.*.files.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('buckets.*.files.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.*.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.*.files.{$fileId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}.create", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertEquals($webhook['data']['name'], 'logo.png'); + $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); + $this->assertNotEmpty($webhook['data']['signature']); + $this->assertEquals($webhook['data']['mimeType'], 'image/png'); + $this->assertEquals($webhook['data']['sizeOriginal'], 47218); + } + + public function testUpdateBucketFile(): void + { + // Set up an enabled storage bucket and create a file + $data = $this->setupStorageBucket(); + $bucketId = $data['bucketId']; + $fileData = $this->setupBucketFile($bucketId); + $fileId = $fileData['fileId']; + + /** + * Test for SUCCESS + */ + $file = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals($file['headers']['status-code'], 200); + $this->assertNotEmpty($file['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.files.{$fileId}.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('buckets.*.files.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('buckets.*.files.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.*.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.*.files.{$fileId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}.update", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertEquals($webhook['data']['name'], 'logo.png'); + $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); + $this->assertNotEmpty($webhook['data']['signature']); + $this->assertEquals($webhook['data']['mimeType'], 'image/png'); + $this->assertEquals($webhook['data']['sizeOriginal'], 47218); + } + + public function testDeleteBucketFile(): void + { + // Set up an enabled storage bucket and create a file + $data = $this->setupStorageBucket(); + $bucketId = $data['bucketId']; + $fileData = $this->setupBucketFile($bucketId); + $fileId = $fileData['fileId']; + + /** + * Test for SUCCESS + */ + $file = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $file['headers']['status-code']); + $this->assertEmpty($file['body']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.files.{$fileId}.delete")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('buckets.*.files.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('buckets.*.files.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.*.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.*.files.{$fileId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}.delete", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertEquals($webhook['data']['name'], 'logo.png'); + $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); + $this->assertNotEmpty($webhook['data']['signature']); + $this->assertEquals($webhook['data']['mimeType'], 'image/png'); + $this->assertEquals($webhook['data']['sizeOriginal'], 47218); + } + + public function testDeleteStorageBucket(): void + { + // Set up an enabled storage bucket + $data = $this->setupStorageBucket(); + $bucketId = $data['bucketId']; + + // Update bucket name before deleting to make test self-sufficient + // (In parallel execution, testUpdateStorageBucket may not have run) + $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $bucketId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'name' => 'Test Bucket Updated', + 'fileSecurity' => true, + ]); + + /** + * Test for SUCCESS + */ + $bucket = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals($bucket['headers']['status-code'], 204); + $this->assertEmpty($bucket['body']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.delete")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('buckets.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("buckets.{$bucketId}.delete", $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'] ?? ''), true); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals('Test Bucket Updated', $webhook['data']['name']); + $this->assertEquals(true, $webhook['data']['enabled']); + $this->assertIsArray($webhook['data']['$permissions']); + } + + public function testCreateTeam(): void + { + /** + * Test for SUCCESS + */ + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => ID::unique(), + 'name' => 'Arsenal' + ]); + + $teamId = $team['body']['$id']; + + $this->assertEquals(201, $team['headers']['status-code']); + $this->assertNotEmpty($team['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("teams.{$teamId}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}.create", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals('Arsenal', $webhook['data']['name']); + $this->assertGreaterThan(-1, $webhook['data']['total']); + $this->assertIsInt($webhook['data']['total']); + $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); + } + + public function testUpdateTeam(): void + { + // Set up a team + $data = $this->setupTeam(); + $teamId = $data['teamId']; + /** + * Test for SUCCESS + */ + $team = $this->client->call(Client::METHOD_PUT, '/teams/' . $teamId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Demo New' + ]); + + $this->assertEquals(200, $team['headers']['status-code']); + $this->assertNotEmpty($team['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("teams.{$teamId}.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}.update", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals('Demo New', $webhook['data']['name']); + $this->assertGreaterThan(-1, $webhook['data']['total']); + $this->assertIsInt($webhook['data']['total']); + $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); + } + + public function testUpdateTeamPrefs(): void + { + // Set up a team + $data = $this->setupTeam(); + $id = $data['teamId']; + + $team = $this->client->call(Client::METHOD_PUT, '/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($this->webhookEventProbe("teams.{$id}.update.prefs")); + $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', + ]); + } + + public function testDeleteTeam(): void + { + /** + * Test for SUCCESS + */ + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => ID::unique(), + 'name' => 'Chelsea' + ]); + + $teamId = $team['body']['$id']; + + $this->assertEquals(201, $team['headers']['status-code']); + $this->assertNotEmpty($team['body']['$id']); + + $team = $this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $webhook = $this->getLastRequest($this->webhookEventProbe("teams.{$teamId}.delete")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}.delete", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals('Chelsea', $webhook['data']['name']); + $this->assertGreaterThan(-1, $webhook['data']['total']); + $this->assertIsInt($webhook['data']['total']); + $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); + } + + public function testCreateTeamMembership(): void + { + // Set up a team + $data = $this->setupTeam(); + $teamId = $data['teamId']; + $email = uniqid() . 'friend@localhost.test'; + + // Create user to ensure team event is triggered after user event + $user = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => 'password', + 'name' => 'Friend User', + ]); + + /** + * Test for SUCCESS + */ + $team = $this->client->call(Client::METHOD_POST, '/teams/' . $teamId . '/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'email' => $email, + 'roles' => ['admin', 'editor'], + 'url' => 'http://localhost:5000/join-us#title' + ]); + + $this->assertEquals(201, $team['headers']['status-code']); + $this->assertNotEmpty($team['body']['$id']); + + $lastEmail = $this->getLastEmail(); + + // `$isAppUser` — no email expected; + $tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html'] ?? ''); + + $secret = $tokens['secret'] ?? ''; + $membershipId = $team['body']['$id']; + + $webhook = $this->getLastRequest($this->webhookEventProbe("teams.{$teamId}.memberships.{$membershipId}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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.*.memberships.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('teams.*.memberships.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.*.memberships.{$membershipId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.*.memberships.{$membershipId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}.memberships.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}.memberships.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}.create", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertNotEmpty($webhook['data']['userId']); + $this->assertNotEmpty($webhook['data']['teamId']); + $this->assertCount(2, $webhook['data']['roles']); + $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['invited'])); + $this->assertEquals(('server' === $this->getSide()), $webhook['data']['confirm']); + } + + public function testDeleteTeamMembership(): void + { + // Set up a team + $data = $this->setupTeam(); + $teamId = $data['teamId']; + $email = uniqid() . 'friend@localhost.test'; + + /** + * Test for SUCCESS + */ + $team = $this->client->call(Client::METHOD_POST, '/teams/' . $teamId . '/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'email' => $email, + 'name' => 'Friend User', + 'roles' => ['admin', 'editor'], + 'url' => 'http://localhost:5000/join-us#title' + ]); + + $membershipId = $team['body']['$id'] ?? ''; + + $this->assertEquals(201, $team['headers']['status-code']); + $this->assertNotEmpty($team['body']['$id']); + + $team = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId . '/memberships/' . $team['body']['$id'], array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $team['headers']['status-code']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("teams.{$teamId}.memberships.{$membershipId}.delete")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $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.*.memberships.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('teams.*.memberships.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.*.memberships.{$membershipId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.*.memberships.{$membershipId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}.memberships.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}.memberships.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}.delete", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertNotEmpty($webhook['data']['userId']); + $this->assertNotEmpty($webhook['data']['teamId']); + $this->assertCount(2, $webhook['data']['roles']); + $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['invited'])); + $this->assertEquals(('server' === $this->getSide()), $webhook['data']['confirm']); + } + + public function testCreateWebhookWithPrivateDomain(): void + { + /** + * Test for FAILURE + */ + $projectId = $this->getProject()['$id']; + $webhook = $this->client->call(Client::METHOD_POST, '/projects/' . $projectId . '/webhooks', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + ], [ + 'name' => 'Webhook Test', + 'enabled' => true, + 'events' => [ + 'databases.*', + 'functions.*', + 'buckets.*', + 'teams.*', + 'users.*' + ], + 'url' => 'http://localhost/webhook', // private domains not allowed + 'security' => false, + ]); + + $this->assertEquals(400, $webhook['headers']['status-code']); + } + + public function testUpdateWebhookWithPrivateDomain(): void + { + /** + * Test for FAILURE + */ + $projectId = $this->getProject()['$id']; + $webhookId = $this->getProject()['webhookId']; + $webhook = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/webhooks/' . $webhookId, [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + ], [ + 'name' => 'Webhook Test', + 'enabled' => true, + 'events' => [ + 'databases.*', + 'functions.*', + 'buckets.*', + 'teams.*', + 'users.*' + ], + 'url' => 'http://localhost/webhook', // private domains not allowed + 'security' => false, + ]); + + $this->assertEquals(400, $webhook['headers']['status-code']); + } + + public function testWebhookAutoDisable(): void + { + $projectId = $this->getProject()['$id']; + $webhookId = $this->getProject()['webhookId']; + + // Create a database for this test + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'AutoDisable DB', + ]); + + $databaseId = $database['body']['$id']; + + $webhook = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/webhooks/' . $webhookId, [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + ], [ + 'name' => 'Webhook Test', + 'enabled' => true, + 'events' => [ + 'databases.*', + 'functions.*', + 'buckets.*', + 'teams.*', + 'users.*' + ], + 'url' => 'http://appwrite-non-existing-domain.com', // set non-existent URL + 'security' => false, + ]); + + $this->assertEquals(200, $webhook['headers']['status-code']); + $this->assertNotEmpty($webhook['body']); + + // trigger webhook for failure event 10 times + for ($i = 0; $i < 10; $i++) { + $newCollection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'newCollection' . $i, + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'documentSecurity' => true, + ]); + + $this->assertEquals($newCollection['headers']['status-code'], 201); + $this->assertNotEmpty($newCollection['body']['$id']); + } + + $this->assertEventually(function () use ($projectId, $webhookId) { + $webhook = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId . '/webhooks/' . $webhookId, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + ])); + + // assert that the webhook is now disabled after 10 consecutive failures + $this->assertEquals($webhook['body']['enabled'], false); + $this->assertEquals($webhook['body']['attempts'], 10); + }, 15000, 500); + } +} diff --git a/tests/e2e/Services/Webhooks/WebhooksCustomClientTest.php b/tests/e2e/Services/ProjectWebhooks/WebhooksCustomClientTest.php similarity index 99% rename from tests/e2e/Services/Webhooks/WebhooksCustomClientTest.php rename to tests/e2e/Services/ProjectWebhooks/WebhooksCustomClientTest.php index 7d01095a36..a6bda320a4 100644 --- a/tests/e2e/Services/Webhooks/WebhooksCustomClientTest.php +++ b/tests/e2e/Services/ProjectWebhooks/WebhooksCustomClientTest.php @@ -1,6 +1,6 @@ client->call(Client::METHOD_POST, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => $name, + ]); + + return [ + 'userId' => $user['body']['$id'], + 'name' => $user['body']['name'], + 'email' => $user['body']['email'], + ]; + } + + /** + * Creates a function and returns function details. + * + * @return array Array containing 'functionId' + */ + protected function setupFunction(): array + { + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test', + 'execute' => [Role::any()->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + + return ['functionId' => $function['body']['$id']]; + } + + /** + * Creates a function deployment and waits for it to be built. + * + * @param string $functionId Function ID + * @return array Array containing 'functionId', 'deploymentId' + */ + protected function setupDeployment(string $functionId): array + { + $stderr = ''; + $stdout = ''; + $folder = 'timeout'; + $code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz"; + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); + + // Create variable first + $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'key' => 'key1', + 'value' => 'value1', + ]); + + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'entrypoint' => 'index.js', + 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), + 'activate' => true + ]); + + $deploymentId = $deployment['body']['$id']; + + // Wait for deployment to be built + $this->awaitDeploymentIsBuilt($functionId, $deploymentId); + + return [ + 'functionId' => $functionId, + 'deploymentId' => $deploymentId, + ]; + } + + // Collection APIs + public function testUpdateCollection(): void + { + // Set up collection with attributes + $data = $this->setupCollectionWithAttributes(); + $id = $data['actorsId']; + $databaseId = $data['databaseId']; + + /** + * Test for SUCCESS + */ + $actors = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'name' => 'Actors1', + 'documentSecurity' => true, + ]); + + $this->assertEquals(200, $actors['headers']['status-code']); + $this->assertNotEmpty($actors['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$id}.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$id}.update", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals('Actors1', $webhook['data']['name']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(4, $webhook['data']['$permissions']); + } + + public function testCreateDeleteIndexes(): void + { + // Set up collection with attributes + $data = $this->setupCollectionWithAttributes(); + $actorsId = $data['actorsId']; + $databaseId = $data['databaseId']; + + $index = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'fullname', + 'type' => 'key', + 'attributes' => ['lastName', 'firstName'], + 'orders' => ['ASC', 'ASC'], + ]); + + $indexKey = $index['body']['key']; + $this->assertEquals(202, $index['headers']['status-code']); + $this->assertEquals('fullname', $index['body']['key']); + + // wait for database worker to create index + $this->assertEventually(function () use ($databaseId, $actorsId) { + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.indexes.*.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.indexes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.indexes.*.create", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); + }, 10000, 500); + + // Remove index + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $actorsId . '/indexes/' . $index['body']['key'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + // // wait for database worker to remove index + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.indexes.*.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + // $this->assertEquals($webhook['method'], 'DELETE'); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.indexes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.indexes.*.update", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); + } + + public function testDeleteCollection(): void + { + /** + * Create database + */ + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], $this->getHeaders()), [ + 'databaseId' => ID::unique(), + 'name' => 'Actors DB', + ]); + + $databaseId = $database['body']['$id']; + + /** + * Test for SUCCESS + */ + $actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Demo', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'documentSecurity' => true, + ]); + + $id = $actors['body']['$id']; + + $this->assertEquals(201, $actors['headers']['status-code']); + $this->assertNotEmpty($actors['body']['$id']); + + $actors = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $actors['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), []); + + $this->assertEquals(204, $actors['headers']['status-code']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$id}.delete")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.collections.{$id}.delete", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals('Demo', $webhook['data']['name']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(4, $webhook['data']['$permissions']); + } + + // Table APIs + public function testUpdateTable(): void + { + // Set up table with columns + $data = $this->setupTableWithColumns(); + $id = $data['actorsId']; + $databaseId = $data['databaseId']; + + /** + * Test for SUCCESS + */ + $actors = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'name' => 'Actors1', + 'rowSecurity' => true, + ]); + + $this->assertEquals(200, $actors['headers']['status-code']); + $this->assertNotEmpty($actors['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$id}.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$id}.update", $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->assertEmpty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals('Actors1', $webhook['data']['name']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(4, $webhook['data']['$permissions']); + } + + public function testCreateDeleteColumnIndexes(): void + { + // Set up table with columns + $data = $this->setupTableWithColumns(); + $actorsId = $data['actorsId']; + $databaseId = $data['databaseId']; + + $index = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'fullname', + 'type' => 'key', + 'columns' => ['lastName', 'firstName'], + 'orders' => ['ASC', 'ASC'], + ]); + + $this->assertEquals(202, $index['headers']['status-code']); + $this->assertEquals('fullname', $index['body']['key']); + + // wait for database worker to create index + $this->assertEventually(function () use ($databaseId, $actorsId) { + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.indexes.*.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*.create", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); + }, 10000, 500); + + // Remove index + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/indexes/' . $index['body']['key'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + // // wait for database worker to remove index + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.indexes.*.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + // $this->assertEquals($webhook['method'], 'DELETE'); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*.update", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); + } + + public function testDeleteTable(): void + { + /** + * Create database + */ + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], $this->getHeaders()), [ + 'databaseId' => ID::unique(), + 'name' => 'Actors DB', + ]); + + $databaseId = $database['body']['$id']; + + /** + * Test for SUCCESS + */ + $actors = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'Demo', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'rowSecurity' => true, + ]); + + $id = $actors['body']['$id']; + + $this->assertEquals(201, $actors['headers']['status-code']); + $this->assertNotEmpty($actors['body']['$id']); + + $actors = $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $actors['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(204, $actors['headers']['status-code']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$id}.delete")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("databases.{$databaseId}.tables.{$id}.delete", $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->assertEmpty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals('Demo', $webhook['data']['name']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(4, $webhook['data']['$permissions']); + } + + public function testCreateUser(): void + { + $email = uniqid() . 'user@localhost.test'; + $password = 'password'; + $name = 'User Name'; + + /** + * Test for SUCCESS + */ + $user = $this->client->call(Client::METHOD_POST, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => $name, + ]); + + $this->assertEquals(201, $user['headers']['status-code']); + $this->assertNotEmpty($user['body']['$id']); + + $id = $user['body']['$id']; + + $webhook = $this->getLastRequest($this->webhookEventProbe("users.{$id}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('users.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.create", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['name'], $name); + $this->assertTrue((new DatetimeValidator())->isValid($webhook['data']['registration'])); + $this->assertTrue($webhook['data']['status']); + $this->assertEquals($webhook['data']['email'], $email); + $this->assertFalse($webhook['data']['emailVerification']); + $this->assertEquals([], $webhook['data']['prefs']); + } + + public function testUpdateUserPrefs(): void + { + // Set up a user + $data = $this->setupUser(); + $id = $data['userId']; + + /** + * Test for SUCCESS + */ + $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $id . '/prefs', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'prefs' => ['a' => 'b'] + ]); + + $this->assertEquals(200, $user['headers']['status-code']); + $this->assertEquals('b', $user['body']['a']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("users.{$id}.update.prefs")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('users.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('users.*.update.prefs', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$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('b', $webhook['data']['a']); + } + + public function testUpdateUserStatus(): void + { + // Set up a user + $data = $this->setupUser(); + $id = $data['userId']; + + /** + * Test for SUCCESS + */ + $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/status', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'status' => false, + ]); + + $this->assertEquals(200, $user['headers']['status-code']); + $this->assertNotEmpty($user['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("users.{$id}.update.status")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('users.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('users.*.update.status', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.update.status", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['name'], $data['name']); + $this->assertTrue((new DatetimeValidator())->isValid($webhook['data']['registration'])); + $this->assertFalse($webhook['data']['status']); + $this->assertEquals($webhook['data']['email'], $data['email']); + $this->assertFalse($webhook['data']['emailVerification']); + } + + public function testDeleteUser(): void + { + // Set up a user + $data = $this->setupUser(); + $id = $data['userId']; + + /** + * Test for SUCCESS + */ + $user = $this->client->call(Client::METHOD_DELETE, '/users/' . $data['userId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $user['headers']['status-code']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("users.{$id}.delete")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('users.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.delete", $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->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['name'], $data['name']); + $this->assertTrue((new DatetimeValidator())->isValid($webhook['data']['registration'])); + // User is created with status=true by default, so webhook shows that status at deletion + $this->assertTrue($webhook['data']['status']); + $this->assertEquals($webhook['data']['email'], $data['email']); + $this->assertFalse($webhook['data']['emailVerification']); + } + + public function testCreateFunction(): void + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test', + 'execute' => [Role::any()->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + + $id = $function['body']['$id'] ?? ''; + + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertNotEmpty($function['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + $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']); + } + + public function testUpdateFunction(): void + { + // Set up a function + $data = $this->setupFunction(); + $id = $data['functionId']; + + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_PUT, '/functions/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Test', + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'execute' => [Role::any()->toString()], + 'vars' => [ + 'key1' => 'value1', + ] + ]); + + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals($function['body']['$id'], $id); + + // Create variable + $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $id . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'key' => 'key1', + 'value' => 'value1', + ]); + + $this->assertEquals(201, $variable['headers']['status-code']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString('functions.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); + } + + public function testCreateDeployment(): void + { + // Set up a function + $data = $this->setupFunction(); + $functionId = $data['functionId']; + + /** + * Test for SUCCESS + */ + $stderr = ''; + $stdout = ''; + $folder = 'timeout'; + $code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz"; + Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); + + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'entrypoint' => 'index.js', + 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), + 'activate' => true + ]); + + $deploymentId = $deployment['body']['$id'] ?? ''; + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$functionId}.deployments.{$deploymentId}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString('functions.*.deployments.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.*.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.deployments.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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->awaitDeploymentIsBuilt($functionId, $deploymentId); + } + + public function testUpdateDeployment(): void + { + // Set up a function with deployment + $data = $this->setupFunction(); + $deploymentData = $this->setupDeployment($data['functionId']); + $id = $deploymentData['functionId']; + $deploymentId = $deploymentData['deploymentId']; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/functions/' . $id . '/deployments/' . $deploymentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + + // Wait for deployment to be built. + $this->assertEventually(function () use ($deploymentId, $id) { + $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.deployments.{$deploymentId}.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString('functions.*.deployments.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString('functions.*.deployments.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.*.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.*.deployments.{$deploymentId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.deployments.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.deployments.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); + }, 10000, 500); + + } + + public function testExecutions(): void + { + // Set up a function with deployment + $data = $this->setupFunction(); + $deploymentData = $this->setupDeployment($data['functionId']); + $id = $deploymentData['functionId']; + + /** + * Test for SUCCESS + */ + $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $id . '/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'async' => true + ]); + + $executionId = $execution['body']['$id'] ?? ''; + + $this->assertEquals(202, $execution['headers']['status-code']); + $this->assertNotEmpty($execution['body']['$id']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.executions.{$executionId}.create")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString('functions.*.executions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString('functions.*.executions.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.*.executions.{$executionId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.*.executions.{$executionId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.executions.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.executions.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.executions.{$executionId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.executions.{$executionId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); + + // wait for timeout function to complete + $this->assertEventually(function () use ($executionId, $id) { + $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.executions.{$executionId}.update")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString('functions.*.executions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString('functions.*.executions.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.*.executions.{$executionId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.*.executions.{$executionId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.executions.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.executions.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.executions.{$executionId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.executions.{$executionId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); + }, 30000, 500); + } + + public function testDeleteDeployment(): void + { + // Set up a function with deployment + $data = $this->setupFunction(); + $deploymentData = $this->setupDeployment($data['functionId']); + $id = $deploymentData['functionId']; + $deploymentId = $deploymentData['deploymentId']; + /** + * Test for SUCCESS + */ + $deployment = $this->client->call(Client::METHOD_DELETE, '/functions/' . $id . '/deployments/' . $deploymentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $deployment['headers']['status-code']); + $this->assertEmpty($deployment['body']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.deployments.{$deploymentId}.delete")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString('functions.*.deployments.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString('functions.*.deployments.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.*.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.*.deployments.{$deploymentId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.deployments.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.deployments.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); + } + + public function testDeleteFunction(): void + { + // Set up a function + $data = $this->setupFunction(); + $id = $data['functionId']; + + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $function['headers']['status-code']); + $this->assertEmpty($function['body']); + + $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.delete")); + $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); + + $this->assertEquals('POST', $webhook['method']); + $this->assertEquals('application/json', $webhook['headers']['Content-Type']); + $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); + // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString('functions.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + // $this->assertStringContainsString("functions.{$id}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); + } +} diff --git a/tests/e2e/Services/Webhooks/WebhooksBase.php b/tests/e2e/Services/Webhooks/WebhooksBase.php index 84748f98a5..4d78af004c 100644 --- a/tests/e2e/Services/Webhooks/WebhooksBase.php +++ b/tests/e2e/Services/Webhooks/WebhooksBase.php @@ -3,1832 +3,39 @@ namespace Tests\E2E\Services\Webhooks; use Appwrite\Tests\Async; -use Appwrite\Tests\Retry; -use CURLFile; use Tests\E2E\Client; -use Utopia\Database\Helpers\ID; -use Utopia\Database\Helpers\Permission; -use Utopia\Database\Helpers\Role; -use Utopia\Database\Validator\Datetime as DatetimeValidator; trait WebhooksBase { use Async; - protected function awaitDeploymentIsBuilt($functionId, $deploymentId): void - { - $this->assertEventually(function () use ($functionId, $deploymentId) { - $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); + // Tests for all auth scenarios - $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body'])); - }, 120000, 500); + public function testListWebhooks(): void + { + $webhooks = $this->listWebhooks(); + + $this->assertSame(200, $webhooks['headers']['status-code']); + $this->assertSame(1, $webhooks['body']['total']); // One created during project setup + $this->assertIsArray($webhooks['body']['webhooks']); + $this->assertCount(1, $webhooks['body']['webhooks']); } + // Helpers + /** - * Create a probe callback that filters webhooks by event pattern. + * @param array $queries */ - private function webhookEventProbe(string $eventPattern): callable + protected function listWebhooks(array $queries = [], bool $total = true): mixed { - return function (array $request) use ($eventPattern) { - $this->assertStringContainsString( - $eventPattern, - $request['headers']['X-Appwrite-Webhook-Events'] ?? '' - ); - }; - } - - public static function getWebhookSignature(array $webhook, string $signatureKey): string - { - $payload = json_encode($webhook['data']); - $url = $webhook['url']; - return base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true)); - } - - /** - * Creates a database and collection with proper attributes for document operations. - * - * @return array Array containing 'databaseId' and 'actorsId' - */ - protected function setupCollectionWithAttributes(): array - { - // Create database - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'databaseId' => ID::unique(), - 'name' => 'Actors DB', - ]); - - $databaseId = $database['body']['$id']; - - // Create collection - $actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'collectionId' => ID::unique(), - 'name' => 'Actors', - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'documentSecurity' => true, - ]); - - $actorsId = $actors['body']['$id']; - - // Create attributes - $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'firstName', - 'size' => 256, - 'required' => true, - ]); - - $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'lastName', - 'size' => 256, - 'required' => true, - ]); - - // Wait for attributes to be available - $this->assertEventually(function () use ($databaseId, $actorsId) { - $collection = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $actorsId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); - $this->assertCount(2, $collection['body']['attributes']); - $this->assertEquals('available', $collection['body']['attributes'][0]['status']); - $this->assertEquals('available', $collection['body']['attributes'][1]['status']); - }, 15000, 500); - - return ['databaseId' => $databaseId, 'actorsId' => $actorsId]; - } - - /** - * Creates a database and table with proper columns for row operations. - * - * @return array Array containing 'databaseId' and 'actorsId' - */ - protected function setupTableWithColumns(): array - { - // Create database - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'databaseId' => ID::unique(), - 'name' => 'Actors DB', - ]); - - $databaseId = $database['body']['$id']; - - // Create table - $actors = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'tableId' => ID::unique(), - 'name' => 'Actors', - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'rowSecurity' => true, - ]); - - $actorsId = $actors['body']['$id']; - - // Create columns - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'firstName', - 'size' => 256, - 'required' => true, - ]); - - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'lastName', - 'size' => 256, - 'required' => true, - ]); - - // Wait for columns to be available - $this->assertEventually(function () use ($databaseId, $actorsId) { - $table = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $actorsId, [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); - $this->assertCount(2, $table['body']['columns']); - $this->assertEquals('available', $table['body']['columns'][0]['status']); - $this->assertEquals('available', $table['body']['columns'][1]['status']); - }, 15000, 500); - - return ['databaseId' => $databaseId, 'actorsId' => $actorsId]; - } - - /** - * Creates an enabled storage bucket. - * - * @return array Array containing 'bucketId' - */ - protected function setupStorageBucket(): array - { - $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'bucketId' => ID::unique(), - 'name' => 'Test Bucket', - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'fileSecurity' => true, - 'enabled' => true, - ]); - - return ['bucketId' => $bucket['body']['$id']]; - } - - /** - * Creates a team and returns its ID. - * - * @param string $name Team name - * @return array Array containing 'teamId' - */ - protected function setupTeam(string $name = 'Arsenal'): array - { - $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + $webhooks = $this->client->call(Client::METHOD_GET, '/webhooks', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'teamId' => ID::unique(), - 'name' => $name + 'querues' => $queries, + 'total' => $total ]); - return ['teamId' => $team['body']['$id']]; - } - - /** - * Creates a team membership and returns membership details including secret. - * - * @param string $teamId The team ID - * @return array Array containing 'teamId', 'membershipId', 'userId', 'secret' - */ - protected function setupTeamMembership(string $teamId): array - { - $email = uniqid() . 'friend@localhost.test'; - - // Create user first to ensure team event is triggered after user event - $this->client->call(Client::METHOD_POST, '/account', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'userId' => ID::unique(), - 'email' => $email, - 'password' => 'password', - 'name' => 'Friend User', - ]); - - // Create membership - $team = $this->client->call(Client::METHOD_POST, '/teams/' . $teamId . '/memberships', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'email' => $email, - 'roles' => ['admin', 'editor'], - 'url' => 'http://localhost:5000/join-us#title' - ]); - - $membershipId = $team['body']['$id']; - $userId = $team['body']['userId']; - - // Get the secret from email (use probe to match correct email by recipient address) - $lastEmail = $this->getLastEmail(1, function ($msg) use ($email) { - $this->assertEquals($email, $msg['to'][0]['address'] ?? ''); - }); - $tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html'] ?? ''); - $secret = $tokens['secret'] ?? ''; - - return [ - 'teamId' => $teamId, - 'membershipId' => $membershipId, - 'userId' => $userId, - 'secret' => $secret, - ]; - } - - /** - * Creates a document in a collection. - * - * @param string $databaseId Database ID - * @param string $collectionId Collection ID - * @return array Array containing document details including 'documentId' - */ - protected function setupDocument(string $databaseId, string $collectionId): array - { - $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'firstName' => 'Chris', - 'lastName' => 'Evans', - ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - return ['documentId' => $document['body']['$id']]; - } - - /** - * Creates a row in a table. - * - * @param string $databaseId Database ID - * @param string $tableId Table ID - * @return array Array containing row details including 'rowId' - */ - protected function setupRow(string $databaseId, string $tableId): array - { - $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'rowId' => ID::unique(), - 'data' => [ - 'firstName' => 'Chris', - 'lastName' => 'Evans', - ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - return ['rowId' => $row['body']['$id']]; - } - - /** - * Creates a file in a bucket. - * - * @param string $bucketId Bucket ID - * @return array Array containing file details including 'fileId' - */ - protected function setupBucketFile(string $bucketId): array - { - $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'fileId' => ID::unique(), - 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'), - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'folderId' => ID::custom('xyz'), - ]); - - return ['fileId' => $file['body']['$id']]; - } - - // Collection APIs - public function testCreateCollection(): void - { - /** - * Create database - */ - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'databaseId' => ID::unique(), - 'name' => 'Actors DB', - ]); - - $databaseId = $database['body']['$id']; - - /** - * Test for SUCCESS - */ - $actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'collectionId' => ID::unique(), - 'name' => 'Actors', - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'documentSecurity' => true, - ]); - - $actorsId = $actors['body']['$id']; - - $this->assertEquals($actors['headers']['status-code'], 201); - $this->assertNotEmpty($actors['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.create", $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'] ?? ''), true); - $this->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals($webhook['data']['name'], 'Actors'); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(4, $webhook['data']['$permissions']); - } - - public function testCreateAttributes(): void - { - /** - * Create database - */ - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'databaseId' => ID::unique(), - 'name' => 'Actors DB', - ]); - - $databaseId = $database['body']['$id']; - - /** - * Create collection - */ - $actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'collectionId' => ID::unique(), - 'name' => 'Actors', - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'documentSecurity' => true, - ]); - - $actorsId = $actors['body']['$id']; - - $firstName = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'firstName', - 'size' => 256, - 'required' => true, - ]); - - $lastName = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'lastName', - 'size' => 256, - 'required' => true, - ]); - - $extra = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'extra', - 'size' => 64, - 'required' => false, - ]); - - $attributeId = $extra['body']['key']; - - $this->assertEquals($firstName['headers']['status-code'], 202); - $this->assertEquals($firstName['body']['key'], 'firstName'); - $this->assertEquals($lastName['headers']['status-code'], 202); - $this->assertEquals($lastName['body']['key'], 'lastName'); - $this->assertEquals($extra['headers']['status-code'], 202); - $this->assertEquals($extra['body']['key'], 'extra'); - - // wait for database worker to kick in - $this->assertEventually(function () use ($databaseId, $actorsId) { - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.attributes.*.create")); - $this->assertNotEmpty($webhook); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.attributes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.attributes.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.attributes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.attributes.*.create", $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->assertNotEmpty($webhook['data']['key']); - $this->assertEquals($webhook['data']['key'], 'extra'); - }, 15000, 500); - - $removed = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/' . $extra['body']['key'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - - $this->assertEquals(204, $removed['headers']['status-code']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.attributes.*.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - // $this->assertEquals($webhook['method'], 'DELETE'); - $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.attributes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.attributes.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.attributes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.attributes.*.update", $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->assertNotEmpty($webhook['data']['key']); - $this->assertEquals($webhook['data']['key'], 'extra'); - } - - public function testCreateDocument(): void - { - // Set up collection with attributes - $data = $this->setupCollectionWithAttributes(); - $actorsId = $data['actorsId']; - $databaseId = $data['databaseId']; - - /** - * Test for SUCCESS - */ - $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'firstName' => 'Chris', - 'lastName' => 'Evans', - ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - $documentId = $document['body']['$id']; - - $this->assertEquals($document['headers']['status-code'], 201); - $this->assertNotEmpty($document['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.create", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals($webhook['data']['firstName'], 'Chris'); - $this->assertEquals($webhook['data']['lastName'], 'Evans'); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(3, $webhook['data']['$permissions']); - } - - public function testUpdateDocument(): void - { - // Set up collection with attributes and create a document - $data = $this->setupCollectionWithAttributes(); - $actorsId = $data['actorsId']; - $databaseId = $data['databaseId']; - $documentData = $this->setupDocument($databaseId, $actorsId); - $documentId = $documentData['documentId']; - - /** - * Test for SUCCESS - */ - $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $actorsId . '/documents/' . $documentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'firstName' => 'Chris1', - 'lastName' => 'Evans2', - ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - $documentId = $document['body']['$id']; - - $this->assertEquals($document['headers']['status-code'], 200); - $this->assertNotEmpty($document['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.update", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals($webhook['data']['firstName'], 'Chris1'); - $this->assertEquals($webhook['data']['lastName'], 'Evans2'); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(3, $webhook['data']['$permissions']); - } - - #[Retry(count: 1)] - public function testDeleteDocument(): void - { - // Set up collection with attributes - $data = $this->setupCollectionWithAttributes(); - $actorsId = $data['actorsId']; - $databaseId = $data['databaseId']; - - /** - * Test for SUCCESS - */ - $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'firstName' => 'Bradly', - 'lastName' => 'Cooper', - - ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - $documentId = $document['body']['$id']; - - $this->assertEquals($document['headers']['status-code'], 201); - $this->assertNotEmpty($document['body']['$id']); - - $document = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $actorsId . '/documents/' . $document['body']['$id'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals($document['headers']['status-code'], 204); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.delete")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.documents.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.*.documents.{$documentId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}.delete", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals($webhook['data']['firstName'], 'Bradly'); - $this->assertEquals($webhook['data']['lastName'], 'Cooper'); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(3, $webhook['data']['$permissions']); - } - - // Table APIs - public function testCreateTable(): void - { - /** - * Create database - */ - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'databaseId' => ID::unique(), - 'name' => 'Actors DB', - ]); - - $databaseId = $database['body']['$id']; - - /** - * Test for SUCCESS - */ - $actors = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'tableId' => ID::unique(), - 'name' => 'Actors', - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'rowSecurity' => true, - ]); - - $actorsId = $actors['body']['$id']; - - $this->assertEquals($actors['headers']['status-code'], 201); - $this->assertNotEmpty($actors['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.create", $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'] ?? ''), true); - $this->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals($webhook['data']['name'], 'Actors'); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(4, $webhook['data']['$permissions']); - } - - public function testCreateColumns(): void - { - /** - * Create database - */ - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'databaseId' => ID::unique(), - 'name' => 'Actors DB', - ]); - - $databaseId = $database['body']['$id']; - - /** - * Create table - */ - $actors = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'tableId' => ID::unique(), - 'name' => 'Actors', - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'rowSecurity' => true, - ]); - - $actorsId = $actors['body']['$id']; - - $firstName = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'firstName', - 'size' => 256, - 'required' => true, - ]); - - $lastName = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'lastName', - 'size' => 256, - 'required' => true, - ]); - - $extra = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'extra', - 'size' => 64, - 'required' => false, - ]); - - $this->assertEquals($firstName['headers']['status-code'], 202); - $this->assertEquals($firstName['body']['key'], 'firstName'); - $this->assertEquals($lastName['headers']['status-code'], 202); - $this->assertEquals($lastName['body']['key'], 'lastName'); - $this->assertEquals($extra['headers']['status-code'], 202); - $this->assertEquals($extra['body']['key'], 'extra'); - - // wait for database worker to kick in - $this->assertEventually(function () use ($databaseId, $actorsId) { - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.columns.*.create")); - $this->assertNotEmpty($webhook); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*.create", $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->assertNotEmpty($webhook['data']['key']); - $this->assertEquals($webhook['data']['key'], 'extra'); - }, 15000, 500); - - $removed = $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/columns/' . $extra['body']['key'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - - $this->assertEquals(204, $removed['headers']['status-code']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.columns.*.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - // $this->assertEquals($webhook['method'], 'DELETE'); - $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*.update", $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->assertNotEmpty($webhook['data']['key']); - $this->assertEquals($webhook['data']['key'], 'extra'); - } - - public function testCreateRow(): void - { - // Set up table with columns - $data = $this->setupTableWithColumns(); - $actorsId = $data['actorsId']; - $databaseId = $data['databaseId']; - - /** - * Test for SUCCESS - */ - $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'rowId' => ID::unique(), - 'data' => [ - 'firstName' => 'Chris', - 'lastName' => 'Evans', - ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - $documentId = $row['body']['$id']; - - $this->assertEquals($row['headers']['status-code'], 201); - $this->assertNotEmpty($row['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.rows.{$documentId}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$documentId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$documentId}.create", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals($webhook['data']['firstName'], 'Chris'); - $this->assertEquals($webhook['data']['lastName'], 'Evans'); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(3, $webhook['data']['$permissions']); - } - - public function testUpdateRow(): void - { - // Set up table with columns and create a row - $data = $this->setupTableWithColumns(); - $actorsId = $data['actorsId']; - $databaseId = $data['databaseId']; - $rowData = $this->setupRow($databaseId, $actorsId); - $rowId = $rowData['rowId']; - - /** - * Test for SUCCESS - */ - $document = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows/' . $rowId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'firstName' => 'Chris1', - 'lastName' => 'Evans2', - ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - $rowId = $document['body']['$id']; - - $this->assertEquals($document['headers']['status-code'], 200); - $this->assertNotEmpty($document['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.update", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals($webhook['data']['firstName'], 'Chris1'); - $this->assertEquals($webhook['data']['lastName'], 'Evans2'); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(3, $webhook['data']['$permissions']); - } - - #[Retry(count: 1)] - public function testDeleteRow(): void - { - // Set up table with columns - $data = $this->setupTableWithColumns(); - $actorsId = $data['actorsId']; - $databaseId = $data['databaseId']; - - /** - * Test for SUCCESS - */ - $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'rowId' => ID::unique(), - 'data' => [ - 'firstName' => 'Bradly', - 'lastName' => 'Cooper', - - ], - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - $rowId = $row['body']['$id']; - - $this->assertEquals($row['headers']['status-code'], 201); - $this->assertNotEmpty($row['body']['$id']); - - $row = $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows/' . $row['body']['$id'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals($row['headers']['status-code'], 204); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.delete")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.delete", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals($webhook['data']['firstName'], 'Bradly'); - $this->assertEquals($webhook['data']['lastName'], 'Cooper'); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(3, $webhook['data']['$permissions']); - } - - public function testCreateStorageBucket(): void - { - /** - * Test for SUCCESS - */ - $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'bucketId' => ID::unique(), - 'name' => 'Test Bucket', - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - $bucketId = $bucket['body']['$id']; - - $this->assertEquals($bucket['headers']['status-code'], 201); - $this->assertNotEmpty($bucket['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('buckets.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.create", $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'] ?? ''), true); - $this->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals('Test Bucket', $webhook['data']['name']); - $this->assertEquals(true, $webhook['data']['enabled']); - $this->assertIsArray($webhook['data']['$permissions']); - } - - public function testUpdateStorageBucket(): void - { - // Set up a storage bucket - $data = $this->setupStorageBucket(); - $bucketId = $data['bucketId']; - - /** - * Test for SUCCESS - */ - $bucket = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $bucketId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'name' => 'Test Bucket Updated', - 'fileSecurity' => true, - 'enabled' => false, - ]); - - $this->assertEquals($bucket['headers']['status-code'], 200); - $this->assertNotEmpty($bucket['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('buckets.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.update", $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'] ?? ''), true); - $this->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals('Test Bucket Updated', $webhook['data']['name']); - $this->assertEquals(false, $webhook['data']['enabled']); - $this->assertIsArray($webhook['data']['$permissions']); - } - - public function testCreateBucketFile(): void - { - // Set up an enabled storage bucket - $data = $this->setupStorageBucket(); - $bucketId = $data['bucketId']; - - /** - * Test for SUCCESS - */ - $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'fileId' => ID::unique(), - 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'), - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'folderId' => ID::custom('xyz'), - ]); - - $fileId = $file['body']['$id']; - - $this->assertEquals($file['headers']['status-code'], 201); - $this->assertNotEmpty($file['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.files.{$fileId}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('buckets.*.files.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('buckets.*.files.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.*.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.*.files.{$fileId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}.create", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertEquals($webhook['data']['name'], 'logo.png'); - $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); - $this->assertNotEmpty($webhook['data']['signature']); - $this->assertEquals($webhook['data']['mimeType'], 'image/png'); - $this->assertEquals($webhook['data']['sizeOriginal'], 47218); - } - - public function testUpdateBucketFile(): void - { - // Set up an enabled storage bucket and create a file - $data = $this->setupStorageBucket(); - $bucketId = $data['bucketId']; - $fileData = $this->setupBucketFile($bucketId); - $fileId = $fileData['fileId']; - - /** - * Test for SUCCESS - */ - $file = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - $this->assertEquals($file['headers']['status-code'], 200); - $this->assertNotEmpty($file['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.files.{$fileId}.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('buckets.*.files.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('buckets.*.files.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.*.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.*.files.{$fileId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}.update", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertEquals($webhook['data']['name'], 'logo.png'); - $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); - $this->assertNotEmpty($webhook['data']['signature']); - $this->assertEquals($webhook['data']['mimeType'], 'image/png'); - $this->assertEquals($webhook['data']['sizeOriginal'], 47218); - } - - public function testDeleteBucketFile(): void - { - // Set up an enabled storage bucket and create a file - $data = $this->setupStorageBucket(); - $bucketId = $data['bucketId']; - $fileData = $this->setupBucketFile($bucketId); - $fileId = $fileData['fileId']; - - /** - * Test for SUCCESS - */ - $file = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(204, $file['headers']['status-code']); - $this->assertEmpty($file['body']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.files.{$fileId}.delete")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('buckets.*.files.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('buckets.*.files.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.*.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.*.files.{$fileId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}.delete", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertEquals($webhook['data']['name'], 'logo.png'); - $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); - $this->assertNotEmpty($webhook['data']['signature']); - $this->assertEquals($webhook['data']['mimeType'], 'image/png'); - $this->assertEquals($webhook['data']['sizeOriginal'], 47218); - } - - public function testDeleteStorageBucket(): void - { - // Set up an enabled storage bucket - $data = $this->setupStorageBucket(); - $bucketId = $data['bucketId']; - - // Update bucket name before deleting to make test self-sufficient - // (In parallel execution, testUpdateStorageBucket may not have run) - $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $bucketId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'name' => 'Test Bucket Updated', - 'fileSecurity' => true, - ]); - - /** - * Test for SUCCESS - */ - $bucket = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - - $this->assertEquals($bucket['headers']['status-code'], 204); - $this->assertEmpty($bucket['body']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("buckets.{$bucketId}.delete")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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('buckets.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('buckets.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("buckets.{$bucketId}.delete", $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'] ?? ''), true); - $this->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals('Test Bucket Updated', $webhook['data']['name']); - $this->assertEquals(true, $webhook['data']['enabled']); - $this->assertIsArray($webhook['data']['$permissions']); - } - - public function testCreateTeam(): void - { - /** - * Test for SUCCESS - */ - $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'teamId' => ID::unique(), - 'name' => 'Arsenal' - ]); - - $teamId = $team['body']['$id']; - - $this->assertEquals(201, $team['headers']['status-code']); - $this->assertNotEmpty($team['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("teams.{$teamId}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}.create", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals('Arsenal', $webhook['data']['name']); - $this->assertGreaterThan(-1, $webhook['data']['total']); - $this->assertIsInt($webhook['data']['total']); - $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); - } - - public function testUpdateTeam(): void - { - // Set up a team - $data = $this->setupTeam(); - $teamId = $data['teamId']; - /** - * Test for SUCCESS - */ - $team = $this->client->call(Client::METHOD_PUT, '/teams/' . $teamId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'name' => 'Demo New' - ]); - - $this->assertEquals(200, $team['headers']['status-code']); - $this->assertNotEmpty($team['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("teams.{$teamId}.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}.update", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals('Demo New', $webhook['data']['name']); - $this->assertGreaterThan(-1, $webhook['data']['total']); - $this->assertIsInt($webhook['data']['total']); - $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); - } - - public function testUpdateTeamPrefs(): void - { - // Set up a team - $data = $this->setupTeam(); - $id = $data['teamId']; - - $team = $this->client->call(Client::METHOD_PUT, '/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($this->webhookEventProbe("teams.{$id}.update.prefs")); - $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', - ]); - } - - public function testDeleteTeam(): void - { - /** - * Test for SUCCESS - */ - $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'teamId' => ID::unique(), - 'name' => 'Chelsea' - ]); - - $teamId = $team['body']['$id']; - - $this->assertEquals(201, $team['headers']['status-code']); - $this->assertNotEmpty($team['body']['$id']); - - $team = $this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $webhook = $this->getLastRequest($this->webhookEventProbe("teams.{$teamId}.delete")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}.delete", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals('Chelsea', $webhook['data']['name']); - $this->assertGreaterThan(-1, $webhook['data']['total']); - $this->assertIsInt($webhook['data']['total']); - $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['$createdAt'])); - } - - public function testCreateTeamMembership(): void - { - // Set up a team - $data = $this->setupTeam(); - $teamId = $data['teamId']; - $email = uniqid() . 'friend@localhost.test'; - - // Create user to ensure team event is triggered after user event - $user = $this->client->call(Client::METHOD_POST, '/account', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'userId' => ID::unique(), - 'email' => $email, - 'password' => 'password', - 'name' => 'Friend User', - ]); - - /** - * Test for SUCCESS - */ - $team = $this->client->call(Client::METHOD_POST, '/teams/' . $teamId . '/memberships', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'email' => $email, - 'roles' => ['admin', 'editor'], - 'url' => 'http://localhost:5000/join-us#title' - ]); - - $this->assertEquals(201, $team['headers']['status-code']); - $this->assertNotEmpty($team['body']['$id']); - - $lastEmail = $this->getLastEmail(); - - // `$isAppUser` — no email expected; - $tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html'] ?? ''); - - $secret = $tokens['secret'] ?? ''; - $membershipId = $team['body']['$id']; - - $webhook = $this->getLastRequest($this->webhookEventProbe("teams.{$teamId}.memberships.{$membershipId}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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.*.memberships.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('teams.*.memberships.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.*.memberships.{$membershipId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.*.memberships.{$membershipId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}.memberships.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}.memberships.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}.create", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertNotEmpty($webhook['data']['userId']); - $this->assertNotEmpty($webhook['data']['teamId']); - $this->assertCount(2, $webhook['data']['roles']); - $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['invited'])); - $this->assertEquals(('server' === $this->getSide()), $webhook['data']['confirm']); - } - - public function testDeleteTeamMembership(): void - { - // Set up a team - $data = $this->setupTeam(); - $teamId = $data['teamId']; - $email = uniqid() . 'friend@localhost.test'; - - /** - * Test for SUCCESS - */ - $team = $this->client->call(Client::METHOD_POST, '/teams/' . $teamId . '/memberships', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'email' => $email, - 'name' => 'Friend User', - 'roles' => ['admin', 'editor'], - 'url' => 'http://localhost:5000/join-us#title' - ]); - - $membershipId = $team['body']['$id'] ?? ''; - - $this->assertEquals(201, $team['headers']['status-code']); - $this->assertNotEmpty($team['body']['$id']); - - $team = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId . '/memberships/' . $team['body']['$id'], array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(204, $team['headers']['status-code']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("teams.{$teamId}.memberships.{$membershipId}.delete")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $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.*.memberships.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('teams.*.memberships.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.*.memberships.{$membershipId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.*.memberships.{$membershipId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}.memberships.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}.memberships.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}.delete", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertNotEmpty($webhook['data']['userId']); - $this->assertNotEmpty($webhook['data']['teamId']); - $this->assertCount(2, $webhook['data']['roles']); - $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['invited'])); - $this->assertEquals(('server' === $this->getSide()), $webhook['data']['confirm']); - } - - public function testCreateWebhookWithPrivateDomain(): void - { - /** - * Test for FAILURE - */ - $projectId = $this->getProject()['$id']; - $webhook = $this->client->call(Client::METHOD_POST, '/projects/' . $projectId . '/webhooks', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'cookie' => 'a_session_console=' . $this->getRoot()['session'], - 'x-appwrite-project' => 'console', - ], [ - 'name' => 'Webhook Test', - 'enabled' => true, - 'events' => [ - 'databases.*', - 'functions.*', - 'buckets.*', - 'teams.*', - 'users.*' - ], - 'url' => 'http://localhost/webhook', // private domains not allowed - 'security' => false, - ]); - - $this->assertEquals(400, $webhook['headers']['status-code']); - } - - public function testUpdateWebhookWithPrivateDomain(): void - { - /** - * Test for FAILURE - */ - $projectId = $this->getProject()['$id']; - $webhookId = $this->getProject()['webhookId']; - $webhook = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/webhooks/' . $webhookId, [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'cookie' => 'a_session_console=' . $this->getRoot()['session'], - 'x-appwrite-project' => 'console', - ], [ - 'name' => 'Webhook Test', - 'enabled' => true, - 'events' => [ - 'databases.*', - 'functions.*', - 'buckets.*', - 'teams.*', - 'users.*' - ], - 'url' => 'http://localhost/webhook', // private domains not allowed - 'security' => false, - ]); - - $this->assertEquals(400, $webhook['headers']['status-code']); - } - - public function testWebhookAutoDisable(): void - { - $projectId = $this->getProject()['$id']; - $webhookId = $this->getProject()['webhookId']; - - // Create a database for this test - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'databaseId' => ID::unique(), - 'name' => 'AutoDisable DB', - ]); - - $databaseId = $database['body']['$id']; - - $webhook = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/webhooks/' . $webhookId, [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'cookie' => 'a_session_console=' . $this->getRoot()['session'], - 'x-appwrite-project' => 'console', - ], [ - 'name' => 'Webhook Test', - 'enabled' => true, - 'events' => [ - 'databases.*', - 'functions.*', - 'buckets.*', - 'teams.*', - 'users.*' - ], - 'url' => 'http://appwrite-non-existing-domain.com', // set non-existent URL - 'security' => false, - ]); - - $this->assertEquals(200, $webhook['headers']['status-code']); - $this->assertNotEmpty($webhook['body']); - - // trigger webhook for failure event 10 times - for ($i = 0; $i < 10; $i++) { - $newCollection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'collectionId' => ID::unique(), - 'name' => 'newCollection' . $i, - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'documentSecurity' => true, - ]); - - $this->assertEquals($newCollection['headers']['status-code'], 201); - $this->assertNotEmpty($newCollection['body']['$id']); - } - - $this->assertEventually(function () use ($projectId, $webhookId) { - $webhook = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId . '/webhooks/' . $webhookId, array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'cookie' => 'a_session_console=' . $this->getRoot()['session'], - 'x-appwrite-project' => 'console', - ])); - - // assert that the webhook is now disabled after 10 consecutive failures - $this->assertEquals($webhook['body']['enabled'], false); - $this->assertEquals($webhook['body']['attempts'], 10); - }, 15000, 500); + return $webhooks; } } diff --git a/tests/e2e/Services/Webhooks/WebhooksConsoleClientTest.php b/tests/e2e/Services/Webhooks/WebhooksConsoleClientTest.php new file mode 100644 index 0000000000..b954ef8600 --- /dev/null +++ b/tests/e2e/Services/Webhooks/WebhooksConsoleClientTest.php @@ -0,0 +1,14 @@ +client->call(Client::METHOD_POST, '/users', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'userId' => ID::unique(), - 'email' => $email, - 'password' => $password, - 'name' => $name, - ]); - - return [ - 'userId' => $user['body']['$id'], - 'name' => $user['body']['name'], - 'email' => $user['body']['email'], - ]; - } - - /** - * Creates a function and returns function details. - * - * @return array Array containing 'functionId' - */ - protected function setupFunction(): array - { - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'functionId' => ID::unique(), - 'name' => 'Test', - 'execute' => [Role::any()->toString()], - 'runtime' => 'node-22', - 'entrypoint' => 'index.js', - 'timeout' => 10, - ]); - - return ['functionId' => $function['body']['$id']]; - } - - /** - * Creates a function deployment and waits for it to be built. - * - * @param string $functionId Function ID - * @return array Array containing 'functionId', 'deploymentId' - */ - protected function setupDeployment(string $functionId): array - { - $stderr = ''; - $stdout = ''; - $folder = 'timeout'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); - - // Create variable first - $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'key' => 'key1', - 'value' => 'value1', - ]); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'entrypoint' => 'index.js', - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), - 'activate' => true - ]); - - $deploymentId = $deployment['body']['$id']; - - // Wait for deployment to be built - $this->awaitDeploymentIsBuilt($functionId, $deploymentId); - - return [ - 'functionId' => $functionId, - 'deploymentId' => $deploymentId, - ]; - } - - // Collection APIs - public function testUpdateCollection(): void - { - // Set up collection with attributes - $data = $this->setupCollectionWithAttributes(); - $id = $data['actorsId']; - $databaseId = $data['databaseId']; - - /** - * Test for SUCCESS - */ - $actors = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $id, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'name' => 'Actors1', - 'documentSecurity' => true, - ]); - - $this->assertEquals(200, $actors['headers']['status-code']); - $this->assertNotEmpty($actors['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$id}.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$id}.update", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); - $this->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals('Actors1', $webhook['data']['name']); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(4, $webhook['data']['$permissions']); - } - - public function testCreateDeleteIndexes(): void - { - // Set up collection with attributes - $data = $this->setupCollectionWithAttributes(); - $actorsId = $data['actorsId']; - $databaseId = $data['databaseId']; - - $index = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/indexes', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'fullname', - 'type' => 'key', - 'attributes' => ['lastName', 'firstName'], - 'orders' => ['ASC', 'ASC'], - ]); - - $indexKey = $index['body']['key']; - $this->assertEquals(202, $index['headers']['status-code']); - $this->assertEquals('fullname', $index['body']['key']); - - // wait for database worker to create index - $this->assertEventually(function () use ($databaseId, $actorsId) { - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.indexes.*.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.indexes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.indexes.*.create", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); - }, 10000, 500); - - // Remove index - $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $actorsId . '/indexes/' . $index['body']['key'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - - // // wait for database worker to remove index - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$actorsId}.indexes.*.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - // $this->assertEquals($webhook['method'], 'DELETE'); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.indexes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$actorsId}.indexes.*.update", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); - } - - public function testDeleteCollection(): void - { - /** - * Create database - */ - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ], $this->getHeaders()), [ - 'databaseId' => ID::unique(), - 'name' => 'Actors DB', - ]); - - $databaseId = $database['body']['$id']; - - /** - * Test for SUCCESS - */ - $actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'collectionId' => ID::unique(), - 'name' => 'Demo', - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'documentSecurity' => true, - ]); - - $id = $actors['body']['$id']; - - $this->assertEquals(201, $actors['headers']['status-code']); - $this->assertNotEmpty($actors['body']['$id']); - - $actors = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $actors['body']['$id'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), []); - - $this->assertEquals(204, $actors['headers']['status-code']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.collections.{$id}.delete")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.collections.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.collections.{$id}.delete", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); - $this->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals('Demo', $webhook['data']['name']); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(4, $webhook['data']['$permissions']); - } - - // Table APIs - public function testUpdateTable(): void - { - // Set up table with columns - $data = $this->setupTableWithColumns(); - $id = $data['actorsId']; - $databaseId = $data['databaseId']; - - /** - * Test for SUCCESS - */ - $actors = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $id, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'name' => 'Actors1', - 'rowSecurity' => true, - ]); - - $this->assertEquals(200, $actors['headers']['status-code']); - $this->assertNotEmpty($actors['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$id}.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$id}.update", $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->assertEmpty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''); - $this->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals('Actors1', $webhook['data']['name']); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(4, $webhook['data']['$permissions']); - } - - public function testCreateDeleteColumnIndexes(): void - { - // Set up table with columns - $data = $this->setupTableWithColumns(); - $actorsId = $data['actorsId']; - $databaseId = $data['databaseId']; - - $index = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/indexes', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'fullname', - 'type' => 'key', - 'columns' => ['lastName', 'firstName'], - 'orders' => ['ASC', 'ASC'], - ]); - - $this->assertEquals(202, $index['headers']['status-code']); - $this->assertEquals('fullname', $index['body']['key']); - - // wait for database worker to create index - $this->assertEventually(function () use ($databaseId, $actorsId) { - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.indexes.*.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*.create", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); - }, 10000, 500); - - // Remove index - $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/indexes/' . $index['body']['key'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - - // // wait for database worker to remove index - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.indexes.*.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - // $this->assertEquals($webhook['method'], 'DELETE'); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*.update", $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->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '')); - } - - public function testDeleteTable(): void - { - /** - * Create database - */ - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ], $this->getHeaders()), [ - 'databaseId' => ID::unique(), - 'name' => 'Actors DB', - ]); - - $databaseId = $database['body']['$id']; - - /** - * Test for SUCCESS - */ - $actors = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'tableId' => ID::unique(), - 'name' => 'Demo', - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'rowSecurity' => true, - ]); - - $id = $actors['body']['$id']; - - $this->assertEquals(201, $actors['headers']['status-code']); - $this->assertNotEmpty($actors['body']['$id']); - - $actors = $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $actors['body']['$id'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - - $this->assertEquals(204, $actors['headers']['status-code']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$id}.delete")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('databases.' . $databaseId . '.tables.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("databases.{$databaseId}.tables.{$id}.delete", $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->assertEmpty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''); - $this->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals('Demo', $webhook['data']['name']); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertCount(4, $webhook['data']['$permissions']); - } - - public function testCreateUser(): void - { - $email = uniqid() . 'user@localhost.test'; - $password = 'password'; - $name = 'User Name'; - - /** - * Test for SUCCESS - */ - $user = $this->client->call(Client::METHOD_POST, '/users', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'userId' => ID::unique(), - 'email' => $email, - 'password' => $password, - 'name' => $name, - ]); - - $this->assertEquals(201, $user['headers']['status-code']); - $this->assertNotEmpty($user['body']['$id']); - - $id = $user['body']['$id']; - - $webhook = $this->getLastRequest($this->webhookEventProbe("users.{$id}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('users.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.create", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals($webhook['data']['name'], $name); - $this->assertTrue((new DatetimeValidator())->isValid($webhook['data']['registration'])); - $this->assertTrue($webhook['data']['status']); - $this->assertEquals($webhook['data']['email'], $email); - $this->assertFalse($webhook['data']['emailVerification']); - $this->assertEquals([], $webhook['data']['prefs']); - } - - public function testUpdateUserPrefs(): void - { - // Set up a user - $data = $this->setupUser(); - $id = $data['userId']; - - /** - * Test for SUCCESS - */ - $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $id . '/prefs', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'prefs' => ['a' => 'b'] - ]); - - $this->assertEquals(200, $user['headers']['status-code']); - $this->assertEquals('b', $user['body']['a']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("users.{$id}.update.prefs")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('users.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('users.*.update.prefs', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$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('b', $webhook['data']['a']); - } - - public function testUpdateUserStatus(): void - { - // Set up a user - $data = $this->setupUser(); - $id = $data['userId']; - - /** - * Test for SUCCESS - */ - $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/status', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'status' => false, - ]); - - $this->assertEquals(200, $user['headers']['status-code']); - $this->assertNotEmpty($user['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("users.{$id}.update.status")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('users.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('users.*.update.status', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.update.status", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals($webhook['data']['name'], $data['name']); - $this->assertTrue((new DatetimeValidator())->isValid($webhook['data']['registration'])); - $this->assertFalse($webhook['data']['status']); - $this->assertEquals($webhook['data']['email'], $data['email']); - $this->assertFalse($webhook['data']['emailVerification']); - } - - public function testDeleteUser(): void - { - // Set up a user - $data = $this->setupUser(); - $id = $data['userId']; - - /** - * Test for SUCCESS - */ - $user = $this->client->call(Client::METHOD_DELETE, '/users/' . $data['userId'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(204, $user['headers']['status-code']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("users.{$id}.delete")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('users.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.delete", $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->assertNotEmpty($webhook['data']['$id']); - $this->assertEquals($webhook['data']['name'], $data['name']); - $this->assertTrue((new DatetimeValidator())->isValid($webhook['data']['registration'])); - // User is created with status=true by default, so webhook shows that status at deletion - $this->assertTrue($webhook['data']['status']); - $this->assertEquals($webhook['data']['email'], $data['email']); - $this->assertFalse($webhook['data']['emailVerification']); - } - - public function testCreateFunction(): void - { - /** - * Test for SUCCESS - */ - $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'functionId' => ID::unique(), - 'name' => 'Test', - 'execute' => [Role::any()->toString()], - 'runtime' => 'node-22', - 'entrypoint' => 'index.js', - 'timeout' => 10, - ]); - - $id = $function['body']['$id'] ?? ''; - - $this->assertEquals(201, $function['headers']['status-code']); - $this->assertNotEmpty($function['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - $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']); - } - - public function testUpdateFunction(): void - { - // Set up a function - $data = $this->setupFunction(); - $id = $data['functionId']; - - /** - * Test for SUCCESS - */ - $function = $this->client->call(Client::METHOD_PUT, '/functions/' . $id, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'name' => 'Test', - 'runtime' => 'node-22', - 'entrypoint' => 'index.js', - 'execute' => [Role::any()->toString()], - 'vars' => [ - 'key1' => 'value1', - ] - ]); - - $this->assertEquals(200, $function['headers']['status-code']); - $this->assertEquals($function['body']['$id'], $id); - - // Create variable - $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $id . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'key' => 'key1', - 'value' => 'value1', - ]); - - $this->assertEquals(201, $variable['headers']['status-code']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString('functions.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); - } - - public function testCreateDeployment(): void - { - // Set up a function - $data = $this->setupFunction(); - $functionId = $data['functionId']; - - /** - * Test for SUCCESS - */ - $stderr = ''; - $stdout = ''; - $folder = 'timeout'; - $code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz"; - Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); - - $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'entrypoint' => 'index.js', - 'code' => new CURLFile($code, 'application/x-gzip', \basename($code)), - 'activate' => true - ]); - - $deploymentId = $deployment['body']['$id'] ?? ''; - - $this->assertEquals(202, $deployment['headers']['status-code']); - $this->assertNotEmpty($deployment['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$functionId}.deployments.{$deploymentId}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString('functions.*.deployments.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.*.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.deployments.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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->awaitDeploymentIsBuilt($functionId, $deploymentId); - } - - public function testUpdateDeployment(): void - { - // Set up a function with deployment - $data = $this->setupFunction(); - $deploymentData = $this->setupDeployment($data['functionId']); - $id = $deploymentData['functionId']; - $deploymentId = $deploymentData['deploymentId']; - - /** - * Test for SUCCESS - */ - $response = $this->client->call(Client::METHOD_PATCH, '/functions/' . $id . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertNotEmpty($response['body']['$id']); - - // Wait for deployment to be built. - $this->assertEventually(function () use ($deploymentId, $id) { - $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.deployments.{$deploymentId}.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString('functions.*.deployments.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString('functions.*.deployments.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.*.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.*.deployments.{$deploymentId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.deployments.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.deployments.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); - }, 10000, 500); - - } - - public function testExecutions(): void - { - // Set up a function with deployment - $data = $this->setupFunction(); - $deploymentData = $this->setupDeployment($data['functionId']); - $id = $deploymentData['functionId']; - - /** - * Test for SUCCESS - */ - $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $id . '/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'async' => true - ]); - - $executionId = $execution['body']['$id'] ?? ''; - - $this->assertEquals(202, $execution['headers']['status-code']); - $this->assertNotEmpty($execution['body']['$id']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.executions.{$executionId}.create")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString('functions.*.executions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString('functions.*.executions.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.*.executions.{$executionId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.*.executions.{$executionId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.executions.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.executions.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.executions.{$executionId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.executions.{$executionId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); - - // wait for timeout function to complete - $this->assertEventually(function () use ($executionId, $id) { - $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.executions.{$executionId}.update")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString('functions.*.executions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString('functions.*.executions.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.*.executions.{$executionId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.*.executions.{$executionId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.executions.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.executions.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.executions.{$executionId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.executions.{$executionId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); - }, 30000, 500); - } - - public function testDeleteDeployment(): void - { - // Set up a function with deployment - $data = $this->setupFunction(); - $deploymentData = $this->setupDeployment($data['functionId']); - $id = $deploymentData['functionId']; - $deploymentId = $deploymentData['deploymentId']; - /** - * Test for SUCCESS - */ - $deployment = $this->client->call(Client::METHOD_DELETE, '/functions/' . $id . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(204, $deployment['headers']['status-code']); - $this->assertEmpty($deployment['body']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.deployments.{$deploymentId}.delete")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString('functions.*.deployments.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString('functions.*.deployments.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.*.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.*.deployments.{$deploymentId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.deployments.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.deployments.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); - } - - public function testDeleteFunction(): void - { - // Set up a function - $data = $this->setupFunction(); - $id = $data['functionId']; - - /** - * Test for SUCCESS - */ - $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $id, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(204, $function['headers']['status-code']); - $this->assertEmpty($function['body']); - - $webhook = $this->getLastRequest($this->webhookEventProbe("functions.{$id}.delete")); - $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']); - - $this->assertEquals('POST', $webhook['method']); - $this->assertEquals('application/json', $webhook['headers']['Content-Type']); - $this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']); - // $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString('functions.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - // $this->assertStringContainsString("functions.{$id}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']); TODO @christyjacob4 : enable test once we allow functions.* 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']); - } }