TenantAtlas/apps/platform/tests/Feature/Findings/FindingExceptionRenewalTest.php
ahmido a2fdca43fd feat: implement heavy governance cost recovery (#242)
## Summary
- implement Spec 209 heavy-governance cost recovery end to end
- add the heavy-governance contract, hotspot inventory, decomposition, snapshots, budget outcome, and author-guidance surfaces in the shared lane support seams
- slim the baseline and findings hotspot families, harden wrapper behavior, and refresh the spec, quickstart, and contract artifacts

## Validation
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/TestLaneCommandContractTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php tests/Feature/Filament/BaselineActionAuthorizationTest.php tests/Feature/Findings/FindingsListFiltersTest.php tests/Feature/Findings/FindingExceptionRenewalTest.php tests/Feature/Findings/FindingWorkflowRowActionsTest.php tests/Feature/Findings/FindingWorkflowViewActionsTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php`
- `./scripts/platform-sail artisan test --compact`

## Outcome
- heavy-governance latest artifacts now agree on an authoritative `330s` threshold with `recalibrated` outcome after the honest rerun
- full suite result: `3760 passed`, `8 skipped`, `23535 assertions`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #242
2026-04-17 13:17:13 +00:00

222 lines
9.8 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
use App\Filament\Resources\FindingExceptionResource\Pages\ViewFindingException;
use App\Filament\Resources\FindingResource\Pages\ViewFinding;
use App\Models\AuditLog;
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\FindingExceptionDecision;
use App\Models\User;
use App\Services\Findings\FindingExceptionService;
use App\Support\Audit\AuditActionId;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
/**
* @return array{0: User, 1: User, 2: \App\Models\Tenant, 3: Finding, 4: FindingExceptionService, 5: FindingException}
*/
function seedApprovedFindingExceptionWindow(): array
{
[$requester, $tenant] = createUserWithTenant(role: 'owner');
$approver = User::factory()->create();
createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager');
$finding = Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_RISK_ACCEPTED,
]);
/** @var FindingExceptionService $service */
$service = app(FindingExceptionService::class);
$requested = $service->request($finding, $tenant, $requester, [
'owner_user_id' => (int) $requester->getKey(),
'request_reason' => 'Initial acceptance window',
'review_due_at' => now()->addDays(5)->toDateTimeString(),
'expires_at' => now()->addDays(20)->toDateTimeString(),
]);
$active = $service->approve($requested, $approver, [
'effective_from' => now()->subDay()->toDateTimeString(),
'expires_at' => now()->addDays(20)->toDateTimeString(),
'approval_reason' => 'Initial approval',
]);
return [$requester, $approver, $tenant, $finding, $service, $active];
}
it('requests and approves a renewal while preserving prior decision history and evidence references', function (): void {
[$requester, $tenant] = createUserWithTenant(role: 'owner');
$approver = User::factory()->create();
createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager');
$finding = Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_RISK_ACCEPTED,
]);
/** @var FindingExceptionService $service */
$service = app(FindingExceptionService::class);
$requested = $service->request($finding, $tenant, $requester, [
'owner_user_id' => (int) $requester->getKey(),
'request_reason' => 'Initial compensating controls approved until maintenance window',
'review_due_at' => now()->addDays(7)->toDateTimeString(),
'expires_at' => now()->addDays(14)->toDateTimeString(),
'evidence_references' => [
[
'label' => 'Initial baseline review',
'source_type' => 'evidence_snapshot',
'source_id' => 'snapshot-001',
'source_fingerprint' => 'fp-initial-001',
'measured_at' => now()->subDay()->toDateTimeString(),
'summary_payload' => ['status' => 'pending_remediation'],
],
],
]);
$service->approve($requested, $approver, [
'effective_from' => now()->subDay()->toDateTimeString(),
'expires_at' => now()->addDays(14)->toDateTimeString(),
'approval_reason' => 'Approved until remediation can be scheduled.',
]);
$this->actingAs($requester);
Filament::setTenant($tenant, true);
Livewire::test(ViewFinding::class, ['record' => $finding->getKey()])
->assertActionVisible('renew_exception')
->callAction('renew_exception', data: [
'owner_user_id' => (int) $requester->getKey(),
'request_reason' => 'Renew for the next maintenance window with updated rollback proof.',
'review_due_at' => now()->addDays(10)->toDateTimeString(),
'expires_at' => now()->addDays(45)->toDateTimeString(),
'evidence_references' => [
[
'label' => 'Rollback drill 2026-03-18',
'source_type' => 'review_pack',
'source_id' => 'rp-2026-03-18',
'source_fingerprint' => 'fp-renew-001',
'measured_at' => now()->subHours(12)->toDateTimeString(),
],
[
'label' => 'Compensating controls review',
'source_type' => 'evidence_snapshot',
'source_id' => 'snapshot-002',
'source_fingerprint' => 'fp-renew-002',
'measured_at' => now()->subHours(6)->toDateTimeString(),
],
],
])
->assertHasNoActionErrors();
$pendingRenewal = FindingException::query()
->with(['currentDecision', 'decisions', 'evidenceReferences'])
->where('finding_id', (int) $finding->getKey())
->firstOrFail();
expect($pendingRenewal->status)->toBe(FindingException::STATUS_PENDING)
->and($pendingRenewal->currentDecision?->decision_type)->toBe(FindingExceptionDecision::TYPE_RENEWAL_REQUESTED)
->and($pendingRenewal->decisions->pluck('decision_type')->all())->toBe([
FindingExceptionDecision::TYPE_REQUESTED,
FindingExceptionDecision::TYPE_APPROVED,
FindingExceptionDecision::TYPE_RENEWAL_REQUESTED,
])
->and($pendingRenewal->evidenceReferences)->toHaveCount(2)
->and($pendingRenewal->evidenceReferences->pluck('label')->all())->toBe([
'Rollback drill 2026-03-18',
'Compensating controls review',
])
->and($pendingRenewal->evidenceReferences->first()?->summary_payload)->toBe([]);
$this->actingAs($approver);
Filament::setCurrentPanel('admin');
Filament::setTenant(null, true);
Filament::bootCurrentPanel();
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
Livewire::withQueryParams([
'exception' => (int) $pendingRenewal->getKey(),
])
->test(FindingExceptionsQueue::class)
->assertActionVisible('approve_selected_exception')
->callAction('approve_selected_exception', data: [
'effective_from' => now()->addDays(14)->toDateTimeString(),
'expires_at' => now()->addDays(45)->toDateTimeString(),
'approval_reason' => 'Renewed while remediation remains scheduled and evidenced.',
])
->assertHasNoActionErrors()
->assertNotified('Exception renewed');
$renewed = $pendingRenewal->fresh(['currentDecision', 'decisions', 'evidenceReferences']);
expect($renewed?->status)->toBe(FindingException::STATUS_ACTIVE)
->and($renewed?->current_validity_state)->toBe(FindingException::VALIDITY_VALID)
->and($renewed?->currentDecision?->decision_type)->toBe(FindingExceptionDecision::TYPE_RENEWED)
->and($renewed?->decisions->pluck('decision_type')->all())->toBe([
FindingExceptionDecision::TYPE_REQUESTED,
FindingExceptionDecision::TYPE_APPROVED,
FindingExceptionDecision::TYPE_RENEWAL_REQUESTED,
FindingExceptionDecision::TYPE_RENEWED,
])
->and($finding->fresh()?->status)->toBe(Finding::STATUS_RISK_ACCEPTED)
->and(AuditLog::query()
->where('action', AuditActionId::FindingExceptionRenewalRequested->value)
->where('resource_type', 'finding_exception')
->where('resource_id', (string) $pendingRenewal->getKey())
->exists())->toBeTrue()
->and(AuditLog::query()
->where('action', AuditActionId::FindingExceptionRenewed->value)
->where('resource_type', 'finding_exception')
->where('resource_id', (string) $pendingRenewal->getKey())
->exists())->toBeTrue();
$this->actingAs($requester);
Filament::setTenant($tenant, true);
Livewire::test(ViewFindingException::class, ['record' => $pendingRenewal->getKey()])
->assertOk()
->assertSee('Rollback drill 2026-03-18')
->assertSee('Compensating controls review')
->assertSee('Renewed while remediation remains scheduled and evidenced.');
});
it('rejects a pending renewal without erasing the prior approved governance window', function (): void {
[$requester, $approver, $tenant, $finding, $service, $active] = seedApprovedFindingExceptionWindow();
$service->renew($active, $requester, [
'owner_user_id' => (int) $requester->getKey(),
'request_reason' => 'Need additional time while vendor patch is validated.',
'review_due_at' => now()->addDays(8)->toDateTimeString(),
'expires_at' => now()->addDays(40)->toDateTimeString(),
'evidence_references' => [
[
'label' => 'Patch validation evidence',
'source_type' => 'review_pack',
],
],
]);
$rejected = $service->reject($active->fresh(['currentDecision']), $approver, [
'rejection_reason' => 'Renewal denied until stronger mitigation evidence is attached.',
]);
expect($rejected->status)->toBe(FindingException::STATUS_ACTIVE)
->and($rejected->current_validity_state)->toBe(FindingException::VALIDITY_VALID)
->and($rejected->currentDecision?->decision_type)->toBe(FindingExceptionDecision::TYPE_REJECTED)
->and($rejected->decisions->pluck('decision_type')->all())->toBe([
FindingExceptionDecision::TYPE_REQUESTED,
FindingExceptionDecision::TYPE_APPROVED,
FindingExceptionDecision::TYPE_RENEWAL_REQUESTED,
FindingExceptionDecision::TYPE_REJECTED,
])
->and($rejected->rejection_reason)->toBe('Renewal denied until stronger mitigation evidence is attached.')
->and($finding->fresh()?->status)->toBe(Finding::STATUS_RISK_ACCEPTED);
});