seed(RoleSeeder::class); $this->orgA = Organisation::factory()->create(); $this->orgB = Organisation::factory()->create(); $this->superAdmin = User::factory()->create(); $this->superAdmin->assignRole('super_admin'); $this->orgAdminA = User::factory()->create(); $this->orgA->users()->attach($this->orgAdminA, ['role' => 'org_admin']); $this->orgAdminB = User::factory()->create(); $this->orgB->users()->attach($this->orgAdminB, ['role' => 'org_admin']); $this->regular = User::factory()->create(); } public function test_super_admin_can_perform_all_abilities(): void { $failure = $this->failureFor($this->orgA); $this->assertTrue($this->policy()->view($this->superAdmin, $failure)); $this->assertTrue($this->policy()->retry($this->superAdmin, $failure)); $this->assertTrue($this->policy()->resolve($this->superAdmin, $failure)); $this->assertTrue($this->policy()->dismiss($this->superAdmin, $failure)); } public function test_org_admin_in_correct_org_can_perform_all_abilities(): void { $failure = $this->failureFor($this->orgA); $this->assertTrue($this->policy()->view($this->orgAdminA, $failure)); $this->assertTrue($this->policy()->retry($this->orgAdminA, $failure)); $this->assertTrue($this->policy()->resolve($this->orgAdminA, $failure)); $this->assertTrue($this->policy()->dismiss($this->orgAdminA, $failure)); } /** * IDOR — RFC V3. An org admin in B must NOT see a failure in A. */ public function test_org_admin_of_other_org_cannot_access_cross_tenant(): void { $failure = $this->failureFor($this->orgA); $this->assertFalse($this->policy()->view($this->orgAdminB, $failure)); $this->assertFalse($this->policy()->retry($this->orgAdminB, $failure)); $this->assertFalse($this->policy()->resolve($this->orgAdminB, $failure)); $this->assertFalse($this->policy()->dismiss($this->orgAdminB, $failure)); } public function test_regular_user_without_admin_role_denied_all(): void { $failure = $this->failureFor($this->orgA); $this->assertFalse($this->policy()->view($this->regular, $failure)); $this->assertFalse($this->policy()->retry($this->regular, $failure)); $this->assertFalse($this->policy()->resolve($this->regular, $failure)); $this->assertFalse($this->policy()->dismiss($this->regular, $failure)); } public function test_view_any_super_admin_yes_org_admin_yes_regular_no(): void { $this->assertTrue($this->policy()->viewAny($this->superAdmin)); $this->assertTrue($this->policy()->viewAny($this->orgAdminA)); $this->assertFalse($this->policy()->viewAny($this->regular)); } public function test_view_returns_false_when_parent_submission_soft_deleted(): void { $failure = $this->failureFor($this->orgA); // Soft-delete the submission. The failure row is preserved (no // cascade on soft-delete); the policy's submission relation // honours the SoftDeletes scope and returns null. FormSubmission::query()->withoutGlobalScopes()->where('id', $failure->form_submission_id)->delete(); $reloaded = FormSubmissionActionFailure::query()->find($failure->id); $this->assertNotNull($reloaded); $this->assertFalse($this->policy()->view($this->superAdmin, $reloaded)); } private function policy(): FormSubmissionActionFailurePolicy { return new FormSubmissionActionFailurePolicy(); } private function failureFor(Organisation $organisation): FormSubmissionActionFailure { $schema = FormSchema::factory()->create(['organisation_id' => $organisation->id]); $submission = FormSubmission::factory()->create([ 'form_schema_id' => $schema->id, 'organisation_id' => $organisation->id, ]); return FormSubmissionActionFailure::factory() ->for($submission, 'submission') ->create(); } }