seed(RoleSeeder::class); $this->organisation = Organisation::factory()->create(); $this->otherOrganisation = Organisation::factory()->create(); $this->orgAdmin = User::factory()->create(); $this->organisation->users()->attach($this->orgAdmin, ['role' => 'org_admin']); $this->outsider = User::factory()->create(); $this->otherOrganisation->users()->attach($this->outsider, ['role' => 'org_admin']); $this->event = Event::factory()->create(['organisation_id' => $this->organisation->id]); $this->otherEvent = Event::factory()->create(['organisation_id' => $this->otherOrganisation->id]); } // ---- CRUD Tests ---- public function test_can_list_locations_for_event(): void { Location::factory()->count(3)->create([ 'event_id' => $this->event->id, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->getJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations"); $response->assertOk(); $this->assertCount(3, $response->json('data')); } public function test_can_create_location(): void { Sanctum::actingAs($this->orgAdmin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations", [ 'name' => 'Hoofdpodium', 'address' => 'Museumplein 1, 1071 DJ Amsterdam', 'lat' => 52.3580, 'lng' => 4.8828, 'description' => 'Het grote podium op het Museumplein', 'access_instructions' => 'Ingang via de noordzijde', ]); $response->assertCreated(); $this->assertEquals('Hoofdpodium', $response->json('data.name')); $this->assertEquals('Museumplein 1, 1071 DJ Amsterdam', $response->json('data.address')); $this->assertDatabaseHas('locations', [ 'event_id' => $this->event->id, 'name' => 'Hoofdpodium', ]); } public function test_can_create_location_with_minimal_data(): void { Sanctum::actingAs($this->orgAdmin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations", [ 'name' => 'Backstage Area', ]); $response->assertCreated(); $this->assertEquals('Backstage Area', $response->json('data.name')); $this->assertNull($response->json('data.address')); $this->assertNull($response->json('data.lat')); $this->assertNull($response->json('data.lng')); } public function test_can_update_location(): void { $location = Location::factory()->create([ 'event_id' => $this->event->id, 'name' => 'Bar Noord', ]); Sanctum::actingAs($this->orgAdmin); $response = $this->putJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations/{$location->id}", [ 'name' => 'Bar Zuid', 'address' => 'Coolsingel 42, 3011 AD Rotterdam', 'lat' => 51.9225, 'lng' => 4.4792, ]); $response->assertOk(); $this->assertEquals('Bar Zuid', $response->json('data.name')); $this->assertEquals('Coolsingel 42, 3011 AD Rotterdam', $response->json('data.address')); $this->assertDatabaseHas('locations', [ 'id' => $location->id, 'name' => 'Bar Zuid', ]); } public function test_can_delete_location(): void { $location = Location::factory()->create([ 'event_id' => $this->event->id, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->deleteJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations/{$location->id}"); $response->assertNoContent(); $this->assertDatabaseMissing('locations', ['id' => $location->id]); } // ---- Authorization Tests ---- public function test_cross_org_cannot_access_locations(): void { Sanctum::actingAs($this->outsider); $response = $this->getJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations"); $response->assertForbidden(); } public function test_cross_org_cannot_create_location(): void { Sanctum::actingAs($this->outsider); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations", [ 'name' => 'Hack Attempt', ]); $response->assertForbidden(); } public function test_cross_org_cannot_update_location(): void { $location = Location::factory()->create([ 'event_id' => $this->event->id, ]); Sanctum::actingAs($this->outsider); $response = $this->putJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations/{$location->id}", [ 'name' => 'Hack Attempt', ]); $response->assertForbidden(); } public function test_cross_org_cannot_delete_location(): void { $location = Location::factory()->create([ 'event_id' => $this->event->id, ]); Sanctum::actingAs($this->outsider); $response = $this->deleteJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations/{$location->id}"); $response->assertForbidden(); } public function test_unauthenticated_cannot_access_locations(): void { $response = $this->getJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations"); $response->assertUnauthorized(); } // ---- Validation Tests ---- public function test_location_requires_name(): void { Sanctum::actingAs($this->orgAdmin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations", [ 'address' => 'Museumplein 1, Amsterdam', ]); $response->assertUnprocessable() ->assertJsonValidationErrors(['name']); } public function test_location_name_max_length(): void { Sanctum::actingAs($this->orgAdmin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations", [ 'name' => str_repeat('a', 256), ]); $response->assertUnprocessable() ->assertJsonValidationErrors(['name']); } public function test_location_validates_lat_lng_range(): void { Sanctum::actingAs($this->orgAdmin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations", [ 'name' => 'Ongeldige Locatie', 'lat' => 91.0, 'lng' => 181.0, ]); $response->assertUnprocessable() ->assertJsonValidationErrors(['lat', 'lng']); } // ---- Scoping Tests ---- public function test_locations_are_scoped_to_event(): void { $eventB = Event::factory()->create(['organisation_id' => $this->organisation->id]); Location::factory()->count(2)->create(['event_id' => $this->event->id]); Location::factory()->count(3)->create(['event_id' => $eventB->id]); Sanctum::actingAs($this->orgAdmin); $response = $this->getJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations"); $response->assertOk(); $this->assertCount(2, $response->json('data')); } public function test_locations_are_ordered_by_name(): void { Location::factory()->create(['event_id' => $this->event->id, 'name' => 'Parkeerterrein P1']); Location::factory()->create(['event_id' => $this->event->id, 'name' => 'Backstage Area']); Location::factory()->create(['event_id' => $this->event->id, 'name' => 'Hoofdpodium']); Sanctum::actingAs($this->orgAdmin); $response = $this->getJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/locations"); $response->assertOk(); $names = collect($response->json('data'))->pluck('name')->all(); $this->assertEquals(['Backstage Area', 'Hoofdpodium', 'Parkeerterrein P1'], $names); } }