seed(RoleSeeder::class); } private function authoriseSubscription(?User $user, FormSubmission $submission): bool { // Resolve the channel callback registered in routes/channels.php. // Broadcast::channel() returns the manager; Broadcast::driver() // exposes the channel callbacks. The cleanest contract test is // to call the callback directly via the registered closure // returned by getChannels(). $channels = Broadcast::getChannels(); $name = "submission.{$submission->id}"; foreach ($channels as $pattern => $callback) { // Pattern is 'submission.{submissionId}'; convert to a regex // so we can match the concrete channel name. $regex = '/^'.str_replace(['{submissionId}', '.'], ['([^.]+)', '\\.'], $pattern).'$/'; if (! preg_match($regex, $name, $matches)) { continue; } $result = $callback($user, $matches[1]); return (bool) $result; } $this->fail("No channel callback registered for {$name}"); } /** * Build a submission tied to the given organisation (or a fresh one * if not specified). The submitter is a fresh User unrelated to any * test fixture so the submitted_by_user_id short-circuit doesn't * accidentally authorise org-admin / super-admin / member tests. */ private function makeSubmission(?Organisation $organisation = null, ?User $submitter = null): FormSubmission { $organisation ??= Organisation::factory()->create(); $schema = FormSchema::factory()->create(['organisation_id' => $organisation->id]); return FormSubmission::factory()->create([ 'form_schema_id' => $schema->id, 'organisation_id' => $organisation->id, 'submitted_by_user_id' => $submitter?->id, ]); } public function test_submitter_is_authorised(): void { $submitter = User::factory()->create(); $submission = $this->makeSubmission(submitter: $submitter); $this->assertTrue($this->authoriseSubscription($submitter, $submission)); } public function test_super_admin_can_subscribe(): void { // Spatie HasRoles app-wide bypass — super_admin can subscribe to // any submission's channel regardless of organisation membership // or submitter relationship. Matches the codebase convention // used in every analogous policy. $superAdmin = User::factory()->create(); $superAdmin->assignRole('super_admin'); $submitter = User::factory()->create(); $submission = $this->makeSubmission(submitter: $submitter); $this->assertTrue($this->authoriseSubscription($superAdmin, $submission)); } public function test_organisation_admin_of_submission_org_can_subscribe(): void { // Pivot-table check: user attached to the submission's // organisation with role='org_admin'. Mirrors the canonical // pattern from FormSubmissionActionFailurePolicy::canAccess. $organisation = Organisation::factory()->create(); $orgAdmin = User::factory()->create(); $organisation->users()->attach($orgAdmin->id, ['role' => 'org_admin']); $submitter = User::factory()->create(); $submission = $this->makeSubmission(organisation: $organisation, submitter: $submitter); $this->assertTrue($this->authoriseSubscription($orgAdmin, $submission)); } public function test_organisation_admin_of_different_org_cannot_subscribe(): void { // Critical multi-tenancy guard: an org_admin of organisation A // must NOT be able to subscribe to a submission's channel when // that submission belongs to organisation B. If this fails, the // pivot-table check is wrong and tenant isolation is broken. $submissionOrg = Organisation::factory()->create(); $otherOrg = Organisation::factory()->create(); $crossTenantAdmin = User::factory()->create(); $otherOrg->users()->attach($crossTenantAdmin->id, ['role' => 'org_admin']); $submitter = User::factory()->create(); $submission = $this->makeSubmission(organisation: $submissionOrg, submitter: $submitter); $this->assertFalse($this->authoriseSubscription($crossTenantAdmin, $submission)); } public function test_regular_organisation_member_cannot_subscribe(): void { // Member of the submission's organisation but with role='org_member', // not 'org_admin'. The pivot-table wherePivot('role', 'org_admin') // filters them out. $organisation = Organisation::factory()->create(); $member = User::factory()->create(); $organisation->users()->attach($member->id, ['role' => 'org_member']); $submitter = User::factory()->create(); $submission = $this->makeSubmission(organisation: $organisation, submitter: $submitter); $this->assertFalse($this->authoriseSubscription($member, $submission)); } public function test_other_authenticated_user_is_denied(): void { // User with NO organisation membership at all → falls through // every auth branch (not submitter, not super_admin, no pivot row). $submitter = User::factory()->create(); $other = User::factory()->create(); $submission = $this->makeSubmission(submitter: $submitter); $this->assertFalse($this->authoriseSubscription($other, $submission)); } public function test_subscription_is_denied_when_submission_does_not_exist(): void { $user = User::factory()->create(); // Build a submission, then delete it so the FK lookup returns null. $submission = $this->makeSubmission(submitter: $user); $submissionId = (string) $submission->id; $submission->forceDelete(); // Reuse the submitter's User and a fake submission shell. $shell = new FormSubmission; $shell->id = $submissionId; $this->assertFalse($this->authoriseSubscription($user, $shell)); } }