Compare commits

...
20 changed files with 813 additions and 0 deletions
+14
View File
@@ -405,6 +405,20 @@ return [
'$description' => 'This event triggers when a provider is deleted.'
],
],
'schedules' => [
'$model' => Response::MODEL_SCHEDULE,
'$resource' => true,
'$description' => 'This event triggers on any schedule event.',
'create' => [
'$description' => 'This event triggers when a schedule is created.',
],
'update' => [
'$description' => 'This event triggers when a schedule is updated.',
],
'delete' => [
'$description' => 'This event triggers when a schedule is deleted.',
],
],
'rules' => [
'$model' => Response::MODEL_PROXY_RULE,
'$resource' => true,
+2
View File
@@ -91,6 +91,8 @@ $admins = [
'subscribers.read',
'tokens.read',
'tokens.write',
'schedules.read',
'schedules.write',
];
return [
+6
View File
@@ -145,6 +145,12 @@ return [ // List of publicly visible scopes
'rules.write' => [
'description' => 'Access to create, update, and delete your project\'s proxy rules',
],
'schedules.read' => [
'description' => 'Access to read your project\'s schedules',
],
'schedules.write' => [
'description' => 'Access to create, update, and delete your project\'s schedules',
],
'migrations.read' => [
'description' => 'Access to read your project\'s migrations',
],
+3
View File
@@ -114,6 +114,7 @@ use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\Schedule;
use Appwrite\Utopia\Response\Model\Session;
use Appwrite\Utopia\Response\Model\Site;
use Appwrite\Utopia\Response\Model\Specification;
@@ -198,6 +199,7 @@ Response::setModel(new BaseList('Metric List', Response::MODEL_METRIC_LIST, 'met
Response::setModel(new BaseList('Variables List', Response::MODEL_VARIABLE_LIST, 'variables', Response::MODEL_VARIABLE));
Response::setModel(new BaseList('Status List', Response::MODEL_HEALTH_STATUS_LIST, 'statuses', Response::MODEL_HEALTH_STATUS));
Response::setModel(new BaseList('Rule List', Response::MODEL_PROXY_RULE_LIST, 'rules', Response::MODEL_PROXY_RULE));
Response::setModel(new BaseList('Schedules List', Response::MODEL_SCHEDULE_LIST, 'schedules', Response::MODEL_SCHEDULE));
Response::setModel(new BaseList('Locale codes list', Response::MODEL_LOCALE_CODE_LIST, 'localeCodes', Response::MODEL_LOCALE_CODE));
Response::setModel(new BaseList('Provider list', Response::MODEL_PROVIDER_LIST, 'providers', Response::MODEL_PROVIDER));
Response::setModel(new BaseList('Message list', Response::MODEL_MESSAGE_LIST, 'messages', Response::MODEL_MESSAGE));
@@ -339,6 +341,7 @@ Response::setModel(new UsageProject());
Response::setModel(new Headers());
Response::setModel(new Specification());
Response::setModel(new Rule());
Response::setModel(new Schedule());
Response::setModel(new TemplateSMS());
Response::setModel(new TemplateEmail());
Response::setModel(new ConsoleVariables());
+1
View File
@@ -0,0 +1 @@
Create a new schedule for a resource.
+1
View File
@@ -0,0 +1 @@
Get a schedule by its unique ID.
+1
View File
@@ -0,0 +1 @@
Get a list of all the project's schedules. You can use the query params to filter your results.
+1
View File
@@ -35,6 +35,7 @@
<directory>./tests/e2e/Services/Webhooks</directory>
<directory>./tests/e2e/Services/Messaging</directory>
<directory>./tests/e2e/Services/Migrations</directory>
<directory>./tests/e2e/Services/Schedules</directory>
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
+2
View File
@@ -11,6 +11,7 @@ use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Health;
use Appwrite\Platform\Modules\Projects;
use Appwrite\Platform\Modules\Proxy;
use Appwrite\Platform\Modules\Schedules;
use Appwrite\Platform\Modules\Sites;
use Appwrite\Platform\Modules\Storage;
use Appwrite\Platform\Modules\Tokens;
@@ -28,6 +29,7 @@ class Appwrite extends Platform
$this->addModule(new Projects\Module());
$this->addModule(new Functions\Module());
$this->addModule(new Health\Module());
$this->addModule(new Schedules\Module());
$this->addModule(new Sites\Module());
$this->addModule(new Console\Module());
$this->addModule(new Proxy\Module());
@@ -0,0 +1,114 @@
<?php
namespace Appwrite\Platform\Modules\Schedules\Http\Schedules;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Task\Validator\Cron;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
class Create extends Action
{
use HTTP;
public static function getName(): string
{
return 'createSchedule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/schedules')
->desc('Create schedule')
->groups(['api', 'schedules'])
->label('scope', 'schedules.write')
->label('audits.event', 'schedule.create')
->label('audits.resource', 'schedule/{response.$id}')
->label('sdk', new Method(
namespace: 'schedules',
group: 'schedules',
name: 'create',
description: '/docs/references/schedules/create.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_SCHEDULE,
)
],
))
->param('resourceType', '', new WhiteList([SCHEDULE_RESOURCE_TYPE_FUNCTION, SCHEDULE_RESOURCE_TYPE_EXECUTION, SCHEDULE_RESOURCE_TYPE_MESSAGE], true), 'The resource type for the schedule. Possible values: ' . implode(', ', [SCHEDULE_RESOURCE_TYPE_FUNCTION, SCHEDULE_RESOURCE_TYPE_EXECUTION, SCHEDULE_RESOURCE_TYPE_MESSAGE]) . '.')
->param('resourceId', '', new UID(), 'The resource ID to associate with this schedule.')
->param('schedule', '', new Cron(), 'Schedule CRON expression.')
->param('active', false, new Boolean(), 'Whether the schedule is active.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $resourceType,
string $resourceId,
string $schedule,
bool $active,
Response $response,
Document $project,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization,
): void {
$collection = match ($resourceType) {
SCHEDULE_RESOURCE_TYPE_FUNCTION => 'functions',
SCHEDULE_RESOURCE_TYPE_EXECUTION => 'executions',
SCHEDULE_RESOURCE_TYPE_MESSAGE => 'messages',
};
$resource = $dbForProject->getDocument($collection, $resourceId);
if ($resource->isEmpty()) {
throw new Exception(match ($resourceType) {
SCHEDULE_RESOURCE_TYPE_FUNCTION => Exception::FUNCTION_NOT_FOUND,
SCHEDULE_RESOURCE_TYPE_EXECUTION => Exception::EXECUTION_NOT_FOUND,
SCHEDULE_RESOURCE_TYPE_MESSAGE => Exception::MESSAGE_NOT_FOUND,
});
}
try {
$doc = $authorization->skip(
fn () => $dbForPlatform->createDocument('schedules', new Document([
'region' => $project->getAttribute('region'),
'resourceType' => $resourceType,
'resourceId' => $resourceId,
'resourceInternalId' => $resource->getSequence(),
'resourceUpdatedAt' => DateTime::now(),
'projectId' => $project->getId(),
'schedule' => $schedule,
'active' => $active,
]))
);
} catch (DuplicateException) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to create schedule. Please try again.');
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($doc, Response::MODEL_SCHEDULE);
}
}
@@ -0,0 +1,76 @@
<?php
namespace Appwrite\Platform\Modules\Schedules\Http\Schedules;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getSchedule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/schedules/:scheduleId')
->desc('Get schedule')
->groups(['api', 'schedules'])
->label('scope', 'schedules.read')
->label('sdk', new Method(
namespace: 'schedules',
group: 'schedules',
name: 'get',
description: '/docs/references/schedules/get.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SCHEDULE,
)
]
))
->param('scheduleId', '', new UID(), 'Schedule ID.')
->inject('response')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $scheduleId,
Response $response,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
): void {
$schedule = $authorization->skip(
fn () => $dbForPlatform->getDocument('schedules', $scheduleId)
);
if ($schedule->isEmpty()) {
throw new Exception(Exception::SCHEDULE_NOT_FOUND);
}
if ($schedule->getAttribute('projectId') !== $project->getId()) {
throw new Exception(Exception::SCHEDULE_NOT_FOUND);
}
$response->dynamic($schedule, Response::MODEL_SCHEDULE);
}
}
@@ -0,0 +1,117 @@
<?php
namespace Appwrite\Platform\Modules\Schedules\Http\Schedules;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Schedules;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Action
{
use HTTP;
public static function getName(): string
{
return 'listSchedules';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/schedules')
->desc('List schedules')
->groups(['api', 'schedules'])
->label('scope', 'schedules.read')
->label('sdk', new Method(
namespace: 'schedules',
group: 'schedules',
name: 'list',
description: '/docs/references/schedules/list.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SCHEDULE_LIST,
)
]
))
->param('queries', [], new Schedules(), '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(', ', Schedules::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('response')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
array $queries,
bool $includeTotal,
Response $response,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
): void {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Always scope to the current project
$queries[] = Query::equal('projectId', [$project->getId()]);
$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());
}
$scheduleId = $cursor->getValue();
$cursorDocument = $authorization->skip(
fn () => $dbForPlatform->getDocument('schedules', $scheduleId)
);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Schedule '{$scheduleId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$schedules = $authorization->skip(
fn () => $dbForPlatform->find('schedules', $queries)
);
$total = $includeTotal ? $authorization->skip(
fn () => $dbForPlatform->count('schedules', $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([
'schedules' => $schedules,
'total' => $total,
]), Response::MODEL_SCHEDULE_LIST);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Platform\Modules\Schedules;
use Appwrite\Platform\Modules\Schedules\Services\Http;
use Utopia\Platform;
class Module extends Platform\Module
{
public function __construct()
{
$this->addService('http', new Http());
}
}
@@ -0,0 +1,20 @@
<?php
namespace Appwrite\Platform\Modules\Schedules\Services;
use Appwrite\Platform\Modules\Schedules\Http\Schedules\Create;
use Appwrite\Platform\Modules\Schedules\Http\Schedules\Get;
use Appwrite\Platform\Modules\Schedules\Http\Schedules\XList;
use Utopia\Platform\Service;
class Http extends Service
{
public function __construct()
{
$this->type = Service::TYPE_HTTP;
$this->addAction(Get::getName(), new Get());
$this->addAction(XList::getName(), new XList());
$this->addAction(Create::getName(), new Create());
}
}
@@ -0,0 +1,24 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Schedules extends Base
{
public const ALLOWED_ATTRIBUTES = [
'resourceType',
'resourceId',
'projectId',
'schedule',
'active',
'region',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('schedules', self::ALLOWED_ATTRIBUTES);
}
}
+4
View File
@@ -223,6 +223,10 @@ class Response extends SwooleResponse
public const MODEL_PROXY_RULE = 'proxyRule';
public const MODEL_PROXY_RULE_LIST = 'proxyRuleList';
// Schedules
public const MODEL_SCHEDULE = 'schedule';
public const MODEL_SCHEDULE_LIST = 'scheduleList';
// Migrations
public const MODEL_MIGRATION = 'migration';
public const MODEL_MIGRATION_LIST = 'migrationList';
@@ -0,0 +1,95 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Schedule extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Schedule ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Schedule creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Schedule update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('resourceType', [
'type' => self::TYPE_STRING,
'description' => 'The resource type associated with this schedule.',
'default' => '',
'example' => 'function',
])
->addRule('resourceId', [
'type' => self::TYPE_STRING,
'description' => 'The resource ID associated with this schedule.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('resourceUpdatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'The date the associated resource was last updated in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('projectId', [
'type' => self::TYPE_STRING,
'description' => 'The project ID associated with this schedule.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('schedule', [
'type' => self::TYPE_STRING,
'description' => 'The CRON schedule expression.',
'default' => '',
'example' => '5 4 * * *',
])
->addRule('active', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether the schedule is active.',
'default' => false,
'example' => true,
])
->addRule('region', [
'type' => self::TYPE_STRING,
'description' => 'The region where the schedule is deployed.',
'default' => '',
'example' => 'fra',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Schedule';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_SCHEDULE;
}
}
+2
View File
@@ -109,6 +109,8 @@ trait ProjectCustom
'subscribers.read',
'migrations.write',
'migrations.read',
'schedules.read',
'schedules.write',
'tokens.read',
'tokens.write',
],
@@ -0,0 +1,47 @@
<?php
namespace Tests\E2E\Services\Schedules;
use Tests\E2E\Client;
use Utopia\Database\Helpers\ID;
trait SchedulesBase
{
protected function createSchedule(array $params = []): array
{
return $this->client->call(Client::METHOD_POST, '/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
}
protected function getSchedule(string $scheduleId): array
{
return $this->client->call(Client::METHOD_GET, '/schedules/' . $scheduleId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
}
protected function listSchedules(array $params = []): array
{
return $this->client->call(Client::METHOD_GET, '/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
}
protected function createFunction(array $params = []): array
{
return $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), array_merge([
'functionId' => ID::unique(),
'name' => 'Test Schedule Function',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'execute' => ['any'],
], $params));
}
}
@@ -0,0 +1,269 @@
<?php
namespace Tests\E2E\Services\Schedules;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
class SchedulesCustomServerTest extends Scope
{
use SchedulesBase;
use ProjectCustom;
use SideServer;
public function testCreateSchedule(): array
{
/**
* Test for SUCCESS
*/
$function = $this->createFunction();
$this->assertEquals(201, $function['headers']['status-code']);
$functionId = $function['body']['$id'];
$response = $this->createSchedule([
'resourceType' => 'function',
'resourceId' => $functionId,
'schedule' => '0 0 * * *',
'active' => true,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['$createdAt']);
$this->assertNotEmpty($response['body']['$updatedAt']);
$this->assertEquals('function', $response['body']['resourceType']);
$this->assertEquals($functionId, $response['body']['resourceId']);
$this->assertNotEmpty($response['body']['resourceUpdatedAt']);
$this->assertNotEmpty($response['body']['projectId']);
$this->assertEquals('0 0 * * *', $response['body']['schedule']);
$this->assertTrue($response['body']['active']);
$this->assertNotEmpty($response['body']['region']);
return ['scheduleId' => $response['body']['$id'], 'functionId' => $functionId];
}
public function testCreateScheduleResourceNotFound(): void
{
// Function not found
$response = $this->createSchedule([
'resourceType' => 'function',
'resourceId' => ID::unique(),
'schedule' => '0 0 * * *',
]);
$this->assertEquals(404, $response['headers']['status-code']);
// Execution not found
$response = $this->createSchedule([
'resourceType' => 'execution',
'resourceId' => ID::unique(),
'schedule' => '*/10 * * * *',
]);
$this->assertEquals(404, $response['headers']['status-code']);
// Message not found
$response = $this->createSchedule([
'resourceType' => 'message',
'resourceId' => ID::unique(),
'schedule' => '0 9 * * 1',
]);
$this->assertEquals(404, $response['headers']['status-code']);
}
public function testCreateScheduleInvalidResourceType(): void
{
$response = $this->createSchedule([
'resourceType' => 'invalid',
'resourceId' => ID::unique(),
'schedule' => '0 0 * * *',
]);
$this->assertEquals(400, $response['headers']['status-code']);
}
public function testCreateScheduleInvalidCron(): void
{
$response = $this->createSchedule([
'resourceType' => 'function',
'resourceId' => ID::unique(),
'schedule' => 'not-a-cron',
]);
$this->assertEquals(400, $response['headers']['status-code']);
}
public function testCreateScheduleMissingRequired(): void
{
// Missing resourceType
$response = $this->createSchedule([
'resourceId' => ID::unique(),
'schedule' => '0 0 * * *',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Missing resourceId
$response = $this->createSchedule([
'resourceType' => 'function',
'schedule' => '0 0 * * *',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Missing schedule
$response = $this->createSchedule([
'resourceType' => 'function',
'resourceId' => ID::unique(),
]);
$this->assertEquals(400, $response['headers']['status-code']);
}
/**
* @depends testCreateSchedule
*/
public function testGetSchedule(array $data): void
{
$scheduleId = $data['scheduleId'];
/**
* Test for SUCCESS
*/
$response = $this->getSchedule($scheduleId);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($scheduleId, $response['body']['$id']);
$this->assertEquals('function', $response['body']['resourceType']);
$this->assertEquals('0 0 * * *', $response['body']['schedule']);
$this->assertTrue($response['body']['active']);
}
public function testGetScheduleNotFound(): void
{
$response = $this->getSchedule('nonexistent');
$this->assertEquals(404, $response['headers']['status-code']);
}
/**
* @depends testCreateSchedule
*/
public function testListSchedules(array $data): void
{
/**
* Test for SUCCESS
*/
$response = $this->listSchedules();
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['schedules']);
$this->assertGreaterThanOrEqual(1, $response['body']['total']);
$this->assertGreaterThanOrEqual(1, \count($response['body']['schedules']));
// Verify schedule structure
$schedule = $response['body']['schedules'][0];
$this->assertArrayHasKey('$id', $schedule);
$this->assertArrayHasKey('$createdAt', $schedule);
$this->assertArrayHasKey('$updatedAt', $schedule);
$this->assertArrayHasKey('resourceType', $schedule);
$this->assertArrayHasKey('resourceId', $schedule);
$this->assertArrayHasKey('projectId', $schedule);
$this->assertArrayHasKey('schedule', $schedule);
$this->assertArrayHasKey('active', $schedule);
$this->assertArrayHasKey('region', $schedule);
}
/**
* @depends testCreateSchedule
*/
public function testListSchedulesWithQuery(array $data): void
{
// Filter by resourceType
$response = $this->listSchedules([
'queries' => [Query::equal('resourceType', ['function'])->toString()],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThanOrEqual(1, $response['body']['total']);
foreach ($response['body']['schedules'] as $schedule) {
$this->assertEquals('function', $schedule['resourceType']);
}
// Filter by active status
$response = $this->listSchedules([
'queries' => [Query::equal('active', [true])->toString()],
]);
$this->assertEquals(200, $response['headers']['status-code']);
foreach ($response['body']['schedules'] as $schedule) {
$this->assertTrue($schedule['active']);
}
}
public function testListSchedulesWithTotalDisabled(): void
{
$response = $this->listSchedules([
'total' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(0, $response['body']['total']);
$this->assertIsArray($response['body']['schedules']);
}
public function testListSchedulesInvalidQuery(): void
{
$response = $this->listSchedules([
'queries' => [Query::equal('nonexistent', ['value'])->toString()],
]);
$this->assertEquals(400, $response['headers']['status-code']);
}
public function testScheduleProjectIsolation(): void
{
// Create a function and schedule in the current project
$function = $this->createFunction();
$this->assertEquals(201, $function['headers']['status-code']);
$response = $this->createSchedule([
'resourceType' => 'function',
'resourceId' => $function['body']['$id'],
'schedule' => '0 12 * * *',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$scheduleId = $response['body']['$id'];
// Create a fresh project with its own key
$freshProject = $this->getProject(true);
// Try to get the schedule from the other project
$response = $this->client->call(\Tests\E2E\Client::METHOD_GET, '/schedules/' . $scheduleId, [
'content-type' => 'application/json',
'x-appwrite-project' => $freshProject['$id'],
'x-appwrite-key' => $freshProject['apiKey'],
]);
$this->assertEquals(404, $response['headers']['status-code']);
// List should not include schedules from other projects
$response = $this->client->call(\Tests\E2E\Client::METHOD_GET, '/schedules', [
'content-type' => 'application/json',
'x-appwrite-project' => $freshProject['$id'],
'x-appwrite-key' => $freshProject['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$scheduleIds = array_column($response['body']['schedules'], '$id');
$this->assertNotContains($scheduleId, $scheduleIds);
}
}