seed(RolePermissionSeeder::class); } #[Test] public function it_rejects_a_request_without_a_token(): void { $this->getJson('/api/v1/pages')->assertUnauthorized(); } #[Test] public function it_rejects_a_token_the_identity_server_does_not_recognise(): void { $this->fakePireonRejectingTokens(); $this->getJson('/api/v1/pages', ['Authorization' => 'Bearer revoked-token']) ->assertUnauthorized(); } #[Test] public function it_resolves_the_caller_and_their_local_permissions(): void { $user = $this->userWithRoles('editor'); $response = $this->getJson('/api/v1/me', $this->apiHeadersFor($user)); $response->assertOk() ->assertJsonPath('data.email', $user->email) ->assertJsonPath('data.roles.0', 'editor'); // Permissions come from this app, not from the identity server. $this->assertContains('pages.create', $response->json('data.permissions')); $this->assertNotContains('roles.create', $response->json('data.permissions')); } #[Test] public function a_locally_deactivated_user_is_denied_even_with_a_valid_token(): void { $user = $this->userWithRoles('editor', ['is_active' => false]); $this->getJson('/api/v1/pages', $this->apiHeadersFor($user)) ->assertUnauthorized(); } #[Test] public function it_returns_content_in_the_requested_locale(): void { // No stored locale preference, so Accept-Language is free to decide // when no explicit ?lang= is given. $user = $this->userWithRoles('editor', ['locale' => null]); Page::factory()->published()->create([ 'title' => ['es' => 'Hola', 'en' => 'Hello', 'ca' => 'Hola-ca'], 'slug' => ['es' => 'hola', 'en' => 'hello', 'ca' => 'hola-ca'], ]); $headers = $this->apiHeadersFor($user); $this->getJson('/api/v1/pages?lang=en', $headers) ->assertOk() ->assertJsonPath('data.0.title', 'Hello') ->assertJsonPath('data.0.locale', 'en'); $this->getJson('/api/v1/pages?lang=ca', $headers) ->assertOk() ->assertJsonPath('data.0.title', 'Hola-ca'); // Accept-Language is honoured when no explicit ?lang= is given. $this->getJson('/api/v1/pages', $headers + ['Accept-Language' => 'en-GB,en;q=0.9']) ->assertOk() ->assertJsonPath('data.0.title', 'Hello') ->assertHeader('Content-Language', 'en'); } #[Test] public function it_can_return_every_translation_on_request(): void { $user = $this->userWithRoles('editor'); Page::factory()->published()->create([ 'title' => ['es' => 'Hola', 'en' => 'Hello', 'ca' => 'Hola-ca'], ]); $this->getJson('/api/v1/pages?with=translations', $this->apiHeadersFor($user)) ->assertOk() ->assertJsonPath('data.0.translations.title.es', 'Hola') ->assertJsonPath('data.0.translations.title.en', 'Hello') ->assertJsonPath('data.0.translations.title.ca', 'Hola-ca'); } #[Test] public function it_falls_back_to_the_default_locale_for_a_missing_translation(): void { $user = $this->userWithRoles('editor'); Page::factory()->published()->create([ 'title' => ['es' => 'Solo español'], 'slug' => ['es' => 'solo-espanol'], ]); $this->getJson('/api/v1/pages?lang=ca', $this->apiHeadersFor($user)) ->assertOk() ->assertJsonPath('data.0.title', 'Solo español'); } #[Test] public function an_editor_can_create_a_page_with_several_translations(): void { $user = $this->userWithRoles('editor'); $category = Category::factory()->create(); $response = $this->postJson('/api/v1/pages', [ 'title' => ['es' => 'Nueva página', 'en' => 'New page'], 'slug' => ['es' => 'nueva-pagina', 'en' => 'new-page'], 'category_id' => $category->id, 'status' => 'draft', ], $this->apiHeadersFor($user)); $response->assertCreated(); $page = Page::firstWhere('id', $response->json('data.id')); $this->assertSame('Nueva página', $page->getTranslation('title', 'es')); $this->assertSame('New page', $page->getTranslation('title', 'en')); $this->assertSame($user->id, $page->author_id); } #[Test] public function a_partial_update_does_not_wipe_other_languages(): void { $user = $this->userWithRoles('editor'); $page = Page::factory()->draft()->create([ 'title' => ['es' => 'Original', 'en' => 'Original EN'], 'slug' => ['es' => 'original', 'en' => 'original-en'], 'author_id' => $user->id, ]); $this->patchJson("/api/v1/pages/{$page->id}", [ 'title' => ['ca' => 'Nou títol'], ], $this->apiHeadersFor($user))->assertOk(); $page->refresh(); $this->assertSame('Nou títol', $page->getTranslation('title', 'ca')); $this->assertSame('Original', $page->getTranslation('title', 'es')); $this->assertSame('Original EN', $page->getTranslation('title', 'en')); } #[Test] public function it_rejects_a_slug_already_used_in_the_same_locale(): void { $user = $this->userWithRoles('editor'); Page::factory()->create(['slug' => ['es' => 'repetido']]); $this->postJson('/api/v1/pages', [ 'title' => ['es' => 'Otra'], 'slug' => ['es' => 'repetido'], ], $this->apiHeadersFor($user)) ->assertUnprocessable() ->assertJsonValidationErrors('slug'); } #[Test] public function the_same_slug_is_allowed_in_a_different_locale(): void { $user = $this->userWithRoles('editor'); Page::factory()->create(['slug' => ['es' => 'compartido']]); $this->postJson('/api/v1/pages', [ 'title' => ['es' => 'Otra'], 'slug' => ['es' => 'distinto', 'en' => 'compartido'], ], $this->apiHeadersFor($user))->assertCreated(); } #[Test] public function it_rejects_an_unsupported_locale_key(): void { $user = $this->userWithRoles('editor'); $this->postJson('/api/v1/pages', [ 'title' => ['es' => 'Título', 'de' => 'Titel'], 'slug' => ['es' => 'titulo'], ], $this->apiHeadersFor($user)) ->assertUnprocessable() ->assertJsonValidationErrors('title'); } #[Test] public function a_viewer_cannot_create_a_page(): void { $user = $this->userWithRoles('viewer'); $this->postJson('/api/v1/pages', [ 'title' => ['es' => 'No permitido'], 'slug' => ['es' => 'no-permitido'], ], $this->apiHeadersFor($user))->assertForbidden(); } #[Test] public function a_viewer_only_sees_published_pages(): void { $user = $this->userWithRoles('viewer'); Page::factory()->published()->create(); Page::factory()->draft()->create(); $response = $this->getJson('/api/v1/pages', $this->apiHeadersFor($user))->assertOk(); $this->assertCount(1, $response->json('data')); $this->assertSame(PageStatus::Published->value, $response->json('data.0.status')); } #[Test] public function publishing_requires_its_own_permission(): void { // An editor who may write content but has had publish revoked. $user = $this->userWithRoles('editor'); $user->revokePermissionTo('pages.publish'); $user->roles()->first()->revokePermissionTo('pages.publish'); $this->postJson('/api/v1/pages', [ 'title' => ['es' => 'Intento'], 'slug' => ['es' => 'intento'], 'status' => 'published', ], $this->apiHeadersFor($user)) ->assertUnprocessable() ->assertJsonValidationErrors('status'); } #[Test] public function a_super_admin_bypasses_every_permission_check(): void { $user = $this->userWithRoles('super_admin'); $this->assertSame(0, $user->roles()->first()->permissions()->count()); $this->postJson('/api/v1/pages', [ 'title' => ['es' => 'Puedo todo'], 'slug' => ['es' => 'puedo-todo'], 'status' => 'published', ], $this->apiHeadersFor($user))->assertCreated(); } }