91 lines
3.7 KiB
PHP
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');
|
|
});
|