diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 8262290f43..52bae4aa1d 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -831,6 +831,17 @@ return [ 'array' => true, 'filters' => [], ], + [ + '$id' => ID::custom('deploymentBadge'), + 'type' => Database::VAR_BOOLEAN, + 'signed' => true, + 'size' => 0, + 'format' => '', + 'filters' => [], + 'required' => false, + 'default' => false, + 'array' => false, + ], ], 'indexes' => [ [ diff --git a/phpunit.xml b/phpunit.xml index a8578995c1..4628447d08 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -38,6 +38,7 @@ ./tests/e2e/Services/Functions/FunctionsBase.php ./tests/e2e/Services/Functions/FunctionsCustomServerTest.php ./tests/e2e/Services/Functions/FunctionsCustomClientTest.php + ./tests/e2e/Services/Badge diff --git a/src/Appwrite/Platform/Modules/Badge/Http/Functions/Get.php b/src/Appwrite/Platform/Modules/Badge/Http/Functions/Get.php index f6af376b34..42e13d63f0 100644 --- a/src/Appwrite/Platform/Modules/Badge/Http/Functions/Get.php +++ b/src/Appwrite/Platform/Modules/Badge/Http/Functions/Get.php @@ -53,7 +53,7 @@ class Get extends Action $message = 'not found'; $color = 'lightgrey'; break; - case !$function->getAttribute('deploymentBadge', true): + case !$function->getAttribute('deploymentBadge', false): $message = 'disabled'; $color = 'lightgrey'; break; diff --git a/src/Appwrite/Platform/Modules/Badge/Http/Sites/Get.php b/src/Appwrite/Platform/Modules/Badge/Http/Sites/Get.php index fd4051cd01..e55e1661c9 100644 --- a/src/Appwrite/Platform/Modules/Badge/Http/Sites/Get.php +++ b/src/Appwrite/Platform/Modules/Badge/Http/Sites/Get.php @@ -53,7 +53,7 @@ class Get extends Action $message = 'not found'; $color = 'lightgrey'; break; - case !$site->getAttribute('deploymentBadge', true): + case !$site->getAttribute('deploymentBadge', false): $message = 'disabled'; $color = 'lightgrey'; break; diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index d72e365fbd..9ab34f2cd4 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -93,6 +93,7 @@ class Update extends Base System::getEnv('_APP_COMPUTE_CPUS', 0), System::getEnv('_APP_COMPUTE_MEMORY', 0) ), 'Runtime specification for the function and builds.', true, ['plan']) + ->param('deploymentBadge', null, new Nullable(new Boolean()), 'Whether to display the deployment badge for this function.', true) ->inject('request') ->inject('response') ->inject('dbForProject') @@ -125,6 +126,7 @@ class Update extends Base bool $providerSilentMode, string $providerRootDirectory, string $specification, + ?bool $deploymentBadge, Request $request, Response $response, Database $dbForProject, @@ -240,6 +242,8 @@ class Update extends Base } } + $deploymentBadge ??= $function->getAttribute('deploymentBadge', false); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ 'execute' => $execute, 'name' => $name, @@ -266,6 +270,7 @@ class Update extends Base 'specification' => $specification, 'buildSpecification' => $specification, 'runtimeSpecification' => $specification, + 'deploymentBadge' => $deploymentBadge, 'search' => implode(' ', [$functionId, $name, $runtime]), ]))); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index e7af7caccd..c71b070a61 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -164,7 +164,7 @@ class Update extends Base } if ($request->getPayload('deploymentBadge', null) === null) { - $deploymentBadge = $site->getAttribute('deploymentBadge', true); + $deploymentBadge = $site->getAttribute('deploymentBadge', false); } $enabled ??= $site->getAttribute('enabled', true); diff --git a/src/Appwrite/Utopia/Response/Model/Func.php b/src/Appwrite/Utopia/Response/Model/Func.php index e33d7663fd..8058fba1f1 100644 --- a/src/Appwrite/Utopia/Response/Model/Func.php +++ b/src/Appwrite/Utopia/Response/Model/Func.php @@ -182,6 +182,12 @@ class Func extends Model 'default' => APP_COMPUTE_SPECIFICATION_DEFAULT, 'example' => APP_COMPUTE_SPECIFICATION_DEFAULT, ]) + ->addRule('deploymentBadge', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Whether to display the deployment badge in the function response.', + 'default' => false, + 'example' => false, + ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/Site.php b/src/Appwrite/Utopia/Response/Model/Site.php index e0c62d47ac..c20f005b76 100644 --- a/src/Appwrite/Utopia/Response/Model/Site.php +++ b/src/Appwrite/Utopia/Response/Model/Site.php @@ -188,7 +188,7 @@ class Site extends Model ->addRule('deploymentBadge', [ 'type' => self::TYPE_BOOLEAN, 'description' => 'Whether to display the deployment badge in the site response.', - 'default' => true, + 'default' => false, 'example' => false, ]) ; diff --git a/tests/e2e/Services/Badge/BadgeTest.php b/tests/e2e/Services/Badge/BadgeTest.php new file mode 100644 index 0000000000..6c060cd95c --- /dev/null +++ b/tests/e2e/Services/Badge/BadgeTest.php @@ -0,0 +1,237 @@ +client->call(Client::METHOD_GET, '/badge/sites/nonexistentid1234567', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('image/svg+xml', $response['headers']['content-type']); + $this->assertStringContainsString('assertStringContainsString('not found', $response['body']); + $this->assertStringContainsString('appwrite sites', $response['body']); + } + + public function testGetFunctionBadgeNotFound(): void + { + $response = $this->client->call(Client::METHOD_GET, '/badge/functions/nonexistentid1234567', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('image/svg+xml', $response['headers']['content-type']); + $this->assertStringContainsString('assertStringContainsString('not found', $response['body']); + $this->assertStringContainsString('appwrite functions', $response['body']); + } + + /** + * deploymentBadge defaults to false — a new site shows 'disabled' without any extra step. + */ + public function testGetSiteBadgeDisabledByDefault(): void + { + $site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'siteId' => ID::unique(), + 'name' => 'Badge Disabled Site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertEquals(201, $site['headers']['status-code']); + $siteId = $site['body']['$id']; + + $response = $this->client->call(Client::METHOD_GET, '/badge/sites/' . $siteId, [ + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('image/svg+xml', $response['headers']['content-type']); + $this->assertStringContainsString('assertStringContainsString('disabled', $response['body']); + $this->assertStringContainsString('appwrite sites', $response['body']); + + $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + } + + /** + * deploymentBadge defaults to false — a new function shows 'disabled' without any extra step. + */ + public function testGetFunctionBadgeDisabledByDefault(): void + { + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Badge Disabled Function', + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + ]); + + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $response = $this->client->call(Client::METHOD_GET, '/badge/functions/' . $functionId, [ + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('image/svg+xml', $response['headers']['content-type']); + $this->assertStringContainsString('assertStringContainsString('disabled', $response['body']); + $this->assertStringContainsString('appwrite functions', $response['body']); + + $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + } + + /** + * When badge is explicitly enabled on a site with no deployments, it shows 'no deployment'. + */ + public function testGetSiteBadgeNoDeployment(): void + { + $site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'siteId' => ID::unique(), + 'name' => 'No Deployment Badge Site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertEquals(201, $site['headers']['status-code']); + $siteId = $site['body']['$id']; + + $update = $this->client->call(Client::METHOD_PUT, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'No Deployment Badge Site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + 'deploymentBadge' => true, + ]); + + $this->assertEquals(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['deploymentBadge']); + + $response = $this->client->call(Client::METHOD_GET, '/badge/sites/' . $siteId, [ + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('image/svg+xml', $response['headers']['content-type']); + $this->assertStringContainsString('assertStringContainsString('no deployment', $response['body']); + $this->assertStringContainsString('appwrite sites', $response['body']); + $this->assertStringContainsString('no-cache', $response['headers']['cache-control'] ?? ''); + + $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + } + + /** + * When badge is explicitly enabled on a function with no deployments, it shows 'no deployment'. + */ + public function testGetFunctionBadgeNoDeployment(): void + { + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'No Deployment Badge Function', + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + ]); + + $this->assertEquals(201, $function['headers']['status-code']); + $functionId = $function['body']['$id']; + + $update = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'No Deployment Badge Function', + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'deploymentBadge' => true, + ]); + + $this->assertEquals(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['deploymentBadge']); + + $response = $this->client->call(Client::METHOD_GET, '/badge/functions/' . $functionId, [ + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('image/svg+xml', $response['headers']['content-type']); + $this->assertStringContainsString('assertStringContainsString('no deployment', $response['body']); + $this->assertStringContainsString('appwrite functions', $response['body']); + $this->assertStringContainsString('no-cache', $response['headers']['cache-control'] ?? ''); + + $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + } + + public function testBadgeEndpointsArePublic(): void + { + $response = $this->client->call(Client::METHOD_GET, '/badge/sites/nonexistentid1234567', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('image/svg+xml', $response['headers']['content-type']); + + $response = $this->client->call(Client::METHOD_GET, '/badge/functions/nonexistentid1234567', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('image/svg+xml', $response['headers']['content-type']); + } +}