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/errors.php b/app/config/errors.php
index e8519fd797..278dbb3458 100644
--- a/app/config/errors.php
+++ b/app/config/errors.php
@@ -1144,6 +1144,11 @@ return [
'description' => 'Webhook with the requested ID could not be found.',
'code' => 404,
],
+ Exception::WEBHOOK_ALREADY_EXISTS => [
+ 'name' => Exception::WEBHOOK_ALREADY_EXISTS,
+ 'description' => 'Webhook with the same ID already exists. Try again with a different ID.',
+ 'code' => 409,
+ ],
Exception::KEY_NOT_FOUND => [
'name' => Exception::KEY_NOT_FOUND,
'description' => 'Key with the requested ID could not be found.',
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/phpunit.xml b/phpunit.xml
index 030d89af8d..9ccbaf47cc 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -34,6 +34,7 @@
./tests/e2e/Services/Storage
./tests/e2e/Services/Tokens
./tests/e2e/Services/Webhooks
+ ./tests/e2e/Services/ProjectWebhooks
./tests/e2e/Services/Messaging
./tests/e2e/Services/Migrations
./tests/e2e/Services/Functions/FunctionsBase.php
diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php
index 95a9c6ddac..a54edf7074 100644
--- a/src/Appwrite/Extend/Exception.php
+++ b/src/Appwrite/Extend/Exception.php
@@ -307,6 +307,7 @@ class Exception extends \Exception
/** Webhooks */
public const string WEBHOOK_NOT_FOUND = 'webhook_not_found';
+ public const string WEBHOOK_ALREADY_EXISTS = 'webhook_already_exists';
/** Router */
public const string ROUTER_HOST_NOT_FOUND = 'router_host_not_found';
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/Init.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Init.php
new file mode 100644
index 0000000000..3a14a12ffb
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Init.php
@@ -0,0 +1,32 @@
+setType(Action::TYPE_INIT)
+ ->groups(['webhooks'])
+ ->inject('project')
+ ->callback(function (Document $project) {
+ if ($project->getId() === 'console') {
+ throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN);
+ }
+
+ if ($project->isEmpty()) {
+ throw new Exception(Exception::PROJECT_NOT_FOUND);
+ }
+ });
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Create.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Create.php
new file mode 100644
index 0000000000..261571a37b
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Create.php
@@ -0,0 +1,128 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/webhooks')
+ ->desc('Create webhook')
+ ->groups(['api', 'webhooks'])
+ ->label('scope', 'webhooks.write')
+ ->label('event', 'webhooks.[webhookId].create')
+ ->label('audits.event', 'webhook.create')
+ ->label('audits.resource', 'webhook/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'webhooks',
+ group: null,
+ name: 'create',
+ description: <<param('webhookId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
+ ->param('url', '', fn () => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.')
+ ->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
+ ->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
+ ->param('enabled', true, new Boolean(), 'Enable or disable a webhook.', true)
+ ->param('security', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
+ ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
+ ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
+ ->inject('response')
+ ->inject('project')
+ ->inject('queueForEvents')
+ ->inject('dbForPlatform')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+
+ /**
+ * @param array $events
+ */
+ public function action(
+ string $webhookId,
+ string $url,
+ string $name,
+ array $events,
+ bool $enabled,
+ bool $security,
+ string $httpUser,
+ string $httpPass,
+ Response $response,
+ Document $project,
+ QueueEvent $queueForEvents,
+ Database $dbForPlatform,
+ Authorization $authorization
+ ) {
+ $webhookId = ($webhookId == 'unique()') ? ID::unique() : $webhookId;
+
+ $webhook = new Document([
+ '$id' => $webhookId,
+ '$permissions' => [],
+ 'projectInternalId' => $project->getSequence(),
+ 'projectId' => $project->getId(),
+ 'name' => $name,
+ 'events' => $events,
+ 'url' => $url,
+ 'security' => $security,
+ 'httpUser' => $httpUser,
+ 'httpPass' => $httpPass,
+ 'signatureKey' => \bin2hex(\random_bytes(64)),
+ 'enabled' => $enabled,
+ ]);
+
+ try {
+ $webhook = $authorization->skip(fn () => $dbForPlatform->createDocument('webhooks', $webhook));
+ } catch (DuplicateException) {
+ throw new Exception(Exception::WEBHOOK_ALREADY_EXISTS);
+ }
+
+ $authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
+
+ $queueForEvents->setParam('webhookId', $webhook->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($webhook, Response::MODEL_WEBHOOK);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Delete.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Delete.php
new file mode 100644
index 0000000000..c63a558b06
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Delete.php
@@ -0,0 +1,93 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/webhooks/:webhookId')
+ ->desc('Delete webhook')
+ ->groups(['api', 'webhooks'])
+ ->label('scope', 'webhooks.write')
+ ->label('event', 'webhooks.[webhookId].delete')
+ ->label('audits.event', 'webhook.delete')
+ ->label('audits.resource', 'webhook/{request.webhookId}')
+ ->label('sdk', new Method(
+ namespace: 'webhooks',
+ group: null,
+ name: 'delete',
+ description: <<param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook ID.', false, ['dbForPlatform'])
+ ->inject('project')
+ ->inject('response')
+ ->inject('dbForPlatform')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $webhookId,
+ Document $project,
+ Response $response,
+ Database $dbForPlatform,
+ Event $queueForEvents,
+ Authorization $authorization
+ ) {
+ $webhook = $authorization->skip(fn () => $dbForPlatform->findOne('webhooks', [
+ Query::equal('$id', [$webhookId]),
+ Query::equal('projectInternalId', [$project->getSequence()]),
+ ]));
+
+ if ($webhook->isEmpty()) {
+ throw new Exception(Exception::WEBHOOK_NOT_FOUND);
+ }
+
+ if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('webhooks', $webhook->getId()))) {
+ throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove document from DB');
+ }
+
+ $authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
+
+ $queueForEvents->setParam('webhookId', $webhook->getId());
+
+ $response->noContent();
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Get.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Get.php
new file mode 100644
index 0000000000..229db1924d
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Get.php
@@ -0,0 +1,77 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/webhooks/:webhookId')
+ ->desc('Get webhook')
+ ->groups(['api', 'webhooks'])
+ ->label('scope', 'webhooks.read')
+ ->label('sdk', new Method(
+ namespace: 'webhooks',
+ group: null,
+ name: 'get',
+ description: <<param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook ID.', false, ['dbForPlatform'])
+ ->inject('project')
+ ->inject('response')
+ ->inject('dbForPlatform')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $webhookId,
+ Document $project,
+ Response $response,
+ Database $dbForPlatform,
+ Authorization $authorization
+ ) {
+ $webhook = $authorization->skip(fn () => $dbForPlatform->findOne('webhooks', [
+ Query::equal('$id', [$webhookId]),
+ Query::equal('projectInternalId', [$project->getSequence()]),
+ ]));
+
+ if ($webhook->isEmpty()) {
+ throw new Exception(Exception::WEBHOOK_NOT_FOUND);
+ }
+
+ $response->dynamic($webhook, Response::MODEL_WEBHOOK);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Signature/Update.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Signature/Update.php
new file mode 100644
index 0000000000..7995192bee
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Signature/Update.php
@@ -0,0 +1,92 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/webhooks/:webhookId/signature')
+ ->desc('Update webhook signature key')
+ ->groups(['api', 'webhooks'])
+ ->label('scope', 'webhooks.write')
+ ->label('event', 'webhooks.[webhookId].update')
+ ->label('audits.event', 'webhooks.update')
+ ->label('audits.resource', 'webhook/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'webhooks',
+ group: null,
+ name: 'updateSignature',
+ description: <<param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook ID.', false, ['dbForPlatform'])
+ ->inject('response')
+ ->inject('project')
+ ->inject('queueForEvents')
+ ->inject('dbForPlatform')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $webhookId,
+ Response $response,
+ Document $project,
+ QueueEvent $queueForEvents,
+ Database $dbForPlatform,
+ Authorization $authorization
+ ) {
+ $webhook = $authorization->skip(fn () => $dbForPlatform->findOne('webhooks', [
+ Query::equal('$id', [$webhookId]),
+ Query::equal('projectInternalId', [$project->getSequence()]),
+ ]));
+
+ if ($webhook->isEmpty()) {
+ throw new Exception(Exception::WEBHOOK_NOT_FOUND);
+ }
+
+ $updates = new Document([
+ 'signatureKey' => \bin2hex(\random_bytes(64)),
+ ]);
+
+ $webhook = $authorization->skip(fn () => $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $updates));
+
+ $authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
+
+ $queueForEvents->setParam('webhookId', $webhook->getId());
+
+ $response->dynamic($webhook, Response::MODEL_WEBHOOK);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Update.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Update.php
new file mode 100644
index 0000000000..abc2f2ef00
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Update.php
@@ -0,0 +1,123 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
+ ->setHttpPath('/v1/webhooks/:webhookId')
+ ->desc('Update webhook')
+ ->groups(['api', 'webhooks'])
+ ->label('scope', 'webhooks.write')
+ ->label('event', 'webhooks.[webhookId].update')
+ ->label('audits.event', 'webhooks.update')
+ ->label('audits.resource', 'webhook/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'webhooks',
+ group: null,
+ name: 'update',
+ description: <<param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook ID.', false, ['dbForPlatform'])
+ ->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
+ ->param('url', '', fn () => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.')
+ ->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
+ ->param('enabled', true, new Boolean(), 'Enable or disable a webhook.', true)
+ ->param('security', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
+ ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
+ ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
+ ->inject('response')
+ ->inject('project')
+ ->inject('queueForEvents')
+ ->inject('dbForPlatform')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $webhookId,
+ string $name,
+ string $url,
+ array $events,
+ bool $enabled,
+ bool $security,
+ string $httpUser,
+ string $httpPass,
+ Response $response,
+ Document $project,
+ QueueEvent $queueForEvents,
+ Database $dbForPlatform,
+ Authorization $authorization
+ ) {
+ $webhook = $authorization->skip(fn () => $dbForPlatform->findOne('webhooks', [
+ Query::equal('$id', [$webhookId]),
+ Query::equal('projectInternalId', [$project->getSequence()]),
+ ]));
+
+ if ($webhook->isEmpty()) {
+ throw new Exception(Exception::WEBHOOK_NOT_FOUND);
+ }
+
+ $updates = new Document([
+ 'name' => $name,
+ 'events' => $events,
+ 'url' => $url,
+ 'security' => $security,
+ 'httpUser' => $httpUser,
+ 'httpPass' => $httpPass,
+ 'enabled' => $enabled,
+ ]);
+
+ if ($enabled) {
+ $updates->setAttribute('attempts', 0);
+ }
+
+ $webhook = $authorization->skip(fn () => $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $updates));
+
+ $authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
+
+ $queueForEvents->setParam('webhookId', $webhook->getId());
+
+ $response->dynamic($webhook, Response::MODEL_WEBHOOK);
+ }
+}
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..35bf762ce1
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/XList.php
@@ -0,0 +1,119 @@
+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(...));
+ }
+
+ /**
+ * @param array $queries
+ */
+ public function action(
+ array $queries,
+ bool $includeTotal,
+ Document $project,
+ Response $response,
+ Database $dbForPlatform,
+ Authorization $authorization
+ ) {
+ 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->findOne('webhooks', [
+ Query::equal('$id', [$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..4805de6ebc
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Webhooks/Services/Http.php
@@ -0,0 +1,31 @@
+type = Service::TYPE_HTTP;
+
+ // Hooks
+ $this->addAction(Init::getName(), new Init());
+
+ // Webhooks
+ $this->addAction(CreateWebhook::getName(), new CreateWebhook());
+ $this->addAction(ListWebhooks::getName(), new ListWebhooks());
+ $this->addAction(GetWebhook::getName(), new GetWebhook());
+ $this->addAction(DeleteWebhook::getName(), new DeleteWebhook());
+ $this->addAction(UpdateWebhook::getName(), new UpdateWebhook());
+ $this->addAction(UpdateWebhookSignature::getName(), new UpdateWebhookSignature());
+ }
+}
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'],
+ ]);
+
+ $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..7ad701b564 100644
--- a/tests/e2e/Services/Webhooks/WebhooksBase.php
+++ b/tests/e2e/Services/Webhooks/WebhooksBase.php
@@ -3,1832 +3,1610 @@
namespace Tests\E2E\Services\Webhooks;
use Appwrite\Tests\Async;
-use Appwrite\Tests\Retry;
-use CURLFile;
use Tests\E2E\Client;
+use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
-use Utopia\Database\Helpers\Permission;
-use Utopia\Database\Helpers\Role;
+use Utopia\Database\Query;
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 testCreateWebhook(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Test Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $this->assertNotEmpty($webhook['body']['$id']);
+ $this->assertEquals('Test Webhook', $webhook['body']['name']);
+ $this->assertEquals('https://appwrite.io', $webhook['body']['url']);
+ $this->assertContains('users.*.create', $webhook['body']['events']);
+ $this->assertCount(1, $webhook['body']['events']);
+ $this->assertEquals(true, $webhook['body']['enabled']);
+ $this->assertEquals(false, $webhook['body']['security']);
+ $this->assertEquals('', $webhook['body']['httpUser']);
+ $this->assertEquals('', $webhook['body']['httpPass']);
+ $this->assertNotEmpty($webhook['body']['signatureKey']);
+ $this->assertEquals(128, \strlen($webhook['body']['signatureKey']));
+ $this->assertEquals(0, $webhook['body']['attempts']);
+ $this->assertEquals('', $webhook['body']['logs']);
+
+ $dateValidator = new DatetimeValidator();
+ $this->assertEquals(true, $dateValidator->isValid($webhook['body']['$createdAt']));
+ $this->assertEquals(true, $dateValidator->isValid($webhook['body']['$updatedAt']));
+
+ // Verify via GET
+ $get = $this->getWebhook($webhook['body']['$id']);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals($webhook['body']['$id'], $get['body']['$id']);
+ $this->assertEquals('Test Webhook', $get['body']['name']);
+
+ // Verify via LIST
+ $list = $this->listWebhooks(null, true);
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(1, $list['body']['total']);
+ $this->assertGreaterThanOrEqual(1, \count($list['body']['webhooks']));
+
+ // Cleanup
+ $this->deleteWebhook($webhook['body']['$id']);
}
- /**
- * Create a probe callback that filters webhooks by event pattern.
- */
- private function webhookEventProbe(string $eventPattern): callable
+ public function testCreateWebhookWithSecurity(): void
{
- return function (array $request) use ($eventPattern) {
- $this->assertStringContainsString(
- $eventPattern,
- $request['headers']['X-Appwrite-Webhook-Events'] ?? ''
- );
- };
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Webhook With Security',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ true,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $this->assertNotEmpty($webhook['body']['$id']);
+ $this->assertEquals(true, $webhook['body']['security']);
+ $this->assertIsBool($webhook['body']['security']);
+
+ // Cleanup
+ $this->deleteWebhook($webhook['body']['$id']);
}
- public static function getWebhookSignature(array $webhook, string $signatureKey): string
+ public function testCreateWebhookWithHttpAuth(): void
{
- $payload = json_encode($webhook['data']);
- $url = $webhook['url'];
- return base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Webhook With HTTP Auth',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ true,
+ 'username',
+ 'password'
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $this->assertNotEmpty($webhook['body']['$id']);
+ $this->assertEquals('username', $webhook['body']['httpUser']);
+ $this->assertEquals('password', $webhook['body']['httpPass']);
+ $this->assertEquals(true, $webhook['body']['security']);
+
+ // Verify via GET
+ $get = $this->getWebhook($webhook['body']['$id']);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals('username', $get['body']['httpUser']);
+
+ // Cleanup
+ $this->deleteWebhook($webhook['body']['$id']);
}
- /**
- * Creates a database and collection with proper attributes for document operations.
- *
- * @return array Array containing 'databaseId' and 'actorsId'
- */
- protected function setupCollectionWithAttributes(): array
+ public function testCreateWebhookEnabled(): 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',
- ]);
+ // Create disabled webhook
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Disabled Webhook',
+ ['users.*.create'],
+ false,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
- $databaseId = $database['body']['$id'];
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $this->assertEquals(false, $webhook['body']['enabled']);
+ $this->assertIsBool($webhook['body']['enabled']);
- // 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,
- ]);
+ // Cleanup
+ $this->deleteWebhook($webhook['body']['$id']);
- $actorsId = $actors['body']['$id'];
+ // Create enabled webhook explicitly
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Enabled Webhook',
+ ['users.*.create'],
+ true,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
- // 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->assertEquals(201, $webhook['headers']['status-code']);
+ $this->assertEquals(true, $webhook['body']['enabled']);
- $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];
+ // Cleanup
+ $this->deleteWebhook($webhook['body']['$id']);
}
- /**
- * Creates a database and table with proper columns for row operations.
- *
- * @return array Array containing 'databaseId' and 'actorsId'
- */
- protected function setupTableWithColumns(): array
+ public function testCreateWebhookWithoutAuthentication(): void
{
- // Create database
- $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
+ $response = $this->client->call(Client::METHOD_POST, '/webhooks', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'databaseId' => ID::unique(),
- 'name' => 'Actors DB',
+ ], [
+ 'webhookId' => ID::unique(),
+ 'name' => 'Test Webhook',
+ 'events' => ['users.*.create'],
+ 'url' => 'https://appwrite.io',
]);
- $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];
+ $this->assertEquals(401, $response['headers']['status-code']);
}
- /**
- * Creates an enabled storage bucket.
- *
- * @return array Array containing 'bucketId'
- */
- protected function setupStorageBucket(): array
+ public function testCreateWebhookInvalidId(): void
{
- $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,
- ]);
+ $webhook = $this->createWebhook(
+ '!invalid-id!',
+ 'Test Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
- return ['bucketId' => $bucket['body']['$id']];
+ $this->assertEquals(400, $webhook['headers']['status-code']);
}
- /**
- * Creates a team and returns its ID.
- *
- * @param string $name Team name
- * @return array Array containing 'teamId'
- */
- protected function setupTeam(string $name = 'Arsenal'): array
+ public function testCreateWebhookMissingName(): void
{
- $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
+ $response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
- 'teamId' => ID::unique(),
- 'name' => $name
+ 'webhookId' => ID::unique(),
+ 'events' => ['users.*.create'],
+ 'url' => 'https://appwrite.io',
]);
- return ['teamId' => $team['body']['$id']];
+ $this->assertEquals(400, $response['headers']['status-code']);
}
- /**
- * 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
+ public function testCreateWebhookMissingUrl(): void
{
- $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([
+ $response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
- 'userId' => ID::unique(),
- 'email' => $email,
- 'password' => 'password',
- 'name' => 'Friend User',
+ 'webhookId' => ID::unique(),
+ 'name' => 'Test Webhook',
+ 'events' => ['users.*.create'],
]);
- // Create membership
- $team = $this->client->call(Client::METHOD_POST, '/teams/' . $teamId . '/memberships', array_merge([
+ $this->assertEquals(400, $response['headers']['status-code']);
+ }
+
+ public function testCreateWebhookMissingEvents(): void
+ {
+ $response = $this->client->call(Client::METHOD_POST, '/webhooks', 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'
+ 'webhookId' => ID::unique(),
+ 'name' => 'Test Webhook',
+ 'url' => 'https://appwrite.io',
]);
- $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,
- ];
+ $this->assertEquals(400, $response['headers']['status-code']);
}
- /**
- * 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
+ public function testCreateWebhookDuplicateId(): void
{
- $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
+ $webhookId = ID::unique();
+
+ $webhook = $this->createWebhook(
+ $webhookId,
+ 'Test Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+
+ // Attempt to create with same ID
+ $duplicate = $this->createWebhook(
+ $webhookId,
+ 'Duplicate Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(409, $duplicate['headers']['status-code']);
+ $this->assertEquals('webhook_already_exists', $duplicate['body']['type']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
+
+ public function testCreateWebhookAudit(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Audit Webhook',
+ ['users.*.create', 'users.*.update.email'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $this->assertNotEmpty($webhook['body']['$id']);
+ $this->assertContains('users.*.create', $webhook['body']['events']);
+ $this->assertContains('users.*.update.email', $webhook['body']['events']);
+ $this->assertCount(2, $webhook['body']['events']);
+
+ // Cleanup
+ $this->deleteWebhook($webhook['body']['$id']);
+ }
+
+ public function testUpdateWebhook(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Original Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Update the webhook
+ $updated = $this->updateWebhook(
+ $webhookId,
+ 'Updated Webhook',
+ ['users.*.delete', 'users.*.sessions.*.delete'],
+ null,
+ 'https://appwrite.io/new',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(200, $updated['headers']['status-code']);
+ $this->assertEquals($webhookId, $updated['body']['$id']);
+ $this->assertEquals('Updated Webhook', $updated['body']['name']);
+ $this->assertEquals('https://appwrite.io/new', $updated['body']['url']);
+ $this->assertContains('users.*.delete', $updated['body']['events']);
+ $this->assertContains('users.*.sessions.*.delete', $updated['body']['events']);
+ $this->assertCount(2, $updated['body']['events']);
+
+ // Verify update persisted via GET
+ $get = $this->getWebhook($webhookId);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals('Updated Webhook', $get['body']['name']);
+ $this->assertEquals('https://appwrite.io/new', $get['body']['url']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
+
+ public function testUpdateWebhookWithSecurity(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Security Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ false,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $this->assertEquals(false, $webhook['body']['security']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Update to enable security
+ $updated = $this->updateWebhook(
+ $webhookId,
+ 'Security Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ true,
+ null,
+ null
+ );
+
+ $this->assertEquals(200, $updated['headers']['status-code']);
+ $this->assertEquals(true, $updated['body']['security']);
+ $this->assertIsBool($updated['body']['security']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
+
+ public function testUpdateWebhookWithHttpAuth(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'HTTP Auth Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ true,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $this->assertEquals('', $webhook['body']['httpUser']);
+ $this->assertEquals('', $webhook['body']['httpPass']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Update with HTTP auth credentials
+ $updated = $this->updateWebhook(
+ $webhookId,
+ 'HTTP Auth Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ true,
+ 'newuser',
+ 'newpass'
+ );
+
+ $this->assertEquals(200, $updated['headers']['status-code']);
+ $this->assertEquals('newuser', $updated['body']['httpUser']);
+ $this->assertEquals('newpass', $updated['body']['httpPass']);
+
+ // Verify via GET
+ $get = $this->getWebhook($webhookId);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals('newuser', $get['body']['httpUser']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
+
+ public function testUpdateWebhookEnabled(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Enabled Webhook',
+ ['users.*.create'],
+ true,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $this->assertEquals(true, $webhook['body']['enabled']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Disable the webhook
+ $updated = $this->updateWebhook(
+ $webhookId,
+ 'Enabled Webhook',
+ ['users.*.create'],
+ false,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(200, $updated['headers']['status-code']);
+ $this->assertEquals(false, $updated['body']['enabled']);
+ $this->assertIsBool($updated['body']['enabled']);
+
+ // Re-enable the webhook (should reset attempts to 0)
+ $updated = $this->updateWebhook(
+ $webhookId,
+ 'Enabled Webhook',
+ ['users.*.create'],
+ true,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(200, $updated['headers']['status-code']);
+ $this->assertEquals(true, $updated['body']['enabled']);
+ $this->assertEquals(0, $updated['body']['attempts']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
+
+ public function testUpdateWebhookWithoutAuthentication(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Auth Test Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Attempt update without authentication
+ $response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'name' => 'Updated Webhook',
+ 'events' => ['users.*.create'],
+ 'url' => 'https://appwrite.io',
+ ]);
+
+ $this->assertEquals(401, $response['headers']['status-code']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
+
+ public function testUpdateWebhookInvalidId(): void
+ {
+ $updated = $this->updateWebhook(
+ 'non-existent-id',
+ 'Updated Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(404, $updated['headers']['status-code']);
+ $this->assertEquals('webhook_not_found', $updated['body']['type']);
+ }
+
+ public function testUpdateWebhookMissingName(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Missing Name Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ $response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, 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()),
- ],
+ 'events' => ['users.*.create'],
+ 'url' => 'https://appwrite.io',
]);
- return ['documentId' => $document['body']['$id']];
+ $this->assertEquals(400, $response['headers']['status-code']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
}
- /**
- * 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
+ public function testUpdateWebhookMissingUrl(): void
{
- $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Missing URL Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ $response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, 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()),
- ],
+ 'name' => 'Missing URL Webhook',
+ 'events' => ['users.*.create'],
]);
- return ['rowId' => $row['body']['$id']];
+ $this->assertEquals(400, $response['headers']['status-code']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
}
- /**
- * 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
+ public function testUpdateWebhookMissingEvents(): void
{
- $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'),
- ]);
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Missing Events Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
- return ['fileId' => $file['body']['$id']];
- }
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['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([
+ $response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, 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()),
- ],
+ 'name' => 'Missing Events Webhook',
+ 'url' => 'https://appwrite.io',
]);
- $documentId = $document['body']['$id'];
+ $this->assertEquals(400, $response['headers']['status-code']);
- $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']);
+ // Cleanup
+ $this->deleteWebhook($webhookId);
}
- public function testUpdateDocument(): void
+ public function testUpdateWebhookDuplicateId(): 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'];
+ // Update endpoint doesn't change the ID, so this tests updating a non-existent webhook
+ $updated = $this->updateWebhook(
+ 'non-existent-id',
+ 'Duplicate Test Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
- /**
- * 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']);
+ $this->assertEquals(404, $updated['headers']['status-code']);
+ $this->assertEquals('webhook_not_found', $updated['body']['type']);
}
- #[Retry(count: 1)]
- public function testDeleteDocument(): void
+ public function testUpdateWebhookAudit(): void
{
- // Set up collection with attributes
- $data = $this->setupCollectionWithAttributes();
- $actorsId = $data['actorsId'];
- $databaseId = $data['databaseId'];
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Audit Update Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
- /**
- * 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',
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
- ],
- 'permissions' => [
- Permission::read(Role::any()),
- Permission::update(Role::any()),
- Permission::delete(Role::any()),
- ],
- ]);
+ // Update with multiple events
+ $updated = $this->updateWebhook(
+ $webhookId,
+ 'Audit Update Webhook Updated',
+ ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.create'],
+ null,
+ 'https://appwrite.io/updated',
+ true,
+ 'user',
+ 'pass'
+ );
- $documentId = $document['body']['$id'];
+ $this->assertEquals(200, $updated['headers']['status-code']);
+ $this->assertEquals($webhookId, $updated['body']['$id']);
+ $this->assertEquals('Audit Update Webhook Updated', $updated['body']['name']);
+ $this->assertContains('users.*.delete', $updated['body']['events']);
+ $this->assertContains('users.*.sessions.*.delete', $updated['body']['events']);
+ $this->assertContains('buckets.*.files.*.create', $updated['body']['events']);
+ $this->assertCount(3, $updated['body']['events']);
+ $this->assertEquals('https://appwrite.io/updated', $updated['body']['url']);
+ $this->assertEquals(true, $updated['body']['security']);
+ $this->assertEquals('user', $updated['body']['httpUser']);
+ $this->assertEquals('pass', $updated['body']['httpPass']);
- $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']);
+ // Cleanup
+ $this->deleteWebhook($webhookId);
}
- // Table APIs
- public function testCreateTable(): void
+ public function testUpdateWebhookSignature(): 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',
- ]);
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Signature Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
- $databaseId = $database['body']['$id'];
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+ $originalSignatureKey = $webhook['body']['signatureKey'];
- /**
- * 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,
- ]);
+ $this->assertNotEmpty($originalSignatureKey);
+ $this->assertEquals(128, \strlen($originalSignatureKey));
- $actorsId = $actors['body']['$id'];
+ // Update signature
+ $updated = $this->updateWebhookSignature($webhookId);
- $this->assertEquals($actors['headers']['status-code'], 201);
- $this->assertNotEmpty($actors['body']['$id']);
+ $this->assertEquals(200, $updated['headers']['status-code']);
+ $this->assertEquals($webhookId, $updated['body']['$id']);
+ $this->assertNotEmpty($updated['body']['signatureKey']);
+ $this->assertEquals(128, \strlen($updated['body']['signatureKey']));
+ $this->assertNotEquals($originalSignatureKey, $updated['body']['signatureKey']);
- $webhook = $this->getLastRequest($this->webhookEventProbe("databases.{$databaseId}.tables.{$actorsId}.create"));
- $signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
+ // Verify new signature persisted via GET
+ $get = $this->getWebhook($webhookId);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertNotEquals($originalSignatureKey, $get['body']['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']);
+ // Test signature update on non-existent webhook
+ $notFound = $this->updateWebhookSignature('non-existent-id');
+ $this->assertEquals(404, $notFound['headers']['status-code']);
+ $this->assertEquals('webhook_not_found', $notFound['body']['type']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
}
- 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']);
- }
+ // URL validation tests
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,
- ]);
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Private Domain Webhook',
+ ['users.*.create'],
+ null,
+ 'http://localhost/webhook',
+ null,
+ null,
+ null
+ );
$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,
- ]);
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Private Domain Update Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Attempt to update URL to private domain
+ $updated = $this->updateWebhook(
+ $webhookId,
+ 'Private Domain Update Webhook',
+ ['users.*.create'],
+ null,
+ 'http://localhost/webhook',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(400, $updated['headers']['status-code']);
+
+ // Verify original URL unchanged
+ $get = $this->getWebhook($webhookId);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals('https://appwrite.io', $get['body']['url']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
+
+ public function testCreateWebhookInvalidUrlScheme(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Invalid Scheme Webhook',
+ ['users.*.create'],
+ null,
+ 'invalid://appwrite.io',
+ null,
+ null,
+ null
+ );
$this->assertEquals(400, $webhook['headers']['status-code']);
}
- public function testWebhookAutoDisable(): void
+ public function testUpdateWebhookInvalidUrlScheme(): void
{
- $projectId = $this->getProject()['$id'];
- $webhookId = $this->getProject()['webhookId'];
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Scheme Update Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
- // Create a database for this test
- $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Attempt to update URL to invalid scheme
+ $updated = $this->updateWebhook(
+ $webhookId,
+ 'Scheme Update Webhook',
+ ['users.*.create'],
+ null,
+ 'invalid://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(400, $updated['headers']['status-code']);
+
+ // Verify original URL unchanged
+ $get = $this->getWebhook($webhookId);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals('https://appwrite.io', $get['body']['url']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
+
+ // Event validation tests
+
+ public function testCreateWebhookInvalidEvents(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Invalid Events Webhook',
+ ['account.unknown'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(400, $webhook['headers']['status-code']);
+ }
+
+ public function testUpdateWebhookInvalidEvents(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Invalid Events Update Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Attempt to update with invalid event
+ $updated = $this->updateWebhook(
+ $webhookId,
+ 'Invalid Events Update Webhook',
+ ['account.unknown'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(400, $updated['headers']['status-code']);
+
+ // Verify original events unchanged
+ $get = $this->getWebhook($webhookId);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertContains('users.*.create', $get['body']['events']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
+
+ // Custom ID test
+
+ public function testCreateWebhookCustomId(): void
+ {
+ $customId = 'my-custom-webhook-id';
+
+ $webhook = $this->createWebhook(
+ $customId,
+ 'Custom ID Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $this->assertEquals($customId, $webhook['body']['$id']);
+
+ // Verify via GET
+ $get = $this->getWebhook($customId);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals($customId, $get['body']['$id']);
+
+ // Cleanup
+ $this->deleteWebhook($customId);
+ }
+
+ // Get webhook tests
+
+ public function testGetWebhook(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Get Test Webhook',
+ ['users.*.create', 'users.*.update.email'],
+ null,
+ 'https://appwrite.io',
+ true,
+ 'myuser',
+ 'mypass'
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ $get = $this->getWebhook($webhookId);
+
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals($webhookId, $get['body']['$id']);
+ $this->assertEquals('Get Test Webhook', $get['body']['name']);
+ $this->assertEquals('https://appwrite.io', $get['body']['url']);
+ $this->assertContains('users.*.create', $get['body']['events']);
+ $this->assertContains('users.*.update.email', $get['body']['events']);
+ $this->assertCount(2, $get['body']['events']);
+ $this->assertEquals(true, $get['body']['enabled']);
+ $this->assertEquals(true, $get['body']['security']);
+ $this->assertEquals('myuser', $get['body']['httpUser']);
+ $this->assertEquals('mypass', $get['body']['httpPass']);
+ $this->assertNotEmpty($get['body']['signatureKey']);
+ $this->assertEquals(128, \strlen($get['body']['signatureKey']));
+ $this->assertEquals(0, $get['body']['attempts']);
+ $this->assertEquals('', $get['body']['logs']);
+
+ $dateValidator = new DatetimeValidator();
+ $this->assertEquals(true, $dateValidator->isValid($get['body']['$createdAt']));
+ $this->assertEquals(true, $dateValidator->isValid($get['body']['$updatedAt']));
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
+
+ public function testGetWebhookNotFound(): void
+ {
+ $get = $this->getWebhook('non-existent-id');
+
+ $this->assertEquals(404, $get['headers']['status-code']);
+ $this->assertEquals('webhook_not_found', $get['body']['type']);
+ }
+
+ public function testGetWebhookWithoutAuthentication(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Auth Get Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Attempt GET without authentication
+ $response = $this->client->call(Client::METHOD_GET, '/webhooks/' . $webhookId, [
'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'];
+ $this->assertEquals(401, $response['headers']['status-code']);
- $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,
- ]);
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
- $this->assertEquals(200, $webhook['headers']['status-code']);
- $this->assertNotEmpty($webhook['body']);
+ // List webhooks tests
- // 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,
- ]);
+ public function testListWebhooks(): void
+ {
+ // Create multiple webhooks
+ $webhook1 = $this->createWebhook(
+ ID::unique(),
+ 'List Webhook Alpha',
+ ['users.*.create'],
+ true,
+ 'https://appwrite.io/alpha',
+ false,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook1['headers']['status-code']);
- $this->assertEquals($newCollection['headers']['status-code'], 201);
- $this->assertNotEmpty($newCollection['body']['$id']);
+ $webhook2 = $this->createWebhook(
+ ID::unique(),
+ 'List Webhook Beta',
+ ['users.*.delete'],
+ false,
+ 'https://appwrite.io/beta',
+ true,
+ 'user',
+ 'pass'
+ );
+ $this->assertEquals(201, $webhook2['headers']['status-code']);
+
+ $webhook3 = $this->createWebhook(
+ ID::unique(),
+ 'List Webhook Gamma',
+ ['users.*.create', 'users.*.delete'],
+ true,
+ 'https://appwrite.io/gamma',
+ false,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook3['headers']['status-code']);
+
+ // List all
+ $list = $this->listWebhooks(null, true);
+
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(3, $list['body']['total']);
+ $this->assertGreaterThanOrEqual(3, \count($list['body']['webhooks']));
+ $this->assertIsArray($list['body']['webhooks']);
+
+ // Verify structure of returned webhooks
+ foreach ($list['body']['webhooks'] as $webhook) {
+ $this->assertArrayHasKey('$id', $webhook);
+ $this->assertArrayHasKey('$createdAt', $webhook);
+ $this->assertArrayHasKey('$updatedAt', $webhook);
+ $this->assertArrayHasKey('name', $webhook);
+ $this->assertArrayHasKey('url', $webhook);
+ $this->assertArrayHasKey('events', $webhook);
+ $this->assertArrayHasKey('security', $webhook);
+ $this->assertArrayHasKey('enabled', $webhook);
+ $this->assertArrayHasKey('signatureKey', $webhook);
+ $this->assertArrayHasKey('attempts', $webhook);
+ $this->assertArrayHasKey('logs', $webhook);
}
- $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',
- ]));
+ // Cleanup
+ $this->deleteWebhook($webhook1['body']['$id']);
+ $this->deleteWebhook($webhook2['body']['$id']);
+ $this->deleteWebhook($webhook3['body']['$id']);
+ }
- // 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);
+ public function testListWebhooksWithLimit(): void
+ {
+ $webhook1 = $this->createWebhook(
+ ID::unique(),
+ 'Limit Webhook 1',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io/one',
+ null,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook1['headers']['status-code']);
+
+ $webhook2 = $this->createWebhook(
+ ID::unique(),
+ 'Limit Webhook 2',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io/two',
+ null,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook2['headers']['status-code']);
+
+ // List with limit of 1
+ $list = $this->listWebhooks([
+ Query::limit(1)->toString(),
+ ], true);
+
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertCount(1, $list['body']['webhooks']);
+ $this->assertGreaterThanOrEqual(2, $list['body']['total']);
+
+ // Cleanup
+ $this->deleteWebhook($webhook1['body']['$id']);
+ $this->deleteWebhook($webhook2['body']['$id']);
+ }
+
+ public function testListWebhooksWithOffset(): void
+ {
+ $webhook1 = $this->createWebhook(
+ ID::unique(),
+ 'Offset Webhook 1',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io/one',
+ null,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook1['headers']['status-code']);
+
+ $webhook2 = $this->createWebhook(
+ ID::unique(),
+ 'Offset Webhook 2',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io/two',
+ null,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook2['headers']['status-code']);
+
+ // List all to get total
+ $listAll = $this->listWebhooks(null, true);
+ $this->assertEquals(200, $listAll['headers']['status-code']);
+ $totalAll = \count($listAll['body']['webhooks']);
+
+ // List with offset
+ $listOffset = $this->listWebhooks([
+ Query::offset(1)->toString(),
+ ], true);
+
+ $this->assertEquals(200, $listOffset['headers']['status-code']);
+ $this->assertCount($totalAll - 1, $listOffset['body']['webhooks']);
+
+ // Cleanup
+ $this->deleteWebhook($webhook1['body']['$id']);
+ $this->deleteWebhook($webhook2['body']['$id']);
+ }
+
+ public function testListWebhooksFilterByName(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'UniqueFilterName-XYZ',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+
+ $list = $this->listWebhooks([
+ Query::equal('name', ['UniqueFilterName-XYZ'])->toString(),
+ ], true);
+
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertEquals(1, $list['body']['total']);
+ $this->assertCount(1, $list['body']['webhooks']);
+ $this->assertEquals('UniqueFilterName-XYZ', $list['body']['webhooks'][0]['name']);
+
+ // Cleanup
+ $this->deleteWebhook($webhook['body']['$id']);
+ }
+
+ public function testListWebhooksFilterByEnabled(): void
+ {
+ $webhookEnabled = $this->createWebhook(
+ ID::unique(),
+ 'Enabled Filter Webhook',
+ ['users.*.create'],
+ true,
+ 'https://appwrite.io/enabled',
+ null,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhookEnabled['headers']['status-code']);
+
+ $webhookDisabled = $this->createWebhook(
+ ID::unique(),
+ 'Disabled Filter Webhook',
+ ['users.*.create'],
+ false,
+ 'https://appwrite.io/disabled',
+ null,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhookDisabled['headers']['status-code']);
+
+ // Filter by enabled=true
+ $listEnabled = $this->listWebhooks([
+ Query::equal('enabled', [true])->toString(),
+ ], true);
+
+ $this->assertEquals(200, $listEnabled['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(1, $listEnabled['body']['total']);
+ foreach ($listEnabled['body']['webhooks'] as $webhook) {
+ $this->assertEquals(true, $webhook['enabled']);
+ }
+
+ // Filter by enabled=false
+ $listDisabled = $this->listWebhooks([
+ Query::equal('enabled', [false])->toString(),
+ ], true);
+
+ $this->assertEquals(200, $listDisabled['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(1, $listDisabled['body']['total']);
+ foreach ($listDisabled['body']['webhooks'] as $webhook) {
+ $this->assertEquals(false, $webhook['enabled']);
+ }
+
+ // Cleanup
+ $this->deleteWebhook($webhookEnabled['body']['$id']);
+ $this->deleteWebhook($webhookDisabled['body']['$id']);
+ }
+
+ public function testListWebhooksFilterByUrl(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'URL Filter Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io/unique-url-filter',
+ null,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+
+ $list = $this->listWebhooks([
+ Query::equal('url', ['https://appwrite.io/unique-url-filter'])->toString(),
+ ], true);
+
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertEquals(1, $list['body']['total']);
+ $this->assertCount(1, $list['body']['webhooks']);
+ $this->assertEquals('https://appwrite.io/unique-url-filter', $list['body']['webhooks'][0]['url']);
+
+ // Cleanup
+ $this->deleteWebhook($webhook['body']['$id']);
+ }
+
+ public function testListWebhooksFilterBySecurity(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Security Filter Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io/sec',
+ true,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+
+ $list = $this->listWebhooks([
+ Query::equal('security', [true])->toString(),
+ ], true);
+
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(1, $list['body']['total']);
+ foreach ($list['body']['webhooks'] as $w) {
+ $this->assertEquals(true, $w['security']);
+ }
+
+ // Cleanup
+ $this->deleteWebhook($webhook['body']['$id']);
+ }
+
+ public function testListWebhooksWithoutTotal(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'No Total Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io/nototal',
+ null,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+
+ // List with total=false
+ $list = $this->listWebhooks(null, false);
+
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertEquals(0, $list['body']['total']);
+ $this->assertGreaterThanOrEqual(1, \count($list['body']['webhooks']));
+
+ // Cleanup
+ $this->deleteWebhook($webhook['body']['$id']);
+ }
+
+ public function testListWebhooksCursorPagination(): void
+ {
+ $webhook1 = $this->createWebhook(
+ ID::unique(),
+ 'Cursor Webhook 1',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io/cursor1',
+ null,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook1['headers']['status-code']);
+
+ $webhook2 = $this->createWebhook(
+ ID::unique(),
+ 'Cursor Webhook 2',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io/cursor2',
+ null,
+ null,
+ null
+ );
+ $this->assertEquals(201, $webhook2['headers']['status-code']);
+
+ // Get first page with limit 1
+ $page1 = $this->listWebhooks([
+ Query::limit(1)->toString(),
+ ], true);
+
+ $this->assertEquals(200, $page1['headers']['status-code']);
+ $this->assertCount(1, $page1['body']['webhooks']);
+ $cursorId = $page1['body']['webhooks'][0]['$id'];
+
+ // Get next page using cursor
+ $page2 = $this->listWebhooks([
+ Query::limit(1)->toString(),
+ Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(),
+ ], true);
+
+ $this->assertEquals(200, $page2['headers']['status-code']);
+ $this->assertCount(1, $page2['body']['webhooks']);
+ $this->assertNotEquals($cursorId, $page2['body']['webhooks'][0]['$id']);
+
+ // Cleanup
+ $this->deleteWebhook($webhook1['body']['$id']);
+ $this->deleteWebhook($webhook2['body']['$id']);
+ }
+
+ public function testListWebhooksWithoutAuthentication(): void
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/webhooks', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]);
+
+ $this->assertEquals(401, $response['headers']['status-code']);
+ }
+
+ public function testListWebhooksInvalidCursor(): void
+ {
+ $list = $this->listWebhooks([
+ Query::cursorAfter(new Document(['$id' => 'non-existent-id']))->toString(),
+ ], true);
+
+ $this->assertEquals(400, $list['headers']['status-code']);
+ }
+
+ // Delete webhook tests
+
+ public function testDeleteWebhook(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Delete Test Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Verify it exists
+ $get = $this->getWebhook($webhookId);
+ $this->assertEquals(200, $get['headers']['status-code']);
+
+ // Delete
+ $delete = $this->deleteWebhook($webhookId);
+ $this->assertEquals(204, $delete['headers']['status-code']);
+ $this->assertEmpty($delete['body']);
+
+ // Verify it no longer exists
+ $get = $this->getWebhook($webhookId);
+ $this->assertEquals(404, $get['headers']['status-code']);
+ $this->assertEquals('webhook_not_found', $get['body']['type']);
+ }
+
+ public function testDeleteWebhookNotFound(): void
+ {
+ $delete = $this->deleteWebhook('non-existent-id');
+
+ $this->assertEquals(404, $delete['headers']['status-code']);
+ $this->assertEquals('webhook_not_found', $delete['body']['type']);
+ }
+
+ public function testDeleteWebhookWithoutAuthentication(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Delete Auth Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Attempt DELETE without authentication
+ $response = $this->client->call(Client::METHOD_DELETE, '/webhooks/' . $webhookId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]);
+
+ $this->assertEquals(401, $response['headers']['status-code']);
+
+ // Verify it still exists
+ $get = $this->getWebhook($webhookId);
+ $this->assertEquals(200, $get['headers']['status-code']);
+
+ // Cleanup
+ $this->deleteWebhook($webhookId);
+ }
+
+ public function testDeleteWebhookRemovedFromList(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Delete List Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ // Get list count before delete
+ $listBefore = $this->listWebhooks(null, true);
+ $this->assertEquals(200, $listBefore['headers']['status-code']);
+ $countBefore = $listBefore['body']['total'];
+
+ // Delete
+ $delete = $this->deleteWebhook($webhookId);
+ $this->assertEquals(204, $delete['headers']['status-code']);
+
+ // Get list count after delete
+ $listAfter = $this->listWebhooks(null, true);
+ $this->assertEquals(200, $listAfter['headers']['status-code']);
+ $this->assertEquals($countBefore - 1, $listAfter['body']['total']);
+
+ // Verify the deleted webhook is not in the list
+ $ids = \array_column($listAfter['body']['webhooks'], '$id');
+ $this->assertNotContains($webhookId, $ids);
+ }
+
+ public function testDeleteWebhookDoubleDelete(): void
+ {
+ $webhook = $this->createWebhook(
+ ID::unique(),
+ 'Double Delete Webhook',
+ ['users.*.create'],
+ null,
+ 'https://appwrite.io',
+ null,
+ null,
+ null
+ );
+
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+ $webhookId = $webhook['body']['$id'];
+
+ // First delete succeeds
+ $delete = $this->deleteWebhook($webhookId);
+ $this->assertEquals(204, $delete['headers']['status-code']);
+
+ // Second delete returns 404
+ $delete = $this->deleteWebhook($webhookId);
+ $this->assertEquals(404, $delete['headers']['status-code']);
+ $this->assertEquals('webhook_not_found', $delete['body']['type']);
+ }
+
+ // Helpers
+
+ /**
+ * @param array|null $queries
+ */
+ protected function listWebhooks(?array $queries, ?bool $total): mixed
+ {
+ $webhooks = $this->client->call(Client::METHOD_GET, '/webhooks', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => $queries,
+ 'total' => $total
+ ]);
+
+ return $webhooks;
+ }
+
+ protected function getWebhook(string $webhookId): mixed
+ {
+ $webhook = $this->client->call(Client::METHOD_GET, '/webhooks/' . $webhookId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ return $webhook;
+ }
+
+ protected function createWebhook(string $webhookId, string $name, array $events, ?bool $enabled, ?string $url, ?bool $security, ?string $httpUser, ?string $httpPass): mixed
+ {
+ $params = [
+ 'webhookId' => $webhookId,
+ 'name' => $name,
+ 'events' => $events,
+ 'url' => $url,
+ ];
+
+ if ($enabled !== null) {
+ $params['enabled'] = $enabled;
+ }
+ if ($security !== null) {
+ $params['security'] = $security;
+ }
+ if ($httpUser !== null) {
+ $params['httpUser'] = $httpUser;
+ }
+ if ($httpPass !== null) {
+ $params['httpPass'] = $httpPass;
+ }
+
+ $webhook = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), $params);
+
+ return $webhook;
+ }
+
+ protected function updateWebhook(string $webhookId, string $name, array $events, ?bool $enabled, ?string $url, ?bool $security, ?string $httpUser, ?string $httpPass): mixed
+ {
+ $params = [
+ 'name' => $name,
+ 'events' => $events,
+ 'url' => $url,
+ ];
+
+ if ($enabled !== null) {
+ $params['enabled'] = $enabled;
+ }
+ if ($security !== null) {
+ $params['security'] = $security;
+ }
+ if ($httpUser !== null) {
+ $params['httpUser'] = $httpUser;
+ }
+ if ($httpPass !== null) {
+ $params['httpPass'] = $httpPass;
+ }
+
+ $webhook = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), $params);
+
+ return $webhook;
+ }
+
+ protected function updateWebhookSignature(string $webhookId): mixed
+ {
+ $webhook = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/signature', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ return $webhook;
+ }
+
+ protected function deleteWebhook(string $webhookId): mixed
+ {
+ $webhook = $this->client->call(Client::METHOD_DELETE, '/webhooks/' . $webhookId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ return $webhook;
}
}
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']);
- }
}