mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
fix: resolve merge conflict keeping both lock and route restore
Merge conflict in Resolvers.php between the coroutine lock (fix/graphql-coroutine-safe-response) and the otel route restore (fix-gql-route-reset from 1.9.x). Both changes are needed: the lock serialises concurrent resolvers while the route restore prevents otel span clobbering.
This commit is contained in:
@@ -512,7 +512,7 @@ jobs:
|
||||
# Services that rely on sequential test method execution (shared static state)
|
||||
FUNCTIONAL_FLAG="--functional"
|
||||
case "${{ matrix.service }}" in
|
||||
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
|
||||
Databases|TablesDB|Functions|Realtime|GraphQL) FUNCTIONAL_FLAG="" ;;
|
||||
esac
|
||||
|
||||
docker compose exec -T \
|
||||
|
||||
@@ -872,18 +872,18 @@ return [
|
||||
],
|
||||
[
|
||||
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
|
||||
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
|
||||
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
|
||||
'introduction' => '0.13.0',
|
||||
'default' => '900',
|
||||
'default' => '2700',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
|
||||
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
|
||||
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
|
||||
'introduction' => '1.7.0',
|
||||
'default' => '900',
|
||||
'default' => '2700',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
|
||||
@@ -231,7 +231,7 @@ function execute(
|
||||
$validations = GraphQL::getStandardValidationRules();
|
||||
|
||||
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
|
||||
$validations[] = new DisableIntrospection();
|
||||
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
|
||||
}
|
||||
|
||||
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@
|
||||
"chillerlan/php-qrcode": "4.3.*",
|
||||
"adhocore/jwt": "1.1.*",
|
||||
"spomky-labs/otphp": "11.*",
|
||||
"webonyx/graphql-php": "14.11.*",
|
||||
"webonyx/graphql-php": "15.31.*",
|
||||
"league/csv": "9.14.*",
|
||||
"enshrined/svg-sanitize": "0.22.*"
|
||||
},
|
||||
|
||||
Generated
+34
-20
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "4fb974e9843f6104e40396e7cad4a833",
|
||||
"content-hash": "f6a87c1012b316e614258f8f57a28e48",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -5381,38 +5381,48 @@
|
||||
},
|
||||
{
|
||||
"name": "webonyx/graphql-php",
|
||||
"version": "v14.11.10",
|
||||
"version": "v15.31.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/webonyx/graphql-php.git",
|
||||
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19"
|
||||
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9c2fdebc6aa01d831bc2969da00e8588cffef19",
|
||||
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19",
|
||||
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/089c4ef7e112df85788cfe06596278a8f99f4aa9",
|
||||
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8"
|
||||
"php": "^7.4 || ^8"
|
||||
},
|
||||
"require-dev": {
|
||||
"amphp/amp": "^2.3",
|
||||
"doctrine/coding-standard": "^6.0",
|
||||
"nyholm/psr7": "^1.2",
|
||||
"amphp/amp": "^2.6",
|
||||
"amphp/http-server": "^2.1",
|
||||
"dms/phpunit-arraysubset-asserts": "dev-master",
|
||||
"ergebnis/composer-normalize": "^2.28",
|
||||
"friendsofphp/php-cs-fixer": "3.94.2",
|
||||
"mll-lab/php-cs-fixer-config": "5.13.0",
|
||||
"nyholm/psr7": "^1.5",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "0.12.82",
|
||||
"phpstan/phpstan-phpunit": "0.12.18",
|
||||
"phpstan/phpstan-strict-rules": "0.12.9",
|
||||
"phpunit/phpunit": "^7.2 || ^8.5",
|
||||
"psr/http-message": "^1.0",
|
||||
"react/promise": "2.*",
|
||||
"simpod/php-coveralls-mirror": "^3.0"
|
||||
"phpstan/extension-installer": "^1.1",
|
||||
"phpstan/phpstan": "2.1.46",
|
||||
"phpstan/phpstan-phpunit": "2.0.16",
|
||||
"phpstan/phpstan-strict-rules": "2.0.10",
|
||||
"phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
|
||||
"psr/http-message": "^1 || ^2",
|
||||
"react/http": "^1.6",
|
||||
"react/promise": "^2.0 || ^3.0",
|
||||
"rector/rector": "^2.0",
|
||||
"symfony/polyfill-php81": "^1.23",
|
||||
"symfony/var-exporter": "^5 || ^6 || ^7 || ^8",
|
||||
"thecodingmachine/safe": "^1.3 || ^2 || ^3",
|
||||
"ticketswap/phpstan-error-formatter": "1.3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
|
||||
"psr/http-message": "To use standard GraphQL server",
|
||||
"react/promise": "To leverage async resolving on React PHP platform"
|
||||
},
|
||||
@@ -5434,15 +5444,19 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/webonyx/graphql-php/issues",
|
||||
"source": "https://github.com/webonyx/graphql-php/tree/v14.11.10"
|
||||
"source": "https://github.com/webonyx/graphql-php/tree/v15.31.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spawnia",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/webonyx-graphql-php",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-05T14:23:37+00:00"
|
||||
"time": "2026-04-11T18:06:15+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
@@ -8449,5 +8463,5 @@
|
||||
"platform-dev": {
|
||||
"ext-fileinfo": "*"
|
||||
},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use Appwrite\Event\Message\Execution as ExecutionMessage;
|
||||
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
|
||||
use Utopia\Bus\Listener;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Log extends Listener
|
||||
{
|
||||
@@ -30,9 +32,27 @@ class Log extends Listener
|
||||
|
||||
public function handle(ExecutionCompleted $event, ExecutionPublisher $publisherForExecutions): void
|
||||
{
|
||||
$project = new Document($event->project);
|
||||
$execution = new Document($event->execution);
|
||||
if ($execution->getAttribute('resourceType', '') === 'functions') {
|
||||
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
|
||||
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
|
||||
$resourceId = $execution->getAttribute('resourceId', '');
|
||||
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
|
||||
Span::init('execution.trace.v1_executions_enqueue');
|
||||
Span::add('datetime', gmdate('c'));
|
||||
Span::add('projectId', $project->getId());
|
||||
Span::add('functionId', $resourceId);
|
||||
Span::add('executionId', $execution->getId());
|
||||
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
|
||||
Span::add('status', $execution->getAttribute('status', ''));
|
||||
Span::current()?->finish();
|
||||
}
|
||||
}
|
||||
|
||||
$publisherForExecutions->enqueue(new ExecutionMessage(
|
||||
project: new Document($event->project),
|
||||
execution: new Document($event->execution),
|
||||
project: $project,
|
||||
execution: $execution,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@ abstract class Adapter implements PromiseAdapter
|
||||
/**
|
||||
* Create a new promise that resolves when all passed in promises resolve.
|
||||
*
|
||||
* @param array $promisesOrValues
|
||||
* @param iterable $promisesOrValues
|
||||
* @return GQLPromise
|
||||
*/
|
||||
abstract public function all(array $promisesOrValues): GQLPromise;
|
||||
abstract public function all(iterable $promisesOrValues): GQLPromise;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,12 @@ class Swoole extends Adapter
|
||||
return new GQLPromise($promise, $this);
|
||||
}
|
||||
|
||||
public function all(array $promisesOrValues): GQLPromise
|
||||
public function all(iterable $promisesOrValues): GQLPromise
|
||||
{
|
||||
if ($promisesOrValues instanceof \Traversable) {
|
||||
$promisesOrValues = \iterator_to_array($promisesOrValues);
|
||||
}
|
||||
|
||||
return new GQLPromise(SwoolePromise::all($promisesOrValues), $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,6 +420,8 @@ class Resolvers
|
||||
$lock = self::getLock($utopia);
|
||||
|
||||
self::acquireLock($lock);
|
||||
|
||||
$original = $utopia->getRoute();
|
||||
try {
|
||||
$request = clone $request;
|
||||
|
||||
@@ -461,6 +463,10 @@ class Resolvers
|
||||
$reject($e);
|
||||
return;
|
||||
} finally {
|
||||
if ($original !== null) {
|
||||
$utopia->setRoute($original);
|
||||
}
|
||||
|
||||
self::releaseLock($lock);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
namespace Appwrite\GraphQL\Types;
|
||||
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
|
||||
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||
class Assoc extends Json
|
||||
{
|
||||
public $name = 'Assoc';
|
||||
public $description = 'The `Assoc` scalar type represents associative array values.';
|
||||
public string $name = 'Assoc';
|
||||
public ?string $description = 'The `Assoc` scalar type represents associative array values.';
|
||||
|
||||
public function serialize($value)
|
||||
{
|
||||
@@ -30,6 +31,10 @@ class Assoc extends Json
|
||||
|
||||
public function parseLiteral(Node $valueNode, ?array $variables = null)
|
||||
{
|
||||
return \json_decode($valueNode->value, true);
|
||||
if ($valueNode instanceof StringValueNode) {
|
||||
return \json_decode($valueNode->value, true);
|
||||
}
|
||||
|
||||
return parent::parseLiteral($valueNode, $variables);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ use GraphQL\Type\Definition\ScalarType;
|
||||
|
||||
class InputFile extends ScalarType
|
||||
{
|
||||
public $name = 'InputFile';
|
||||
public $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
|
||||
public string $name = 'InputFile';
|
||||
public ?string $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
|
||||
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
|
||||
|
||||
public function serialize($value)
|
||||
|
||||
@@ -14,8 +14,8 @@ use GraphQL\Type\Definition\ScalarType;
|
||||
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||
class Json extends ScalarType
|
||||
{
|
||||
public $name = 'Json';
|
||||
public $description = 'The `JSON` scalar type represents JSON values as specified by
|
||||
public string $name = 'Json';
|
||||
public ?string $description = 'The `JSON` scalar type represents JSON values as specified by
|
||||
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
|
||||
|
||||
public function serialize($value)
|
||||
|
||||
@@ -21,6 +21,7 @@ use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Multiple;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
|
||||
@@ -68,6 +69,7 @@ class Create extends Action
|
||||
->param('tls', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
|
||||
->param('authUsername', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
|
||||
->param('authPassword', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
|
||||
->param('secret', null, new Nullable(new Text(256, 8)), 'Webhook secret key. If not provided, a new key will be generated automatically. Key must be at least 8 characters long, and at max 256 characters.', optional: true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
@@ -88,6 +90,7 @@ class Create extends Action
|
||||
bool $tls,
|
||||
string $authUsername,
|
||||
string $authPassword,
|
||||
?string $secret,
|
||||
Response $response,
|
||||
Document $project,
|
||||
QueueEvent $queueForEvents,
|
||||
@@ -107,7 +110,7 @@ class Create extends Action
|
||||
'security' => $tls,
|
||||
'httpUser' => $authUsername,
|
||||
'httpPass' => $authPassword,
|
||||
'signatureKey' => \bin2hex(\random_bytes(64)),
|
||||
'signatureKey' => $secret ?? \bin2hex(\random_bytes(64)),
|
||||
'enabled' => $enabled,
|
||||
]);
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ class Get extends Action
|
||||
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhook->removeAttribute('signatureKey');
|
||||
|
||||
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature;
|
||||
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Secret;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -15,6 +15,8 @@ use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
@@ -22,7 +24,7 @@ class Update extends Action
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateWebhookSignature';
|
||||
return 'updateWebhookSecret';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
@@ -52,6 +54,7 @@ class Update extends Action
|
||||
]
|
||||
))
|
||||
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook ID.', false, ['dbForPlatform'])
|
||||
->param('secret', null, new Nullable(new Text(256, 8)), 'Webhook secret key. If not provided, a new key will be generated automatically. Key must be at least 8 characters long, and at max 256 characters.', optional: true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
@@ -62,6 +65,7 @@ class Update extends Action
|
||||
|
||||
public function action(
|
||||
string $webhookId,
|
||||
?string $secret,
|
||||
Response $response,
|
||||
Document $project,
|
||||
QueueEvent $queueForEvents,
|
||||
@@ -78,7 +82,7 @@ class Update extends Action
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'signatureKey' => \bin2hex(\random_bytes(64)),
|
||||
'signatureKey' => $secret ?? \bin2hex(\random_bytes(64)),
|
||||
]);
|
||||
|
||||
$webhook = $authorization->skip(fn () => $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $updates));
|
||||
@@ -118,6 +118,8 @@ class Update extends Action
|
||||
|
||||
$queueForEvents->setParam('webhookId', $webhook->getId());
|
||||
|
||||
$webhook->removeAttribute('signatureKey');
|
||||
|
||||
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,10 @@ class XList extends Action
|
||||
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.");
|
||||
}
|
||||
|
||||
foreach ($webhooks as $webhook) {
|
||||
$webhook->removeAttribute('signatureKey');
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'webhooks' => $webhooks,
|
||||
'total' => $total,
|
||||
|
||||
@@ -6,7 +6,7 @@ use Appwrite\Platform\Modules\Webhooks\Http\Init;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Create as CreateWebhook;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Delete as DeleteWebhook;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Get as GetWebhook;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature\Update as UpdateWebhookSignature;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Secret\Update as UpdateWebhookSecret;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Update as UpdateWebhook;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\XList as ListWebhooks;
|
||||
use Utopia\Platform\Service;
|
||||
@@ -26,6 +26,6 @@ class Http extends Service
|
||||
$this->addAction(GetWebhook::getName(), new GetWebhook());
|
||||
$this->addAction(DeleteWebhook::getName(), new DeleteWebhook());
|
||||
$this->addAction(UpdateWebhook::getName(), new UpdateWebhook());
|
||||
$this->addAction(UpdateWebhookSignature::getName(), new UpdateWebhookSignature());
|
||||
$this->addAction(UpdateWebhookSecret::getName(), new UpdateWebhookSecret());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use Cron\CronExpression;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
|
||||
/**
|
||||
* ScheduleFunctions
|
||||
@@ -88,7 +90,7 @@ class ScheduleFunctions extends ScheduleBase
|
||||
$scheduleKey = $delayConfig['key'];
|
||||
// Ensure schedule was not deleted
|
||||
if (!\array_key_exists($scheduleKey, $this->schedules)) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
$schedule = $this->schedules[$scheduleKey];
|
||||
@@ -102,8 +104,22 @@ class ScheduleFunctions extends ScheduleBase
|
||||
->setFunction($schedule['resource'])
|
||||
->setMethod('POST')
|
||||
->setPath('/')
|
||||
->setProject($schedule['project'])
|
||||
->trigger();
|
||||
->setProject($schedule['project']);
|
||||
|
||||
$projectDoc = $schedule['project'];
|
||||
$functionDoc = $schedule['resource'];
|
||||
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
|
||||
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
|
||||
if ($traceProjectId !== '' && $traceFunctionId !== '' && $projectDoc->getId() === $traceProjectId && $functionDoc->getId() === $traceFunctionId) {
|
||||
Span::init('execution.trace.v1_functions_enqueue');
|
||||
Span::add('datetime', gmdate('c'));
|
||||
Span::add('projectId', $projectDoc->getId());
|
||||
Span::add('functionId', $functionDoc->getId());
|
||||
Span::add('scheduleId', $schedule['$id'] ?? '');
|
||||
Span::current()?->finish();
|
||||
}
|
||||
|
||||
$queueForFunctions->trigger();
|
||||
|
||||
$this->recordEnqueueDelay($delayConfig['nextDate']);
|
||||
}
|
||||
|
||||
@@ -759,11 +759,19 @@ class Deletes extends Action
|
||||
$databasesToClean
|
||||
));
|
||||
} elseif ($sharedTablesV1) {
|
||||
/**
|
||||
* Temporary disabling deletes for internal collections
|
||||
*/
|
||||
$queries = \array_map(
|
||||
fn ($id) => Query::notEqual('$id', $id),
|
||||
$projectCollectionIds
|
||||
);
|
||||
|
||||
$queries[] = Query::orderAsc();
|
||||
|
||||
$this->deleteByGroup(
|
||||
Database::METADATA,
|
||||
[
|
||||
Query::orderAsc()
|
||||
],
|
||||
$queries,
|
||||
$dbForProject
|
||||
);
|
||||
} elseif ($sharedTablesV2) {
|
||||
|
||||
@@ -7,6 +7,8 @@ use Exception;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Executions extends Action
|
||||
{
|
||||
@@ -39,6 +41,20 @@ class Executions extends Action
|
||||
throw new Exception('Missing execution');
|
||||
}
|
||||
|
||||
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
|
||||
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
|
||||
$resourceId = $execution->getAttribute('resourceId', '');
|
||||
if ($traceProjectId !== '' && $traceFunctionId !== '' && $executionMessage->project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
|
||||
Span::init('execution.trace.executions_worker_upsert');
|
||||
Span::add('datetime', gmdate('c'));
|
||||
Span::add('projectId', $executionMessage->project->getId());
|
||||
Span::add('functionId', $resourceId);
|
||||
Span::add('executionId', $execution->getId());
|
||||
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
|
||||
Span::add('resourceType', $execution->getAttribute('resourceType', ''));
|
||||
Span::current()?->finish();
|
||||
}
|
||||
|
||||
$dbForProject->upsertDocument('executions', $execution);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use Utopia\Database\Query;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Functions extends Action
|
||||
@@ -115,6 +116,22 @@ class Functions extends Action
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addTag('type', $type);
|
||||
|
||||
if (empty($events) && !$function->isEmpty()) {
|
||||
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
|
||||
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
|
||||
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
|
||||
Span::init('execution.trace.functions_worker_dequeue');
|
||||
Span::add('datetime', gmdate('c'));
|
||||
Span::add('projectId', $project->getId());
|
||||
Span::add('functionId', $function->getId());
|
||||
Span::add('payloadType', $type);
|
||||
Span::add('queuePid', $message->getPid());
|
||||
Span::add('queueName', $message->getQueue());
|
||||
Span::add('messageTimestamp', (string) $message->getTimestamp());
|
||||
Span::current()?->finish();
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($events)) {
|
||||
$limit = 100;
|
||||
$sum = 100;
|
||||
@@ -304,6 +321,20 @@ class Functions extends Action
|
||||
'duration' => 0.0,
|
||||
]);
|
||||
|
||||
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
|
||||
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
|
||||
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
|
||||
Span::init('execution.trace.functions_worker_before_execution_completed_bus_fail');
|
||||
Span::add('datetime', gmdate('c'));
|
||||
Span::add('projectId', $project->getId());
|
||||
Span::add('functionId', $function->getId());
|
||||
Span::add('executionId', $execution->getId());
|
||||
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
|
||||
Span::add('trigger', $trigger);
|
||||
Span::add('status', $execution->getAttribute('status', ''));
|
||||
Span::current()?->finish();
|
||||
}
|
||||
|
||||
$bus->dispatch(new ExecutionCompleted(
|
||||
execution: $execution->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
@@ -522,6 +553,18 @@ class Functions extends Action
|
||||
$source = $deployment->getAttribute('buildPath', '');
|
||||
$extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz';
|
||||
$command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\"";
|
||||
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
|
||||
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
|
||||
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
|
||||
Span::init('execution.trace.functions_worker_before_executor');
|
||||
Span::add('datetime', gmdate('c'));
|
||||
Span::add('projectId', $project->getId());
|
||||
Span::add('functionId', $functionId);
|
||||
Span::add('executionId', $executionId);
|
||||
Span::add('deploymentId', $deployment->getId());
|
||||
Span::add('trigger', $trigger);
|
||||
Span::current()?->finish();
|
||||
}
|
||||
$executionResponse = $executor->createExecution(
|
||||
projectId: $project->getId(),
|
||||
deploymentId: $deploymentId,
|
||||
@@ -594,6 +637,19 @@ class Functions extends Action
|
||||
$errorCode = $th->getCode();
|
||||
} finally {
|
||||
/** Persist final execution status and record usage */
|
||||
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
|
||||
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
|
||||
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
|
||||
Span::init('execution.trace.functions_worker_before_execution_completed_bus');
|
||||
Span::add('datetime', gmdate('c'));
|
||||
Span::add('projectId', $project->getId());
|
||||
Span::add('functionId', $functionId);
|
||||
Span::add('executionId', $execution->getId());
|
||||
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
|
||||
Span::add('status', $execution->getAttribute('status', ''));
|
||||
Span::add('trigger', $trigger);
|
||||
Span::current()?->finish();
|
||||
}
|
||||
$bus->dispatch(new ExecutionCompleted(
|
||||
execution: $execution->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
|
||||
@@ -71,7 +71,7 @@ class Webhook extends Model
|
||||
])
|
||||
->addRule('secret', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Signature key which can be used to validate incoming webhook payloads.',
|
||||
'description' => 'Signature key which can be used to validate incoming webhook payloads. Only returned on creation and secret rotation.',
|
||||
'default' => '',
|
||||
'example' => 'ad3d581ca230e2b7059c545e5a',
|
||||
])
|
||||
|
||||
@@ -22,7 +22,7 @@ class Comment
|
||||
'Every Git commit and branch gets its own deployment URL automatically',
|
||||
'Custom domains work with both CNAME for subdomains and NS records for apex domains',
|
||||
'HTTPS and SSL certificates are handled automatically for all your Sites',
|
||||
'Functions can run for up to 15 minutes before timing out',
|
||||
'Function builds can take up to 45 minutes before timing out',
|
||||
'Schedule functions to run as often as every minute with cron expressions',
|
||||
'Environment variables can be scoped per function or shared across your project',
|
||||
'Function scopes give you fine-grained control over API permissions',
|
||||
|
||||
@@ -694,10 +694,10 @@ trait WebhooksBase
|
||||
$this->assertEquals(128, \strlen($updated['body']['secret']));
|
||||
$this->assertNotEquals($originalSecret, $updated['body']['secret']);
|
||||
|
||||
// Verify new secret persisted via GET
|
||||
// Verify secret is not exposed via GET
|
||||
$get = $this->getWebhook($webhookId);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertNotEquals($originalSecret, $get['body']['secret']);
|
||||
$this->assertEmpty($get['body']['secret']);
|
||||
|
||||
// Test secret update on non-existent webhook
|
||||
$notFound = $this->updateWebhookSecret('non-existent-id');
|
||||
@@ -708,6 +708,351 @@ trait WebhooksBase
|
||||
$this->deleteWebhook($webhookId);
|
||||
}
|
||||
|
||||
public function testSecretRotationZeroDowntime(): void
|
||||
{
|
||||
// Create webhook pointing to request-catcher so deliveries are captured
|
||||
$webhook = $this->createWebhook(
|
||||
ID::unique(),
|
||||
'Rotation Test Webhook',
|
||||
['users.*.create'],
|
||||
null,
|
||||
'http://request-catcher-webhook:5000/',
|
||||
false,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $webhook['headers']['status-code']);
|
||||
$webhookId = $webhook['body']['$id'];
|
||||
$originalSecret = $webhook['body']['secret'];
|
||||
$this->assertNotEmpty($originalSecret);
|
||||
$this->assertEquals(128, \strlen($originalSecret));
|
||||
|
||||
// Step 1: Trigger user creation with the original auto-generated secret
|
||||
$email1 = uniqid() . 'rotation1@localhost.test';
|
||||
$user1 = $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' => $email1,
|
||||
'password' => 'password',
|
||||
'name' => 'Rotation User 1',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $user1['headers']['status-code']);
|
||||
$userId1 = $user1['body']['$id'];
|
||||
|
||||
// Verify webhook delivery is signed with the original secret
|
||||
$this->assertEventually(function () use ($userId1, $originalSecret) {
|
||||
$delivery = $this->getLastRequest(function (array $request) use ($userId1) {
|
||||
$this->assertStringContainsString(
|
||||
"users.{$userId1}.create",
|
||||
$request['headers']['X-Appwrite-Webhook-Events'] ?? ''
|
||||
);
|
||||
});
|
||||
|
||||
$this->assertNotEmpty($delivery);
|
||||
$payload = json_encode($delivery['data']);
|
||||
$url = $delivery['url'];
|
||||
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $originalSecret, true));
|
||||
$this->assertEquals($signatureExpected, $delivery['headers']['X-Appwrite-Webhook-Signature']);
|
||||
}, 15000, 500);
|
||||
|
||||
// Step 2: Rotate the secret to a known custom value
|
||||
$newSecret = 'new-key-after-rotation';
|
||||
$updated = $this->updateWebhookSecret($webhookId, $newSecret);
|
||||
|
||||
$this->assertEquals(200, $updated['headers']['status-code']);
|
||||
$this->assertEquals($newSecret, $updated['body']['secret']);
|
||||
$this->assertNotEquals($originalSecret, $updated['body']['secret']);
|
||||
|
||||
// Step 3: Trigger another user creation — should be signed with the new secret
|
||||
$email2 = uniqid() . 'rotation2@localhost.test';
|
||||
$user2 = $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' => $email2,
|
||||
'password' => 'password',
|
||||
'name' => 'Rotation User 2',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $user2['headers']['status-code']);
|
||||
$userId2 = $user2['body']['$id'];
|
||||
|
||||
// Verify webhook delivery is signed with the new rotated secret
|
||||
$this->assertEventually(function () use ($userId2, $newSecret) {
|
||||
$delivery = $this->getLastRequest(function (array $request) use ($userId2) {
|
||||
$this->assertStringContainsString(
|
||||
"users.{$userId2}.create",
|
||||
$request['headers']['X-Appwrite-Webhook-Events'] ?? ''
|
||||
);
|
||||
});
|
||||
|
||||
$this->assertNotEmpty($delivery);
|
||||
$payload = json_encode($delivery['data']);
|
||||
$url = $delivery['url'];
|
||||
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $newSecret, true));
|
||||
$this->assertEquals($signatureExpected, $delivery['headers']['X-Appwrite-Webhook-Signature']);
|
||||
}, 15000, 500);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteWebhook($webhookId);
|
||||
}
|
||||
|
||||
public function testCreateWebhookWithCustomSecret(): void
|
||||
{
|
||||
$customSecret = 'custom-secret-key';
|
||||
|
||||
// Create webhook with a custom secret pointing to request-catcher
|
||||
$webhook = $this->createWebhook(
|
||||
ID::unique(),
|
||||
'Custom Secret Webhook',
|
||||
['users.*.create'],
|
||||
null,
|
||||
'http://request-catcher-webhook:5000/',
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
$customSecret
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $webhook['headers']['status-code']);
|
||||
$webhookId = $webhook['body']['$id'];
|
||||
$this->assertEquals($customSecret, $webhook['body']['secret']);
|
||||
|
||||
// Trigger user creation to generate a webhook delivery
|
||||
$email = uniqid() . 'customsecret@localhost.test';
|
||||
$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' => 'Custom Secret User',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $user['headers']['status-code']);
|
||||
$userId = $user['body']['$id'];
|
||||
|
||||
// Verify webhook delivery is signed with the custom secret
|
||||
$this->assertEventually(function () use ($userId, $customSecret) {
|
||||
$delivery = $this->getLastRequest(function (array $request) use ($userId) {
|
||||
$this->assertStringContainsString(
|
||||
"users.{$userId}.create",
|
||||
$request['headers']['X-Appwrite-Webhook-Events'] ?? ''
|
||||
);
|
||||
});
|
||||
|
||||
$this->assertNotEmpty($delivery);
|
||||
$payload = json_encode($delivery['data']);
|
||||
$url = $delivery['url'];
|
||||
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $customSecret, true));
|
||||
$this->assertEquals($signatureExpected, $delivery['headers']['X-Appwrite-Webhook-Signature']);
|
||||
}, 15000, 500);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteWebhook($webhookId);
|
||||
}
|
||||
|
||||
public function testCreateWebhookSecretMinLength(): void
|
||||
{
|
||||
// 7 chars — below minimum of 8
|
||||
$webhook = $this->createWebhook(
|
||||
ID::unique(),
|
||||
'Short Secret Webhook',
|
||||
['users.*.create'],
|
||||
null,
|
||||
'https://appwrite.io',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
'short12'
|
||||
);
|
||||
|
||||
$this->assertEquals(400, $webhook['headers']['status-code']);
|
||||
|
||||
// 8 chars — exactly at minimum
|
||||
$webhook = $this->createWebhook(
|
||||
ID::unique(),
|
||||
'Min Secret Webhook',
|
||||
['users.*.create'],
|
||||
null,
|
||||
'https://appwrite.io',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
'exact8ch'
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $webhook['headers']['status-code']);
|
||||
$this->assertEquals('exact8ch', $webhook['body']['secret']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteWebhook($webhook['body']['$id']);
|
||||
}
|
||||
|
||||
public function testCreateWebhookSecretMaxLength(): void
|
||||
{
|
||||
// 256 chars — exactly at maximum
|
||||
$maxSecret = str_repeat('a', 256);
|
||||
$webhook = $this->createWebhook(
|
||||
ID::unique(),
|
||||
'Max Secret Webhook',
|
||||
['users.*.create'],
|
||||
null,
|
||||
'https://appwrite.io',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$maxSecret
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $webhook['headers']['status-code']);
|
||||
$this->assertEquals($maxSecret, $webhook['body']['secret']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteWebhook($webhook['body']['$id']);
|
||||
|
||||
// 257 chars — above maximum
|
||||
$tooLongSecret = str_repeat('a', 257);
|
||||
$webhook = $this->createWebhook(
|
||||
ID::unique(),
|
||||
'Too Long Secret Webhook',
|
||||
['users.*.create'],
|
||||
null,
|
||||
'https://appwrite.io',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$tooLongSecret
|
||||
);
|
||||
|
||||
$this->assertEquals(400, $webhook['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateWebhookSecretMinLength(): void
|
||||
{
|
||||
$webhook = $this->createWebhook(
|
||||
ID::unique(),
|
||||
'Secret Min Update Webhook',
|
||||
['users.*.create'],
|
||||
null,
|
||||
'https://appwrite.io',
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $webhook['headers']['status-code']);
|
||||
$webhookId = $webhook['body']['$id'];
|
||||
|
||||
// 7 chars — below minimum of 8
|
||||
$updated = $this->updateWebhookSecret($webhookId, 'short12');
|
||||
$this->assertEquals(400, $updated['headers']['status-code']);
|
||||
|
||||
// 8 chars — exactly at minimum
|
||||
$updated = $this->updateWebhookSecret($webhookId, 'exact8ch');
|
||||
$this->assertEquals(200, $updated['headers']['status-code']);
|
||||
$this->assertEquals('exact8ch', $updated['body']['secret']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteWebhook($webhookId);
|
||||
}
|
||||
|
||||
public function testUpdateWebhookSecretMaxLength(): void
|
||||
{
|
||||
$webhook = $this->createWebhook(
|
||||
ID::unique(),
|
||||
'Secret Max Update Webhook',
|
||||
['users.*.create'],
|
||||
null,
|
||||
'https://appwrite.io',
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $webhook['headers']['status-code']);
|
||||
$webhookId = $webhook['body']['$id'];
|
||||
|
||||
// 256 chars — exactly at maximum
|
||||
$maxSecret = str_repeat('a', 256);
|
||||
$updated = $this->updateWebhookSecret($webhookId, $maxSecret);
|
||||
$this->assertEquals(200, $updated['headers']['status-code']);
|
||||
$this->assertEquals($maxSecret, $updated['body']['secret']);
|
||||
|
||||
// 257 chars — above maximum
|
||||
$tooLongSecret = str_repeat('a', 257);
|
||||
$updated = $this->updateWebhookSecret($webhookId, $tooLongSecret);
|
||||
$this->assertEquals(400, $updated['headers']['status-code']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteWebhook($webhookId);
|
||||
}
|
||||
|
||||
public function testWebhookSecretNotExposedInResponses(): void
|
||||
{
|
||||
// Create webhook — secret IS returned on creation
|
||||
$webhook = $this->createWebhook(
|
||||
ID::unique(),
|
||||
'Secret Exposure Test',
|
||||
['users.*.create'],
|
||||
null,
|
||||
'https://appwrite.io',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
'my-custom-secret'
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $webhook['headers']['status-code']);
|
||||
$webhookId = $webhook['body']['$id'];
|
||||
$this->assertEquals('my-custom-secret', $webhook['body']['secret']);
|
||||
$this->assertArrayNotHasKey('signatureKey', $webhook['body']);
|
||||
|
||||
// Get webhook — secret must not be exposed
|
||||
$get = $this->getWebhook($webhookId);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertEmpty($get['body']['secret']);
|
||||
$this->assertArrayNotHasKey('signatureKey', $get['body']);
|
||||
|
||||
// List webhooks — secret must not be exposed
|
||||
$list = $this->listWebhooks(null, true);
|
||||
$this->assertEquals(200, $list['headers']['status-code']);
|
||||
foreach ($list['body']['webhooks'] as $item) {
|
||||
$this->assertEmpty($item['secret']);
|
||||
$this->assertArrayNotHasKey('signatureKey', $item);
|
||||
}
|
||||
|
||||
// Update webhook — secret must not be exposed
|
||||
$updated = $this->updateWebhook(
|
||||
$webhookId,
|
||||
'Secret Exposure Test Updated',
|
||||
['users.*.create'],
|
||||
null,
|
||||
'https://appwrite.io',
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
$this->assertEquals(200, $updated['headers']['status-code']);
|
||||
$this->assertEmpty($updated['body']['secret']);
|
||||
$this->assertArrayNotHasKey('signatureKey', $updated['body']);
|
||||
|
||||
// Update webhook secret — secret IS returned on rotation
|
||||
$rotated = $this->updateWebhookSecret($webhookId, 'rotated-secret-key');
|
||||
$this->assertEquals(200, $rotated['headers']['status-code']);
|
||||
$this->assertEquals('rotated-secret-key', $rotated['body']['secret']);
|
||||
$this->assertArrayNotHasKey('signatureKey', $rotated['body']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteWebhook($webhookId);
|
||||
}
|
||||
|
||||
// URL validation tests
|
||||
|
||||
public function testCreateWebhookWithPrivateDomain(): void
|
||||
@@ -883,6 +1228,12 @@ trait WebhooksBase
|
||||
{
|
||||
$customId = 'my-custom-webhook-id';
|
||||
|
||||
// Clean up stale webhook from a previous run if it exists
|
||||
$existing = $this->getWebhook($customId);
|
||||
if ($existing['headers']['status-code'] === 200) {
|
||||
$this->deleteWebhook($customId);
|
||||
}
|
||||
|
||||
$webhook = $this->createWebhook(
|
||||
$customId,
|
||||
'Custom ID Webhook',
|
||||
@@ -902,6 +1253,19 @@ trait WebhooksBase
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertEquals($customId, $get['body']['$id']);
|
||||
|
||||
// Ensure duplicate creation fails
|
||||
$duplicate = $this->createWebhook(
|
||||
$customId,
|
||||
'Duplicate Custom ID Webhook',
|
||||
['users.*.create'],
|
||||
null,
|
||||
'https://appwrite.io',
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
$this->assertEquals(409, $duplicate['headers']['status-code']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteWebhook($customId);
|
||||
}
|
||||
@@ -937,8 +1301,7 @@ trait WebhooksBase
|
||||
$this->assertEquals(true, $get['body']['tls']);
|
||||
$this->assertEquals('myuser', $get['body']['authUsername']);
|
||||
$this->assertEquals('mypass', $get['body']['authPassword']);
|
||||
$this->assertNotEmpty($get['body']['secret']);
|
||||
$this->assertEquals(128, \strlen($get['body']['secret']));
|
||||
$this->assertEmpty($get['body']['secret']);
|
||||
$this->assertEquals(0, $get['body']['attempts']);
|
||||
$this->assertEquals('', $get['body']['logs']);
|
||||
|
||||
@@ -1645,7 +2008,7 @@ trait WebhooksBase
|
||||
$this->assertEquals(true, $get['body']['security']);
|
||||
$this->assertEquals('getuser', $get['body']['httpUser']);
|
||||
$this->assertEquals('getpass', $get['body']['httpPass']);
|
||||
$this->assertNotEmpty($get['body']['signatureKey']);
|
||||
$this->assertEmpty($get['body']['signatureKey']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteWebhook($webhookId);
|
||||
@@ -1779,7 +2142,7 @@ trait WebhooksBase
|
||||
return $webhook;
|
||||
}
|
||||
|
||||
protected function createWebhook(string $webhookId, string $name, array $events, ?bool $enabled, ?string $url, ?bool $tls, ?string $authUsername, ?string $authPassword): mixed
|
||||
protected function createWebhook(string $webhookId, string $name, array $events, ?bool $enabled, ?string $url, ?bool $tls, ?string $authUsername, ?string $authPassword, ?string $secret = null): mixed
|
||||
{
|
||||
$params = [
|
||||
'webhookId' => $webhookId,
|
||||
@@ -1800,6 +2163,9 @@ trait WebhooksBase
|
||||
if ($authPassword !== null) {
|
||||
$params['authPassword'] = $authPassword;
|
||||
}
|
||||
if ($secret !== null) {
|
||||
$params['secret'] = $secret;
|
||||
}
|
||||
|
||||
$webhook = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
@@ -1838,12 +2204,17 @@ trait WebhooksBase
|
||||
return $webhook;
|
||||
}
|
||||
|
||||
protected function updateWebhookSecret(string $webhookId): mixed
|
||||
protected function updateWebhookSecret(string $webhookId, ?string $secret = null): mixed
|
||||
{
|
||||
$params = [];
|
||||
if ($secret !== null) {
|
||||
$params['secret'] = $secret;
|
||||
}
|
||||
|
||||
$webhook = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/secret', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $webhook;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Tests\Unit\GraphQL;
|
||||
|
||||
use Appwrite\GraphQL\Types\Mapper;
|
||||
use Appwrite\Utopia\Response;
|
||||
use GraphQL\Type\Definition\NamedType;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Swoole\Http\Response as SwooleResponse;
|
||||
|
||||
@@ -24,6 +25,7 @@ class BuilderTest extends TestCase
|
||||
{
|
||||
$model = $this->response->getModel(Response::MODEL_TABLE);
|
||||
$type = Mapper::model(\ucfirst($model->getType()));
|
||||
$this->assertEquals('Table', $type->name);
|
||||
$this->assertInstanceOf(NamedType::class, $type);
|
||||
$this->assertEquals('Table', $type->name());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user