listSpecifications(); $this->assertEquals(200, $specifications['headers']['status-code']); $this->assertGreaterThanOrEqual(2, $specifications['body']['total']); $this->assertArrayHasKey(0, $specifications['body']['specifications']); $this->assertArrayHasKey(1, $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]); $this->assertArrayHasKey('memory', $specifications['body']['specifications'][1]); $this->assertArrayHasKey('cpus', $specifications['body']['specifications'][1]); $this->assertArrayHasKey('enabled', $specifications['body']['specifications'][1]); $this->assertArrayHasKey('slug', $specifications['body']['specifications'][1]); $site = $this->createSite([ 'buildRuntime' => 'node-22', 'framework' => 'other', 'name' => 'Specs site', 'siteId' => ID::unique(), 'buildSpecification' => $specifications['body']['specifications'][0]['slug'], 'runtimeSpecification' => $specifications['body']['specifications'][1]['slug'], ]); $this->assertEquals(201, $site['headers']['status-code']); $this->assertEquals($specifications['body']['specifications'][0]['slug'], $site['body']['buildSpecification']); $this->assertEquals($specifications['body']['specifications'][1]['slug'], $site['body']['runtimeSpecification']); $site = $this->getSite($site['body']['$id']); $this->assertEquals(200, $site['headers']['status-code']); $this->assertEquals($specifications['body']['specifications'][0]['slug'], $site['body']['buildSpecification']); $this->assertEquals($specifications['body']['specifications'][1]['slug'], $site['body']['runtimeSpecification']); $this->cleanupSite($site['body']['$id']); $site = $this->createSite([ 'buildRuntime' => 'node-22', 'framework' => 'other', 'name' => 'Specs site', 'siteId' => ID::unique(), 'buildSpecification' => 'cheap-please' ]); $this->assertEquals(400, $site['headers']['status-code']); $site = $this->createSite([ 'buildRuntime' => 'node-22', 'framework' => 'other', 'name' => 'Specs site', 'siteId' => ID::unique(), 'runtimeSpecification' => 'cheap-please' ]); $this->assertEquals(400, $site['headers']['status-code']); } public function testCreateSite(): void { /** * Test for SUCCESS */ $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $dateValidator = new DateTimeValidator(); $this->assertEquals(201, $site['headers']['status-code']); $this->assertNotEmpty($site['body']['$id']); $this->assertEquals('Test Site', $site['body']['name']); $this->assertEquals('other', $site['body']['framework']); $this->assertEquals(true, $dateValidator->isValid($site['body']['$createdAt'])); $this->assertEquals(true, $dateValidator->isValid($site['body']['$updatedAt'])); $this->assertEquals('node-22', $site['body']['buildRuntime']); $this->assertEquals(null, $site['body']['fallbackFile']); $this->assertEquals('./', $site['body']['outputDirectory']); $variable = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'siteKey1', 'value' => 'siteValue1', ]); $variable2 = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'siteKey2', 'value' => 'siteValue2', ]); $variable3 = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'siteKey3', 'value' => 'siteValue3', ]); $this->assertEquals(201, $variable['headers']['status-code']); $this->assertEquals(201, $variable2['headers']['status-code']); $this->assertEquals(201, $variable3['headers']['status-code']); $this->cleanupSite($siteId); } public function testConsoleAvailabilityEndpoint(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Test Site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'outputDirectory' => './', 'fallbackFile' => '', ]); $this->assertNotEmpty($siteId); $domain = $this->setupSiteDomain($siteId); $response = $this->client->call(Client::METHOD_GET, '/console/resources', [ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], 'x-appwrite-project' => 'console', ], [ 'type' => 'rules', 'value' => $domain, ]); $this->assertEquals(409, $response['headers']['status-code']); // domain unavailable $nonExistingDomain = "non-existent-subdomain.sites.localhost"; $response = $this->client->call(Client::METHOD_GET, '/console/resources', [ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], 'x-appwrite-project' => 'console', ], [ 'type' => 'rules', 'value' => $nonExistingDomain, ]); $this->assertEquals(204, $response['headers']['status-code']); // domain available $this->cleanupSite($siteId); $this->assertEventually(function () use ($siteId) { $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::equal('deploymentResourceId', [$siteId])->toString() ] ]); $this->assertEquals(200, $rule['headers']['status-code']); $this->assertEquals(0, $rule['body']['total']); }, 5000, 500); $response = $this->client->call(Client::METHOD_GET, '/console/resources', [ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], 'x-appwrite-project' => 'console', ], [ 'type' => 'rules', 'value' => $domain, ]); $this->assertEquals(204, $response['headers']['status-code']); // domain available as site is deleted } public function testVariables(): void { $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $this->assertEquals(201, $site['headers']['status-code']); $this->assertNotEmpty($site['body']['$id']); $this->assertEquals('Test Site', $site['body']['name']); $variable = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'siteKey1', 'value' => 'siteValue1', 'secret' => false, ]); $this->assertEquals(201, $variable['headers']['status-code']); $this->assertNotEmpty($variable['body']['$id']); $this->assertEquals('siteKey1', $variable['body']['key']); $this->assertEquals('siteValue1', $variable['body']['value']); $this->assertEquals(false, $variable['body']['secret']); $variable2 = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'siteKey2', 'value' => 'siteValue2', 'secret' => false, ]); $this->assertEquals(201, $variable2['headers']['status-code']); $this->assertNotEmpty($variable2['body']['$id']); $this->assertEquals('siteKey2', $variable2['body']['key']); $this->assertEquals('siteValue2', $variable2['body']['value']); $this->assertEquals(false, $variable2['body']['secret']); $secretVariable = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'siteKey3', 'value' => 'siteValue3', 'secret' => true, ]); $this->assertEquals(201, $secretVariable['headers']['status-code']); $this->assertNotEmpty($secretVariable['body']['$id']); $this->assertEquals('siteKey3', $secretVariable['body']['key']); $this->assertEquals('', $secretVariable['body']['value']); $this->assertEquals(true, $secretVariable['body']['secret']); $variable = $this->getVariable($siteId, $variable['body']['$id']); $this->assertEquals(200, $variable['headers']['status-code']); $this->assertNotEmpty($variable['body']['$id']); $this->assertEquals('siteKey1', $variable['body']['key']); $this->assertEquals('siteValue1', $variable['body']['value']); $this->assertEquals(false, $variable['body']['secret']); $secretVariable = $this->getVariable($siteId, $secretVariable['body']['$id']); $this->assertEquals(200, $secretVariable['headers']['status-code']); $this->assertNotEmpty($secretVariable['body']['$id']); $this->assertEquals('siteKey3', $secretVariable['body']['key']); $this->assertEquals('', $secretVariable['body']['value']); $this->assertEquals(true, $secretVariable['body']['secret']); $variable = $this->updateVariable($siteId, $variable['body']['$id'], [ 'key' => 'siteKey1Updated', 'value' => 'siteValue1Updated', ]); $this->assertEquals(200, $variable['headers']['status-code']); $this->assertNotEmpty($variable['body']['$id']); $this->assertEquals('siteKey1Updated', $variable['body']['key']); $this->assertEquals('siteValue1Updated', $variable['body']['value']); $this->assertEquals(false, $variable['body']['secret']); $variable = $this->updateVariable($siteId, $variable['body']['$id'], [ 'key' => 'siteKey1Updated', 'secret' => true, ]); $this->assertEquals(200, $variable['headers']['status-code']); $this->assertNotEmpty($variable['body']['$id']); $this->assertEquals('siteKey1Updated', $variable['body']['key']); $this->assertEquals('', $variable['body']['value']); $this->assertEquals(true, $variable['body']['secret']); $secretVariable = $this->updateVariable($siteId, $secretVariable['body']['$id'], [ 'key' => 'siteKey3', 'value' => 'siteValue3Updated', ]); $this->assertEquals(200, $secretVariable['headers']['status-code']); $this->assertNotEmpty($secretVariable['body']['$id']); $this->assertEquals('siteKey3', $secretVariable['body']['key']); $this->assertEquals('', $secretVariable['body']['value']); $this->assertEquals(true, $secretVariable['body']['secret']); $response = $this->updateVariable($siteId, $secretVariable['body']['$id'], [ 'key' => 'siteKey3', 'secret' => false, ]); $this->assertEquals(400, $response['headers']['status-code']); $secretVariable = $this->getVariable($siteId, $secretVariable['body']['$id']); $this->assertEquals(200, $secretVariable['headers']['status-code']); $this->assertNotEmpty($secretVariable['body']['$id']); $this->assertEquals('siteKey3', $secretVariable['body']['key']); $this->assertEquals('', $secretVariable['body']['value']); $this->assertEquals(true, $secretVariable['body']['secret']); $variables = $this->listVariables($siteId); $this->assertEquals(200, $variables['headers']['status-code']); $this->assertCount(3, $variables['body']['variables']); $response = $this->deleteVariable($siteId, $variable['body']['$id']); $this->assertEquals(204, $response['headers']['status-code']); $response = $this->deleteVariable($siteId, $variable2['body']['$id']); $this->assertEquals(204, $response['headers']['status-code']); $response = $this->deleteVariable($siteId, $secretVariable['body']['$id']); $this->assertEquals(204, $response['headers']['status-code']); $variables = $this->listVariables($siteId); $this->assertEquals(200, $variables['headers']['status-code']); $this->assertCount(0, $variables['body']['variables']); $this->cleanupSite($siteId); } public function testListVariablesWithLimit(): void { $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test List Variables Limit', 'outputDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $this->assertEquals(201, $site['headers']['status-code']); $variable1 = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'LIMIT_KEY_1', 'value' => 'limit-value-1', ]); $this->assertEquals(201, $variable1['headers']['status-code']); $variable2 = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'LIMIT_KEY_2', 'value' => 'limit-value-2', ]); $this->assertEquals(201, $variable2['headers']['status-code']); // List with limit of 1 $response = $this->listVariables($siteId, [ 'queries' => [ Query::limit(1)->toString(), ], 'total' => true, ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertCount(1, $response['body']['variables']); $this->assertGreaterThanOrEqual(2, $response['body']['total']); $this->cleanupSite($siteId); } public function testListVariablesWithoutTotal(): void { $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test List Variables No Total', 'outputDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $this->assertEquals(201, $site['headers']['status-code']); $variable = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'NO_TOTAL_KEY', 'value' => 'no-total-value', ]); $this->assertEquals(201, $variable['headers']['status-code']); // List with total=false $response = $this->listVariables($siteId, [ 'total' => false, ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(0, $response['body']['total']); $this->assertGreaterThanOrEqual(1, \count($response['body']['variables'])); $this->cleanupSite($siteId); } public function testListVariablesCursorPagination(): void { $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test List Variables Cursor', 'outputDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $this->assertEquals(201, $site['headers']['status-code']); $variable1 = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'CURSOR_KEY_1', 'value' => 'cursor-value-1', ]); $this->assertEquals(201, $variable1['headers']['status-code']); $variable2 = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'CURSOR_KEY_2', 'value' => 'cursor-value-2', ]); $this->assertEquals(201, $variable2['headers']['status-code']); // Get first page with limit 1 $page1 = $this->listVariables($siteId, [ 'queries' => [ Query::limit(1)->toString(), ], 'total' => true, ]); $this->assertEquals(200, $page1['headers']['status-code']); $this->assertCount(1, $page1['body']['variables']); $cursorId = $page1['body']['variables'][0]['$id']; // Get next page using cursor $page2 = $this->listVariables($siteId, [ 'queries' => [ Query::limit(1)->toString(), Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(), ], 'total' => true, ]); $this->assertEquals(200, $page2['headers']['status-code']); $this->assertCount(1, $page2['body']['variables']); $this->assertNotEquals($cursorId, $page2['body']['variables'][0]['$id']); $this->cleanupSite($siteId); } public function testUpdateVariableKey(): void { $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Update Variable Key', 'outputDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $this->assertEquals(201, $site['headers']['status-code']); $variable = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'KEY_BEFORE', 'value' => 'unchanged-value', 'secret' => false ]); $this->assertEquals(201, $variable['headers']['status-code']); $variableId = $variable['body']['$id']; // Update only key $response = $this->updateVariable($siteId, $variableId, [ 'key' => 'KEY_AFTER', ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('KEY_AFTER', $response['body']['key']); $this->assertEquals('unchanged-value', $response['body']['value']); $this->cleanupSite($siteId); } public function testUpdateVariableValueOnly(): void { $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Update Variable Value', 'outputDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $this->assertEquals(201, $site['headers']['status-code']); $variable = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'UNCHANGED_KEY', 'value' => 'value-before', 'secret' => false ]); $this->assertEquals(201, $variable['headers']['status-code']); $variableId = $variable['body']['$id']; // Update only value $response = $this->updateVariable($siteId, $variableId, [ 'value' => 'value-after', ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('UNCHANGED_KEY', $response['body']['key']); $this->assertEquals('value-after', $response['body']['value']); $this->cleanupSite($siteId); } public function testUpdateVariableNoOp(): void { $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Update Variable NoOp', 'outputDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $this->assertEquals(201, $site['headers']['status-code']); $variable = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'NOOP_KEY', 'value' => 'noop-value', 'secret' => false ]); $this->assertEquals(201, $variable['headers']['status-code']); $variableId = $variable['body']['$id']; // Update with no parameters should fail with 400 $response = $this->updateVariable($siteId, $variableId, []); $this->assertEquals(400, $response['headers']['status-code']); $this->cleanupSite($siteId); } public function testUpdateVariableNotFound(): void { $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Update Variable Not Found', 'outputDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $this->assertEquals(201, $site['headers']['status-code']); $response = $this->updateVariable($siteId, 'non-existent-id', [ 'key' => 'NEW_KEY', 'value' => 'new-value', ]); $this->assertEquals(404, $response['headers']['status-code']); $this->assertEquals('variable_not_found', $response['body']['type']); $this->cleanupSite($siteId); } public function testCreateVariableInvalidId(): void { $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Invalid Variable ID', 'outputDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $this->assertEquals(201, $site['headers']['status-code']); $variable = $this->createVariable($siteId, [ 'variableId' => '!invalid-id!', 'key' => 'INVALID_ID_KEY', 'value' => 'value', ]); $this->assertEquals(400, $variable['headers']['status-code']); $this->cleanupSite($siteId); } public function testCreateVariableDuplicateId(): void { $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Duplicate Variable ID', 'outputDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $this->assertEquals(201, $site['headers']['status-code']); $variableId = ID::unique(); $variable = $this->createVariable($siteId, [ 'variableId' => $variableId, 'key' => 'DUP_ID_KEY_1', 'value' => 'value1', ]); $this->assertEquals(201, $variable['headers']['status-code']); // Attempt to create with same ID $duplicate = $this->createVariable($siteId, [ 'variableId' => $variableId, 'key' => 'DUP_ID_KEY_2', 'value' => 'value2', ]); $this->assertEquals(409, $duplicate['headers']['status-code']); $this->assertEquals('variable_already_exists', $duplicate['body']['type']); $this->cleanupSite($siteId); } // This is first Sites test with Proxy // If this fails, it may not be related to variables; but Router flow failing public function testVariablesE2E(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Astro site', 'framework' => 'astro', 'adapter' => 'ssr', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build', 'installCommand' => 'npm ci', 'fallbackFile' => '', ]); $this->assertNotEmpty($siteId); $domain = $this->setupSiteDomain($siteId); $secretVariable = $this->createVariable($siteId, [ 'variableId' => ID::unique(), 'key' => 'name', 'value' => 'Appwrite', ]); $this->assertEquals(201, $secretVariable['headers']['status-code']); $this->assertNotEmpty($secretVariable['body']['$id']); $this->assertEquals('name', $secretVariable['body']['key']); $this->assertEquals('', $secretVariable['body']['value']); $this->assertEquals(true, $secretVariable['body']['secret']); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('astro'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $domain = $this->getSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Env variable is Appwrite", $response['body']); $this->assertStringNotContainsString("Variable not found", $response['body']); $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertGreaterThan(0, $deployment['body']['sourceSize']); $this->assertGreaterThan(0, $deployment['body']['buildSize']); $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; $this->assertEquals($totalSize, $deployment['body']['totalSize']); $site = $this->getSite($siteId); $this->assertEquals(200, $site['headers']['status-code']); $this->assertNotEmpty($site['body']['deploymentId']); $this->assertNotEmpty($site['body']['deploymentCreatedAt']); $this->assertEquals($deployment['body']['$id'], $site['body']['deploymentId']); $this->assertEquals($deployment['body']['$createdAt'], $site['body']['deploymentCreatedAt']); $this->cleanupSite($siteId); } public function testAdapterDetectionAstroSSR(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Astro SSR site', 'framework' => 'astro', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build', 'installCommand' => 'npm ci', ]); $this->assertNotEmpty($siteId); $site = $this->getSite($siteId); $this->assertEquals('200', $site['headers']['status-code']); $this->assertArrayHasKey('adapter', $site['body']); $this->assertEmpty($site['body']['adapter']); $domain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($domain); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('astro'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $this->assertEventually(function () use ($siteId, &$site) { $site = $this->getSite($siteId); $this->assertEquals('ssr', $site['body']['adapter']); }); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->cleanupSite($siteId); } #[Retry(count: 3)] public function testAdapterDetectionAstroStatic(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Astro static site', 'framework' => 'astro', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build', 'installCommand' => 'npm ci', ]); $this->assertNotEmpty($siteId); $site = $this->getSite($siteId); $this->assertEquals('200', $site['headers']['status-code']); $this->assertArrayHasKey('adapter', $site['body']); $this->assertEmpty($site['body']['adapter']); $domain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($domain); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('astro-static'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $site = $this->getSite($siteId); $this->assertEquals('200', $site['headers']['status-code']); $this->assertEquals('static', $site['body']['adapter']); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->cleanupSite($siteId); } public function testAdapterDetectionStatic(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Static site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'outputDirectory' => '', 'buildCommand' => '', 'installCommand' => '', ]); $this->assertNotEmpty($siteId); $site = $this->getSite($siteId); $this->assertEquals('200', $site['headers']['status-code']); $this->assertArrayHasKey('adapter', $site['body']); $this->assertEmpty($site['body']['adapter']); $domain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($domain); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $site = $this->getSite($siteId); $this->assertEquals('200', $site['headers']['status-code']); $this->assertEquals('static', $site['body']['adapter']); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->cleanupSite($siteId); } public function testAdapterDetectionStaticSPA(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Static site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'outputDirectory' => '', 'buildCommand' => '', 'installCommand' => '', ]); $this->assertNotEmpty($siteId); $site = $this->getSite($siteId); $this->assertEquals('200', $site['headers']['status-code']); $this->assertArrayHasKey('adapter', $site['body']); $this->assertArrayHasKey('fallbackFile', $site['body']); $this->assertEmpty($site['body']['adapter']); $this->assertEmpty($site['body']['fallbackFile']); $domain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($domain); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $site = $this->getSite($siteId); $this->assertEquals('200', $site['headers']['status-code']); $this->assertEquals('static', $site['body']['adapter']); $this->assertEquals('main.html', $site['body']['fallbackFile']); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString('Main page', $response['body']); $response = $proxyClient->call(Client::METHOD_GET, '/something'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString('Main page', $response['body']); $this->cleanupSite($siteId); } public function testSettingsForRollback(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Static site', 'framework' => 'astro', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build', 'installCommand' => 'npm ci', ]); $this->assertNotEmpty($siteId); $site = $this->getSite($siteId); $this->assertEquals('200', $site['headers']['status-code']); $this->assertEmpty($site['body']['adapter']); $this->assertEmpty($site['body']['fallbackFile']); $domain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($domain); $deploymentId1 = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('astro-static'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId1); $site = $this->getSite($siteId); $this->assertEquals('200', $site['headers']['status-code']); $this->assertEquals('static', $site['body']['adapter']); $this->assertEquals('index.html', $site['body']['fallbackFile']); $site = $this->updateSite([ 'name' => 'SSR site', 'framework' => 'astro', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build', 'installCommand' => 'npm ci', 'adapter' => 'ssr', 'fallbackFile' => '', '$id' => $siteId, ]); $this->assertEquals('200', $site['headers']['status-code']); $this->assertEquals('ssr', $site['body']['adapter']); $this->assertEmpty($site['body']['fallbackFile']); $deploymentId2 = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('astro'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId2); $site = $this->getSite($siteId); $this->assertEquals('200', $site['headers']['status-code']); $this->assertEquals('ssr', $site['body']['adapter']); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro SSR", $response['body']); $response = $proxyClient->call(Client::METHOD_GET, '/not-found'); $this->assertEquals(404, $response['headers']['status-code']); $response = $this->updateSiteDeployment($siteId, $deploymentId1); $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro static", $response['body']); $response = $proxyClient->call(Client::METHOD_GET, '/not-found'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro static", $response['body']); $this->cleanupSite($siteId); } public function testListSites(): void { /** * Test for SUCCESS */ $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'analog', 'name' => 'Test List Sites', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $sites = $this->listSites([ 'search' => 'Test List Sites', ]); $this->assertEquals($sites['headers']['status-code'], 200); $this->assertCount(1, $sites['body']['sites']); $this->assertEquals($sites['body']['sites'][0]['name'], 'Test List Sites'); // Test pagination limit $sites = $this->listSites([ 'queries' => [ Query::limit(1)->toString(), ], ]); $this->assertEquals($sites['headers']['status-code'], 200); $this->assertCount(1, $sites['body']['sites']); // Test pagination offset with search filter (to only count our test site) $sites = $this->listSites([ 'search' => 'Test List Sites', 'queries' => [ Query::offset(1)->toString(), ], ]); $this->assertEquals($sites['headers']['status-code'], 200); $this->assertCount(0, $sites['body']['sites']); // Test filter enabled (with search to isolate our test site) $sites = $this->listSites([ 'search' => 'Test List Sites', 'queries' => [ Query::equal('enabled', [true])->toString(), ], ]); $this->assertEquals($sites['headers']['status-code'], 200); $this->assertCount(1, $sites['body']['sites']); // Test filter disabled (with search to isolate our test site) $sites = $this->listSites([ 'search' => 'Test List Sites', 'queries' => [ Query::equal('enabled', [false])->toString(), ], ]); $this->assertEquals($sites['headers']['status-code'], 200); $this->assertCount(0, $sites['body']['sites']); // Test search name $sites = $this->listSites([ 'search' => 'Test List Sites' ]); $this->assertEquals($sites['headers']['status-code'], 200); $this->assertCount(1, $sites['body']['sites']); $this->assertEquals($sites['body']['sites'][0]['$id'], $siteId); // Test search framework ('analog' used because PostgreSQL treats 'other' as a fulltext stop word) $sites = $this->listSites([ 'search' => 'analog' ]); $this->assertEquals($sites['headers']['status-code'], 200); $this->assertCount(1, $sites['body']['sites']); $this->assertEquals($sites['body']['sites'][0]['$id'], $siteId); /** * Test pagination */ $siteId2 = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test List Sites 2', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $sites = $this->listSites([ 'search' => 'Test List Sites', ]); $this->assertEquals($sites['headers']['status-code'], 200); $this->assertEquals($sites['body']['total'], 2); $this->assertIsArray($sites['body']['sites']); $this->assertCount(2, $sites['body']['sites']); $this->assertEquals($sites['body']['sites'][0]['name'], 'Test List Sites'); $this->assertEquals($sites['body']['sites'][1]['name'], 'Test List Sites 2'); $sites1 = $this->listSites([ 'search' => 'Test List Sites', 'queries' => [ Query::cursorAfter(new Document(['$id' => $sites['body']['sites'][0]['$id']]))->toString(), ], ]); $this->assertEquals($sites1['headers']['status-code'], 200); $this->assertCount(1, $sites1['body']['sites']); $this->assertEquals($sites1['body']['sites'][0]['name'], 'Test List Sites 2'); $sites2 = $this->listSites([ 'search' => 'Test List Sites', 'queries' => [ Query::cursorBefore(new Document(['$id' => $sites['body']['sites'][1]['$id']]))->toString(), ], ]); $this->assertEquals($sites2['headers']['status-code'], 200); $this->assertCount(1, $sites2['body']['sites']); $this->assertEquals($sites2['body']['sites'][0]['name'], 'Test List Sites'); /** * Test for FAILURE */ $sites = $this->listSites([ 'queries' => [ Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(), ], ]); $this->assertEquals($sites['headers']['status-code'], 400); $this->cleanupSite($siteId); $this->cleanupSite($siteId2); } public function testGetSite(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); /** * Test for SUCCESS */ $site = $this->getSite($siteId); $this->assertEquals($site['headers']['status-code'], 200); $this->assertEquals($site['body']['name'], 'Test Site'); /** * Test for FAILURE */ $site = $this->getSite('x'); $this->assertEquals($site['headers']['status-code'], 404); $this->cleanupSite($siteId); } public function testUpdateSite(): void { $site = $this->createSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $siteId = $site['body']['$id'] ?? ''; $this->assertEquals(201, $site['headers']['status-code']); $this->assertNotEmpty($site['body']['$id']); $this->assertEquals('Test Site', $site['body']['name']); $site = $this->updateSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site Updated', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', '$id' => $siteId, 'installCommand' => 'npm ci' ]); $dateValidator = new DatetimeValidator(); $this->assertEquals(200, $site['headers']['status-code']); $this->assertNotEmpty($site['body']['$id']); $this->assertEquals('Test Site Updated', $site['body']['name']); $this->assertEquals(true, $dateValidator->isValid($site['body']['$createdAt'])); $this->assertEquals(true, $dateValidator->isValid($site['body']['$updatedAt'])); $this->assertEquals('npm ci', $site['body']['installCommand']); $this->cleanupSite($siteId); } // public function testCreateDeploymentFromCLI() { // // TODO: Implement testCreateDeploymentFromCLI() later // } public function testCreateDeploymentWithSingleContentRangeChunk(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site Single Chunk Range', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $code = $this->packageSite('static-single-file'); $size = \filesize($code->getFilename()); $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], 'content-range' => 'bytes 0-' . ($size - 1) . '/' . $size, ], $this->getHeaders()), [ 'code' => $code, 'activate' => true, ]); $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); $deploymentId = $deployment['body']['$id']; $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('ready', $deployment['body']['status']); }, 120000, 500); $this->cleanupSite($siteId); } public function testCreateDeploymentOutOfOrder(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site Out of Order Upload', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); // Create a temporary large site package for chunked upload $tempDir = sys_get_temp_dir() . '/appwrite-test-site-' . uniqid(); mkdir($tempDir, 0777, true); file_put_contents($tempDir . '/index.html', 'Hello World'); // Add a large dummy file to make the package span multiple chunks file_put_contents($tempDir . '/large.bin', random_bytes(12 * 1024 * 1024)); // 12MB non-compressible $codePath = $tempDir . '/code.tar.gz'; Console::execute("cd $tempDir && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); $totalSize = filesize($codePath); $chunkSize = 5 * 1024 * 1024; // 5MB chunks $mimeType = 'application/x-gzip'; $chunksTotal = (int) ceil($totalSize / $chunkSize); $this->assertGreaterThanOrEqual(2, $chunksTotal, 'Test file must span at least 2 chunks'); // Read all chunks into memory $handle = fopen($codePath, "rb"); $this->assertNotFalse($handle, "Could not open test resource: $codePath"); $chunks = []; for ($i = 0; $i < $chunksTotal; $i++) { $start = $i * $chunkSize; $end = min($start + $chunkSize, $totalSize); $length = $end - $start; $data = fread($handle, $length); $chunks[] = [ 'data' => $data, 'start' => $start, 'end' => $end - 1, 'index' => $i, ]; } fclose($handle); // Upload chunks in out-of-order sequence: last chunk first, then first, then second $uploadOrder = [count($chunks) - 1, 0, 1]; $deploymentId = ''; $deployment = null; foreach ($uploadOrder as $chunkIndex) { $chunk = $chunks[$chunkIndex]; $curlFile = new \CURLFile( 'data://' . $mimeType . ';base64,' . base64_encode($chunk['data']), $mimeType, 'code.tar.gz' ); $headers = [ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], 'content-range' => 'bytes ' . $chunk['start'] . '-' . $chunk['end'] . '/' . $totalSize, ]; if (!empty($deploymentId)) { $headers['x-appwrite-id'] = $deploymentId; } $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge($headers, $this->getHeaders()), [ 'code' => $curlFile, 'activate' => true, ]); $this->assertEquals(202, $deployment['headers']['status-code']); $deploymentId = $deployment['body']['$id']; } // Upload remaining chunks in any order to complete the file $remainingChunks = []; for ($i = 2; $i < count($chunks) - 1; $i++) { $remainingChunks[] = $i; } shuffle($remainingChunks); foreach ($remainingChunks as $chunkIndex) { $chunk = $chunks[$chunkIndex]; $curlFile = new \CURLFile( 'data://' . $mimeType . ';base64,' . base64_encode($chunk['data']), $mimeType, 'code.tar.gz' ); $headers = [ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], 'content-range' => 'bytes ' . $chunk['start'] . '-' . $chunk['end'] . '/' . $totalSize, 'x-appwrite-id' => $deploymentId, ]; $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge($headers, $this->getHeaders()), [ 'code' => $curlFile, 'activate' => true, ]); $this->assertEquals(202, $deployment['headers']['status-code']); } // Wait for build to complete $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('ready', $deployment['body']['status']); }, 120000, 500); // Clean up temp files unlink($codePath); unlink($tempDir . '/index.html'); unlink($tempDir . '/large.bin'); rmdir($tempDir); $this->cleanupSite($siteId); } public function testCreateDeployment() { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $deployment = $this->createDeployment($siteId, [ 'siteId' => $siteId, 'code' => $this->packageSite('static-single-file'), 'activate' => true, ]); $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); $this->assertEquals('waiting', $deployment['body']['status']); $this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt'])); $deploymentIdActive = $deployment['body']['$id'] ?? ''; $this->assertEventually(function () use ($siteId, $deploymentIdActive) { $deployment = $this->getDeployment($siteId, $deploymentIdActive); $this->assertEquals('ready', $deployment['body']['status']); }, 120000, 500); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => 'false' ]); $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); $deploymentIdInactive = $deployment['body']['$id'] ?? ''; $this->assertEventually(function () use ($siteId, $deploymentIdInactive) { $deployment = $this->getDeployment($siteId, $deploymentIdInactive); $this->assertEquals('ready', $deployment['body']['status']); }, 120000, 500); $site = $this->getSite($siteId); $this->assertEquals(200, $site['headers']['status-code']); $this->assertEquals($deploymentIdActive, $site['body']['deploymentId']); $this->assertNotEquals($deploymentIdInactive, $site['body']['deploymentId']); $this->cleanupDeployment($siteId, $deploymentIdActive); $this->cleanupDeployment($siteId, $deploymentIdInactive); $this->cleanupSite($siteId); } #[Retry(count: 3)] public function testCancelDeploymentBuild(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), '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->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('building', $deployment['body']['status']); }, 100000, 250); $deployment = $this->cancelDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('canceled', $deployment['body']['status']); // Ensures worker got eventually aware of cancellation and reacted properly $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertStringContainsString('Build has been canceled.', $deployment['body']['buildLogs']); }); $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('canceled', $deployment['body']['status']); $this->cleanupDeployment($siteId, $deploymentId); $this->cleanupSite($siteId); } public function testUpdateDeployment(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => 'false' ]); $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals('ready', $deployment['body']['status']); }, 120000, 500); /** * Test for SUCCESS */ $dateValidator = new DatetimeValidator(); $response = $this->updateSiteDeployment($siteId, $deploymentId); $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($deploymentId, $response['body']['deploymentId']); $this->cleanupDeployment($siteId, $deploymentId); $this->cleanupSite($siteId); } public function testListDeployments(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => 'false' ]); $deploymentIdActive = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => 'false' ]); $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); $deploymentIdInactive = $deployment['body']['$id'] ?? ''; $deployments = $this->listDeployments($siteId); $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertEquals($deployments['body']['total'], 2); $this->assertIsArray($deployments['body']['deployments']); $this->assertCount(2, $deployments['body']['deployments']); $this->assertArrayHasKey('sourceSize', $deployments['body']['deployments'][0]); $this->assertArrayHasKey('buildSize', $deployments['body']['deployments'][0]); $deployments = $this->listDeployments($siteId, [ 'queries' => [ Query::limit(1)->toString(), ], ]); $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertCount(1, $deployments['body']['deployments']); $deployments = $this->listDeployments($siteId, [ 'queries' => [ Query::select(['status'])->toString(), ], ]); $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertArrayHasKey('status', $deployments['body']['deployments'][0]); $this->assertArrayHasKey('status', $deployments['body']['deployments'][1]); $this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][0]); $this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][1]); // Extra select query check, for attribute not allowed by filter queries $deployments = $this->listDeployments($siteId, [ 'queries' => [ Query::select(['buildLogs'])->toString(), ], ]); $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertArrayHasKey('buildLogs', $deployments['body']['deployments'][0]); $this->assertArrayHasKey('buildLogs', $deployments['body']['deployments'][1]); $this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][0]); $this->assertArrayNotHasKey('sourceSize', $deployments['body']['deployments'][1]); $deployments = $this->listDeployments($siteId, [ 'queries' => [ Query::offset(1)->toString(), ], ]); $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertCount(1, $deployments['body']['deployments']); $deployments = $this->listDeployments( $siteId, [ 'queries' => [ Query::equal('type', ['manual'])->toString(), ], ] ); $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertEquals(2, $deployments['body']['total']); $deployments = $this->listDeployments( $siteId, [ 'queries' => [ Query::equal('type', ['vcs'])->toString(), ], ] ); $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertEquals(0, $deployments['body']['total']); $deployments = $this->listDeployments( $siteId, [ 'queries' => [ Query::equal('type', ['invalid-string'])->toString(), ], ] ); $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertEquals(0, $deployments['body']['total']); $deployments = $this->listDeployments( $siteId, [ 'queries' => [ Query::greaterThan('sourceSize', 10000)->toString(), ], ] ); $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertEquals(0, $deployments['body']['total']); $deployments = $this->listDeployments( $siteId, [ 'queries' => [ Query::greaterThan('sourceSize', 0)->toString(), ], ] ); $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertEquals(2, $deployments['body']['total']); $deployments = $this->listDeployments( $siteId, [ 'queries' => [ Query::greaterThan('sourceSize', -100)->toString(), ], ] ); $this->assertEquals($deployments['headers']['status-code'], 200); $this->assertEquals(2, $deployments['body']['total']); /** * Ensure size output and size filters work exactly. * Prevents buildSize being counted towards deployment size */ $deployments = $this->listDeployments( $siteId, [ 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( $siteId, [ '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']); } $this->cleanupDeployment($siteId, $deploymentIdActive); $this->cleanupDeployment($siteId, $deploymentIdInactive); $this->cleanupSite($siteId); } public function testGetDeployment(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => 'false' ]); $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals('ready', $deployment['body']['status']); }, 120000, 500); /** * Test for SUCCESS */ $deployment = $this->getDeployment($siteId, $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($siteId, 'x'); $this->assertEquals($deployment['headers']['status-code'], 404); $this->cleanupDeployment($siteId, $deploymentId); $this->cleanupSite($siteId); } public function testUpdateSpecs(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); /** * Test for SUCCESS */ // Change the function specs $site = $this->updateSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', '$id' => $siteId, 'runtimeSpecification' => Specification::S_1VCPU_1GB, ]); $this->assertEquals(200, $site['headers']['status-code']); $this->assertNotEmpty($site['body']['$id']); $this->assertEquals(Specification::S_1VCPU_1GB, $site['body']['runtimeSpecification']); // Change the specs to 1vcpu 512mb $site = $this->updateSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', '$id' => $siteId, 'runtimeSpecification' => Specification::S_1VCPU_512MB, ]); $this->assertEquals(200, $site['headers']['status-code']); $this->assertNotEmpty($site['body']['$id']); $this->assertEquals(Specification::S_1VCPU_512MB, $site['body']['runtimeSpecification']); /** * Test for FAILURE */ $site = $this->updateSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', '$id' => $siteId, 'buildSpecification' => 's-2vcpu-512mb', // Invalid specification ]); $this->assertEquals(400, $site['headers']['status-code']); $this->assertStringStartsWith('Invalid `buildSpecification` param: Specification must be one of:', $site['body']['message']); $site = $this->updateSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', '$id' => $siteId, 'runtimeSpecification' => 's-2vcpu-512mb', // Invalid specification ]); $this->assertEquals(400, $site['headers']['status-code']); $this->assertStringStartsWith('Invalid `runtimeSpecification` param: Specification must be one of:', $site['body']['message']); $this->cleanupSite($siteId); } public function testDeleteDeployment(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => 'false' ]); $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals('ready', $deployment['body']['status']); }, 120000, 500); /** * Test for SUCCESS */ $deployment = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId . '/deployments/' . $deploymentId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(204, $deployment['headers']['status-code']); $this->assertEmpty($deployment['body']); $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals(404, $deployment['headers']['status-code']); } public function testDeleteSite(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $site = $this->deleteSite($siteId); $this->assertEquals(204, $site['headers']['status-code']); $this->assertEmpty($site['body']); $function = $this->getSite($siteId); $this->assertEquals(404, $function['headers']['status-code']); } public function testDeleteSiteRulesCleanup(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Test Rules Cleanup Site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'outputDirectory' => './', 'fallbackFile' => '', ]); $this->assertNotEmpty($siteId); // Create a manual deployment rule (type = 'deployment') $domain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($domain); // Create a redirect rule (type = 'redirect') $redirectDomain = \uniqid() . '-redirect-cleanup.custom.localhost'; $redirectRule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'domain' => $redirectDomain, 'url' => 'https://appwrite.io', 'statusCode' => 301, 'resourceType' => 'site', 'resourceId' => $siteId, ]); $this->assertEquals(201, $redirectRule['headers']['status-code']); $this->assertNotEmpty($redirectRule['body']['$id']); // Verify both rules exist (no type filter — catches all rule types) $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::equal('deploymentResourceId', [$siteId])->toString() ] ]); $this->assertEquals(200, $rules['headers']['status-code']); $this->assertGreaterThanOrEqual(2, $rules['body']['total']); // Delete the site $this->cleanupSite($siteId); // Verify ALL rules (deployment + redirect) are cleaned up $this->assertEventually(function () use ($siteId) { $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::equal('deploymentResourceId', [$siteId])->toString() ] ]); $this->assertEquals(200, $rules['headers']['status-code']); $this->assertEquals(0, $rules['body']['total']); }, 5000, 500); } public function testGetFrameworks(): void { $frameworks = $this->client->call(Client::METHOD_GET, '/sites/frameworks', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ])); $this->assertEquals(200, $frameworks['headers']['status-code']); $this->assertGreaterThan(0, $frameworks['body']['total']); $framework = $frameworks['body']['frameworks'][0]; $this->assertArrayHasKey('name', $framework); $this->assertArrayHasKey('key', $framework); $this->assertArrayHasKey('buildRuntime', $framework); $this->assertArrayHasKey('runtimes', $framework); $this->assertArrayHasKey('adapters', $framework); $this->assertIsArray($framework['adapters']); $this->assertArrayHasKey('key', $framework['adapters'][0]); $this->assertArrayHasKey('installCommand', $framework['adapters'][0]); $this->assertArrayHasKey('buildCommand', $framework['adapters'][0]); $this->assertArrayHasKey('outputDirectory', $framework['adapters'][0]); } public function testSiteStatic(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Non-SPA site', 'framework' => 'other', 'adapter' => 'static', 'buildRuntime' => 'static-1', 'outputDirectory' => './', 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', ]); $this->assertNotEmpty($siteId); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static-spa'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $domain = $this->setupSiteDomain($siteId); $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']); $this->assertStringContainsString("Index page", $response['body']); $this->assertArrayHasKey('x-appwrite-log-id', $response['headers']); $this->assertNotEmpty($response['headers']['x-appwrite-log-id']); $response = $proxyClient->call(Client::METHOD_GET, '/contact', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ])); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Contact page", $response['body']); $response = $proxyClient->call(Client::METHOD_GET, '/non-existing', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ])); $this->assertEquals(404, $response['headers']['status-code']); $this->assertStringContainsString("Page not found", $response['body']); // Title $this->assertStringContainsString("Go to homepage", $response['body']); // Button $this->assertStringNotContainsString("Powered by", $response['body']); // Brand $this->cleanupSite($siteId); } public function testSiteStaticSPA(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'SPA site', 'framework' => 'other', 'adapter' => 'static', 'buildRuntime' => 'static-1', 'outputDirectory' => './', 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '404.html', ]); $this->assertNotEmpty($siteId); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static-spa'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $domain = $this->setupSiteDomain($siteId); $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']); $this->assertStringContainsString("Index page", $response['body']); $response = $proxyClient->call(Client::METHOD_GET, '/contact', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ])); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Contact page", $response['body']); $response = $proxyClient->call(Client::METHOD_GET, '/non-existing', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ])); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Customized 404 page", $response['body']); $this->assertStringNotContainsString("Powered by", $response['body']); //brand $this->cleanupSite($siteId); } #[Retry(count: 3)] public function testSiteTemplate(): void { $template = $this->getTemplate('playground-for-astro'); $this->assertEquals(200, $template['headers']['status-code']); $template = $template['body']; $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Template site', 'framework' => $template['frameworks'][0]['key'], 'adapter' => $template['frameworks'][0]['adapter'], 'buildRuntime' => $template['frameworks'][0]['buildRuntime'], 'outputDirectory' => $template['frameworks'][0]['outputDirectory'], 'buildCommand' => $template['frameworks'][0]['buildCommand'], 'installCommand' => $template['frameworks'][0]['installCommand'], 'fallbackFile' => $template['frameworks'][0]['fallbackFile'], ]); $this->assertNotEmpty($siteId); $deployment = $this->createTemplateDeployment($siteId, [ 'repository' => $template['providerRepositoryId'], 'owner' => $template['providerOwner'], 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], 'type' => 'tag', 'reference' => $template['providerVersion'], 'activate' => true ]); $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals(0, $deployment['body']['sourceSize']); $this->assertEquals(0, $deployment['body']['buildSize']); $this->assertEquals(0, $deployment['body']['totalSize']); $this->assertEventually(function () use ($siteId) { $site = $this->getSite($siteId); $this->assertNotEmpty($site['body']['deploymentId']); }, 120000, 500); $domain = $this->setupSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); $this->assertStringContainsString("Hello, Astronaut!", $response['body']); $response = $proxyClient->call(Client::METHOD_GET, '/about'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); $this->assertStringContainsString("About Me", $response['body']); $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertGreaterThan(0, $deployment['body']['sourceSize']); $this->assertGreaterThan(0, $deployment['body']['buildSize']); $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; $this->assertEquals($totalSize, $deployment['body']['totalSize']); $this->cleanupSite($siteId); } public function testCreateSiteFromTemplateBranch() { $template = $this->getTemplate('playground-for-astro'); $this->assertEquals(200, $template['headers']['status-code']); $template = $template['body']; $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Astro Blog - Branch Test', 'framework' => $template['frameworks'][0]['key'], 'adapter' => $template['frameworks'][0]['adapter'], 'buildRuntime' => $template['frameworks'][0]['buildRuntime'], 'outputDirectory' => $template['frameworks'][0]['outputDirectory'], 'buildCommand' => $template['frameworks'][0]['buildCommand'], 'installCommand' => $template['frameworks'][0]['installCommand'], 'fallbackFile' => $template['frameworks'][0]['fallbackFile'], ]); $this->assertNotEmpty($siteId); // Deploy using branch $deployment = $this->createTemplateDeployment($siteId, [ 'repository' => $template['providerRepositoryId'], 'owner' => $template['providerOwner'], 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], 'type' => 'branch', 'reference' => 'main', 'activate' => true ]); $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals(0, $deployment['body']['sourceSize']); $this->assertEquals(0, $deployment['body']['buildSize']); $this->assertEquals(0, $deployment['body']['totalSize']); $this->assertEventually(function () use ($siteId) { $site = $this->getSite($siteId); $this->assertNotEmpty($site['body']['deploymentId']); }, 120000, 500); $domain = $this->setupSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); $this->assertStringContainsString("Hello, Astronaut!", $response['body']); $response = $proxyClient->call(Client::METHOD_GET, '/about'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); $this->assertStringContainsString("About Me", $response['body']); $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertGreaterThan(0, $deployment['body']['sourceSize']); $this->assertGreaterThan(0, $deployment['body']['buildSize']); $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; $this->assertEquals($totalSize, $deployment['body']['totalSize']); $this->cleanupSite($siteId); } public function testCreateSiteFromTemplateCommit() { $template = $this->getTemplate('playground-for-astro'); $this->assertEquals(200, $template['headers']['status-code']); // Get latest commit using helper function $latestCommit = $this->helperGetLatestCommit( $template['body']['providerOwner'], $template['body']['providerRepositoryId'] ); $this->assertNotNull($latestCommit); $template = $template['body']; $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Astro Blog - Commit Test', 'framework' => $template['frameworks'][0]['key'], 'adapter' => $template['frameworks'][0]['adapter'], 'buildRuntime' => $template['frameworks'][0]['buildRuntime'], 'outputDirectory' => $template['frameworks'][0]['outputDirectory'], 'buildCommand' => $template['frameworks'][0]['buildCommand'], 'installCommand' => $template['frameworks'][0]['installCommand'], 'fallbackFile' => $template['frameworks'][0]['fallbackFile'], ]); $this->assertNotEmpty($siteId); // Deploy using commit $deployment = $this->createTemplateDeployment($siteId, [ 'repository' => $template['providerRepositoryId'], 'owner' => $template['providerOwner'], 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], 'type' => 'commit', 'reference' => $latestCommit, 'activate' => true ]); $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals(0, $deployment['body']['sourceSize']); $this->assertEquals(0, $deployment['body']['buildSize']); $this->assertEquals(0, $deployment['body']['totalSize']); $this->assertEventually(function () use ($siteId) { $site = $this->getSite($siteId); $this->assertNotEmpty($site['body']['deploymentId']); }, 120000, 500); $domain = $this->setupSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); $this->assertStringContainsString("Hello, Astronaut!", $response['body']); $response = $proxyClient->call(Client::METHOD_GET, '/about'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); $this->assertStringContainsString("About Me", $response['body']); $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertGreaterThan(0, $deployment['body']['sourceSize']); $this->assertGreaterThan(0, $deployment['body']['buildSize']); $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; $this->assertEquals($totalSize, $deployment['body']['totalSize']); $this->cleanupSite($siteId); } public function testSiteDomainReclaiming(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Startup site', 'framework' => 'other', 'adapter' => 'static', 'buildRuntime' => 'static-1', 'outputDirectory' => './', 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', ]); $this->assertNotEmpty($siteId); $subdomain = 'startup' . \uniqid(); $domain = $this->setupSiteDomain($siteId, $subdomain); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $domain = $this->getSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringNotContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); $site2 = $this->createSite([ 'siteId' => ID::unique(), 'name' => 'Startup 2 site', 'framework' => 'other', 'adapter' => 'static', 'buildRuntime' => 'static-1', 'outputDirectory' => './', 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', ]); $siteId2 = $site2['body']['$id']; $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0]; $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'domain' => $subdomain . '.' . $sitesDomain, 'siteId' => $siteId2, ]); $this->assertEquals(409, $rule['headers']['status-code']); $this->cleanupSite($siteId); $this->assertEventually(function () use ($domain) { $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::equal('domain', [$domain])->toString(), ], ]); $this->assertEquals(200, $rules['headers']['status-code']); $this->assertEquals(0, $rules['body']['total']); }, 120000, 500); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(404, $response['headers']['status-code']); $this->assertStringContainsString("This page is empty, but you can make it yours.", $response['body']); $site = $this->createSite([ 'siteId' => ID::unique(), 'name' => 'Startup 2 site', 'framework' => 'other', 'adapter' => 'static', 'buildRuntime' => 'static-1', 'outputDirectory' => './', 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', ]); $this->assertEquals(201, $site['headers']['status-code']); $this->assertNotEmpty($site['body']['$id']); $siteId = $site['body']['$id']; $domain = $this->setupSiteDomain($siteId, $subdomain); $this->assertNotEmpty($domain); $this->cleanupSite($site['body']['$id']); } public function testSitePreviewBranding(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'A site', 'framework' => 'other', 'adapter' => 'static', 'buildRuntime' => 'static-1', 'outputDirectory' => './', 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', ]); $this->assertNotEmpty($siteId); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $siteDomain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($siteDomain); $deploymentDomain = $this->getDeploymentDomain($deploymentId); $this->assertNotEmpty($deploymentDomain); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $siteDomain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Hello Appwrite", $response['body']); $this->assertStringNotContainsString("Preview by", $response['body']); $contentLength = $response['headers']['content-length']; $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $deploymentDomain); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); $this->assertStringContainsString('/console/auth/preview', $response['headers']['location']); $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); $apiKey = $jwtObj->encode([ 'projectCheckDisabled' => true, 'previewAuthDisabled' => true, ]); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ 'x-appwrite-key' => API_KEY_EPHEMERAL . '_' . $apiKey, ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Hello Appwrite", $response['body']); $this->assertStringContainsString("Preview by", $response['body']); $this->assertGreaterThan($contentLength, $response['headers']['content-length']); $response = $proxyClient->call(Client::METHOD_GET, '/non-existing-path', followRedirects: false, headers: [ 'x-appwrite-key' => API_KEY_EPHEMERAL . '_' . $apiKey, ]); $this->assertEquals(404, $response['headers']['status-code']); $this->assertStringContainsString("Page not found", $response['body']); $this->assertStringNotContainsString("Preview by", $response['body']); $this->assertGreaterThan($contentLength, $response['headers']['content-length']); $this->cleanupSite($siteId); } public function testSiteCors(): void { // Create rule together with site $subdomain = 'startup' . \uniqid(); $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Startup site', 'framework' => 'other', 'adapter' => 'static', 'buildRuntime' => 'static-1', 'outputDirectory' => './', 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', 'subdomain' => $subdomain ]); $this->assertNotEmpty($siteId); $this->setupSiteDomain($siteId, $subdomain); $domain = $this->getSiteDomain($siteId); $this->assertNotEmpty($domain); $url = 'http://' . $domain; $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'referer' => $url, 'origin' => $url, ])); $this->assertEquals($url, $response['headers']['access-control-allow-origin']); $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => 'unknown', 'referer' => $url, 'origin' => $url, ])); $this->assertArrayNotHasKey('access-control-allow-origin', $response['headers']); $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'referer' => 'http://unknown.com', 'origin' => 'http://unknown.com' ])); $this->assertArrayNotHasKey('access-control-allow-origin', $response['headers']); } public function testSiteDownload(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'fallbackFile' => '', 'framework' => 'other', 'name' => 'Test Site', 'adapter' => 'static', 'outputDirectory' => './', 'providerBranch' => 'main', 'providerRootDirectory' => './', 'siteId' => ID::unique() ]); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => true ]); $this->assertNotEmpty($deploymentId); $response = $this->getDeploymentDownload($siteId, $deploymentId, 'source'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('application/gzip', $response['headers']['content-type']); $this->assertGreaterThan(0, $response['headers']['content-length']); $this->assertGreaterThan(0, \strlen($response['body'])); $deploymentMd5 = \md5($response['body']); $response = $this->getDeploymentDownload($siteId, $deploymentId, 'output'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('application/gzip', $response['headers']['content-type']); $this->assertGreaterThan(0, $response['headers']['content-length']); $this->assertGreaterThan(0, \strlen($response['body'])); $buildMd5 = \md5($response['body']); $this->assertNotEquals($deploymentMd5, $buildMd5); $this->cleanupSite($siteId); } public function testSSRLogs(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'SSR site', 'framework' => 'astro', 'adapter' => 'ssr', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build', 'installCommand' => 'npm ci', 'fallbackFile' => '', ]); $this->assertNotEmpty($siteId); $domain = $this->setupSiteDomain($siteId); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('astro'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $domain = $this->getSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/logs-inline'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Inline logs printed.", $response['body']); // Poll for execution logs to be written (async) // Filter by requestPath to avoid picking up screenshot worker executions // Wait for both the execution entry AND its logs field to be populated $logs = null; $timeout = 120; $start = \time(); while (\time() - $start < $timeout) { $logs = $this->listLogs($siteId, [ Query::orderDesc('$createdAt')->toString(), Query::equal('requestPath', ['/logs-inline'])->toString(), Query::limit(1)->toString(), ]); if (!empty($logs['body']['executions']) && !empty($logs['body']['executions'][0]['logs'])) { break; } \usleep(500000); } $this->assertNotEmpty($logs['body']['executions'], 'Execution logs were not available within timeout'); $this->assertNotNull($logs['body']['executions'][0]['logs'], 'Execution logs content was not populated within timeout'); $this->assertEquals(200, $logs['headers']['status-code']); $this->assertStringContainsString($deploymentId, $logs['body']['executions'][0]['deploymentId']); $this->assertStringContainsString("GET", $logs['body']['executions'][0]['requestMethod']); $this->assertStringContainsString("/logs-inline", $logs['body']['executions'][0]['requestPath']); $this->assertStringContainsString("Log1", $logs['body']['executions'][0]['logs']); $this->assertStringContainsString("Log2", $logs['body']['executions'][0]['logs']); $this->assertStringContainsString("Error1", $logs['body']['executions'][0]['errors']); $this->assertStringContainsString("Error2", $logs['body']['executions'][0]['errors']); $log1Id = $logs['body']['executions'][0]['$id']; $this->assertNotEmpty($log1Id); $logs = $this->listLogs($siteId, [ Query::orderDesc('$createdAt')->toString(), Query::limit(1)->toString(), Query::equal('deploymentId', [$deploymentId])->toString() ]); $this->assertEquals(200, $logs['headers']['status-code']); $this->assertGreaterThanOrEqual(1, $logs['body']['total']); $this->assertCount(1, $logs['body']['executions']); $logs = $this->listLogs($siteId, [ Query::orderDesc('$createdAt')->toString(), Query::limit(1)->toString(), Query::equal('deploymentId', ['some-random-id'])->toString() ]); $this->assertEquals(200, $logs['headers']['status-code']); $this->assertEquals(0, $logs['body']['total']); $this->assertCount(0, $logs['body']['executions']); $response = $proxyClient->call(Client::METHOD_GET, '/logs-action'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Action logs printed.", $response['body']); $logs = null; $start = \time(); while (\time() - $start < $timeout) { $logs = $this->listLogs($siteId, [ Query::orderDesc('$createdAt')->toString(), Query::equal('requestPath', ['/logs-action'])->toString(), Query::limit(1)->toString(), ]); if (!empty($logs['body']['executions']) && !empty($logs['body']['executions'][0]['logs'])) { break; } \usleep(500000); } $this->assertNotEmpty($logs['body']['executions'], 'Action execution logs were not available within timeout'); $this->assertNotNull($logs['body']['executions'][0]['logs'], 'Action execution logs content was not populated within timeout'); $this->assertEquals(200, $logs['headers']['status-code']); $this->assertStringContainsString($deploymentId, $logs['body']['executions'][0]['deploymentId']); $this->assertStringContainsString("GET", $logs['body']['executions'][0]['requestMethod']); $this->assertStringContainsString("/logs-action", $logs['body']['executions'][0]['requestPath']); $this->assertStringContainsString("Log1", $logs['body']['executions'][0]['logs']); $this->assertStringContainsString("Log2", $logs['body']['executions'][0]['logs']); $this->assertStringContainsString("Error1", $logs['body']['executions'][0]['errors']); $this->assertStringContainsString("Error2", $logs['body']['executions'][0]['errors']); $log2Id = $logs['body']['executions'][0]['$id']; $this->assertNotEmpty($log2Id); $this->assertNotEquals($log1Id, $log2Id); $site = $this->updateSite( [ '$id' => $siteId, 'name' => 'SSR site', 'framework' => 'astro', 'adapter' => 'ssr', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build', 'installCommand' => 'npm ci', 'fallbackFile' => '', 'logging' => false // set logging to false ] ); $this->assertEquals(200, $site['headers']['status-code']); // Wait for the logging config change to propagate to the site runtime $this->assertEventually(function () use ($proxyClient) { $response = $proxyClient->call(Client::METHOD_GET, '/logs-inline'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Inline logs printed.", $response['body']); }, 15_000, 500); // Poll for the NEW log entry (after logging was disabled) to appear $timeout = 30; $start = \time(); $newLog = null; while (\time() - $start < $timeout) { $logs = $this->listLogs($siteId, [ Query::orderDesc('$createdAt')->toString(), Query::equal('requestPath', ['/logs-inline'])->toString(), Query::limit(1)->toString(), ]); if ( !empty($logs['body']['executions']) && $logs['body']['executions'][0]['$id'] !== $log1Id ) { $newLog = $logs['body']['executions'][0]; break; } \usleep(500000); } $this->assertNotNull($newLog, 'New log entry should appear after logging-disabled request'); $this->assertEquals("GET", $newLog['requestMethod']); $this->assertEquals("/logs-inline", $newLog['requestPath']); $this->assertEmpty($newLog['logs']); $this->assertEmpty($newLog['errors']); $log1Id = $newLog['$id']; $this->assertNotEmpty($log1Id); $response = $proxyClient->call(Client::METHOD_GET, '/logs-action'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Action logs printed.", $response['body']); // Poll for the NEW log entry for /logs-action $start = \time(); $newLog = null; while (\time() - $start < $timeout) { $logs = $this->listLogs($siteId, [ Query::orderDesc('$createdAt')->toString(), Query::equal('requestPath', ['/logs-action'])->toString(), Query::limit(1)->toString(), ]); if ( !empty($logs['body']['executions']) && $logs['body']['executions'][0]['$id'] !== $log2Id ) { $newLog = $logs['body']['executions'][0]; break; } \usleep(500000); } $this->assertNotNull($newLog, 'New log entry should appear after logging-disabled /logs-action request'); $this->assertEquals("GET", $newLog['requestMethod']); $this->assertEquals("/logs-action", $newLog['requestPath']); $this->assertEmpty($newLog['logs']); $this->assertEmpty($newLog['errors']); $log2Id = $logs['body']['executions'][0]['$id']; $this->assertNotEmpty($log2Id); $this->cleanupSite($siteId); } public function testDuplicateDeployment(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'framework' => 'other', 'name' => 'Duplicate deployment Site', 'adapter' => 'static', 'fallbackFile' => '404.html', 'siteId' => ID::unique() ]); $this->assertNotEmpty($siteId); $domain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($domain); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $deploymentId1 = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static-spa'), 'activate' => true ]); $this->assertNotEmpty($deploymentId1); $response = $proxyClient->call(Client::METHOD_GET, '/not-found'); $this->assertStringContainsString("Customized 404 page", $response['body']); $site = $this->updateSite([ '$id' => $siteId, 'buildRuntime' => 'node-22', 'framework' => 'other', 'name' => 'Duplicate deployment Site', 'adapter' => 'static', 'fallbackFile' => 'index.html', ]); $this->assertEquals(200, $site['headers']['status-code']); $this->assertEquals('index.html', $site['body']['fallbackFile']); $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments/duplicate', array_merge([ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], 'x-sdk-language' => 'cli' ], $this->getHeaders()), [ 'deploymentId' => $deploymentId1, ]); $this->assertEquals(202, $deployment['headers']['status-code']); $deploymentId2 = $deployment['body']['$id']; $this->assertNotEmpty($deploymentId2); $deployment = $this->getDeployment($siteId, $deploymentId2); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertGreaterThan(0, $deployment['body']['sourceSize']); $this->assertEquals(0, $deployment['body']['buildSize']); $this->assertEquals($deployment['body']['sourceSize'], $deployment['body']['totalSize']); $this->assertEquals('cli', $deployment['body']['type']); // create another duplicate deployment with manual trigger $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments/duplicate', array_merge([ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'deploymentId' => $deploymentId1, ]); $this->assertEquals(202, $deployment['headers']['status-code']); $deploymentId2 = $deployment['body']['$id']; $this->assertNotEmpty($deploymentId2); $deployment = $this->getDeployment($siteId, $deploymentId2); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertGreaterThan(0, $deployment['body']['sourceSize']); $this->assertEquals(0, $deployment['body']['buildSize']); $this->assertEquals($deployment['body']['sourceSize'], $deployment['body']['totalSize']); $this->assertEquals('manual', $deployment['body']['type']); $this->assertEventually(function () use ($siteId, $deploymentId2) { $site = $this->getSite($siteId); $this->assertEquals($deploymentId2, $site['body']['deploymentId']); }, 120000, 500); $response = $proxyClient->call(Client::METHOD_GET, '/not-found'); $this->assertStringContainsString("Index page", $response['body']); $deployment = $this->getDeployment($siteId, $deploymentId2); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertGreaterThan(0, $deployment['body']['sourceSize']); $this->assertGreaterThan(0, $deployment['body']['buildSize']); $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; $this->assertEquals($totalSize, $deployment['body']['totalSize']); $this->cleanupSite($siteId); } public function testUpdateDeploymentStatus(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'framework' => 'other', 'name' => 'Activate test Site', 'siteId' => ID::unique(), 'adapter' => 'static', ]); $this->assertNotEmpty($siteId); $site = $this->getSite($siteId); $this->assertEquals(200, $site['headers']['status-code']); $this->assertArrayHasKey('latestDeploymentId', $site['body']); $this->assertArrayHasKey('latestDeploymentCreatedAt', $site['body']); $this->assertArrayHasKey('latestDeploymentStatus', $site['body']); $this->assertEmpty($site['body']['latestDeploymentId']); $this->assertEmpty($site['body']['latestDeploymentCreatedAt']); $this->assertEmpty($site['body']['latestDeploymentStatus']); $domain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($domain); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $deploymentId1 = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static'), 'activate' => true ]); $this->assertNotEmpty($deploymentId1); $site = $this->getSite($siteId); $this->assertEquals(200, $site['headers']['status-code']); $this->assertEquals($deploymentId1, $site['body']['latestDeploymentId']); $this->assertEquals('ready', $site['body']['latestDeploymentStatus']); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString('Hello Appwrite', $response['body']); $deploymentId2 = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static-spa'), 'activate' => true ]); $this->assertNotEmpty($deploymentId2); $site = $this->getSite($siteId); $this->assertEquals(200, $site['headers']['status-code']); $this->assertEquals($deploymentId2, $site['body']['latestDeploymentId']); $this->assertEquals('ready', $site['body']['latestDeploymentStatus']); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString('Index page', $response['body']); $site = $this->getSite($siteId); $this->assertEquals(200, $site['headers']['status-code']); $this->assertEquals($deploymentId2, $site['body']['deploymentId']); $this->assertEquals($deploymentId2, $site['body']['latestDeploymentId']); $this->assertEquals('ready', $site['body']['latestDeploymentStatus']); $site = $this->updateSiteDeployment($siteId, $deploymentId1); $this->assertEquals(200, $site['headers']['status-code']); $this->assertEquals($deploymentId1, $site['body']['deploymentId']); $site = $this->getSite($siteId); $this->assertEquals(200, $site['headers']['status-code']); $this->assertEquals($deploymentId1, $site['body']['deploymentId']); $this->assertEquals($deploymentId2, $site['body']['latestDeploymentId']); $this->assertEquals('ready', $site['body']['latestDeploymentStatus']); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString('Hello Appwrite', $response['body']); $deployment = $this->deleteDeployment($siteId, $deploymentId2); $this->assertEquals(204, $deployment['headers']['status-code']); $site = $this->getSite($siteId); $this->assertEquals(200, $site['headers']['status-code']); $this->assertEquals($deploymentId1, $site['body']['latestDeploymentId']); $this->assertEquals('ready', $site['body']['latestDeploymentStatus']); $this->cleanupSite($siteId); } public function testPreviewDomain(): void { $siteId = $this->setupSite([ 'buildRuntime' => 'node-22', 'framework' => 'other', 'name' => 'Authorized preview site', 'siteId' => ID::unique(), 'adapter' => 'static', ]); $this->assertNotEmpty($siteId); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static'), 'activate' => true ]); $this->assertNotEmpty($deploymentId); $domain = $this->getDeploymentDomain($deploymentId); $this->assertNotEmpty($domain); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/contact', followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); $this->assertStringContainsString('/console/auth/preview', $response['headers']['location']); $this->assertStringContainsString('projectId=' . $this->getProject()['$id'], $response['headers']['location']); $this->assertStringContainsString('origin=', $response['headers']['location']); $this->assertStringContainsString('path=%2Fcontact', $response['headers']['location']); $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'x-appwrite-project' => 'console', ]), [ 'email' => $this->getRoot()['email'], 'password' => 'password' ]); $this->assertEquals(201, $session['headers']['status-code']); $this->assertNotEmpty($session['cookies']['a_session_console']); $this->assertNotEmpty($session['body']['$id']); $cookie = 'a_session_console=' . $session['cookies']['a_session_console']; $jwt = $this->client->call(Client::METHOD_POST, '/account/jwts', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'cookie' => $cookie, 'x-appwrite-project' => 'console', ]), []); $this->assertEquals(201, $jwt['headers']['status-code']); $this->assertNotEmpty($jwt['body']['jwt']); $response = $proxyClient->call(Client::METHOD_GET, '/_appwrite/authorize', params: [ 'jwt' => $jwt['body']['jwt'], 'path' => '/contact' ], followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); $this->assertArrayHasKey('set-cookie', $response['headers']); $this->assertStringContainsString('a_jwt_console=', $response['headers']['set-cookie']); // due to swoole update; no more httponly $this->assertStringContainsString('HttpOnly', $response['headers']['set-cookie']); $this->assertStringContainsString('domain=' . $domain, $response['headers']['set-cookie']); $this->assertStringContainsString('path=/', $response['headers']['set-cookie']); $this->assertNotEmpty($response['cookies']['a_jwt_console']); $this->assertEquals($jwt['body']['jwt'], $response['cookies']['a_jwt_console']); $response = $proxyClient->call(Client::METHOD_GET, '/contact', headers: [ 'cookie' => 'a_jwt_console=' . $jwt['body']['jwt'] ], followRedirects: false); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Contact page", $response['body']); $this->assertStringContainsString("Preview by", $response['body']); // Failure: Session missing (old bad, new ok) $session = $this->client->call(Client::METHOD_DELETE, '/account/sessions/current', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'cookie' => $cookie, 'x-appwrite-project' => 'console', ]), []); $this->assertEquals(204, $session['headers']['status-code']); $response = $proxyClient->call(Client::METHOD_GET, '/contact', headers: [ 'cookie' => 'a_jwt_console=' . $jwt['body']['jwt'] ], followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); $this->assertStringContainsString('/console/auth/preview', $response['headers']['location']); // Failure: User missing $cookie = 'a_session_console=' .$this->getRoot()['session']; $jwt = $this->client->call(Client::METHOD_POST, '/account/jwts', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'cookie' => $cookie, 'x-appwrite-project' => 'console', ]), []); $this->assertEquals(201, $jwt['headers']['status-code']); $this->assertNotEmpty($jwt['body']['jwt']); $response = $proxyClient->call(Client::METHOD_GET, '/contact', headers: [ 'cookie' => 'a_jwt_console=' . $jwt['body']['jwt'] ], followRedirects: false); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Contact page", $response['body']); $this->assertStringContainsString("Preview by", $response['body']); $user = $this->client->call(Client::METHOD_PATCH, '/account/status', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'cookie' => $cookie, 'x-appwrite-project' => 'console', ]), []); $this->assertEquals(200, $user['headers']['status-code']); $this->assertFalse($user['body']['status']); $response = $proxyClient->call(Client::METHOD_GET, '/contact', headers: [ 'cookie' => 'a_jwt_console=' . $jwt['body']['jwt'] ], followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); $this->assertStringContainsString('/console/auth/preview', $response['headers']['location']); // Failure: Membership missing $email = \uniqid() . 'newuser@appwrite.io'; $user = $this->client->call(Client::METHOD_POST, '/account', [ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'x-appwrite-project' => 'console', ], [ 'userId' => ID::unique(), 'email' => $email, 'password' => 'password' ]); $this->assertEquals(201, $user['headers']['status-code']); $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'x-appwrite-project' => 'console', ], [ 'email' => $email, 'password' => 'password', ]); $this->assertEquals(201, $session['headers']['status-code']); $this->assertNotEmpty($session['cookies']['a_session_console']); $cookie = 'a_session_console=' . $session['cookies']['a_session_console']; $jwt = $this->client->call(Client::METHOD_POST, '/account/jwts', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'cookie' => $cookie, 'x-appwrite-project' => 'console', ]), []); $this->assertEquals(201, $jwt['headers']['status-code']); $this->assertNotEmpty($jwt['body']['jwt']); $response = $proxyClient->call(Client::METHOD_GET, '/contact', headers: [ 'cookie' => 'a_jwt_console=' . $jwt['body']['jwt'] ], followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); $this->assertStringContainsString('/console/auth/preview', $response['headers']['location']); $this->cleanupSite($siteId); } public function testInvalidSSRSource(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Astro SSR Site', 'framework' => 'astro', 'adapter' => 'ssr', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build', 'installCommand' => 'npm ci', ]); $this->assertNotEmpty($siteId); $site = $this->getSite($siteId); $this->assertEquals(200, $site['headers']['status-code']); $this->assertArrayHasKey('adapter', $site['body']); $this->assertEquals('ssr', $site['body']['adapter']); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('astro-static'), 'activate' => true ]); $this->assertEquals(202, $deployment['headers']['status-code']); $deploymentId = $deployment['body']['$id']; $this->assertNotEmpty($deploymentId); $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals('failed', $deployment['body']['status'], 'Deployment status is failed, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 100000, 500); $this->cleanupSite($siteId); } public function testDomainForFailedDeployment(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Test Site', 'framework' => 'astro', 'buildRuntime' => 'node-22', 'buildCommand' => 'cd random' ]); $this->assertNotEmpty($siteId); $domain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($domain); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('astro'), 'activate' => true ]); $this->assertEquals(202, $deployment['headers']['status-code']); $deploymentId = $deployment['body']['$id']; $this->assertNotEmpty($deploymentId); $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals('failed', $deployment['body']['status'], json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 100000, 500); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertStringContainsString('This page is empty, activate a deployment to make it live.', $response['body']); $this->cleanupSite($siteId); } public function testPermanentRedirect(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Sub project site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'outputDirectory' => './' ]); $this->assertNotEmpty($siteId); $domain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($domain); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('sub-directories'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $this->assertEventually(function () use ($proxyClient) { $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString('Sub-directory index', $response['body']); }, 30000, 500); $this->assertEventually(function () use ($proxyClient) { $response1 = $proxyClient->call(Client::METHOD_GET, '/project1'); $this->assertEquals(200, $response1['headers']['status-code']); $this->assertStringContainsString('Sub-directory project1', $response1['body']); }, 30000, 500); $response2 = $proxyClient->call(Client::METHOD_GET, '/project1/'); $this->assertEquals(200, $response2['headers']['status-code']); $this->assertStringContainsString('Sub-directory project1', $response2['body']); $this->cleanupSite($siteId); } public function testDeploymentCommandEscaping(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'A site', 'framework' => 'other', 'adapter' => 'static', 'buildRuntime' => 'static-1', 'outputDirectory' => './', 'buildCommand' => "echo 'Hello two'", 'installCommand' => 'echo "Hello one"', 'fallbackFile' => '', ]); $this->assertNotEmpty($siteId); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertStringContainsString('Hello one', $deployment['body']['buildLogs']); $this->assertStringContainsString('Hello two', $deployment['body']['buildLogs']); $this->cleanupSite($siteId); } #[Retry(count: 3)] public function testErrorPages(): void { // non-existent domain page $domain = 'non-existent-page.sites.localhost'; $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(404, $response['headers']['status-code']); $this->assertStringContainsString('Nothing is here yet', $response['body']); $this->assertStringContainsString('Start with this domain', $response['body']); $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Static site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'outputDirectory' => './', 'buildCommand' => 'sleep 5 && cd non-existing-directory', ]); $this->assertNotEmpty($siteId); $domain = $this->setupSiteDomain($siteId); // test canceled deployment error page $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => 'true' ]); $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); $deployment = $this->cancelDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('canceled', $deployment['body']['status']); $deploymentDomain = $this->getDeploymentDomain($deploymentId); $this->assertNotEmpty($deploymentDomain); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $deploymentDomain); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); $apiKey = $jwtObj->encode([ 'projectCheckDisabled' => true, 'previewAuthDisabled' => true, ]); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ 'x-appwrite-key' => API_KEY_EPHEMERAL . '_' . $apiKey, ]); $this->assertEquals(400, $response['headers']['status-code']); $deployment = $this->getDeployment($siteId, $deploymentId); $status = $deployment['body']['status'] ?? ''; $expectedMessage = match ($status) { 'failed' => 'Deployment build failed', 'canceled' => 'Deployment build canceled', default => 'Deployment is still building', }; $this->assertStringContainsString($expectedMessage, $response['body']); $expectedCta = match ($status) { 'failed' => 'View logs', 'canceled' => 'View deployments', default => 'Reload', }; $this->assertStringContainsString($expectedCta, $response['body']); // check site domain for no active deployments $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(404, $response['headers']['status-code']); $this->assertStringContainsString('No active deployments', $response['body']); $this->assertStringContainsString('View deployments', $response['body']); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => 'true' ]); $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertNotEmpty($deploymentId); $deploymentDomain = $this->getDeploymentDomain($deploymentId); $this->assertNotEmpty($deploymentDomain); $proxyClient->setEndpoint('http://' . $deploymentDomain); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); // deployment is still building error page $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ 'x-appwrite-key' => API_KEY_EPHEMERAL . '_' . $apiKey, ]); $this->assertEquals(400, $response['headers']['status-code']); $this->assertStringContainsString("Deployment is still building", $response['body']); $this->assertStringContainsString("View logs", $response['body']); $this->assertStringContainsString("Reload", $response['body']); $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals('failed', $deployment['body']['status']); }, 120000, 500); // deployment failed error page $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ 'x-appwrite-key' => API_KEY_EPHEMERAL . '_' . $apiKey, ]); $this->assertEquals(400, $response['headers']['status-code']); $this->assertStringContainsString("Deployment build failed", $response['body']); $this->assertStringContainsString("View logs", $response['body']); $this->cleanupSite($siteId); } public function testEmptySiteSource(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Empty source site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'outputDirectory' => './', ]); $this->assertNotEmpty($siteId); // Prepare empty site folder // We cant use .gitkeep, because that would make deployment non-empty $stdout = ''; $stderr = ''; $folderPath = realpath(__DIR__ . '/../../../resources/sites') . '/empty'; Console::execute("mkdir -p $folderPath", '', $stdout, $stderr); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('empty'), 'activate' => true ]); $this->assertEquals(202, $deployment['headers']['status-code']); $deploymentId = $deployment['body']['$id']; $this->assertNotEmpty($deploymentId); $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals('failed', $deployment['body']['status'], 'Deployment status does not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); $this->assertStringContainsString('Error:', $deployment['body']['buildLogs'], 'Deployment logs do not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 100000, 500); $this->cleanupSite($siteId); } public function testOutputDirectoryEmpty(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Empty output directory', 'framework' => 'other', 'buildRuntime' => 'node-22', 'outputDirectory' => './empty-directory', 'buildCommand' => 'mkdir -p ./empty-directory' ]); $this->assertNotEmpty($siteId); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => true ]); $this->assertEquals(202, $deployment['headers']['status-code']); $deploymentId = $deployment['body']['$id']; $this->assertNotEmpty($deploymentId); $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals('failed', $deployment['body']['status'], 'Deployment status does not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); $this->assertStringContainsString('Error:', $deployment['body']['buildLogs'], 'Deployment logs do not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 100000, 500); $this->cleanupSite($siteId); } public function testOutputDirectoryMissing(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Missing output directory', 'framework' => 'other', 'buildRuntime' => 'node-22', 'outputDirectory' => './non-existing-directory', ]); $this->assertNotEmpty($siteId); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('static-single-file'), 'activate' => true ]); $this->assertEquals(202, $deployment['headers']['status-code']); $deploymentId = $deployment['body']['$id']; $this->assertNotEmpty($deploymentId); $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals('failed', $deployment['body']['status'], 'Deployment status does not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); $this->assertStringContainsString('No such file or directory', $deployment['body']['buildLogs'], 'Deployment logs do not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 100000, 500); $this->cleanupSite($siteId); } public function testBuildErrorLogs(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Astro SSR site', 'framework' => 'astro', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build', 'installCommand' => 'echo "custom error" && npm ci', 'adapter' => 'ssr', ]); $this->assertNotEmpty($siteId); $site = $this->getSite($siteId); $this->assertEquals('200', $site['headers']['status-code']); $domain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($domain); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('astro-static'), 'activate' => true ]); $this->assertEquals(202, $deployment['headers']['status-code']); $deploymentId = $deployment['body']['$id']; $this->assertNotEmpty($deploymentId); $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals('failed', $deployment['body']['status'], 'Deployment status is failed, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 100000, 500); $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertStringContainsString('custom error', $deployment['body']['buildLogs']); $this->assertStringContainsString('Adapter mismatch', $deployment['body']['buildLogs']); $this->cleanupSite($siteId); } public function testCookieHeader() { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Astro site', 'framework' => 'astro', 'adapter' => 'ssr', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build', 'installCommand' => 'npm ci', 'fallbackFile' => '', ]); $this->assertNotEmpty($siteId); $domain = $this->setupSiteDomain($siteId); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('astro'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $domain = $this->getSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/cookies', [ 'cookie' => 'custom-session-id=abcd123; custom-user-id=efgh456' ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals("abcd123;efgh456", $response['body']); $this->assertEquals("value-one", $response['cookies']['my-cookie-one']); $this->assertEquals("value-two", $response['cookies']['my-cookie-two']); $this->cleanupSite($siteId); } public function testSiteCustomStartCommand(): void { $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Astro site', 'framework' => 'astro', 'adapter' => 'ssr', 'startCommand' => 'node custom-server.js', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build', 'installCommand' => 'npm ci', 'fallbackFile' => '', ]); $this->assertNotEmpty($siteId); $domain = $this->setupSiteDomain($siteId); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('astro-custom-start-command'), 'activate' => 'true' ]); $this->assertNotEmpty($deploymentId); $domain = $this->getSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Homepage OK", $response['body']); $response = $proxyClient->call(Client::METHOD_GET, '/ssr'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("SSR OK", $response['body']); $originalBody = $response['body']; $response = $proxyClient->call(Client::METHOD_GET, '/ssr'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("SSR OK", $response['body']); $this->assertNotEquals($originalBody, $response['body']); // Includes Date.now() $response = $proxyClient->call(Client::METHOD_GET, '/ssr-custom'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Custom SSR OK", $response['body']); $originalBody = $response['body']; $response = $proxyClient->call(Client::METHOD_GET, '/ssr-custom'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Custom SSR OK", $response['body']); $this->assertNotEquals($originalBody, $response['body']); // Includes Date.now() $response = $proxyClient->call(Client::METHOD_GET, '/non-existing'); $this->assertEquals(500, $response['headers']['status-code']); $this->assertStringContainsString("Custom error", $response['body']); $this->cleanupSite($siteId); } public function testSiteSpecifications() { // Check if the site specifications are correctly set in builds $site = $this->createSite([ 'siteId' => ID::unique(), 'name' => 'Astro site', 'framework' => 'astro', 'adapter' => 'ssr', 'buildRuntime' => 'node-22', 'outputDirectory' => './dist', 'buildCommand' => 'npm run build && echo $APPWRITE_SITE_MEMORY:$APPWRITE_SITE_CPUS', 'installCommand' => 'npm ci', 'fallbackFile' => '', 'buildSpecification' => Specification::S_1VCPU_1GB, 'runtimeSpecification' => Specification::S_05VCPU_512MB, ]); $this->assertEquals(201, $site['headers']['status-code']); $this->assertEquals(Specification::S_1VCPU_1GB, $site['body']['buildSpecification']); $this->assertEquals(Specification::S_05VCPU_512MB, $site['body']['runtimeSpecification']); $this->assertNotEmpty($site['body']['$id']); $siteId = $site['body']['$id'] ?? ''; $domain = $this->setupSiteDomain($siteId); $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('astro'), 'activate' => true ]); $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); // TODO: This assertion is not testing what we set in create function, because build worker currently overrides to minimal build specs $this->assertStringContainsString('2048:1', $deployment['body']['buildLogs']); }, 10000, 500); // Check if the sites specifications are correctly set in executions $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/specs'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); $this->assertEquals('512', $response['body']['APPWRITE_SITE_MEMORY']); $this->assertEquals('0.5', $response['body']['APPWRITE_SITE_CPUS']); $this->cleanupSite($siteId); } public function testSiteDeploymentRetention(): void { $siteIds = []; // Default $response = $this->createSite([ 'siteId' => ID::unique(), 'name' => 'Test retention site', 'framework' => 'other', 'buildRuntime' => 'node-22', ]); $this->assertSame(201, $response['headers']['status-code']); $this->assertSame(0, $response['body']['deploymentRetention']); $siteIds[] = $response['body']['$id']; $response = $this->getSite($response['body']['$id']); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame(0, $response['body']['deploymentRetention']); // Success values $response = $this->createSite([ 'siteId' => ID::unique(), 'name' => 'Test retention site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'deploymentRetention' => 0 ]); $this->assertSame(201, $response['headers']['status-code']); $this->assertSame(0, $response['body']['deploymentRetention']); $siteIds[] = $response['body']['$id']; $response = $this->getSite($response['body']['$id']); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame(0, $response['body']['deploymentRetention']); $response = $this->createSite([ 'siteId' => ID::unique(), 'name' => 'Test retention site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'deploymentRetention' => 180 ]); $this->assertSame(201, $response['headers']['status-code']); $this->assertSame(180, $response['body']['deploymentRetention']); $siteIds[] = $response['body']['$id']; $response = $this->getSite($response['body']['$id']); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame(180, $response['body']['deploymentRetention']); // Failure values $response = $this->createSite([ 'siteId' => ID::unique(), 'name' => 'Test retention site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'deploymentRetention' => 999999 ]); $this->assertSame(400, $response['headers']['status-code']); $response = $this->createSite([ 'siteId' => ID::unique(), 'name' => 'Test retention site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'deploymentRetention' => -1 ]); $this->assertSame(400, $response['headers']['status-code']); // Update flow $response = $this->createSite([ 'siteId' => ID::unique(), 'name' => 'Test retention site', 'framework' => 'other', 'buildRuntime' => 'node-22', 'deploymentRetention' => 180 ]); $this->assertSame(201, $response['headers']['status-code']); $this->assertSame(180, $response['body']['deploymentRetention']); $siteIds[] = $response['body']['$id']; $siteIdToUpdate = $response['body']['$id']; $response = $this->updateSite([ '$id' => $siteIdToUpdate, 'name' => 'Test retention site', 'framework' => 'other', 'deploymentRetention' => 90 ]); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame(90, $response['body']['deploymentRetention']); $response = $this->getSite($siteIdToUpdate); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame(90, $response['body']['deploymentRetention']); $response = $this->updateSite([ '$id' => $siteIdToUpdate, 'name' => 'Test retention site', 'framework' => 'other', ]); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame(0, $response['body']['deploymentRetention']); $response = $this->getSite($siteIdToUpdate); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame(0, $response['body']['deploymentRetention']); // Failed update flow $response = $this->updateSite([ '$id' => $siteIdToUpdate, 'name' => 'Test retention site', 'framework' => 'other', 'deploymentRetention' => -1 ]); $this->assertSame(400, $response['headers']['status-code']); $response = $this->updateSite([ '$id' => $siteIdToUpdate, 'name' => 'Test retention site', 'framework' => 'other', 'deploymentRetention' => 999999 ]); $this->assertSame(400, $response['headers']['status-code']); foreach ($siteIds as $siteId) { $this->cleanupSite($siteId); } } }