seed(RoleSeeder::class); // Organisation A $this->orgA = Organisation::factory()->create(); $this->adminA = User::factory()->create(); $this->orgA->users()->attach($this->adminA, ['role' => 'org_admin']); $this->crowdTypeA = CrowdType::factory()->systemType('VOLUNTEER')->create(['organisation_id' => $this->orgA->id]); $this->eventA = Event::factory()->create([ 'organisation_id' => $this->orgA->id, 'status' => 'registration_open', ]); // Organisation B $this->orgB = Organisation::factory()->create(); $this->adminB = User::factory()->create(); $this->orgB->users()->attach($this->adminB, ['role' => 'org_admin']); $this->crowdTypeB = CrowdType::factory()->systemType('VOLUNTEER')->create(['organisation_id' => $this->orgB->id]); $this->eventB = Event::factory()->create([ 'organisation_id' => $this->orgB->id, 'status' => 'registration_open', ]); } // --- Cross-tenant event access --- public function test_cannot_view_other_org_event(): void { Sanctum::actingAs($this->adminA); $response = $this->getJson("/api/v1/organisations/{$this->orgA->id}/events/{$this->eventB->id}"); $response->assertNotFound(); } public function test_cannot_list_other_org_events(): void { Sanctum::actingAs($this->adminA); $response = $this->getJson("/api/v1/organisations/{$this->orgA->id}/events"); $response->assertOk(); $eventIds = collect($response->json('data'))->pluck('id'); $this->assertContains($this->eventA->id, $eventIds->all()); $this->assertNotContains($this->eventB->id, $eventIds->all()); } // --- Cross-tenant person assignment --- public function test_cannot_create_person_with_other_org_crowd_type(): void { Sanctum::actingAs($this->adminA); $response = $this->postJson("/api/v1/events/{$this->eventA->id}/persons", [ 'crowd_type_id' => $this->crowdTypeB->id, 'first_name' => 'Test', 'last_name' => 'User', 'email' => 'test@example.com', ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('crowd_type_id'); } public function test_cannot_assign_person_from_other_org_to_shift(): void { Sanctum::actingAs($this->adminA); $personB = Person::factory()->approved()->create([ 'event_id' => $this->eventB->id, 'crowd_type_id' => $this->crowdTypeB->id, ]); $sectionA = FestivalSection::factory()->create(['event_id' => $this->eventA->id]); $timeSlotA = TimeSlot::factory()->create(['event_id' => $this->eventA->id]); $shiftA = Shift::factory()->create([ 'festival_section_id' => $sectionA->id, 'time_slot_id' => $timeSlotA->id, ]); $response = $this->postJson( "/api/v1/events/{$this->eventA->id}/sections/{$sectionA->id}/shifts/{$shiftA->id}/assign", ['person_id' => $personB->id] ); $response->assertUnprocessable(); $response->assertJsonValidationErrors('person_id'); } // --- Cross-tenant crowd list operations --- public function test_cannot_add_person_from_other_org_to_crowd_list(): void { Sanctum::actingAs($this->adminA); $personB = Person::factory()->approved()->create([ 'event_id' => $this->eventB->id, 'crowd_type_id' => $this->crowdTypeB->id, ]); $crowdListA = CrowdList::factory()->create([ 'event_id' => $this->eventA->id, 'crowd_type_id' => $this->crowdTypeA->id, ]); $response = $this->postJson( "/api/v1/events/{$this->eventA->id}/crowd-lists/{$crowdListA->id}/persons", ['person_id' => $personB->id] ); $response->assertUnprocessable(); $response->assertJsonValidationErrors('person_id'); } // --- Cross-tenant bulk operations --- public function test_cannot_bulk_approve_other_org_assignments(): void { Sanctum::actingAs($this->adminA); $sectionB = FestivalSection::factory()->create(['event_id' => $this->eventB->id]); $timeSlotB = TimeSlot::factory()->create(['event_id' => $this->eventB->id]); $shiftB = Shift::factory()->create([ 'festival_section_id' => $sectionB->id, 'time_slot_id' => $timeSlotB->id, ]); $personB = Person::factory()->approved()->create([ 'event_id' => $this->eventB->id, 'crowd_type_id' => $this->crowdTypeB->id, ]); $assignmentB = ShiftAssignment::factory()->create([ 'shift_id' => $shiftB->id, 'person_id' => $personB->id, 'time_slot_id' => $timeSlotB->id, ]); $response = $this->postJson( "/api/v1/events/{$this->eventA->id}/shift-assignments/bulk-approve", ['assignment_ids' => [$assignmentB->id]] ); $response->assertUnprocessable(); $response->assertJsonValidationErrors('assignment_ids.0'); } // --- Cross-tenant invitation revocation --- public function test_cannot_revoke_other_org_invitation(): void { Sanctum::actingAs($this->adminA); $invitationB = UserInvitation::factory()->create([ 'organisation_id' => $this->orgB->id, 'invited_by_user_id' => $this->adminB->id, ]); $response = $this->deleteJson( "/api/v1/organisations/{$this->orgA->id}/invitations/{$invitationB->id}" ); $response->assertNotFound(); } // --- Cross-tenant company reference --- public function test_cannot_create_person_with_other_org_company(): void { Sanctum::actingAs($this->adminA); $companyB = Company::factory()->create(['organisation_id' => $this->orgB->id]); $response = $this->postJson("/api/v1/events/{$this->eventA->id}/persons", [ 'crowd_type_id' => $this->crowdTypeA->id, 'first_name' => 'Test', 'last_name' => 'User', 'email' => 'test@example.com', 'company_id' => $companyB->id, ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('company_id'); } // --- Cross-tenant crowd list creation --- public function test_cannot_create_crowd_list_with_other_org_crowd_type(): void { Sanctum::actingAs($this->adminA); $response = $this->postJson("/api/v1/events/{$this->eventA->id}/crowd-lists", [ 'crowd_type_id' => $this->crowdTypeB->id, 'name' => 'Test List', 'type' => 'accreditation', ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('crowd_type_id'); } // --- Cross-tenant event parent reference --- public function test_cannot_set_parent_event_from_other_org(): void { Sanctum::actingAs($this->adminA); $response = $this->putJson( "/api/v1/organisations/{$this->orgA->id}/events/{$this->eventA->id}", ['parent_event_id' => $this->eventB->id] ); $response->assertUnprocessable(); $response->assertJsonValidationErrors('parent_event_id'); } // --- OrganisationScope filters correctly --- public function test_organisation_scope_filters_persons(): void { $personA = Person::factory()->approved()->create([ 'event_id' => $this->eventA->id, 'crowd_type_id' => $this->crowdTypeA->id, ]); Person::factory()->approved()->create([ 'event_id' => $this->eventB->id, 'crowd_type_id' => $this->crowdTypeB->id, ]); Sanctum::actingAs($this->adminA); $response = $this->getJson("/api/v1/events/{$this->eventA->id}/persons"); $response->assertOk(); $personIds = collect($response->json('data'))->pluck('id'); $this->assertContains($personA->id, $personIds->all()); $this->assertCount(1, $personIds); } // --- Portal cross-event access --- public function test_portal_me_cannot_access_other_org_event(): void { $volunteer = User::factory()->create(); Person::factory()->approved()->create([ 'event_id' => $this->eventA->id, 'crowd_type_id' => $this->crowdTypeA->id, 'user_id' => $volunteer->id, 'email' => $volunteer->email, ]); Sanctum::actingAs($volunteer); // Volunteer tries to access event from org B (where they have no person record) $response = $this->getJson("/api/v1/portal/me?event_id={$this->eventB->id}"); $response->assertNotFound(); } }