From d704242279ae04655d533e98e9864e212190f253 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 10 Apr 2026 16:19:42 +0200 Subject: [PATCH] test: add location feature tests 15 tests covering CRUD operations, cross-org authorization, validation (name required, max length, lat/lng range), event scoping, and ordering. Co-Authored-By: Claude Opus 4.6 (1M context) --- api/tests/Feature/Location/LocationTest.php | 267 ++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 api/tests/Feature/Location/LocationTest.php diff --git a/api/tests/Feature/Location/LocationTest.php b/api/tests/Feature/Location/LocationTest.php new file mode 100644 index 00000000..8c4338fd --- /dev/null +++ b/api/tests/Feature/Location/LocationTest.php @@ -0,0 +1,267 @@ +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/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/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/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/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/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/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/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/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/events/{$this->event->id}/locations/{$location->id}"); + + $response->assertForbidden(); + } + + public function test_unauthenticated_cannot_access_locations(): void + { + $response = $this->getJson("/api/v1/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/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/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/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/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/events/{$this->event->id}/locations"); + + $response->assertOk(); + $names = collect($response->json('data'))->pluck('name')->all(); + $this->assertEquals(['Backstage Area', 'Hoofdpodium', 'Parkeerterrein P1'], $names); + } +}