seed(RolePermissionSeeder::class); } protected function actingWithRole(string $role): User { $user = User::factory()->create(); $user->syncRoles([$role]); $this->withHeaders($this->apiHeadersFor($user)); return $user; } #[Test] public function a_project_arrives_with_its_team_stack_client_and_services(): void { $this->actingWithRole('editor'); [$ada, $linus] = Member::factory()->count(2)->create(); $technologies = Technology::factory()->count(2)->create(); $client = Client::factory()->create(); $service = Service::factory()->create(); $response = $this->postJson('/api/v1/projects?lang=es&with=translations', [ 'title' => ['es' => 'Catálogo abierto', 'en' => 'Open catalogue'], 'slug' => ['es' => 'catalogo', 'en' => 'catalogue'], 'tagline' => ['es' => 'Una herramienta pequeña.'], 'status' => 'active', 'client_id' => $client->id, 'lead_member_id' => $ada->id, 'technology_ids' => $technologies->pluck('id')->all(), 'service_ids' => [$service->id], 'members' => [['id' => $ada->id, 'role' => 'Responsable'], ['id' => $linus->id]], ])->assertCreated(); $response ->assertJsonPath('data.title', 'Catálogo abierto') ->assertJsonPath('data.translations.title.en', 'Open catalogue') ->assertJsonPath('data.status', 'active') ->assertJsonPath('data.client_id', $client->id) ->assertJsonCount(2, 'data.technology_ids') ->assertJsonPath('data.members.0.role', 'Responsable'); $project = Project::query()->sole(); $this->assertSame('Responsable', $project->members()->find($ada->id)->pivot->role); $this->assertNull($project->members()->find($linus->id)->pivot->role); } #[Test] public function a_partial_update_leaves_relations_alone_unless_they_are_sent(): void { $this->actingWithRole('editor'); $project = Project::factory()->create(); $technology = Technology::factory()->create(); $project->technologies()->attach($technology); $this->patchJson("/api/v1/projects/{$project->id}", ['status' => 'paused']) ->assertOk() ->assertJsonPath('data.technology_ids', [$technology->id]); $this->patchJson("/api/v1/projects/{$project->id}", ['technology_ids' => []]) ->assertOk() ->assertJsonPath('data.technology_ids', []); $this->assertSame(ProjectStatus::Paused, $project->fresh()->status); } #[Test] public function writing_a_project_is_not_publishing_it(): void { // A writer authors projects but may not put them on the site. $this->actingWithRole('writer'); $this->postJson('/api/v1/projects', [ 'title' => ['es' => 'Borrador'], 'slug' => ['es' => 'borrador'], 'published_at' => now()->toIso8601String(), ]) ->assertUnprocessable() ->assertJsonValidationErrors('published_at'); $this->postJson('/api/v1/projects', [ 'title' => ['es' => 'Borrador'], 'slug' => ['es' => 'borrador'], ])->assertCreated(); } #[Test] public function an_editor_may_publish_a_project(): void { $this->actingWithRole('editor'); $this->postJson('/api/v1/projects', [ 'title' => ['es' => 'Publicado'], 'slug' => ['es' => 'publicado'], 'published_at' => '2026-09-01T10:00:00Z', ]) ->assertCreated() ->assertJsonPath('data.published_at', '2026-09-01T10:00:00+00:00'); } #[Test] public function a_deleted_project_can_be_brought_back(): void { $this->actingWithRole('editor'); $project = Project::factory()->create(); $this->deleteJson("/api/v1/projects/{$project->id}")->assertNoContent(); $this->assertSoftDeleted($project); $this->getJson('/api/v1/projects?trashed=1')->assertJsonCount(1, 'data'); $this->postJson("/api/v1/projects/{$project->id}/restore")->assertOk(); $this->assertNotSoftDeleted($project); } #[Test] public function a_member_arrives_with_links_and_technologies(): void { $this->actingWithRole('editor'); $technology = Technology::factory()->create(); $this->postJson('/api/v1/members?lang=es', [ 'name' => 'Ada Lovelace', 'slug' => 'ada', 'headline' => ['es' => 'Backend', 'en' => 'Backend'], 'github_username' => 'ada', 'links' => ['web' => 'https://ada.example.org'], 'is_founder' => true, 'joined_at' => '2024-09-01', 'technology_ids' => [$technology->id], ]) ->assertCreated() ->assertJsonPath('data.headline', 'Backend') ->assertJsonPath('data.links.web', 'https://ada.example.org') ->assertJsonPath('data.joined_at', '2024-09-01') ->assertJsonPath('data.technology_ids', [$technology->id]); } #[Test] public function member_slugs_and_links_are_checked(): void { $this->actingWithRole('editor'); Member::factory()->create(['slug' => 'ada']); $this->postJson('/api/v1/members', ['name' => 'Ada', 'slug' => 'ada']) ->assertUnprocessable() ->assertJsonValidationErrors('slug'); $this->postJson('/api/v1/members', ['name' => 'Bob', 'slug' => 'bob', 'links' => ['web' => 'not a url']]) ->assertUnprocessable() ->assertJsonValidationErrors('links.web'); } #[Test] public function someone_may_edit_their_own_profile_without_members_update(): void { $viewer = $this->actingWithRole('viewer'); $own = Member::factory()->create(['user_id' => $viewer->id]); $someoneElse = Member::factory()->create(); $this->patchJson("/api/v1/members/{$own->id}", ['github_username' => 'me'])->assertOk(); $this->patchJson("/api/v1/members/{$someoneElse->id}", ['github_username' => 'them'])->assertForbidden(); } #[Test] public function a_service_links_to_the_work_behind_it(): void { $this->actingWithRole('editor'); $project = Project::factory()->create(); $technology = Technology::factory()->create(); $this->postJson('/api/v1/services?lang=es', [ 'title' => ['es' => 'Formación'], 'slug' => ['es' => 'formacion'], 'tagline' => ['es' => 'Talleres prácticos.'], 'icon' => 'support', 'is_published' => true, 'project_ids' => [$project->id], 'technology_ids' => [$technology->id], ]) ->assertCreated() ->assertJsonPath('data.title', 'Formación') ->assertJsonPath('data.is_published', true) ->assertJsonPath('data.project_ids', [$project->id]); } #[Test] public function writing_a_service_is_not_publishing_it(): void { $this->actingWithRole('writer'); $service = Service::factory()->create(['is_published' => false]); $this->patchJson("/api/v1/services/{$service->id}", ['is_published' => true]) ->assertUnprocessable() ->assertJsonValidationErrors('is_published'); $this->patchJson("/api/v1/services/{$service->id}", ['tagline' => ['es' => 'Nuevo texto']]) ->assertOk(); $this->assertFalse($service->fresh()->is_published); } #[Test] public function a_reader_can_list_but_not_write(): void { $this->actingWithRole('viewer'); Project::factory()->create(); $this->getJson('/api/v1/projects')->assertOk()->assertJsonCount(1, 'data'); $this->getJson('/api/v1/services')->assertOk(); $this->getJson('/api/v1/members')->assertOk(); $this->postJson('/api/v1/projects', ['title' => ['es' => 'X'], 'slug' => ['es' => 'x']])->assertForbidden(); $this->postJson('/api/v1/services', ['title' => ['es' => 'X'], 'slug' => ['es' => 'x']])->assertForbidden(); } }