appwrite/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
2025-03-11 18:19:25 +01:00

2101 lines
80 KiB
PHP

<?php
namespace Tests\E2E\Services\Functions;
use Appwrite\Platform\Modules\Compute\Specification;
use Appwrite\Tests\Retry;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\CLI\Console;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\System\System;
class FunctionsCustomServerTest extends Scope
{
use FunctionsBase;
use ProjectCustom;
use SideServer;
public function testListSpecs(): void
{
$specifications = $this->listSpecifications();
$this->assertEquals(200, $specifications['headers']['status-code']);
$this->assertGreaterThan(0, $specifications['body']['total']);
$this->assertArrayHasKey(0, $specifications['body']['specifications']);
$this->assertArrayHasKey('memory', $specifications['body']['specifications'][0]);
$this->assertArrayHasKey('cpus', $specifications['body']['specifications'][0]);
$this->assertArrayHasKey('enabled', $specifications['body']['specifications'][0]);
$this->assertArrayHasKey('slug', $specifications['body']['specifications'][0]);
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Specs function',
'runtime' => 'php-8.0',
'specification' => $specifications['body']['specifications'][0]['slug']
]);
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $function['body']['specification']);
$function = $this->getFunction($function['body']['$id']);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $function['body']['specification']);
$this->cleanupFunction($function['body']['$id']);
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Specs function',
'runtime' => 'php-8.0',
'specification' => 'cheap-please'
]);
$this->assertEquals(400, $function['headers']['status-code']);
}
public function testCreateFunction(): array
{
/**
* Test for SUCCESS
*/
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'events' => [
'buckets.*.create',
'buckets.*.delete',
],
'timeout' => 10,
]);
$functionId = $function['body']['$id'] ?? '';
$dateValidator = new DatetimeValidator();
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$this->assertEquals('Test', $function['body']['name']);
$this->assertEquals('php-8.0', $function['body']['runtime']);
$this->assertEquals(true, $dateValidator->isValid($function['body']['$createdAt']));
$this->assertEquals(true, $dateValidator->isValid($function['body']['$updatedAt']));
$this->assertEquals('', $function['body']['deployment']);
$this->assertEquals([
'buckets.*.create',
'buckets.*.delete',
], $function['body']['events']);
$this->assertEmpty($function['body']['schedule']);
$this->assertEquals(10, $function['body']['timeout']);
$variable = $this->createVariable($functionId, [
'key' => 'funcKey1',
'value' => 'funcValue1',
]);
$variable2 = $this->createVariable($functionId, [
'key' => 'funcKey2',
'value' => 'funcValue2',
]);
$variable3 = $this->createVariable($functionId, [
'key' => 'funcKey3',
'value' => 'funcValue3',
]);
$this->assertEquals(201, $variable['headers']['status-code']);
$this->assertEquals(201, $variable2['headers']['status-code']);
$this->assertEquals(201, $variable3['headers']['status-code']);
return [
'functionId' => $functionId,
];
}
/**
* @depends testCreateFunction
*/
public function testListFunctions(array $data): array
{
/**
* Test for SUCCESS
*/
// Test search id
$functions = $this->listFunctions([
'search' => $data['functionId']
]);
$this->assertEquals($functions['headers']['status-code'], 200);
$this->assertCount(1, $functions['body']['functions']);
$this->assertEquals($functions['body']['functions'][0]['name'], 'Test');
// Test pagination limit
$functions = $this->listFunctions([
'queries' => [
Query::limit(1)->toString(),
],
]);
$this->assertEquals($functions['headers']['status-code'], 200);
$this->assertCount(1, $functions['body']['functions']);
// Test pagination offset
$functions = $this->listFunctions([
'queries' => [
Query::offset(1)->toString(),
],
]);
$this->assertEquals($functions['headers']['status-code'], 200);
$this->assertCount(0, $functions['body']['functions']);
// Test filter enabled
$functions = $this->listFunctions([
'queries' => [
Query::equal('enabled', [true])->toString(),
],
]);
$this->assertEquals($functions['headers']['status-code'], 200);
$this->assertCount(1, $functions['body']['functions']);
// Test filter disabled
$functions = $this->listFunctions([
'queries' => [
Query::equal('enabled', [false])->toString(),
],
]);
$this->assertEquals($functions['headers']['status-code'], 200);
$this->assertCount(0, $functions['body']['functions']);
// Test search name
$functions = $this->listFunctions([
'search' => 'Test'
]);
$this->assertEquals($functions['headers']['status-code'], 200);
$this->assertCount(1, $functions['body']['functions']);
$this->assertEquals($functions['body']['functions'][0]['$id'], $data['functionId']);
// Test search runtime
$functions = $this->listFunctions([
'search' => 'php-8.0'
]);
$this->assertEquals($functions['headers']['status-code'], 200);
$this->assertCount(1, $functions['body']['functions']);
$this->assertEquals($functions['body']['functions'][0]['$id'], $data['functionId']);
/**
* Test pagination
*/
$this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test 2',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'events' => [
'buckets.*.create',
'buckets.*.delete',
],
'timeout' => 10,
]);
$functions = $this->listFunctions();
$this->assertEquals($functions['headers']['status-code'], 200);
$this->assertEquals($functions['body']['total'], 2);
$this->assertIsArray($functions['body']['functions']);
$this->assertCount(2, $functions['body']['functions']);
$this->assertEquals($functions['body']['functions'][0]['name'], 'Test');
$this->assertEquals($functions['body']['functions'][1]['name'], 'Test 2');
$functions1 = $this->listFunctions([
'queries' => [
Query::cursorAfter(new Document(['$id' => $functions['body']['functions'][0]['$id']]))->toString(),
],
]);
$this->assertEquals($functions1['headers']['status-code'], 200);
$this->assertCount(1, $functions1['body']['functions']);
$this->assertEquals($functions1['body']['functions'][0]['name'], 'Test 2');
$functions2 = $this->listFunctions([
'queries' => [
Query::cursorBefore(new Document(['$id' => $functions['body']['functions'][1]['$id']]))->toString(),
],
]);
$this->assertEquals($functions2['headers']['status-code'], 200);
$this->assertCount(1, $functions2['body']['functions']);
$this->assertEquals($functions2['body']['functions'][0]['name'], 'Test');
/**
* Test for FAILURE
*/
$functions = $this->listFunctions([
'queries' => [
Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(),
],
]);
$this->assertEquals($functions['headers']['status-code'], 400);
return $data;
}
/**
* @depends testListFunctions
*/
public function testGetFunction(array $data): array
{
/**
* Test for SUCCESS
*/
$function = $this->getFunction($data['functionId']);
$this->assertEquals($function['headers']['status-code'], 200);
$this->assertEquals($function['body']['name'], 'Test');
/**
* Test for FAILURE
*/
$function = $this->getFunction('x');
$this->assertEquals($function['headers']['status-code'], 404);
return $data;
}
/**
* @depends testGetFunction
*/
public function testUpdateFunction($data): array
{
/**
* Test for SUCCESS
*/
$function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test1',
'events' => [
'users.*.update.name',
'users.*.update.email',
],
'schedule' => '0 0 1 1 *',
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
]);
$dateValidator = new DatetimeValidator();
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$this->assertEquals('Test1', $function['body']['name']);
$this->assertEquals(true, $dateValidator->isValid($function['body']['$createdAt']));
$this->assertEquals(true, $dateValidator->isValid($function['body']['$updatedAt']));
$this->assertEquals('', $function['body']['deployment']);
$this->assertEquals([
'users.*.update.name',
'users.*.update.email',
], $function['body']['events']);
$this->assertEquals('0 0 1 1 *', $function['body']['schedule']);
$this->assertEquals(15, $function['body']['timeout']);
// Create a variable for later tests
$variable = $this->createVariable($data['functionId'], [
'key' => 'GLOBAL_VARIABLE',
'value' => 'Global Variable Value',
]);
$this->assertEquals(201, $variable['headers']['status-code']);
return $data;
}
public function testCreateDeploymentFromCLI()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'events' => [
'users.*.create',
'users.*.delete',
],
'schedule' => '0 0 1 1 *', // Once a year
'timeout' => 10,
]);
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', [
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
'x-sdk-language' => 'cli',
], [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'activate' => true
]);
$this->assertEquals(202, $deployment['headers']['status-code']);
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('ready', $deployment['body']['status']);
$this->assertEquals('cli', $deployment['body']['type']);
}, 500000, 1000);
}
public function testCreateFunctionAndDeploymentFromTemplate()
{
$starterTemplate = $this->getTemplate('starter');
$this->assertEquals(200, $starterTemplate['headers']['status-code']);
$phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) {
return $runtime['name'] === 'php-8.0';
}))[0];
// If this fails, the template has variables, and this test needs to be updated
$this->assertEmpty($starterTemplate['body']['variables']);
$function = $this->createFunction(
[
'functionId' => ID::unique(),
'name' => $starterTemplate['body']['name'],
'runtime' => 'php-8.0',
'execute' => $starterTemplate['body']['permissions'],
'entrypoint' => $phpRuntime['entrypoint'],
'events' => $starterTemplate['body']['events'],
'schedule' => $starterTemplate['body']['cron'],
'timeout' => $starterTemplate['body']['timeout'],
'commands' => $phpRuntime['commands'],
'scopes' => $starterTemplate['body']['scopes'],
'templateRepository' => $starterTemplate['body']['providerRepositoryId'],
'templateOwner' => $starterTemplate['body']['providerOwner'],
'templateRootDirectory' => $phpRuntime['providerRootDirectory'],
'templateVersion' => $starterTemplate['body']['providerVersion'],
]
);
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$functionId = $function['body']['$id'] ?? '';
$deployment = $this->createTemplateDeployment(
$functionId,
[
'functionId' => ID::unique(),
'activate' => true,
'repository' => $starterTemplate['body']['providerRepositoryId'],
'owner' => $starterTemplate['body']['providerOwner'],
'rootDirectory' => $phpRuntime['providerRootDirectory'],
'version' => $starterTemplate['body']['providerVersion'],
]
);
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$deployments = $this->listDeployments($functionId);
$this->assertEquals(200, $deployments['headers']['status-code']);
$this->assertEquals(1, $deployments['body']['total']);
$lastDeployment = $deployments['body']['deployments'][0];
$this->assertNotEmpty($lastDeployment['$id']);
$this->assertEquals(0, $lastDeployment['sourceSize']);
$deploymentId = $lastDeployment['$id'];
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('ready', $deployment['body']['status']);
}, 50000, 1000);
$function = $this->getFunction($functionId);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertEquals($deploymentId, $function['body']['deployment']);
// Test starter code is used and that dynamic keys work
$execution = $this->createExecution($functionId, [
'path' => '/ping',
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals("completed", $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertEquals("Pong", $execution['body']['responseBody']);
$this->assertEmpty($execution['body']['errors']);
// Test execution logged correct total users
$users = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], $this->getHeaders()), []);
$this->assertEquals(200, $users['headers']['status-code']);
$this->assertIsInt($users['body']['total']);
$totalUsers = $users['body']['total'];
$this->assertStringContainsString("Total users: " . $totalUsers, $execution['body']['logs']);
// Execute function again but async
$execution = $this->createExecution($functionId, [
'path' => '/ping',
'async' => true
]);
$this->assertEquals(202, $execution['headers']['status-code']);
$this->assertNotEmpty($execution['body']['$id']);
$this->assertEquals('waiting', $execution['body']['status']);
$executionId = $execution['body']['$id'] ?? '';
$this->assertEventually(function () use ($functionId, $executionId, $totalUsers) {
$execution = $this->getExecution($functionId, $executionId);
$this->assertEquals(200, $execution['headers']['status-code']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEmpty($execution['body']['responseBody']);
$this->assertEmpty($execution['body']['errors']);
$this->assertStringContainsString("Total users: " . $totalUsers, $execution['body']['logs']);
}, 10000, 500);
$function = $this->deleteFunction($functionId);
}
/**
* @depends testUpdateFunction
*/
public function testCreateDeployment($data): array
{
/**
* Test for SUCCESS
*/
$functionId = $data['functionId'];
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('php'),
'activate' => true
]);
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt']));
$this->assertEquals('index.php', $deployment['body']['entrypoint']);
$deploymentIdActive = $deployment['body']['$id'] ?? '';
$this->assertEventually(function () use ($functionId, $deploymentIdActive) {
$deployment = $this->getDeployment($functionId, $deploymentIdActive);
$this->assertEquals('ready', $deployment['body']['status']);
}, 50000, 500);
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('php'),
'activate' => 'false'
]);
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$deploymentIdInactive = $deployment['body']['$id'] ?? '';
$this->assertEventually(function () use ($functionId, $deploymentIdInactive) {
$deployment = $this->getDeployment($functionId, $deploymentIdInactive);
$this->assertEquals('ready', $deployment['body']['status']);
}, 50000, 500);
$function = $this->getFunction($functionId);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertEquals($deploymentIdActive, $function['body']['deployment']);
$this->assertNotEquals($deploymentIdInactive, $function['body']['deployment']);
$deployment = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId . '/deployments/' . $deploymentIdInactive, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(204, $deployment['headers']['status-code']);
return array_merge($data, ['deploymentId' => $deploymentIdActive]);
}
/**
* @depends testUpdateFunction
*/
#[Retry(count: 3)]
public function testCancelDeploymentBuild($data): void
{
$functionId = $data['functionId'];
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('php'),
'activate' => 'false'
]);
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt']));
$this->assertEquals('index.php', $deployment['body']['entrypoint']);
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('building', $deployment['body']['status']);
}, 100000, 250);
$deployment = $this->cancelDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('canceled', $deployment['body']['status']);
/**
* Build worker still runs the build.
* 30s sleep gives worker enough time to finish build.
* After build finished, it should still be canceled, not ready.
*/
\sleep(30);
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('canceled', $deployment['body']['status']);
}
/**
* @depends testUpdateFunction
*/
public function testCreateDeploymentLarge($data): array
{
/**
* Test for Large Code File SUCCESS
*/
$functionId = $data['functionId'];
$folder = 'php-large';
$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 .", '', $this->stdout, $this->stderr);
$chunkSize = 5 * 1024 * 1024;
$handle = @fopen($code, "rb");
$mimeType = 'application/x-gzip';
$counter = 0;
$size = filesize($code);
$headers = [
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id']
];
$id = '';
while (!feof($handle)) {
$curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'php-large-fx.tar.gz');
$headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size - 1) . '/' . $size;
if (!empty($id)) {
$headers['x-appwrite-id'] = $id;
}
$largeTag = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge($headers, $this->getHeaders()), [
'entrypoint' => 'index.php',
'code' => $curlFile,
'activate' => true,
'commands' => 'cp blue.mp4 copy.mp4 && ls -al' // +7MB buildSize
]);
$counter++;
$id = $largeTag['body']['$id'];
}
@fclose($handle);
$this->assertEquals(202, $largeTag['headers']['status-code']);
$this->assertNotEmpty($largeTag['body']['$id']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($largeTag['body']['$createdAt']));
$this->assertEquals('index.php', $largeTag['body']['entrypoint']);
$this->assertGreaterThan(1024 * 1024 * 5, $largeTag['body']['sourceSize']); // ~7MB video file
$this->assertLessThan(1024 * 1024 * 10, $largeTag['body']['sourceSize']); // ~7MB video file
$deploymentSize = $largeTag['body']['sourceSize'];
$deploymentId = $largeTag['body']['$id'];
$this->assertEventually(function () use ($functionId, $deploymentId, $deploymentSize) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('ready', $deployment['body']['status']);
$this->assertEquals($deploymentSize, $deployment['body']['sourceSize']);
$this->assertGreaterThan(1024 * 1024 * 10, $deployment['body']['buildSize']); // ~7MB video file + 10MB sample file
}, 500000, 1000);
return $data;
}
/**
* @depends testCreateDeployment
*/
public function testUpdateDeployment($data): array
{
/**
* Test for SUCCESS
*/
$dateValidator = new DatetimeValidator();
$response = $this->client->call(Client::METHOD_PATCH, '/functions/' . $data['functionId'] . '/deployments/' . $data['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']);
$this->assertEquals(true, $dateValidator->isValid($response['body']['$createdAt']));
$this->assertEquals(true, $dateValidator->isValid($response['body']['$updatedAt']));
$this->assertEquals($data['deploymentId'], $response['body']['deployment']);
return $data;
}
/**
* @depends testCreateDeployment
*/
public function testListDeployments(array $data): array
{
/**
* Test for SUCCESS
*/
$functionId = $data['functionId'];
$deployments = $this->listDeployments($functionId);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertEquals($deployments['body']['total'], 3);
$this->assertIsArray($deployments['body']['deployments']);
$this->assertCount(3, $deployments['body']['deployments']);
$this->assertArrayHasKey('sourceSize', $deployments['body']['deployments'][0]);
$this->assertArrayHasKey('buildSize', $deployments['body']['deployments'][0]);
$deployments = $this->listDeployments($functionId, [
'queries' => [
Query::limit(1)->toString(),
],
]);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertCount(1, $deployments['body']['deployments']);
$deployments = $this->listDeployments($functionId, [
'queries' => [
Query::offset(1)->toString(),
],
]);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertCount(2, $deployments['body']['deployments']);
$deployments = $this->listDeployments($functionId, [
'search' => 'php-8.0'
]);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertEquals(3, $deployments['body']['total']);
$this->assertIsArray($deployments['body']['deployments']);
$this->assertCount(3, $deployments['body']['deployments']);
$this->assertEquals($deployments['body']['deployments'][0]['$id'], $data['deploymentId']);
$deployments = $this->listDeployments(
$functionId,
[
'queries' => [
Query::equal('type', ['manual'])->toString(),
],
]
);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertEquals(3, $deployments['body']['total']);
$deployments = $this->listDeployments(
$functionId,
[
'queries' => [
Query::equal('type', ['vcs'])->toString(),
],
]
);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertEquals(0, $deployments['body']['total']);
$deployments = $this->listDeployments(
$functionId,
[
'queries' => [
Query::equal('type', ['invalid-string'])->toString(),
],
]
);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertEquals(0, $deployments['body']['total']);
$deployments = $this->listDeployments(
$functionId,
[
'queries' => [
Query::greaterThan('sourceSize', 10000)->toString(),
],
]
);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertEquals(1, $deployments['body']['total']);
$deployments = $this->listDeployments(
$functionId,
[
'queries' => [
Query::greaterThan('sourceSize', 0)->toString(),
],
]
);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertEquals(3, $deployments['body']['total']);
$deployments = $this->listDeployments(
$functionId,
[
'queries' => [
Query::greaterThan('sourceSize', -100)->toString(),
],
]
);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertEquals(3, $deployments['body']['total']);
/**
* Ensure size output and size filters work exactly.
* Prevents buildSize being counted towards deployemtn size
*/
$deployments = $this->listDeployments(
$functionId,
[
Query::limit(1)->toString(),
]
);
$this->assertEquals(200, $deployments['headers']['status-code']);
$this->assertGreaterThanOrEqual(1, $deployments['body']['total']);
$this->assertNotEmpty($deployments['body']['deployments'][0]['$id']);
$this->assertNotEmpty($deployments['body']['deployments'][0]['sourceSize']);
$deploymentId = $deployments['body']['deployments'][0]['$id'];
$deploymentSize = $deployments['body']['deployments'][0]['sourceSize'];
$deployments = $this->listDeployments(
$functionId,
[
'queries' => [
Query::equal('sourceSize', [$deploymentSize])->toString(),
],
]
);
$this->assertEquals(200, $deployments['headers']['status-code']);
$this->assertGreaterThan(0, $deployments['body']['total']);
$matchingDeployment = array_filter(
$deployments['body']['deployments'],
fn ($deployment) => $deployment['$id'] === $deploymentId
);
$this->assertNotEmpty($matchingDeployment, "Deployment with ID {$deploymentId} not found");
if (!empty($matchingDeployment)) {
$deployment = reset($matchingDeployment);
$this->assertEquals($deploymentSize, $deployment['sourceSize']);
}
return $data;
}
/**
* @depends testCreateDeployment
*/
public function testGetDeployment(array $data): array
{
/**
* Test for SUCCESS
*/
$deployment = $this->getDeployment($data['functionId'], $data['deploymentId']);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertGreaterThan(0, $deployment['body']['buildDuration']);
$this->assertNotEmpty($deployment['body']['status']);
$this->assertNotEmpty($deployment['body']['buildLogs']);
$this->assertArrayHasKey('sourceSize', $deployment['body']);
$this->assertArrayHasKey('buildSize', $deployment['body']);
/**
* Test for FAILURE
*/
$deployment = $this->getDeployment($data['functionId'], 'x');
$this->assertEquals($deployment['headers']['status-code'], 404);
return $data;
}
/**
* @depends testUpdateDeployment
*/
public function testCreateExecution($data): array
{
/**
* Test for SUCCESS
*/
$execution = $this->createExecution($data['functionId'], [
'async' => 'false',
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertNotEmpty($execution['body']['$id']);
$this->assertNotEmpty($execution['body']['functionId']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($execution['body']['$createdAt']));
$this->assertEquals($data['functionId'], $execution['body']['functionId']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertStringContainsString($execution['body']['functionId'], $execution['body']['responseBody']);
$this->assertStringContainsString($data['deploymentId'], $execution['body']['responseBody']);
$this->assertStringContainsString('Test1', $execution['body']['responseBody']);
$this->assertStringContainsString('http', $execution['body']['responseBody']);
$this->assertStringContainsString('PHP', $execution['body']['responseBody']);
$this->assertStringContainsString('8.0', $execution['body']['responseBody']);
$this->assertStringContainsString('Global Variable Value', $execution['body']['responseBody']);
// $this->assertStringContainsString('êä', $execution['body']['responseBody']); // tests unknown utf-8 chars
$this->assertNotEmpty($execution['body']['errors']);
$this->assertNotEmpty($execution['body']['logs']);
$this->assertLessThan(10, $execution['body']['duration']);
$executionId = $execution['body']['$id'] ?? '';
$execution = $this->createExecution($data['functionId'], [
'async' => 'false',
'path' => '/?code=400'
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(400, $execution['body']['responseStatusCode']);
$execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/executions/' . $execution['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(204, $execution['headers']['status-code']);
return array_merge($data, ['executionId' => $executionId]);
}
/**
* @depends testCreateExecution
*/
public function testListExecutions(array $data): array
{
/**
* Test for SUCCESS
*/
$executions = $this->listExecutions($data['functionId']);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertEquals(1, $executions['body']['total']);
$this->assertIsArray($executions['body']['executions']);
$this->assertCount(1, $executions['body']['executions']);
$executions = $this->listExecutions($data['functionId'], [
'queries' => [
Query::limit(1)->toString(),
],
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(1, $executions['body']['executions']);
$executions = $this->listExecutions($data['functionId'], [
'queries' => [
Query::offset(0)->toString(),
],
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(1, $executions['body']['executions']);
$executions = $this->listExecutions($data['functionId'], [
'queries' => [
Query::equal('trigger', ['http'])->toString(),
],
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(1, $executions['body']['executions']);
/**
* Test search queries
*/
$executions = $this->listExecutions($data['functionId'], [
'search' => $data['executionId'],
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertEquals(1, $executions['body']['total']);
$this->assertIsInt($executions['body']['total']);
$this->assertCount(1, $executions['body']['executions']);
$this->assertEquals($data['functionId'], $executions['body']['executions'][0]['functionId']);
$executions = $this->listExecutions($data['functionId'], [
'search' => $data['functionId'],
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertEquals(1, $executions['body']['total']);
$this->assertIsInt($executions['body']['total']);
$this->assertCount(1, $executions['body']['executions']);
$this->assertEquals($data['executionId'], $executions['body']['executions'][0]['$id']);
return $data;
}
/**
* @depends testUpdateDeployment
*/
public function testSyncCreateExecution($data): array
{
/**
* Test for SUCCESS
*/
$execution = $this->createExecution($data['functionId'], [
// Testing default value, should be 'async' => 'false'
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertStringContainsString('Test1', $execution['body']['responseBody']);
$this->assertStringContainsString('http', $execution['body']['responseBody']);
$this->assertStringContainsString('PHP', $execution['body']['responseBody']);
$this->assertStringContainsString('8.0', $execution['body']['responseBody']);
// $this->assertStringContainsString('êä', $execution['body']['response']); // tests unknown utf-8 chars
$this->assertLessThan(1.500, $execution['body']['duration']);
return $data;
}
/**
* @depends testListExecutions
*/
public function testGetExecution(array $data): array
{
/**
* Test for SUCCESS
*/
$execution = $this->getExecution($data['functionId'], $data['executionId']);
$this->assertEquals($execution['headers']['status-code'], 200);
$this->assertEquals($execution['body']['$id'], $data['executionId']);
/**
* Test for FAILURE
*/
$function = $this->getExecution($data['functionId'], 'x');
$this->assertEquals($function['headers']['status-code'], 404);
return $data;
}
/**
* @depends testGetExecution
*/
public function testDeleteExecution($data): array
{
/**
* Test for SUCCESS
*/
$execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/executions/' . $data['executionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $execution['headers']['status-code']);
$this->assertEmpty($execution['body']);
$execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/executions/' . $data['executionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(404, $execution['headers']['status-code']);
$this->assertStringContainsString('Execution with the requested ID could not be found', $execution['body']['message']);
/**
* Test for FAILURE
*/
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/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']);
$execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/executions/' . $executionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(400, $execution['headers']['status-code']);
$this->assertStringContainsString('execution_in_progress', $execution['body']['type']);
$this->assertStringContainsString('Can\'t delete ongoing execution.', $execution['body']['message']);
return $data;
}
/**
* @depends testGetExecution
*/
public function testUpdateSpecs($data): array
{
/**
* Test for SUCCESS
*/
// Change the function specs
$function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test1',
'events' => [
'users.*.update.name',
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'specification' => Specification::S_1VCPU_1GB,
]);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$this->assertEquals(Specification::S_1VCPU_1GB, $function['body']['specification']);
// Verify the updated specs
$execution = $this->createExecution($data['functionId']);
$output = json_decode($execution['body']['responseBody'], true);
$this->assertEquals(1, $output['APPWRITE_FUNCTION_CPUS']);
$this->assertEquals(1024, $output['APPWRITE_FUNCTION_MEMORY']);
// Change the specs to 1vcpu 512mb
$function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test1',
'events' => [
'users.*.update.name',
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'specification' => Specification::S_1VCPU_512MB,
]);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$this->assertEquals(Specification::S_1VCPU_512MB, $function['body']['specification']);
// Verify the updated specs
$execution = $this->createExecution($data['functionId']);
$output = json_decode($execution['body']['responseBody'], true);
$this->assertEquals(1, $output['APPWRITE_FUNCTION_CPUS']);
$this->assertEquals(512, $output['APPWRITE_FUNCTION_MEMORY']);
/**
* Test for FAILURE
*/
$function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test1',
'events' => [
'users.*.update.name',
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'specification' => 's-2vcpu-512mb', // Invalid specification
]);
$this->assertEquals(400, $function['headers']['status-code']);
$this->assertStringStartsWith('Invalid `specification` param: Specification must be one of:', $function['body']['message']);
return $data;
}
/**
* @depends testGetExecution
*/
public function testDeleteDeployment($data): array
{
/**
* Test for SUCCESS
*/
$deployment = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/deployments/' . $data['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']);
$deployment = $this->getDeployment($data['functionId'], $data['deploymentId']);
$this->assertEquals(404, $deployment['headers']['status-code']);
return $data;
}
/**
* @depends testCreateDeployment
*/
public function testDeleteFunction($data): array
{
/**
* Test for SUCCESS
*/
$function = $this->deleteFunction($data['functionId']);
$this->assertEquals(204, $function['headers']['status-code']);
$this->assertEmpty($function['body']);
$function = $this->getFunction($data['functionId']);
$this->assertEquals(404, $function['headers']['status-code']);
return $data;
}
public function testExecutionTimeout()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test php-8.0',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'events' => [],
'schedule' => '',
'timeout' => 5, // Should timeout after 5 seconds
]);
$this->setupDeployment($functionId, [
'code' => $this->packageFunction('timeout'),
'activate' => true,
]);
$execution = $this->createExecution($functionId, [
'async' => true
]);
$this->assertEquals(202, $execution['headers']['status-code']);
$executionId = $execution['body']['$id'] ?? '';
\sleep(5); // Wait for the function to timeout
$this->assertEventually(function () use ($functionId, $executionId) {
$execution = $this->getExecution($functionId, $executionId);
$this->assertEquals(200, $execution['headers']['status-code']);
$this->assertEquals('failed', $execution['body']['status']);
$this->assertEquals(500, $execution['body']['responseStatusCode']);
$this->assertGreaterThan(2, $execution['body']['duration']);
$this->assertLessThan(20, $execution['body']['duration']);
$this->assertEquals('', $execution['body']['responseBody']);
$this->assertEquals('', $execution['body']['logs']);
$this->assertStringContainsString('timed out', $execution['body']['errors']);
}, 10000, 500);
$this->cleanupFunction($functionId);
}
/**
*
* @return array<mixed>
*/
public function provideCustomExecutions(): array
{
return [
['folder' => 'php-fn', 'name' => 'php-8.0', 'entrypoint' => 'index.php', 'runtimeName' => 'PHP', 'runtimeVersion' => '8.0'],
['folder' => 'node', 'name' => 'node-18.0', 'entrypoint' => 'index.js', 'runtimeName' => 'Node.js', 'runtimeVersion' => '18.0'],
// TODO: Re-enable; temporarly disabled due to OPR v5 issues
// ['folder' => 'python', 'name' => 'python-3.9', 'entrypoint' => 'main.py', 'runtimeName' => 'Python', 'runtimeVersion' => '3.9'],
['folder' => 'ruby', 'name' => 'ruby-3.1', 'entrypoint' => 'main.rb', 'runtimeName' => 'Ruby', 'runtimeVersion' => '3.1'],
// Swift and Dart disabled on purpose, as it's very slow.
// [ 'folder' => 'dart', 'name' => 'dart-2.15', 'entrypoint' => 'main.dart', 'runtimeName' => 'Dart', 'runtimeVersion' => '2.15' ],
// [ 'folder' => 'swift', 'name' => 'swift-5.5', 'entrypoint' => 'index.swift', 'runtimeName' => 'Swift', 'runtimeVersion' => '5.5' ],
];
}
/**
* @param string $folder
* @param string $name
* @param string $entrypoint
*
* @dataProvider provideCustomExecutions
* @depends testExecutionTimeout
*/
public function testCreateCustomExecution(string $folder, string $name, string $entrypoint, string $runtimeName, string $runtimeVersion)
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test ' . $name,
'runtime' => $name,
'entrypoint' => $entrypoint,
'events' => [],
'timeout' => 15,
]);
$variable = $this->createVariable($functionId, [
'key' => 'CUSTOM_VARIABLE',
'value' => 'variable'
]);
$this->assertEquals(201, $variable['headers']['status-code']);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => $entrypoint,
'code' => $this->packageFunction($folder),
'activate' => true
]);
$execution = $this->createExecution($functionId, [
'body' => 'foobar',
'async' => 'false'
]);
$output = json_decode($execution['body']['responseBody'], true);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertEquals($functionId, $output['APPWRITE_FUNCTION_ID']);
$this->assertEquals('Test ' . $name, $output['APPWRITE_FUNCTION_NAME']);
$this->assertEquals($deploymentId, $output['APPWRITE_FUNCTION_DEPLOYMENT']);
$this->assertEquals('http', $output['APPWRITE_FUNCTION_TRIGGER']);
$this->assertEquals($runtimeName, $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals($runtimeVersion, $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']);
$this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']);
$this->assertEquals('variable', $output['CUSTOM_VARIABLE']);
$this->assertEmpty($output['APPWRITE_FUNCTION_USER_ID']);
$this->assertEmpty($output['APPWRITE_FUNCTION_JWT']);
$this->assertEquals($this->getProject()['$id'], $output['APPWRITE_FUNCTION_PROJECT_ID']);
$this->assertStringContainsString('Amazing Function Log', $execution['body']['logs']);
$this->assertEmpty($execution['body']['errors']);
$executionId = $execution['body']['$id'] ?? '';
$executions = $this->listExecutions($functionId);
$this->assertEquals($executions['headers']['status-code'], 200);
$this->assertEquals($executions['body']['total'], 1);
$this->assertIsArray($executions['body']['executions']);
$this->assertCount(1, $executions['body']['executions']);
$this->assertEquals($executions['body']['executions'][0]['$id'], $executionId);
$this->assertEquals($executions['body']['executions'][0]['trigger'], 'http');
$this->assertStringContainsString('Amazing Function Log', $executions['body']['executions'][0]['logs']);
$this->cleanupFunction($functionId);
}
public function testCreateCustomExecutionBinaryResponse()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'timeout' => 15,
'execute' => ['any']
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-response'),
'activate' => true
]);
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
'accept' => 'multipart/form-data', // Accept binary response
], $this->getHeaders()), [
'body' => null,
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertStringContainsString('multipart/form-data', $execution['headers']['content-type']);
$contentType = explode(';', $execution['headers']['content-type']);
$this->assertStringContainsString('boundary=----', $contentType[1]);
$bytes = unpack('C*byte', $execution['body']['responseBody']);
$this->assertCount(3, $bytes);
$this->assertEquals(0, $bytes['byte1']);
$this->assertEquals(10, $bytes['byte2']);
$this->assertEquals(255, $bytes['byte3']);
/**
* Test for FAILURE
*/
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
'accept' => 'application/json', // Accept JSON response
], $this->getHeaders()), [
'body' => null,
]);
$this->assertEquals(400, $execution['headers']['status-code']);
$this->assertStringContainsString('Failed to parse response', $execution['body']['message']);
$this->cleanupFunction($functionId);
}
public function testCreateCustomExecutionBinaryRequest()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'timeout' => 15,
'execute' => ['any']
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-request'),
'activate' => true
]);
$bytes = pack('C*', ...[0, 20, 255]);
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
'content-type' => 'multipart/form-data', // Send binary request
'x-appwrite-project' => $this->getProject()['$id'],
'accept' => 'application/json',
], $this->getHeaders()), [
'body' => $bytes,
], false);
$executionBody = json_decode($execution['body'], true);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals(\md5($bytes), $executionBody['responseBody']);
$this->assertStringStartsWith('application/json', $execution['headers']['content-type']);
/**
* Test for FAILURE
*/
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
'content-type' => 'application/json', // Send JSON headers
'x-appwrite-project' => $this->getProject()['$id'],
'accept' => 'application/json',
], $this->getHeaders()), [
'body' => $bytes,
], false);
$executionBody = json_decode($execution['body'], true);
$this->assertNotEquals(\md5($bytes), $executionBody['responseBody']);
$this->cleanupFunction($functionId);
}
public function testv2Function()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP V2',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'events' => [],
'timeout' => 15,
]);
$variable = $this->client->call(Client::METHOD_PATCH, '/mock/functions-v2', [
'content-type' => 'application/json',
'origin' => 'http://localhost',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-mode' => 'admin',
], [
'functionId' => $functionId
]);
$this->assertEquals(204, $variable['headers']['status-code']);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-v2'),
'activate' => true
]);
$execution = $this->createExecution($functionId, [
'body' => 'foobar',
'async' => 'false'
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$output = json_decode($execution['body']['responseBody'], true);
$this->assertEquals(true, $output['v2Woks']);
$this->cleanupFunction($functionId);
}
public function testGetRuntimes()
{
$runtimes = $this->client->call(Client::METHOD_GET, '/functions/runtimes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $runtimes['headers']['status-code']);
$this->assertGreaterThan(0, $runtimes['body']['total']);
$runtime = $runtimes['body']['runtimes'][0];
$this->assertArrayHasKey('$id', $runtime);
$this->assertArrayHasKey('name', $runtime);
$this->assertArrayHasKey('key', $runtime);
$this->assertArrayHasKey('version', $runtime);
$this->assertArrayHasKey('logo', $runtime);
$this->assertArrayHasKey('image', $runtime);
$this->assertArrayHasKey('base', $runtime);
$this->assertArrayHasKey('supports', $runtime);
}
public function testEventTrigger()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Event executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'events' => [
'users.*.create',
],
'timeout' => 15,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-event'),
'activate' => true
]);
// Create user as an event trigger
$user = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'name' => 'Event User'
]);
$this->assertEquals(201, $user['headers']['status-code']);
$userId = $user['body']['$id'] ?? '';
$this->assertEventually(function () use ($functionId, $userId) {
$executions = $this->listExecutions($functionId);
$lastExecution = $executions['body']['executions'][0];
$this->assertEquals('completed', $lastExecution['status']);
$this->assertEquals(204, $lastExecution['responseStatusCode']);
$this->assertStringContainsString($userId, $lastExecution['logs']);
$this->assertStringContainsString('Event User', $lastExecution['logs']);
}, 10000, 500);
$this->cleanupFunction($functionId);
// Cleanup user
$user = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], []);
$this->assertEquals(204, $user['headers']['status-code']);
}
public function testScopes()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Scopes executions',
'commands' => 'sh setup.sh && composer install',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'scopes' => ['users.read'],
'timeout' => 15,
]);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-scopes'),
'activate' => true,
]);
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertStringContainsStringIgnoringCase("200 OK", $deployment['body']['buildLogs']);
$this->assertStringContainsStringIgnoringCase('"total":', $deployment['body']['buildLogs']);
$this->assertStringContainsStringIgnoringCase('"users":', $deployment['body']['buildLogs']);
$execution = $this->createExecution($functionId, [
'async' => 'false',
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertGreaterThan(0, $execution['body']['duration']);
$this->assertNotEmpty($execution['body']['responseBody']);
$this->assertStringContainsString("total", $execution['body']['responseBody']);
$execution = $this->createExecution($functionId, [
'async' => true,
]);
$this->assertEquals(202, $execution['headers']['status-code']);
$this->assertNotEmpty($execution['body']['$id']);
$executionId = $execution['body']['$id'] ?? '';
$this->assertEventually(function () use ($functionId, $executionId) {
$execution = $this->getExecution($functionId, $executionId);
$this->assertEquals(200, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertGreaterThan(0, $execution['body']['duration']);
$this->assertNotEmpty($execution['body']['logs']);
$this->assertStringContainsString("total", $execution['body']['logs']);
}, 10000, 500);
$this->cleanupFunction($functionId);
}
public function testCookieExecution()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Cookie executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'timeout' => 15,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-cookie'),
'activate' => true
]);
$cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=val:ue4; cookie5=value5';
$execution = $this->createExecution($functionId, [
'async' => 'false',
'headers' => [
'cookie' => $cookie
]
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertEquals($cookie, $execution['body']['responseBody']);
$this->assertGreaterThan(0, $execution['body']['duration']);
$this->cleanupFunction($functionId);
}
public function testFunctionsDomain()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Cookie executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'timeout' => 15,
'execute' => ['any']
]);
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-cookie'),
'activate' => true
]);
$cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=val:ue4; cookie5=value5';
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => $cookie
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($cookie, $response['body']);
// Async execution document creation
$this->assertEventually(function () use ($functionId) {
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertEquals(1, count($executions['body']['executions']));
});
// Await Aggregation
sleep(System::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', 30));
$this->assertEventually(function () use ($functionId) {
$response = $this->getUsage($functionId, [
'range' => '24h'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(19, count($response['body']));
$this->assertEquals('24h', $response['body']['range']);
$this->assertEquals(1, $response['body']['executionsTotal']);
}, 25000, 1000);
$this->cleanupFunction($functionId);
}
public function testFunctionsDomainBinaryResponse()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'timeout' => 15,
'execute' => ['any']
]);
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-response'),
'activate' => true
]);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/', [], [], false);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$bytes = unpack('C*byte', $response['body']);
$this->assertCount(3, $bytes);
$this->assertEquals(0, $bytes['byte1']);
$this->assertEquals(10, $bytes['byte2']);
$this->assertEquals(255, $bytes['byte3']);
$this->cleanupFunction($functionId);
}
public function testFunctionsDomainBinaryRequest()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'timeout' => 15,
'execute' => ['any']
]);
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-request'),
'activate' => true
]);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$bytes = pack('C*', ...[0, 20, 255]);
$response = $proxyClient->call(Client::METHOD_POST, '/', ['content-type' => 'text/plain'], $bytes, false);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(\md5($bytes), $response['body']);
$this->cleanupFunction($functionId);
}
public function testResponseFilters()
{
// create function with 1.5.0 response format
$response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.5.0', // add response format header
], $this->getHeaders()), [
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'timeout' => 15,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertArrayNotHasKey('scopes', $response['body']);
$this->assertArrayNotHasKey('specification', $response['body']);
// get function with 1.5.0 response format header
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $response['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.5.0', // add response format header
], $this->getHeaders()));
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertArrayNotHasKey('scopes', $function['body']);
$this->assertArrayNotHasKey('specification', $function['body']);
$function = $this->getFunction($function['body']['$id']);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertArrayHasKey('scopes', $function['body']);
$this->assertArrayHasKey('specification', $function['body']);
$functionId = $function['body']['$id'] ?? '';
$this->cleanupFunction($functionId);
}
public function testRequestFilters()
{
$function1Id = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'timeout' => 15,
'execute' => ['any']
]);
$function2Id = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test2',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'timeout' => 15,
'execute' => ['any']
]);
// list functions using request filters
$response = $this->client->call(
Client::METHOD_GET,
'/functions',
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.4.0', // Set response format for 1.4 syntax
], $this->getHeaders()),
[
'queries' => [ 'equal("name", ["Test2"])' ]
]
);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['functions']);
$this->assertEquals('Test2', $response['body']['functions'][0]['name']);
$this->cleanupFunction($function1Id);
$this->cleanupFunction($function2Id);
}
public function testFunctionLogging()
{
$function = $this->createFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'name' => 'Logging Test',
'entrypoint' => 'index.js',
'logging' => false,
'execute' => ['any']
]);
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertFalse($function['body']['logging']);
$this->assertNotEmpty($function['body']['$id']);
$functionId = $function['body']['$id'] ?? '';
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'code' => $this->packageFunction('node'),
'activate' => true
]);
// Sync Executions test
$execution = $this->createExecution($functionId);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEmpty($execution['body']['logs']);
$this->assertEmpty($execution['body']['errors']);
// Async Executions test
$execution = $this->createExecution($functionId, [
'async' => true
]);
$this->assertEquals(202, $execution['headers']['status-code']);
$this->assertEmpty($execution['body']['logs']);
$this->assertEmpty($execution['body']['errors']);
$this->assertNotEmpty($execution['body']['$id']);
$executionId = $execution['body']['$id'] ?? '';
$this->assertEventually(function () use ($functionId, $executionId) {
$execution = $this->getExecution($functionId, $executionId);
$this->assertEquals(200, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEmpty($execution['body']['logs']);
$this->assertEmpty($execution['body']['errors']);
}, 10000, 500);
// Domain Executions test
$domain = $this->getFunctionDomain($functionId);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
]));
$this->assertEquals(200, $response['headers']['status-code']);
$executions = $this->listExecutions($functionId, [
'queries' => [
Query::limit(1)->toString(),
Query::orderDesc('$id')->toString(),
]
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(1, $executions['body']['executions']);
$this->assertEmpty($executions['body']['executions'][0]['logs']);
$this->assertEmpty($executions['body']['executions'][0]['errors']);
// Ensure executions count
$executions = $this->listExecutions($functionId);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(3, $executions['body']['executions']);
// Double check logs and errors are empty
foreach ($executions['body']['executions'] as $execution) {
$this->assertEmpty($execution['logs']);
$this->assertEmpty($execution['errors']);
}
$this->cleanupFunction($functionId);
}
public function testFunctionSpecifications()
{
// Check if the function specifications are correctly set in builds
$function = $this->createFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'name' => 'Specification Test',
'entrypoint' => 'index.js',
'logging' => false,
'execute' => ['any'],
'specification' => Specification::S_2VCPU_2GB,
'commands' => 'echo $APPWRITE_FUNCTION_MEMORY:$APPWRITE_FUNCTION_CPUS',
]);
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertEquals(Specification::S_2VCPU_2GB, $function['body']['specification']);
$this->assertNotEmpty($function['body']['$id']);
$functionId = $functionId = $function['body']['$id'] ?? '';
$deploymentId = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('node'),
'activate' => true
]);
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertTrue(str_contains($deployment['body']['buildLogs'], '2048:2'));
}, 10000, 500);
// Check if the function specifications are correctly set in executions
$execution = $this->createExecution($functionId);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertNotEmpty($execution['body']['$id']);
$executionResponse = json_decode($execution['body']['responseBody'], true);
$this->assertEquals('2048', $executionResponse['APPWRITE_FUNCTION_MEMORY']);
$this->assertEquals('2', $executionResponse['APPWRITE_FUNCTION_CPUS']);
$this->cleanupFunction($functionId);
}
public function testDuplicateDeployment(): void
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'name' => 'Duplicate Deployment Test',
'entrypoint' => 'index.js',
'commands' => ''
]);
$this->assertNotEmpty($functionId);
$deploymentId1 = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('node'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId1);
$execution = $this->createExecution($functionId);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertStringContainsString('APPWRITE_FUNCTION_ID', $execution['body']['responseBody']);
$function = $this->updateFunction($functionId, [
'runtime' => 'node-18.0',
'name' => 'Duplicate Deployment Test',
'entrypoint' => 'index.js',
'commands' => 'rm index.js && mv maintenance.js index.js'
]);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertStringContainsString('maintenance.js', $function['body']['commands']);
$deploymentId2 = $this->setupDuplicateDeployment($functionId, $deploymentId1);
$this->assertNotEmpty($deploymentId2);
$execution = $this->createExecution($functionId);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertStringContainsString('Maintenance', $execution['body']['responseBody']);
$this->cleanupFunction($functionId);
}
public function testUpdateDeploymentStatus(): void
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'runtime' => 'php-8.0',
'name' => 'Re-activate Test',
'entrypoint' => 'index.php',
]);
$this->assertNotEmpty($functionId);
$deploymentId1 = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('php-cookie'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId1);
$execution = $this->createExecution($functionId, [
'headers' => [ 'cookie' => 'cookieName=cookieValue' ]
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertNotEmpty($execution['body']['$id']);
$this->assertStringContainsString('cookieValue', $execution['body']['responseBody']);
$deploymentId2 = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('php'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId2);
$execution = $this->createExecution($functionId);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertNotEmpty($execution['body']['$id']);
$this->assertStringContainsString('UNICODE_TEST', $execution['body']['responseBody']);
$function = $this->getFunction($functionId);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertEquals($deploymentId2, $function['body']['deployment']);
$function = $this->updateFunctionDeployment($functionId, $deploymentId1);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertEquals($deploymentId1, $function['body']['deployment']);
$function = $this->getFunction($functionId);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertEquals($deploymentId1, $function['body']['deployment']);
$execution = $this->createExecution($functionId, [
'headers' => [ 'cookie' => 'cookieName=cookieValue' ]
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertNotEmpty($execution['body']['$id']);
$this->assertStringContainsString('cookieValue', $execution['body']['responseBody']);
$this->cleanupFunction($functionId);
}
}