create(['name' => 'Assigned Operator']); createUserWithTenant(tenant: $tenant, user: $assignee, role: 'operator'); $finding = Finding::factory()->for($tenant)->create([ 'workspace_id' => (int) $tenant->workspace_id, 'status' => Finding::STATUS_TRIAGED, 'owner_user_id' => (int) $owner->getKey(), 'assignee_user_id' => (int) $assignee->getKey(), ]); app(FindingNotificationService::class)->dispatch($finding, AlertRule::EVENT_FINDINGS_ASSIGNED); $notification = $assignee->notifications() ->where('type', FindingEventNotification::class) ->latest('id') ->first(); expect($notification)->not->toBeNull() ->and(data_get($notification?->data, 'format'))->toBe('filament') ->and(data_get($notification?->data, 'title'))->toBe('Finding assigned') ->and(data_get($notification?->data, 'actions.0.label'))->toBe('Open finding') ->and(data_get($notification?->data, 'actions.0.url')) ->toBe(FindingResource::getUrl('view', ['record' => $finding], panel: 'tenant', tenant: $tenant)) ->and(data_get($notification?->data, 'finding_event.event_type'))->toBe(AlertRule::EVENT_FINDINGS_ASSIGNED) ->and(data_get($notification?->data, 'finding_event.recipient_reason'))->toBe('new_assignee'); }); it('returns 404 when a finding notification link is opened after tenant access is removed', function (): void { [$owner, $tenant] = createUserWithTenant(role: 'owner'); $assignee = User::factory()->create(['name' => 'Removed Operator']); createUserWithTenant(tenant: $tenant, user: $assignee, role: 'operator'); $finding = Finding::factory()->for($tenant)->create([ 'workspace_id' => (int) $tenant->workspace_id, 'status' => Finding::STATUS_TRIAGED, 'owner_user_id' => (int) $owner->getKey(), 'assignee_user_id' => (int) $assignee->getKey(), ]); app(FindingNotificationService::class)->dispatch($finding, AlertRule::EVENT_FINDINGS_ASSIGNED); $url = data_get($assignee->notifications()->latest('id')->first(), 'data.actions.0.url'); TenantMembership::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('user_id', (int) $assignee->getKey()) ->delete(); app(CapabilityResolver::class)->clearCache(); $this->actingAs($assignee) ->get($url) ->assertNotFound(); }); it('returns 403 when a finding notification link is opened by an in-scope member without findings view capability', function (): void { [$owner, $tenant] = createUserWithTenant(role: 'owner'); $assignee = User::factory()->create(['name' => 'Scoped Operator']); createUserWithTenant(tenant: $tenant, user: $assignee, role: 'operator'); $finding = Finding::factory()->for($tenant)->create([ 'workspace_id' => (int) $tenant->workspace_id, 'status' => Finding::STATUS_TRIAGED, 'owner_user_id' => (int) $owner->getKey(), 'assignee_user_id' => (int) $assignee->getKey(), ]); app(FindingNotificationService::class)->dispatch($finding, AlertRule::EVENT_FINDINGS_ASSIGNED); $url = data_get($assignee->notifications()->latest('id')->first(), 'data.actions.0.url'); Gate::define(Capabilities::TENANT_FINDINGS_VIEW, fn (): bool => false); $this->actingAs($assignee); Filament::setTenant($tenant, true); $this->get($url)->assertForbidden(); });