diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index afbfd383ca..c5d38c1897 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2,6 +2,9 @@ namespace Tests\E2E\Services\Sites; +use Ahc\Jwt\JWT; +use Appwrite\Platform\Modules\Compute\Specification; +use Appwrite\Tests\Retry; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; @@ -9,6 +12,8 @@ use Tests\E2E\Scopes\SideServer; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; +use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\System\System; class SitesCustomServerTest extends Scope { @@ -16,6 +21,463 @@ class SitesCustomServerTest extends Scope use ProjectCustom; use SideServer; + public function testListSpecs(): void + { + $specifications = $this->listSpecifications(); + $this->assertEquals(200, $specifications['headers']['status-code']); + $this->assertGreaterThan(0, $specifications['body']['total']); + $this->assertArrayHasKey(0, $specifications['body']['specifications']); + $this->assertArrayHasKey('memory', $specifications['body']['specifications'][0]); + $this->assertArrayHasKey('cpus', $specifications['body']['specifications'][0]); + $this->assertArrayHasKey('enabled', $specifications['body']['specifications'][0]); + $this->assertArrayHasKey('slug', $specifications['body']['specifications'][0]); + + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'framework' => 'other', + 'name' => 'Specs site', + 'siteId' => ID::unique(), + 'specification' => $specifications['body']['specifications'][0]['slug'] + ]); + $this->assertEquals(201, $site['headers']['status-code']); + $this->assertEquals($specifications['body']['specifications'][0]['slug'], $site['body']['specification']); + + $site = $this->getSite($site['body']['$id']); + $this->assertEquals(200, $site['headers']['status-code']); + $this->assertEquals($specifications['body']['specifications'][0]['slug'], $site['body']['specification']); + + $this->cleanupSite($site['body']['$id']); + + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'framework' => 'other', + 'name' => 'Specs site', + 'siteId' => ID::unique(), + 'specification' => '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, [ + 'key' => 'siteKey1', + 'value' => 'siteValue1', + ]); + $variable2 = $this->createVariable($siteId, [ + 'key' => 'siteKey2', + 'value' => 'siteValue2', + ]); + $variable3 = $this->createVariable($siteId, [ + '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]) + ] + ]); + + $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, [ + '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, [ + '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, [ + '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); + } + + // 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 install', + 'fallbackFile' => '', + ]); + + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + + $secretVariable = $this->createVariable($siteId, [ + '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']); + + $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 install', + ]); + $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); + + $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); + } + + 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 install', + ]); + $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'), + '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 { @@ -201,4 +663,1558 @@ class SitesCustomServerTest extends Scope $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() + ]); + + $this->assertNotNull($siteId); + + /** + * 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 install' + ]); + + $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 install', $site['body']['installCommand']); + + $this->cleanupSite($siteId); + } + + // public function testCreateDeploymentFromCLI() { + // // TODO: Implement testCreateDeploymentFromCLI() later + // } + + public function testCreateSiteAndDeploymentFromTemplate() + { + $starterTemplate = $this->getTemplate('nextjs-starter'); + $this->assertEquals(200, $starterTemplate['headers']['status-code']); + + $nextjsFramework = array_values(array_filter($starterTemplate['body']['frameworks'], function ($framework) { + return $framework['key'] === 'nextjs'; + }))[0]; + + // If this fails, the template has variables, and this test needs to be updated + $this->assertEmpty($starterTemplate['body']['variables']); + + $site = $this->createSite( + [ + 'siteId' => ID::unique(), + 'name' => $starterTemplate['body']['name'], + 'framework' => $nextjsFramework['key'], + 'adapter' => $nextjsFramework['adapter'], + 'buildCommand' => $nextjsFramework['buildCommand'], + 'buildRuntime' => $nextjsFramework['buildRuntime'], + 'fallbackFile' => $nextjsFramework['fallbackFile'], + 'installCommand' => $nextjsFramework['installCommand'], + 'outputDirectory' => $nextjsFramework['outputDirectory'], + 'providerRootDirectory' => $nextjsFramework['providerRootDirectory'], + ] + ); + + $this->assertEquals(201, $site['headers']['status-code']); + $this->assertNotEmpty($site['body']['$id']); + + $siteId = $site['body']['$id'] ?? ''; + + $deployment = $this->createTemplateDeployment( + $siteId, + [ + 'owner' => $starterTemplate['body']['providerOwner'], + 'repository' => $starterTemplate['body']['providerRepositoryId'], + 'rootDirectory' => $nextjsFramework['providerRootDirectory'], + 'version' => $starterTemplate['body']['providerVersion'], + 'activate' => true, + ] + ); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deployments = $this->listDeployments($siteId); + + $this->assertEquals(200, $deployments['headers']['status-code']); + $this->assertEquals(1, $deployments['body']['total']); + + $lastDeployment = $deployments['body']['deployments'][0]; + + $this->assertNotEmpty($lastDeployment['$id']); + $this->assertEquals(0, $lastDeployment['sourceSize']); + + $deploymentId = $lastDeployment['$id']; + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $deployment = $this->getDeployment($siteId, $deploymentId); + + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('ready', $deployment['body']['status']); + }, 300000, 1000); + + $site = $this->getSite($siteId); + $this->assertEquals(200, $site['headers']['status-code']); + + $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() + ]); + + $this->assertNotNull($siteId); + + $deployment = $this->createDeployment($siteId, [ + 'siteId' => $siteId, + 'code' => $this->packageSite('static'), + 'activate' => true, + ]); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + $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']); + }, 50000, 500); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('static'), + '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']); + }, 50000, 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() + ]); + + $this->assertNotNull($siteId); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('static'), + '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']); + + /** + * Build worker still runs the build. + * 30s sleep gives worker enough time to finish build. + * After build finished, it should still be canceled, not ready. + */ + \sleep(30); + + $deployment = $this->getDeployment($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() + ]); + + $this->assertNotNull($siteId); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('static'), + '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']); + }, 50000, 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() + ]); + + $this->assertNotNull($siteId); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => 'false' + ]); + + $deploymentIdActive = $deployment['body']['$id'] ?? ''; + $this->assertEquals(202, $deployment['headers']['status-code']); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('static'), + '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::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() + ]); + + $this->assertNotNull($siteId); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => 'false' + ]); + + $deploymentId = $deployment['body']['$id'] ?? ''; + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $deployment = $this->getDeployment($siteId, $deploymentId); + + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 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() + ]); + + $this->assertNotNull($siteId); + + /** + * 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, + 'specification' => Specification::S_1VCPU_1GB, + ]); + + $this->assertEquals(200, $site['headers']['status-code']); + $this->assertNotEmpty($site['body']['$id']); + $this->assertEquals(Specification::S_1VCPU_1GB, $site['body']['specification']); + + // Change the specs to 1vcpu 512mb + $site = $this->updateSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Site', + 'outputDirectory' => './', + 'providerBranch' => 'main', + 'providerRootDirectory' => './', + '$id' => $siteId, + 'specification' => Specification::S_1VCPU_512MB, + ]); + + $this->assertEquals(200, $site['headers']['status-code']); + $this->assertNotEmpty($site['body']['$id']); + $this->assertEquals(Specification::S_1VCPU_512MB, $site['body']['specification']); + + /** + * Test for FAILURE + */ + + $site = $this->updateSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Site', + 'outputDirectory' => './', + 'providerBranch' => 'main', + 'providerRootDirectory' => './', + '$id' => $siteId, + 'specification' => 's-2vcpu-512mb', // Invalid specification + ]); + + $this->assertEquals(400, $site['headers']['status-code']); + $this->assertStringStartsWith('Invalid `specification` 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() + ]); + + $this->assertNotNull($siteId); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => 'false' + ]); + + $deploymentId = $deployment['body']['$id'] ?? ''; + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $deployment = $this->getDeployment($siteId, $deploymentId); + + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 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() + ]); + + $this->assertNotNull($siteId); + + $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 testGetFrameworks(): void + { + $frameworks = $this->client->call(Client::METHOD_GET, '/sites/frameworks', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $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); + } + + 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']); + + $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->assertStringContainsString("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); + } + + public function testSiteTemplate(): void + { + $template = $this->getTemplate('astro-starter'); + $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'], + 'version' => $template['providerVersion'], + 'activate' => true + ]); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $this->assertEventually(function () use ($siteId) { + $site = $this->getSite($siteId); + $this->assertNotEmpty($site['body']['deploymentId']); + }, 50000, 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']); + + $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']; + + $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 . '.' . System::getEnv('_APP_DOMAIN_SITES', ''), + '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']); + }, 50000, 500); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(401, $response['headers']['status-code']); + $this->assertStringContainsString("This domain is not connected to any Appwrite resource yet", $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); + + $oldDeploymentDomain = $this->getDeploymentDomain($deploymentId); + $this->assertNotEmpty($oldDeploymentDomain); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => 'true' + ]); + $this->assertNotEmpty($deploymentId); + + $newDeploymentDomain = $this->getDeploymentDomain($deploymentId); + $this->assertNotEmpty($newDeploymentDomain); + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $newDeploymentDomain); + $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://' . $oldDeploymentDomain); + $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_DYNAMIC . '_' . $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']); + + $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->assertNotEquals($url, $response['headers']['access-control-allow-origin']); + $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); + + $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->assertNotEquals($url, $response['headers']['access-control-allow-origin']); + $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); + } + + public function testSiteScreenshot(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Themed site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static-themed'), + 'activate' => 'true' + ]); + + $this->assertNotEmpty($deploymentId); + + $domain = $this->getSiteDomain($siteId); + $this->assertNotEmpty($domain); + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Themed website", $response['body']); + $this->assertStringContainsString("@media (prefers-color-scheme: dark)", $response['body']); + + $deployment = $this->getDeployment($siteId, $deploymentId); + + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['screenshotLight']); + $this->assertNotEmpty($deployment['body']['screenshotDark']); + + $screenshotId = $deployment['body']['screenshotLight']; + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([ + ], $this->getHeaders())); + + $this->assertEquals(200, $file['headers']['status-code']); + $this->assertNotEmpty(200, $file['body']); + $this->assertGreaterThan(1, $file['headers']['content-length']); + $this->assertEquals('image/png', $file['headers']['content-type']); + + $screenshotHash = \md5($file['body']); + $this->assertNotEmpty($screenshotHash); + + $screenshotId = $deployment['body']['screenshotDark']; + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([ + ], $this->getHeaders())); + + $this->assertEquals(200, $file['headers']['status-code']); + $this->assertNotEmpty(200, $file['body']); + $this->assertGreaterThan(1, $file['headers']['content-length']); + $this->assertEquals('image/png', $file['headers']['content-type']); + + $screenshotDarkHash = \md5($file['body']); + $this->assertNotEmpty($screenshotDarkHash); + + $this->assertNotEquals($screenshotDarkHash, $screenshotHash); + + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin"); + $this->assertEquals(404, $file['headers']['status-code']); + + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin"); + $this->assertEquals(404, $file['headers']['status-code']); + + $this->cleanupSite($siteId); + } + + 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'), + '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 install', + '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']); + + $logs = $this->listLogs($siteId, [ + Query::orderDesc('$createdAt')->toString(), + Query::limit(1)->toString(), + ]); + $this->assertEquals(200, $logs['headers']['status-code']); + $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); + + $response = $proxyClient->call(Client::METHOD_GET, '/logs-action'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Action logs printed.", $response['body']); + + $logs = $this->listLogs($siteId, [ + Query::orderDesc('$createdAt')->toString(), + Query::limit(1)->toString(), + ]); + $this->assertEquals(200, $logs['headers']['status-code']); + $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); + + $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']); + + $deploymentId2 = $this->setupDuplicateDeployment($siteId, $deploymentId1); + $this->assertNotEmpty($deploymentId2); + + $response = $proxyClient->call(Client::METHOD_GET, '/not-found'); + $this->assertStringContainsString("Index page", $response['body']); + + $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); + + $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); + + $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); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('Index page', $response['body']); + + $function = $this->getSite($siteId); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals($deploymentId2, $function['body']['deploymentId']); + + $function = $this->updateSiteDeployment($siteId, $deploymentId1); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals($deploymentId1, $function['body']['deploymentId']); + + $function = $this->getSite($siteId); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals($deploymentId1, $function['body']['deploymentId']); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('Hello Appwrite', $response['body']); + + $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); + + // Create second deployment to make first one a preview + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentId); + + $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']); + $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 + $user = $this->client->call(Client::METHOD_POST, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + ], [ + 'userId' => ID::unique(), + 'email' => 'newuser@appwrite.io', + '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' => 'newuser@appwrite.io', + '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); + } }