diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 5166429e32..8254a22ac0 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -98,6 +98,9 @@ Http::init() ->inject('authorization') ->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) { $route = $utopia->getRoute(); + if ($route === null) { + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + } /** * Handle user authentication and session validation. @@ -489,6 +492,10 @@ Http::init() $request->setUser($user); $route = $utopia->getRoute(); + if ($route === null) { + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + } + $path = $route->getMatchedPath(); $databaseType = match (true) { str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB, diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Labels/Update.php similarity index 60% rename from src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php rename to src/Appwrite/Platform/Modules/Project/Http/Project/Labels/Update.php index de11bb0091..24d1c48cf1 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Labels/Update.php @@ -1,20 +1,15 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) - ->setHttpPath('/v1/projects/:projectId/labels') + ->setHttpPath('/v1/project/labels') + ->httpAlias('/v1/projects/:projectId/labels') ->desc('Update project labels') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') + ->groups(['api', 'project']) + ->label('scope', 'project.write') + ->label('event', 'labels.*.update') + ->label('audits.event', 'project.labels.update') + ->label('audits.resource', 'project.labels/{response.$id}') ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', + namespace: 'project', + group: null, name: 'updateLabels', description: <<param('projectId', '', new UID(), 'Project unique ID.') ->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), APP_LIMIT_ARRAY_LABELS_SIZE), 'Array of project labels. Replaces the previous labels. Maximum of ' . APP_LIMIT_ARRAY_LABELS_SIZE . ' labels are allowed, each up to 36 alphanumeric characters long.') ->inject('response') ->inject('dbForPlatform') + ->inject('project') ->callback($this->action(...)); } @@ -67,17 +60,11 @@ class Update extends Action * @param array $labels */ public function action( - string $projectId, array $labels, Response $response, - Database $dbForPlatform + Database $dbForPlatform, + Document $project ): void { - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - $labels = (array) \array_values(\array_unique($labels)); $project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels])); diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 049ff78969..ad61947750 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -16,6 +16,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Update as Updat use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Create as CreateWindowsPlatform; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Update as UpdateWindowsPlatform; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatforms; +use Appwrite\Platform\Modules\Project\Http\Project\Labels\Update as UpdateProjectLabels; use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable; use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable; use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable; @@ -31,6 +32,9 @@ class Http extends Service // Hooks $this->addAction(Init::getName(), new Init()); + + // Project + $this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels()); // Variables $this->addAction(CreateVariable::getName(), new CreateVariable()); diff --git a/src/Appwrite/Platform/Modules/Projects/Services/Http.php b/src/Appwrite/Platform/Modules/Projects/Services/Http.php index 8b0d6f87c8..8275e664d5 100644 --- a/src/Appwrite/Platform/Modules/Projects/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Projects/Services/Http.php @@ -8,7 +8,6 @@ use Appwrite\Platform\Modules\Projects\Http\DevKeys\Get as GetDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\Update as UpdateDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\XList as ListDevKeys; use Appwrite\Platform\Modules\Projects\Http\Projects\Create as CreateProject; -use Appwrite\Platform\Modules\Projects\Http\Projects\Labels\Update as UpdateProjectLabels; use Appwrite\Platform\Modules\Projects\Http\Projects\Team\Update as UpdateProjectTeam; use Appwrite\Platform\Modules\Projects\Http\Projects\Update as UpdateProject; use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects; @@ -31,7 +30,6 @@ class Http extends Service $this->addAction(CreateProject::getName(), new CreateProject()); $this->addAction(UpdateProject::getName(), new UpdateProject()); $this->addAction(ListProjects::getName(), new ListProjects()); - $this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels()); $this->addAction(UpdateProjectTeam::getName(), new UpdateProjectTeam()); $this->addAction(CreateSchedule::getName(), new CreateSchedule()); diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index eea53d9ea8..f6eb963967 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -1324,8 +1324,8 @@ class UsageTest extends Scope $this->assertEquals($requestsTotal, $response['body']['requests'][array_key_last($response['body']['requests'])]['value']); $this->validateDates($response['body']['requests']); // vectordbTotal should reflect only VectorsDB instances, not relational databases. - $this->assertEquals($vectordbTotal, $response['body']['vectordbDatabasesTotal']); - $this->assertEquals($documentsTotal, $response['body']['vectordbDocumentsTotal']); + $this->assertEquals($vectordbTotal, $response['body']['vectorsdbDatabasesTotal']); + $this->assertEquals($documentsTotal, $response['body']['vectorsdbDocumentsTotal']); }); $response = $this->client->call( diff --git a/tests/e2e/Services/Project/LabelsBase.php b/tests/e2e/Services/Project/LabelsBase.php new file mode 100644 index 0000000000..2b7074ef46 --- /dev/null +++ b/tests/e2e/Services/Project/LabelsBase.php @@ -0,0 +1,224 @@ +updateLabels(['frontend', 'backend']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertIsArray($response['body']['labels']); + $this->assertCount(2, $response['body']['labels']); + $this->assertContains('frontend', $response['body']['labels']); + $this->assertContains('backend', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsReplace(): void + { + $response = $this->updateLabels(['alpha', 'beta']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['labels']); + $this->assertContains('alpha', $response['body']['labels']); + $this->assertContains('beta', $response['body']['labels']); + + // Replace with new labels + $response = $this->updateLabels(['gamma', 'delta', 'epsilon']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(3, $response['body']['labels']); + $this->assertContains('gamma', $response['body']['labels']); + $this->assertContains('delta', $response['body']['labels']); + $this->assertContains('epsilon', $response['body']['labels']); + $this->assertNotContains('alpha', $response['body']['labels']); + $this->assertNotContains('beta', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsEmpty(): void + { + // Set some labels first + $response = $this->updateLabels(['toRemove']); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['labels']); + + // Clear all labels + $response = $this->updateLabels([]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertIsArray($response['body']['labels']); + $this->assertCount(0, $response['body']['labels']); + } + + public function testUpdateLabelsDeduplicated(): void + { + $response = $this->updateLabels(['duplicate', 'duplicate', 'unique']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['labels']); + $this->assertContains('duplicate', $response['body']['labels']); + $this->assertContains('unique', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsSingleLabel(): void + { + $response = $this->updateLabels(['solo']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['labels']); + $this->assertContains('solo', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsWithoutAuthentication(): void + { + $response = $this->updateLabels(['unauthorized'], false); + + $this->assertSame(401, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidLabelTooLong(): void + { + $response = $this->updateLabels([str_repeat('a', 37)]); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidLabelCharacters(): void + { + $response = $this->updateLabels(['invalid-label!']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsAlphanumericOnly(): void + { + $response = $this->updateLabels(['ABC123', 'lowercase', 'UPPERCASE', '0123456789']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(4, $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsMaxLength(): void + { + $label = str_repeat('a', 36); + $response = $this->updateLabels([$label]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['labels']); + $this->assertContains($label, $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsIdempotent(): void + { + $labels = ['stable', 'production']; + + $first = $this->updateLabels($labels); + $this->assertSame(200, $first['headers']['status-code']); + + $second = $this->updateLabels($labels); + $this->assertSame(200, $second['headers']['status-code']); + + $this->assertSame($first['body']['labels'], $second['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsDeduplicatedOrder(): void + { + $response = $this->updateLabels(['b', 'a', 'b']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertCount(2, $response['body']['labels']); + $this->assertSame('b', $response['body']['labels'][0]); + $this->assertSame('a', $response['body']['labels'][1]); + + // Cleanup + $this->updateLabels([]); + } + + public function testUpdateLabelsInvalidHyphen(): void + { + $response = $this->updateLabels(['my-label']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidUnderscore(): void + { + $response = $this->updateLabels(['my_label']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidSpace(): void + { + $response = $this->updateLabels(['my label']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsInvalidEmptyString(): void + { + $response = $this->updateLabels(['']); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateLabelsResponseModel(): void + { + $response = $this->updateLabels(['test']); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertArrayHasKey('$id', $response['body']); + $this->assertArrayHasKey('name', $response['body']); + $this->assertArrayHasKey('labels', $response['body']); + $this->assertIsArray($response['body']['labels']); + $this->assertContains('test', $response['body']['labels']); + + // Cleanup + $this->updateLabels([]); + } + + // Helpers + + /** + * @param array $labels + */ + protected function updateLabels(array $labels, bool $authenticated = true): mixed + { + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]; + + if ($authenticated) { + $headers = array_merge($headers, $this->getHeaders()); + } + + return $this->client->call(\Tests\E2E\Client::METHOD_PUT, '/project/labels', $headers, [ + 'labels' => $labels, + ]); + } +} diff --git a/tests/e2e/Services/Project/LabelsConsoleClientTest.php b/tests/e2e/Services/Project/LabelsConsoleClientTest.php new file mode 100644 index 0000000000..dd724338d6 --- /dev/null +++ b/tests/e2e/Services/Project/LabelsConsoleClientTest.php @@ -0,0 +1,14 @@ +