Add webhook tests
This commit is contained in:
parent
c9cd4c126c
commit
3a1fe3b2a7
4 changed files with 294 additions and 48 deletions
|
@ -899,7 +899,7 @@ App::post('/v1/projects/:projectId/webhooks')
|
|||
->label('sdk.response.model', Response::MODEL_WEBHOOK)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
|
||||
->param('enabled', true, new Boolean(), 'Enable or disable a webhook.', true)
|
||||
->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true)
|
||||
->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('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request'])
|
||||
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
|
||||
|
@ -1024,7 +1024,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
|
|||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('webhookId', '', new UID(), 'Webhook unique ID.')
|
||||
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
|
||||
->param('enabled', true, new Boolean(), 'Enable or disable a webhook.', true)
|
||||
->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true)
|
||||
->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('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request'])
|
||||
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
|
||||
|
|
|
@ -159,51 +159,7 @@ class Webhooks extends Action
|
|||
|
||||
if ($attempts >= self::MAX_FAILED_ATTEMPTS) {
|
||||
$webhook->setAttribute('enabled', false);
|
||||
|
||||
$memberships = $dbForConsole->find('memberships', [
|
||||
Query::equal('teamInternalId', [$project->getAttribute('teamInternalId')]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
$userIds = array_column(\array_map(fn ($membership) => $membership->getArrayCopy(), $memberships), 'userId');
|
||||
|
||||
$users = $dbForConsole->find('users', [
|
||||
Query::equal('$id', $userIds),
|
||||
]);
|
||||
|
||||
$protocol = App::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$hostname = App::getEnv('_APP_DOMAIN');
|
||||
$projectId = $project->getId();
|
||||
$webhookId = $webhook->getId();
|
||||
|
||||
$template = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-webhook-failed.tpl');
|
||||
|
||||
$template->setParam('{{webhook}}', $webhook->getAttribute('name'));
|
||||
$template->setParam('{{project}}', $project->getAttribute('name'));
|
||||
$template->setParam('{{url}}', $webhook->getAttribute('url'));
|
||||
$template->setParam('{{error}}', $curlError ?? 'The server returned ' . $statusCode . ' status code');
|
||||
$template->setParam('{{redirect}}', $protocol . '://' . $hostname . "/console/project-$projectId/settings/webhooks/$webhookId");
|
||||
$template->setParam('{{attempts}}', $attempts);
|
||||
|
||||
$subject = 'Webhook deliveries have been paused';
|
||||
$body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl');
|
||||
|
||||
$body
|
||||
->setParam('{{subject}}', $subject)
|
||||
->setParam('{{message}}', $template->render())
|
||||
->setParam('{{year}}', date("Y"));
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setBody($body->render());
|
||||
|
||||
foreach ($users as $user) {
|
||||
$queueForMails
|
||||
->setVariables(['user' => $user->getAttribute('name', '')])
|
||||
->setName($user->getAttribute('name', ''))
|
||||
->setRecipient($user->getAttribute('email'))
|
||||
->trigger();
|
||||
}
|
||||
$this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForConsole, $queueForMails);
|
||||
}
|
||||
|
||||
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
|
||||
|
@ -216,4 +172,61 @@ class Webhooks extends Action
|
|||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $attempts
|
||||
* @param mixed $statusCode
|
||||
* @param Document $webhook
|
||||
* @param Document $project
|
||||
* @param Database $dbForConsole
|
||||
* @param Mail $queueForMails
|
||||
* @return void
|
||||
*/
|
||||
public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForConsole, Mail $queueForMails): void
|
||||
{
|
||||
$memberships = $dbForConsole->find('memberships', [
|
||||
Query::equal('teamInternalId', [$project->getAttribute('teamInternalId')]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
$userIds = array_column(\array_map(fn ($membership) => $membership->getArrayCopy(), $memberships), 'userId');
|
||||
|
||||
$users = $dbForConsole->find('users', [
|
||||
Query::equal('$id', $userIds),
|
||||
]);
|
||||
|
||||
$protocol = App::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$hostname = App::getEnv('_APP_DOMAIN');
|
||||
$projectId = $project->getId();
|
||||
$webhookId = $webhook->getId();
|
||||
|
||||
$template = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-webhook-failed.tpl');
|
||||
|
||||
$template->setParam('{{webhook}}', $webhook->getAttribute('name'));
|
||||
$template->setParam('{{project}}', $project->getAttribute('name'));
|
||||
$template->setParam('{{url}}', $webhook->getAttribute('url'));
|
||||
$template->setParam('{{error}}', $curlError ?? 'The server returned ' . $statusCode . ' status code');
|
||||
$template->setParam('{{redirect}}', $protocol . '://' . $hostname . "/console/project-$projectId/settings/webhooks/$webhookId");
|
||||
$template->setParam('{{attempts}}', $attempts);
|
||||
|
||||
$subject = 'Webhook deliveries have been paused';
|
||||
$body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl');
|
||||
|
||||
$body
|
||||
->setParam('{{subject}}', $subject)
|
||||
->setParam('{{message}}', $template->render())
|
||||
->setParam('{{year}}', date("Y"));
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setBody($body->render());
|
||||
|
||||
foreach ($users as $user) {
|
||||
$queueForMails
|
||||
->setVariables(['user' => $user->getAttribute('name', '')])
|
||||
->setName($user->getAttribute('name', ''))
|
||||
->setRecipient($user->getAttribute('email'))
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ class Webhook extends Model
|
|||
])
|
||||
->addRule('logs', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Webhooks last failed delivery attempt logs.',
|
||||
'description' => 'Webhook error logs from the most recent failure.',
|
||||
'default' => '',
|
||||
'example' => 'Failed to connect to remote server.',
|
||||
])
|
||||
|
|
|
@ -997,4 +997,237 @@ trait WebhooksBase
|
|||
$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']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateCollection
|
||||
*/
|
||||
public function testCreateDisabledWebhook(array $data): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$webhookId = $this->getProject()['webhookId'];
|
||||
$databaseId = $data['databaseId'];
|
||||
|
||||
// create a new collection
|
||||
$collection1 = $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' => 'Collection1',
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals($collection1['headers']['status-code'], 201);
|
||||
$this->assertNotEmpty($collection1['body']['$id']);
|
||||
|
||||
// update webhook and set it to disabled
|
||||
$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' => false,
|
||||
'events' => [
|
||||
'databases.*',
|
||||
'functions.*',
|
||||
'buckets.*',
|
||||
'teams.*',
|
||||
'users.*'
|
||||
],
|
||||
'url' => 'http://request-catcher:5000/webhook',
|
||||
'security' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $webhook['headers']['status-code']);
|
||||
$this->assertNotEmpty($webhook['body']);
|
||||
|
||||
$webhook = $this->getLastRequest();
|
||||
$this->assertEquals($webhook['data']['name'], 'Collection1');
|
||||
|
||||
sleep(5);
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
// create another collection: This event should not trigger the webhook.
|
||||
$collection2 = $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' => 'Collection2',
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals($collection2['headers']['status-code'], 201);
|
||||
$this->assertNotEmpty($collection2['body']['$id']);
|
||||
|
||||
$webhook = $this->getLastRequest();
|
||||
|
||||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $webhookId);
|
||||
$this->assertNotEquals($webhook['data']['name'], 'Collection2');
|
||||
|
||||
// re-enable the webhook again
|
||||
$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, // set webhook to enabled again
|
||||
'events' => [
|
||||
'databases.*',
|
||||
'functions.*',
|
||||
'buckets.*',
|
||||
'teams.*',
|
||||
'users.*'
|
||||
],
|
||||
'url' => 'http://request-catcher:5000/webhook',
|
||||
'security' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $webhook['headers']['status-code']);
|
||||
$this->assertNotEmpty($webhook['body']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateCollection
|
||||
*/
|
||||
public function testWebhookAutoDisable(array $data): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$webhookId = $this->getProject()['webhookId'];
|
||||
$databaseId = $data['databaseId'];
|
||||
|
||||
$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']);
|
||||
}
|
||||
|
||||
sleep(10);
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue