seed(RoleSeeder::class); $this->organisation = Organisation::factory()->create(); $this->admin = User::factory()->create(); $this->organisation->users()->attach($this->admin, ['role' => 'org_admin']); $this->crowdType = CrowdType::factory()->systemType('VOLUNTEER')->create([ 'organisation_id' => $this->organisation->id, ]); $this->event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'registration_open', ]); } // --- XSS Payload Storage --- public function test_xss_in_person_name_is_stored_safely(): void { Sanctum::actingAs($this->admin); $xssPayload = ''; $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => $this->crowdType->id, 'first_name' => $xssPayload, 'last_name' => 'Normal', 'email' => 'xss-test@example.com', ]); $response->assertCreated(); // Value is stored as-is (Laravel escapes on output via {{ }}) $this->assertDatabaseHas('persons', [ 'first_name' => $xssPayload, 'email' => 'xss-test@example.com', ]); // API resource should return the raw string (Vue's {{ }} escapes it) $response->assertJsonPath('data.first_name', $xssPayload); } public function test_xss_in_event_name_is_stored_safely(): void { Sanctum::actingAs($this->admin); $xssPayload = '">'; $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events", [ 'name' => $xssPayload, 'slug' => 'xss-test-event', 'start_date' => '2026-07-01', 'end_date' => '2026-07-03', ]); $response->assertCreated(); $response->assertJsonPath('data.name', $xssPayload); } public function test_xss_in_section_name_is_stored_safely(): void { Sanctum::actingAs($this->admin); $xssPayload = ''; $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/sections", [ 'name' => $xssPayload, ]); $response->assertCreated(); $response->assertJsonPath('data.name', $xssPayload); } // --- Oversized Input Rejection --- public function test_oversized_person_name_rejected(): void { Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => $this->crowdType->id, 'first_name' => str_repeat('A', 256), 'last_name' => 'Normal', 'email' => 'oversize@example.com', ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('first_name'); } public function test_oversized_event_name_rejected(): void { Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events", [ 'name' => str_repeat('A', 256), 'slug' => 'oversize-test', 'start_date' => '2026-07-01', 'end_date' => '2026-07-03', ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('name'); } public function test_oversized_remarks_rejected(): void { Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => $this->crowdType->id, 'first_name' => 'Test', 'last_name' => 'User', 'email' => 'remarks@example.com', 'remarks' => str_repeat('A', 5001), ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('remarks'); } // --- Invalid Enum Values --- public function test_invalid_event_status_rejected(): void { Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events", [ 'name' => 'Enum Test', 'slug' => 'enum-test', 'start_date' => '2026-07-01', 'end_date' => '2026-07-03', 'status' => 'hacked_status', ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('status'); } public function test_invalid_person_status_rejected(): void { Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => $this->crowdType->id, 'first_name' => 'Test', 'last_name' => 'User', 'email' => 'enum@example.com', 'status' => 'superadmin', ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('status'); } public function test_invalid_event_type_rejected(): void { Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events", [ 'name' => 'Type Test', 'slug' => 'type-test', 'start_date' => '2026-07-01', 'end_date' => '2026-07-03', 'event_type' => 'malicious', ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('event_type'); } // --- Cross-Org Foreign Key Validation --- public function test_person_with_cross_org_crowd_type_rejected(): void { $otherOrg = Organisation::factory()->create(); $otherCrowdType = CrowdType::factory()->create(['organisation_id' => $otherOrg->id]); Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => $otherCrowdType->id, 'first_name' => 'FK', 'last_name' => 'Test', 'email' => 'fk@example.com', ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('crowd_type_id'); } public function test_person_with_cross_org_company_rejected(): void { $otherOrg = Organisation::factory()->create(); $otherCompany = Company::factory()->create(['organisation_id' => $otherOrg->id]); Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => $this->crowdType->id, 'first_name' => 'Company', 'last_name' => 'Test', 'email' => 'company-fk@example.com', 'company_id' => $otherCompany->id, ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('company_id'); } public function test_shift_with_cross_event_location_rejected(): void { $otherEvent = Event::factory()->create(['organisation_id' => $this->organisation->id]); $otherLocation = Location::factory()->create(['event_id' => $otherEvent->id]); $section = FestivalSection::factory()->create(['event_id' => $this->event->id]); $timeSlot = TimeSlot::factory()->create(['event_id' => $this->event->id]); Sanctum::actingAs($this->admin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/sections/{$section->id}/shifts", [ 'time_slot_id' => $timeSlot->id, 'location_id' => $otherLocation->id, 'slots_total' => 5, 'slots_open_for_claiming' => 3, ] ); $response->assertUnprocessable(); $response->assertJsonValidationErrors('location_id'); } public function test_event_with_cross_org_parent_rejected(): void { $otherOrg = Organisation::factory()->create(); $otherEvent = Event::factory()->create(['organisation_id' => $otherOrg->id]); Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events", [ 'name' => 'Sub Event', 'slug' => 'sub-event-test', 'start_date' => '2026-07-01', 'end_date' => '2026-07-03', 'parent_event_id' => $otherEvent->id, ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('parent_event_id'); } public function test_assign_shift_with_cross_event_person_rejected(): void { $otherEvent = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'registration_open', ]); $otherPerson = Person::factory()->approved()->create([ 'event_id' => $otherEvent->id, 'crowd_type_id' => $this->crowdType->id, ]); $section = FestivalSection::factory()->create(['event_id' => $this->event->id]); $timeSlot = TimeSlot::factory()->create(['event_id' => $this->event->id]); $shift = Shift::factory()->create([ 'festival_section_id' => $section->id, 'time_slot_id' => $timeSlot->id, ]); Sanctum::actingAs($this->admin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/sections/{$section->id}/shifts/{$shift->id}/assign", ['person_id' => $otherPerson->id] ); $response->assertUnprocessable(); $response->assertJsonValidationErrors('person_id'); } // --- ULID Format Validation --- public function test_invalid_ulid_format_rejected(): void { Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => 'not-a-valid-ulid', 'first_name' => 'Test', 'last_name' => 'User', 'email' => 'ulid@example.com', ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('crowd_type_id'); } public function test_nonexistent_ulid_rejected(): void { Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => '01JZZZZZZZZZZZZZZZZZZZZZZ', 'first_name' => 'Test', 'last_name' => 'User', 'email' => 'nonexistent@example.com', ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors('crowd_type_id'); } // --- SQL Injection Resistance --- public function test_sql_injection_in_string_fields_harmless(): void { Sanctum::actingAs($this->admin); $sqlPayload = "'; DROP TABLE users; --"; $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => $this->crowdType->id, 'first_name' => $sqlPayload, 'last_name' => 'Normal', 'email' => 'sql@example.com', ]); $response->assertCreated(); // Table still exists $this->assertDatabaseCount('users', 1); // the admin user $this->assertDatabaseHas('persons', ['first_name' => $sqlPayload]); } }