TenantAtlas/tests/Feature/Findings/FindingExceptionRevocationTest.php
2026-03-20 02:05:50 +01:00

91 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
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 Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
it('revokes an approved exception from the tenant finding surface and keeps the audit trail intelligible', 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' => 'Temporary exception while remediation is scheduled.',
'review_due_at' => now()->addDays(5)->toDateTimeString(),
'expires_at' => now()->addDays(30)->toDateTimeString(),
'evidence_references' => [
[
'label' => 'Initial review note',
'source_type' => 'review_pack',
'source_id' => 'rp-initial',
'source_fingerprint' => 'fp-initial',
'measured_at' => now()->subDay()->toDateTimeString(),
],
],
]);
$service->approve($requested, $approver, [
'effective_from' => now()->subDay()->toDateTimeString(),
'expires_at' => now()->addDays(30)->toDateTimeString(),
'approval_reason' => 'Approved with compensating controls.',
]);
$this->actingAs($requester);
Filament::setTenant($tenant, true);
Livewire::test(ViewFinding::class, ['record' => $finding->getKey()])
->assertActionVisible('revoke_exception')
->callAction('revoke_exception', data: [
'revocation_reason' => 'Compensating controls no longer exist in production.',
])
->assertHasNoActionErrors()
->assertNotified('Exception revoked');
$revoked = FindingException::query()
->with(['currentDecision', 'decisions', 'evidenceReferences'])
->where('finding_id', (int) $finding->getKey())
->firstOrFail();
expect($revoked->status)->toBe(FindingException::STATUS_REVOKED)
->and($revoked->current_validity_state)->toBe(FindingException::VALIDITY_REVOKED)
->and($revoked->currentDecision?->decision_type)->toBe(FindingExceptionDecision::TYPE_REVOKED)
->and($revoked->decisions->pluck('decision_type')->all())->toBe([
FindingExceptionDecision::TYPE_REQUESTED,
FindingExceptionDecision::TYPE_APPROVED,
FindingExceptionDecision::TYPE_REVOKED,
])
->and($revoked->revocation_reason)->toBe('Compensating controls no longer exist in production.')
->and($revoked->revoked_at)->not->toBeNull()
->and($revoked->evidenceReferences)->toHaveCount(1)
->and($finding->fresh()?->status)->toBe(Finding::STATUS_RISK_ACCEPTED)
->and(AuditLog::query()
->where('action', AuditActionId::FindingExceptionRevoked->value)
->where('resource_type', 'finding_exception')
->where('resource_id', (string) $revoked->getKey())
->exists())->toBeTrue();
Livewire::test(ViewFinding::class, ['record' => $finding->getKey()])
->assertSee('Risk governance')
->assertSee('Revoked');
});