From f2445c2a372142d113ca0e1030b559884db0eda5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 6 Feb 2025 14:22:35 +0100 Subject: [PATCH 1/6] Add SPA tests --- .../Services/Sites/SitesCustomServerTest.php | 65 +++++++++++++++++++ tests/resources/sites/static-spa/404.html | 10 +++ tests/resources/sites/static-spa/contact.html | 10 +++ tests/resources/sites/static-spa/index.html | 10 +++ 4 files changed, 95 insertions(+) create mode 100644 tests/resources/sites/static-spa/404.html create mode 100644 tests/resources/sites/static-spa/contact.html create mode 100644 tests/resources/sites/static-spa/index.html diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index d5863aea2f..de13c9e1be 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -939,5 +939,70 @@ class SitesCustomServerTest extends Scope $this->assertArrayHasKey('adapters', $framework); } + public function testSPASite(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'SPA site', + 'framework' => 'other', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '404.html', + ]); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static-spa'), + 'activate' => 'true' + ]); + + $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('resourceId', [$siteId])->toString(), + Query::equal('resourceType', ['site'])->toString(), + ], + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(1, $rules['body']['total']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertNotEmpty($rules['body']['rules'][0]['domain']); + + $domain = $rules['body']['rules'][0]['domain']; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + \var_dump($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("Not found page", $response['body']); + } + // TODO: Add tests for deletion of resources when site is deleted } diff --git a/tests/resources/sites/static-spa/404.html b/tests/resources/sites/static-spa/404.html new file mode 100644 index 0000000000..d2ae1e77d9 --- /dev/null +++ b/tests/resources/sites/static-spa/404.html @@ -0,0 +1,10 @@ + + + + + + + +

Not found page

+ + diff --git a/tests/resources/sites/static-spa/contact.html b/tests/resources/sites/static-spa/contact.html new file mode 100644 index 0000000000..1ef7dc9497 --- /dev/null +++ b/tests/resources/sites/static-spa/contact.html @@ -0,0 +1,10 @@ + + + + + + + +

Contact page

+ + diff --git a/tests/resources/sites/static-spa/index.html b/tests/resources/sites/static-spa/index.html new file mode 100644 index 0000000000..3fd2262803 --- /dev/null +++ b/tests/resources/sites/static-spa/index.html @@ -0,0 +1,10 @@ + + + + + + + +

Index page

+ + From 21c224709fcf4c74d0ff55084887ba0b40af8ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 8 Feb 2025 10:36:11 +0100 Subject: [PATCH 2/6] Finish SPA integration --- app/controllers/general.php | 16 +- app/views/general/404.phtml | 185 ++++++++++++++++++ docker-compose.yml | 2 +- .../Modules/Sites/Http/Sites/Create.php | 2 +- .../Modules/Sites/Http/Sites/Update.php | 2 +- .../Services/Sites/SitesCustomServerTest.php | 111 +++++++---- tests/resources/sites/static-spa/404.html | 2 +- 7 files changed, 275 insertions(+), 45 deletions(-) create mode 100644 app/views/general/404.phtml diff --git a/app/controllers/general.php b/app/controllers/general.php index 986d482421..0b9770ff76 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -214,10 +214,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw default => null }; - if ($resource->getAttribute('adapter', '') === 'static') { - $runtime = $runtimes['static-1'] ?? null; - } - if (\is_null($runtime)) { throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported'); } @@ -385,6 +381,11 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw 'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''), ]); + // SPA fallbackFile override + if ($resource->getAttribute('adapter', '') === 'static' && $resource->getAttribute('fallbackFile', '') !== '') { + $vars['OPEN_RUNTIMES_STATIC_FALLBACK'] = $resource->getAttribute('fallbackFile', ''); + } + /** Execute function */ $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); try { @@ -446,6 +447,13 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw requestTimeout: 30 ); + // Branded 404 override + if ($executionResponse['statusCode'] === 404 && $resource->getAttribute('adapter', '') === 'static') { + $layout = new View(__DIR__ . '/../views/general/404.phtml'); + $executionResponse['body'] = $layout->render(); + $executionResponse['headers']['content-length'] = \strlen($executionResponse['body']); + } + $headersFiltered = []; foreach ($executionResponse['headers'] as $key => $value) { if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { diff --git a/app/views/general/404.phtml b/app/views/general/404.phtml new file mode 100644 index 0000000000..7ec1cfbf21 --- /dev/null +++ b/app/views/general/404.phtml @@ -0,0 +1,185 @@ + + + + + + + 404 Not Found + + + + + +
+
+
Page not found
+

The page you’re looking for doesn’t exist.

+ +
+
+ +
+

Powered by

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d3f40e1928..5deb04316e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -204,7 +204,7 @@ services: appwrite-console: <<: *x-logging container_name: appwrite-console - image: appwrite/console:5.3.0-sites-rc.2 + image: appwrite/console:5.3.0-sites-rc.4 restart: unless-stopped networks: - appwrite diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index 079f2fec49..1f6a0e7a76 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -76,7 +76,7 @@ class Create extends Base ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) ->param('subdomain', '', new CustomId(), 'Unique custom sub-domain. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true) ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.') - ->param('adapter', '', new Text(8192, 0), 'Framework adapter. Allows: static, ssr', true) + ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) ->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true) ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index 578e0cf164..b8dcc06305 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -73,7 +73,7 @@ class Update extends Base ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true) - ->param('adapter', '', new Text(8192, 0), 'Framework adapter. Usuallly allows: static, ssr', true) + ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true) ->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index de13c9e1be..61371b2ba0 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -742,41 +742,6 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($siteId); } - // public function testLoadSite(): void - // { - // $site = $this->createSite([ - // 'buildRuntime' => 'ssr-22', - // 'fallbackFile' => null, - // 'framework' => 'other', - // 'name' => 'Test Site', - // 'outputDirectory' => './', - // 'providerBranch' => 'main', - // 'providerRootDirectory' => './', - // 'siteId' => ID::unique() - // ]); - - // $siteId = $site['body']['$id'] ?? ''; - // $this->assertNotEmpty($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']); - // }, 30000, 300); - - // // get rule for this site from rules collection - - // $response = $this->client->call(Client::METHOD_GET, $domain); - // var_dump($response); - // } - public function testUpdateSpecs(): void { $siteId = $this->setupSite([ @@ -939,12 +904,81 @@ class SitesCustomServerTest extends Scope $this->assertArrayHasKey('adapters', $framework); } - public function testSPASite(): void + 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' => '', + ]); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static-spa'), + 'activate' => 'true' + ]); + + $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('resourceId', [$siteId])->toString(), + Query::equal('resourceType', ['site'])->toString(), + ], + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(1, $rules['body']['total']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertNotEmpty($rules['body']['rules'][0]['domain']); + + $domain = $rules['body']['rules'][0]['domain']; + + $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' => '', @@ -1001,7 +1035,10 @@ class SitesCustomServerTest extends Scope ])); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertStringContainsString("Not found page", $response['body']); + $this->assertStringContainsString("Customized 404 page", $response['body']); + $this->assertStringNotContainsString("Powered by", $response['body']); // Brand + + $this->cleanupSite($siteId); } // TODO: Add tests for deletion of resources when site is deleted diff --git a/tests/resources/sites/static-spa/404.html b/tests/resources/sites/static-spa/404.html index d2ae1e77d9..2a51f36d22 100644 --- a/tests/resources/sites/static-spa/404.html +++ b/tests/resources/sites/static-spa/404.html @@ -5,6 +5,6 @@ -

Not found page

+

Customized 404 page

From e269614a51b04f962bfd51a6550bc6e1473e4659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 8 Feb 2025 10:48:49 +0100 Subject: [PATCH 3/6] Update tests/e2e/Services/Sites/SitesCustomServerTest.php --- tests/e2e/Services/Sites/SitesCustomServerTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 61371b2ba0..86ed333c46 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1011,8 +1011,6 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - \var_dump($domain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], From 5e2abc539fe2076e3d1d8d2175efc5077d81307c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 8 Feb 2025 12:16:25 +0100 Subject: [PATCH 4/6] Add helper for site domain --- tests/e2e/Services/Sites/SitesBase.php | 23 +++++++++++ .../Services/Sites/SitesCustomServerTest.php | 40 +++++-------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index d19f20b24d..277064ead5 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -6,6 +6,7 @@ use Appwrite\Tests\Async; use CURLFile; use Tests\E2E\Client; use Utopia\CLI\Console; +use Utopia\Database\Query; trait SitesBase { @@ -215,4 +216,26 @@ trait SitesBase return $site; } + + protected function getSiteDomain(string $siteId): string + { + $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('resourceId', [$siteId])->toString(), + Query::equal('resourceType', ['site'])->toString(), + ], + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertGreaterThanOrEqual(1, $rules['body']['total']); + $this->assertGreaterThanOrEqual(1, \count($rules['body']['rules'])); + $this->assertNotEmpty($rules['body']['rules'][0]['domain']); + + $domain = $rules['body']['rules'][0]['domain']; + + return $domain; + } } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 86ed333c46..e91df25f69 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -918,27 +918,16 @@ class SitesCustomServerTest extends Scope 'fallbackFile' => '', ]); + $this->assertNotEmpty($siteId); + $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static-spa'), 'activate' => 'true' ]); - $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('resourceId', [$siteId])->toString(), - Query::equal('resourceType', ['site'])->toString(), - ], - ]); + $this->assertNotEmpty($deploymentId); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); - - $domain = $rules['body']['rules'][0]['domain']; + $domain = $this->getSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); @@ -986,27 +975,16 @@ class SitesCustomServerTest extends Scope 'fallbackFile' => '404.html', ]); + $this->assertNotEmpty($siteId); + $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static-spa'), 'activate' => 'true' ]); - $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('resourceId', [$siteId])->toString(), - Query::equal('resourceType', ['site'])->toString(), - ], - ]); - - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); - - $domain = $rules['body']['rules'][0]['domain']; + $this->assertNotEmpty($deploymentId); + + $domain = $this->getSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); From aec9d91a68daa5e81503ba25f0d6d60066566d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 10 Feb 2025 09:17:59 +0100 Subject: [PATCH 5/6] Formatting fix --- tests/e2e/Services/Sites/SitesCustomServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 425ce2c4f4..ed435ee15b 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -982,7 +982,7 @@ class SitesCustomServerTest extends Scope ]); $this->assertNotEmpty($deploymentId); - + $domain = $this->getSiteDomain($siteId); $proxyClient = new Client(); From d79ab99202cb769aeaf5db406643de0ab7ee164c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 25 Feb 2025 18:22:40 +0100 Subject: [PATCH 6/6] Test fix after merge --- tests/e2e/Services/Sites/SitesCustomServerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 454e552f15..4bfa910255 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1195,7 +1195,7 @@ class SitesCustomServerTest extends Scope $this->assertNotEmpty($deploymentId); - $domain = $this->getSiteDomain($siteId); + $domain = $this->setupSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); @@ -1251,7 +1251,7 @@ class SitesCustomServerTest extends Scope $this->assertNotEmpty($deploymentId); - $domain = $this->getSiteDomain($siteId); + $domain = $this->setupSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain);