create(); createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner'); $createException = function (array $attributes) use ($tenant, $requester, $approver): FindingException { $finding = Finding::factory()->for($tenant)->create(); return FindingException::query()->create(array_merge([ 'workspace_id' => (int) $tenant->workspace_id, 'tenant_id' => (int) $tenant->getKey(), 'finding_id' => (int) $finding->getKey(), 'requested_by_user_id' => (int) $requester->getKey(), 'owner_user_id' => (int) $requester->getKey(), 'approved_by_user_id' => (int) $approver->getKey(), 'status' => FindingException::STATUS_ACTIVE, 'current_validity_state' => FindingException::VALIDITY_VALID, 'request_reason' => 'Temporary governance coverage', 'approval_reason' => 'Compensating controls accepted', 'requested_at' => now()->subDays(10), 'approved_at' => now()->subDays(9), 'effective_from' => now()->subDays(9), 'expires_at' => now()->addDays(21), 'review_due_at' => now()->addDays(14), 'evidence_summary' => ['reference_count' => 0], ], $attributes)); }; $pending = $createException([ 'approved_by_user_id' => null, 'status' => FindingException::STATUS_PENDING, 'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT, 'approval_reason' => null, 'approved_at' => null, 'effective_from' => null, 'expires_at' => now()->addDays(10), 'review_due_at' => now()->addDays(7), ]); $active = $createException([]); $expired = $createException([ 'expires_at' => now()->subDay(), 'review_due_at' => now()->subDays(3), ]); app(FindingRiskGovernanceResolver::class)->syncExceptionState($expired); $this->actingAs($viewer); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::test(ListFindingExceptions::class) ->assertCanSeeTableRecords([$pending, $active, $expired]) ->filterTable('status', FindingException::STATUS_EXPIRED) ->assertCanSeeTableRecords([$expired]) ->assertCanNotSeeTableRecords([$pending, $active]); }); it('renders exception detail with owner, approver, and validity context for tenant viewers', function (): void { [$viewer, $tenant] = createUserWithTenant(role: 'readonly'); [$requester] = createUserWithTenant(tenant: $tenant, role: 'owner'); $approver = User::factory()->create(); createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner'); $finding = Finding::factory()->for($tenant)->create(); $exception = FindingException::query()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'tenant_id' => (int) $tenant->getKey(), 'finding_id' => (int) $finding->getKey(), 'requested_by_user_id' => (int) $requester->getKey(), 'owner_user_id' => (int) $requester->getKey(), 'approved_by_user_id' => (int) $approver->getKey(), 'status' => FindingException::STATUS_EXPIRING, 'current_validity_state' => FindingException::VALIDITY_EXPIRING, 'request_reason' => 'Temporary exception request', 'approval_reason' => 'Valid until remediation window closes', 'requested_at' => now()->subDays(5), 'approved_at' => now()->subDays(4), 'effective_from' => now()->subDays(4), 'expires_at' => now()->addDays(2), 'review_due_at' => now()->addDay(), 'evidence_summary' => ['reference_count' => 0], ]); $this->actingAs($viewer); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::test(ViewFindingException::class, ['record' => $exception->getKey()]) ->assertOk() ->assertSee('Validity') ->assertSee('Expiring') ->assertSee($requester->name) ->assertSee($approver->name); }); it('shows a single clear empty-state action when no tenant exceptions match', function (): void { [$viewer, $tenant] = createUserWithTenant(role: 'readonly'); $this->actingAs($viewer); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::test(ListFindingExceptions::class) ->assertSee('No exceptions match this view') ->assertTableEmptyStateActionsExistInOrder(['open_findings']); }); it('bridges tenant approval queue links into the admin workspace context', function (): void { [$viewer, $tenant] = createUserWithTenant(role: 'owner', workspaceRole: 'owner'); $otherWorkspace = Workspace::factory()->create(); $this->actingAs($viewer) ->withSession([WorkspaceContext::SESSION_KEY => (int) $otherWorkspace->getKey()]) ->get(route('admin.finding-exceptions.open-queue', ['tenant' => (string) $tenant->external_id])) ->assertRedirect( \App\Filament\Pages\Monitoring\FindingExceptionsQueue::getUrl([ 'tenant' => (string) $tenant->external_id, ], panel: 'admin') ); expect(session(WorkspaceContext::SESSION_KEY))->toBe((int) $tenant->workspace_id) ->and(session(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) ->toHaveKey((string) $tenant->workspace_id, (int) $tenant->getKey()); }); // --- Enterprise UX Hardening (Spec 166 Phase 6b) --- it('shows finding severity badge in exception register table', function (): void { [$viewer, $tenant] = createUserWithTenant(role: 'readonly'); [$requester] = createUserWithTenant(tenant: $tenant, role: 'owner'); $finding = Finding::factory()->for($tenant)->create(['severity' => Finding::SEVERITY_HIGH]); $exception = FindingException::query()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'tenant_id' => (int) $tenant->getKey(), 'finding_id' => (int) $finding->getKey(), 'requested_by_user_id' => (int) $requester->getKey(), 'owner_user_id' => (int) $requester->getKey(), 'status' => FindingException::STATUS_ACTIVE, 'current_validity_state' => FindingException::VALIDITY_VALID, 'request_reason' => 'Test severity badge', 'requested_at' => now(), 'review_due_at' => now()->addDays(14), 'evidence_summary' => ['reference_count' => 0], ]); $this->actingAs($viewer); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::test(ListFindingExceptions::class) ->assertCanSeeTableRecords([$exception]) ->assertTableColumnExists('finding.severity') ->assertSee('High'); }); it('shows descriptive finding title instead of bare Finding #ID', function (): void { [$viewer, $tenant] = createUserWithTenant(role: 'readonly'); [$requester] = createUserWithTenant(tenant: $tenant, role: 'owner'); $finding = Finding::factory()->for($tenant)->create([ 'finding_type' => Finding::FINDING_TYPE_DRIFT, 'subject_external_id' => 'test-policy-id', ]); $exception = FindingException::query()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'tenant_id' => (int) $tenant->getKey(), 'finding_id' => (int) $finding->getKey(), 'requested_by_user_id' => (int) $requester->getKey(), 'owner_user_id' => (int) $requester->getKey(), 'status' => FindingException::STATUS_ACTIVE, 'current_validity_state' => FindingException::VALIDITY_VALID, 'request_reason' => 'Test finding title', 'requested_at' => now(), 'review_due_at' => now()->addDays(14), 'evidence_summary' => ['reference_count' => 0], ]); $this->actingAs($viewer); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::test(ListFindingExceptions::class) ->assertCanSeeTableRecords([$exception]) ->assertSee('Drift'); }); it('shows expires_at column with relative time description', function (): void { [$viewer, $tenant] = createUserWithTenant(role: 'readonly'); [$requester] = createUserWithTenant(tenant: $tenant, role: 'owner'); $finding = Finding::factory()->for($tenant)->create(); $exception = FindingException::query()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'tenant_id' => (int) $tenant->getKey(), 'finding_id' => (int) $finding->getKey(), 'requested_by_user_id' => (int) $requester->getKey(), 'owner_user_id' => (int) $requester->getKey(), 'status' => FindingException::STATUS_EXPIRING, 'current_validity_state' => FindingException::VALIDITY_EXPIRING, 'request_reason' => 'Test relative time', 'requested_at' => now(), 'review_due_at' => now()->addDays(3), 'expires_at' => now()->addDays(5), 'evidence_summary' => ['reference_count' => 0], ]); $this->actingAs($viewer); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::test(ListFindingExceptions::class) ->assertCanSeeTableRecords([$exception]) ->assertTableColumnExists('expires_at'); // Verify the relative time helper directly (column descriptions aren't in Livewire test HTML) expect(FindingExceptionResource::relativeTimeDescription(now()))->toBe('Today'); expect(FindingExceptionResource::relativeTimeDescription(now()->addDay()))->toBe('Tomorrow'); expect(FindingExceptionResource::relativeTimeDescription(now()->addDays(3)))->toBe('In 3 days'); expect(FindingExceptionResource::relativeTimeDescription(now()->addDays(14)))->toBe('In 14 days'); expect(FindingExceptionResource::relativeTimeDescription(null))->toBeNull(); }); it('renders stats overview widget above exception register table', function (): void { [$viewer, $tenant] = createUserWithTenant(role: 'readonly'); $this->actingAs($viewer); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::test(ListFindingExceptions::class) ->assertOk() ->assertSeeLivewire(FindingExceptionStatsOverview::class); }); it('returns correct stats counts for current tenant', function (): void { [$viewer, $tenant] = createUserWithTenant(role: 'readonly'); [$requester] = createUserWithTenant(tenant: $tenant, role: 'owner'); $createException = fn (string $status, string $validity) => FindingException::query()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'tenant_id' => (int) $tenant->getKey(), 'finding_id' => (int) Finding::factory()->for($tenant)->create()->getKey(), 'requested_by_user_id' => (int) $requester->getKey(), 'owner_user_id' => (int) $requester->getKey(), 'status' => $status, 'current_validity_state' => $validity, 'request_reason' => 'Stats test', 'requested_at' => now(), 'review_due_at' => now()->addDays(14), 'evidence_summary' => ['reference_count' => 0], ]); $createException(FindingException::STATUS_ACTIVE, FindingException::VALIDITY_VALID); $createException(FindingException::STATUS_ACTIVE, FindingException::VALIDITY_VALID); $createException(FindingException::STATUS_EXPIRING, FindingException::VALIDITY_EXPIRING); $createException(FindingException::STATUS_EXPIRED, FindingException::VALIDITY_EXPIRED); $createException(FindingException::STATUS_PENDING, FindingException::VALIDITY_MISSING_SUPPORT); $this->actingAs($viewer); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $stats = FindingExceptionResource::exceptionStatsForCurrentTenant(); expect($stats)->toMatchArray([ 'active' => 2, 'expiring' => 1, 'expired' => 1, 'pending' => 1, 'total' => 5, ]); }); it('segments exception register with quick-tabs for needs-action, active, and historical', function (): void { [$viewer, $tenant] = createUserWithTenant(role: 'readonly'); [$requester] = createUserWithTenant(tenant: $tenant, role: 'owner'); $createException = fn (string $status, string $validity) => FindingException::query()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'tenant_id' => (int) $tenant->getKey(), 'finding_id' => (int) Finding::factory()->for($tenant)->create()->getKey(), 'requested_by_user_id' => (int) $requester->getKey(), 'owner_user_id' => (int) $requester->getKey(), 'status' => $status, 'current_validity_state' => $validity, 'request_reason' => 'Tab test', 'requested_at' => now(), 'review_due_at' => now()->addDays(14), 'evidence_summary' => ['reference_count' => 0], ]); $active = $createException(FindingException::STATUS_ACTIVE, FindingException::VALIDITY_VALID); $expiring = $createException(FindingException::STATUS_EXPIRING, FindingException::VALIDITY_EXPIRING); $rejected = $createException(FindingException::STATUS_REJECTED, FindingException::VALIDITY_REJECTED); $this->actingAs($viewer); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $list = Livewire::test(ListFindingExceptions::class); // All tab shows everything $list->assertCanSeeTableRecords([$active, $expiring, $rejected]); // Needs action tab shows expiring (pending, expiring, expired) $list->set('activeTab', 'needs_action') ->assertCanSeeTableRecords([$expiring]) ->assertCanNotSeeTableRecords([$active, $rejected]); // Active tab shows only active $list->set('activeTab', 'active') ->assertCanSeeTableRecords([$active]) ->assertCanNotSeeTableRecords([$expiring, $rejected]); // Historical tab shows rejected/revoked/superseded $list->set('activeTab', 'historical') ->assertCanSeeTableRecords([$rejected]) ->assertCanNotSeeTableRecords([$active, $expiring]); });