TenantAtlas/apps/platform/tests/Feature/Findings/FindingExceptionRegisterTest.php
Ahmed Darrazi 68ff50d460
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m45s
feat: finding exceptions accepted risk resolution guidance v1 (spec 354)
Implemented the accepted risk resolution guidance, including the AcceptedRiskResolutionAdapter, guidance cards, and updated related Filament views. Added unit, feature, and browser tests.
2026-06-05 04:18:59 +02:00

349 lines
15 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\FindingExceptionResource;
use App\Filament\Resources\FindingExceptionResource\Pages\ListFindingExceptions;
use App\Filament\Resources\FindingExceptionResource\Pages\ViewFindingException;
use App\Filament\Widgets\ManagedEnvironment\FindingExceptionStatsOverview;
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Findings\FindingRiskGovernanceResolver;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
it('shows pending, active, and expired exceptions in the tenant register with lifecycle filters', function (): void {
[$viewer, $tenant] = createMinimalUserWithTenant(role: 'readonly');
[$requester] = createMinimalUserWithTenant(tenant: $tenant, role: 'owner');
$approver = User::factory()->create();
createMinimalUserWithTenant(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,
'managed_environment_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] = createMinimalUserWithTenant(role: 'readonly');
[$requester] = createMinimalUserWithTenant(tenant: $tenant, role: 'owner');
$approver = User::factory()->create();
createMinimalUserWithTenant(tenant: $tenant, user: $approver, role: 'owner');
$finding = Finding::factory()->for($tenant)->create();
$exception = FindingException::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_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] = createMinimalUserWithTenant(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] = createMinimalUserWithTenant(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', ['environment' => (string) $tenant->external_id]))
->assertRedirect(
\App\Filament\Pages\Monitoring\FindingExceptionsQueue::getUrl([
'environment_id' => (int) $tenant->getKey(),
], panel: 'admin')
);
expect(session(WorkspaceContext::SESSION_KEY))->toBe((int) $tenant->workspace_id)
->and(session(WorkspaceContext::LAST_ENVIRONMENT_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] = createMinimalUserWithTenant(role: 'readonly');
[$requester] = createMinimalUserWithTenant(tenant: $tenant, role: 'owner');
$finding = Finding::factory()->for($tenant)->create(['severity' => Finding::SEVERITY_HIGH]);
$exception = FindingException::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_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] = createMinimalUserWithTenant(role: 'readonly');
[$requester] = createMinimalUserWithTenant(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,
'managed_environment_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] = createMinimalUserWithTenant(role: 'readonly');
[$requester] = createMinimalUserWithTenant(tenant: $tenant, role: 'owner');
$finding = Finding::factory()->for($tenant)->create();
$exception = FindingException::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_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] = createMinimalUserWithTenant(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] = createMinimalUserWithTenant(role: 'readonly');
[$requester] = createMinimalUserWithTenant(tenant: $tenant, role: 'owner');
$createException = fn (string $status, string $validity) => FindingException::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_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] = createMinimalUserWithTenant(role: 'readonly');
[$requester] = createMinimalUserWithTenant(tenant: $tenant, role: 'owner');
$createException = fn (string $status, string $validity) => FindingException::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_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]);
});