seed(RoleSeeder::class); $this->organisation = Organisation::factory()->create(); $this->orgAdmin = User::factory()->create(); $this->organisation->users()->attach($this->orgAdmin, ['role' => 'org_admin']); } private function transitionUrl(Event $event): string { return "/api/v1/organisations/{$this->organisation->id}/events/{$event->id}/transition"; } // --- Valid transitions --- public function test_transition_draft_to_published(): void { $event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'draft', ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson($this->transitionUrl($event), ['status' => 'published']); $response->assertOk(); $this->assertDatabaseHas('events', ['id' => $event->id, 'status' => 'published']); } public function test_transition_published_to_registration_open(): void { $event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'published', ]); // Create prerequisites: time slot + section TimeSlot::factory()->create(['event_id' => $event->id]); FestivalSection::factory()->create(['event_id' => $event->id]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson($this->transitionUrl($event), ['status' => 'registration_open']); $response->assertOk(); $this->assertDatabaseHas('events', ['id' => $event->id, 'status' => 'registration_open']); } public function test_transition_published_back_to_draft(): void { $event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'published', ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson($this->transitionUrl($event), ['status' => 'draft']); $response->assertOk(); $this->assertDatabaseHas('events', ['id' => $event->id, 'status' => 'draft']); } // --- Invalid transitions --- public function test_transition_draft_to_showday_fails(): void { $event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'draft', ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson($this->transitionUrl($event), ['status' => 'showday']); $response->assertUnprocessable() ->assertJsonPath('current_status', 'draft') ->assertJsonPath('requested_status', 'showday'); } public function test_transition_closed_to_anything_fails(): void { $event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'closed', ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson($this->transitionUrl($event), ['status' => 'draft']); $response->assertUnprocessable() ->assertJsonPath('allowed_transitions', []); } // --- Prerequisite checks --- public function test_transition_registration_open_without_timeslots_fails(): void { $event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'published', ]); // Create a section but no time slot FestivalSection::factory()->create(['event_id' => $event->id]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson($this->transitionUrl($event), ['status' => 'registration_open']); $response->assertUnprocessable(); $this->assertContains( 'At least one time slot must exist before opening registration.', $response->json('errors') ); } // --- Regular update cannot change status --- public function test_regular_update_cannot_change_status(): void { $event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'draft', ]); Sanctum::actingAs($this->orgAdmin); $this->putJson("/api/v1/organisations/{$this->organisation->id}/events/{$event->id}", [ 'status' => 'published', ]); // Status should remain draft — the field is ignored by UpdateEventRequest $this->assertDatabaseHas('events', ['id' => $event->id, 'status' => 'draft']); } // --- Festival cascade --- public function test_festival_showday_cascades_to_children(): void { $festival = Event::factory()->festival()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'buildup', ]); $childDraft = Event::factory()->subEvent($festival)->create(['status' => 'draft']); $childPublished = Event::factory()->subEvent($festival)->create(['status' => 'published']); $childBuildup = Event::factory()->subEvent($festival)->create(['status' => 'buildup']); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson($this->transitionUrl($festival), ['status' => 'showday']); $response->assertOk(); // All children in earlier statuses should be cascaded to showday $this->assertDatabaseHas('events', ['id' => $childDraft->id, 'status' => 'showday']); $this->assertDatabaseHas('events', ['id' => $childPublished->id, 'status' => 'showday']); $this->assertDatabaseHas('events', ['id' => $childBuildup->id, 'status' => 'showday']); } // --- EventResource includes allowed_transitions --- public function test_allowed_transitions_in_resource(): void { $event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'draft', ]); Sanctum::actingAs($this->orgAdmin); $response = $this->getJson("/api/v1/organisations/{$this->organisation->id}/events/{$event->id}"); $response->assertOk() ->assertJsonPath('data.allowed_transitions', ['published']); } // --- Auth --- public function test_unauthenticated_user_cannot_transition(): void { $event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'draft', ]); $response = $this->postJson($this->transitionUrl($event), ['status' => 'published']); $response->assertUnauthorized(); } public function test_outsider_cannot_transition(): void { $event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'draft', ]); $outsider = User::factory()->create(); Sanctum::actingAs($outsider); $response = $this->postJson($this->transitionUrl($event), ['status' => 'published']); $response->assertForbidden(); } }