TenantAtlas/apps/platform/tests/Unit/ResolutionGuidance/Spec354AcceptedRiskResolutionAdapterTest.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

267 lines
11 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\FindingExceptionDecision;
use App\Support\ResolutionGuidance\Adapters\AcceptedRiskResolutionAdapter;
use App\Support\ResolutionGuidance\ResolutionAction;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
function spec354AdapterFixture(
array $findingAttributes = [],
array $exceptionAttributes = [],
?string $decisionType = FindingExceptionDecision::TYPE_APPROVED,
array $decisionMetadata = [],
): array {
[$owner, $tenant] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
$approver = \App\Models\User::factory()->create();
createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager');
$finding = Finding::factory()
->for($tenant)
->riskAccepted()
->create(array_merge([
'workspace_id' => (int) $tenant->workspace_id,
], $findingAttributes));
$exception = 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) $owner->getKey(),
'owner_user_id' => (int) $owner->getKey(),
'approved_by_user_id' => (int) $approver->getKey(),
'status' => FindingException::STATUS_ACTIVE,
'current_validity_state' => FindingException::VALIDITY_VALID,
'request_reason' => 'Temporary compensating control is still in place.',
'approval_reason' => 'Accepted until scheduled remediation is complete.',
'requested_at' => now()->subDays(10),
'approved_at' => now()->subDays(9),
'effective_from' => now()->subDays(9),
'review_due_at' => now()->addDays(14),
'expires_at' => now()->addDays(30),
'evidence_summary' => ['reference_count' => 0],
], $exceptionAttributes));
if ($decisionType !== null) {
$decision = $exception->decisions()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'actor_user_id' => (int) $approver->getKey(),
'decision_type' => $decisionType,
'reason' => 'Spec354 adapter test decision.',
'metadata' => $decisionMetadata,
'decided_at' => now()->subDays(9),
]);
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
}
return [$tenant, $finding->fresh(), $exception->fresh(['finding', 'tenant', 'owner', 'currentDecision', 'evidenceReferences'])];
}
it('classifies ready and expiring accepted-risk states', function (): void {
[, , $ready] = spec354AdapterFixture();
[, , $expiring] = spec354AdapterFixture(
exceptionAttributes: [
'review_due_at' => now()->addDay(),
'expires_at' => now()->addDays(2),
],
);
$adapter = app(AcceptedRiskResolutionAdapter::class);
expect($adapter->forQueue($ready)['key'])->toBe('accepted_risk.ready')
->and($adapter->forQueue($ready)['title'])->toBe(__('localization.accepted_risk_guidance.title_ready'))
->and($adapter->forQueue($expiring)['key'])->toBe('accepted_risk.expiring')
->and($adapter->forQueue($expiring)['title'])->toBe(__('localization.accepted_risk_guidance.title_expiring'));
});
it('classifies expired accepted-risk state', function (): void {
[, , $exception] = spec354AdapterFixture(
exceptionAttributes: [
'status' => FindingException::STATUS_ACTIVE,
'current_validity_state' => FindingException::VALIDITY_VALID,
'review_due_at' => now()->subDays(3),
'expires_at' => now()->subDay(),
],
decisionType: FindingExceptionDecision::TYPE_APPROVED,
);
$guidance = app(AcceptedRiskResolutionAdapter::class)->forQueue($exception);
expect($guidance['title'])->toBe(__('localization.accepted_risk_guidance.title_expired'));
});
it('classifies revoked accepted-risk state', function (): void {
[, , $exception] = spec354AdapterFixture(
exceptionAttributes: [
'status' => FindingException::STATUS_REVOKED,
'current_validity_state' => FindingException::VALIDITY_REVOKED,
'revoked_at' => now()->subDay(),
],
decisionType: FindingExceptionDecision::TYPE_REVOKED,
);
$guidance = app(AcceptedRiskResolutionAdapter::class)->forQueue($exception);
expect($guidance['title'])->toBe(__('localization.accepted_risk_guidance.title_revoked'));
});
it('classifies rejected accepted-risk state', function (): void {
[, , $exception] = spec354AdapterFixture(
exceptionAttributes: [
'status' => FindingException::STATUS_REJECTED,
'current_validity_state' => FindingException::VALIDITY_REJECTED,
'rejected_at' => now()->subDay(),
],
decisionType: FindingExceptionDecision::TYPE_REJECTED,
);
$guidance = app(AcceptedRiskResolutionAdapter::class)->forQueue($exception);
expect($guidance['title'])->toBe(__('localization.accepted_risk_guidance.title_rejected'));
});
it('distinguishes pending initial review from pending renewal', function (): void {
[, , $pendingInitial] = spec354AdapterFixture(
exceptionAttributes: [
'status' => FindingException::STATUS_PENDING,
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
'approved_by_user_id' => null,
'approved_at' => null,
'effective_from' => null,
'approval_reason' => null,
],
decisionType: FindingExceptionDecision::TYPE_REQUESTED,
);
[, , $pendingRenewal] = spec354AdapterFixture(
exceptionAttributes: [
'status' => FindingException::STATUS_PENDING,
'current_validity_state' => FindingException::VALIDITY_VALID,
],
decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED,
);
$adapter = app(AcceptedRiskResolutionAdapter::class);
expect($adapter->forQueue($pendingInitial)['title'])->toBe(__('localization.accepted_risk_guidance.title_pending'))
->and($adapter->forQueue($pendingInitial)['primary_action']['label'])->toBe(__('localization.accepted_risk_guidance.next_step_pending'))
->and($adapter->forQueue($pendingRenewal)['title'])->toBe(__('localization.accepted_risk_guidance.title_pending_renewal'))
->and($adapter->forQueue($pendingRenewal)['primary_action']['label'])->toBe(__('localization.accepted_risk_guidance.next_step_pending_renewal'));
});
it('keeps lapsed carried-over governance dominant over pending renewal guidance', function (): void {
[, , $expiredRenewal] = spec354AdapterFixture(
exceptionAttributes: [
'status' => FindingException::STATUS_PENDING,
'current_validity_state' => FindingException::VALIDITY_VALID,
],
decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED,
decisionMetadata: [
'previous_review_due_at' => now()->subDays(2)->toIso8601String(),
'previous_expires_at' => now()->subDay()->toIso8601String(),
],
);
[, , $expiringRenewal] = spec354AdapterFixture(
exceptionAttributes: [
'status' => FindingException::STATUS_PENDING,
'current_validity_state' => FindingException::VALIDITY_VALID,
],
decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED,
decisionMetadata: [
'previous_review_due_at' => now()->addDay()->toIso8601String(),
'previous_expires_at' => now()->addDays(2)->toIso8601String(),
],
);
$adapter = app(AcceptedRiskResolutionAdapter::class);
expect($adapter->forQueue($expiredRenewal)['key'])->toBe('accepted_risk.expired')
->and($adapter->forQueue($expiringRenewal)['key'])->toBe('accepted_risk.expiring');
});
it('classifies missing support, fresh decision required, and incomplete governance states', function (): void {
[, , $missingSupport] = spec354AdapterFixture(
exceptionAttributes: [
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
'status' => FindingException::STATUS_ACTIVE,
],
);
[, , $freshDecision] = spec354AdapterFixture(
findingAttributes: [
'status' => Finding::STATUS_NEW,
],
);
[, , $incompleteGovernance] = spec354AdapterFixture(
exceptionAttributes: [
'owner_user_id' => null,
'request_reason' => '',
'review_due_at' => null,
],
);
$adapter = app(AcceptedRiskResolutionAdapter::class);
expect($adapter->forQueue($missingSupport)['key'])->toBe('accepted_risk.missing_support')
->and($adapter->forQueue($freshDecision)['key'])->toBe('accepted_risk.fresh_decision_required')
->and($adapter->forQueue($freshDecision)['reason'])->toContain('fresh decision')
->and($adapter->forDetail($incompleteGovernance)['key'])->toBe('accepted_risk.incomplete_governance')
->and($adapter->forDetail($incompleteGovernance)['technical_details'])->toHaveKey(__('localization.accepted_risk_guidance.detail_missing_fields_label'));
});
it('keeps missing support as review focus instead of a fake primary action', function (): void {
[, , $exception] = spec354AdapterFixture(
exceptionAttributes: [
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
'status' => FindingException::STATUS_ACTIVE,
],
);
$guidance = app(AcceptedRiskResolutionAdapter::class)->forDetail($exception);
expect($guidance['primary_action']['type'])->toBe(ResolutionAction::TYPE_NONE)
->and($guidance['primary_action']['label'])->toBe(__('localization.accepted_risk_guidance.next_step_missing_support'))
->and($guidance['primary_action']['label'])->toContain('Review whether');
});
it('localizes dominant accepted-risk guidance copy for german locale', function (): void {
$originalLocale = app()->getLocale();
[, , $exception] = spec354AdapterFixture(
exceptionAttributes: [
'review_due_at' => now()->addDay(),
'expires_at' => now()->addDays(2),
],
);
app()->setLocale('de');
$guidance = app(AcceptedRiskResolutionAdapter::class)->forQueue($exception);
expect($guidance['reason'])->toBe(__('localization.accepted_risk_guidance.reason_expiring'))
->and($guidance['impact'])->toBe(__('localization.accepted_risk_guidance.impact_expiring'))
->and($guidance['reason'])->not->toContain('The current accepted-risk governance window');
app()->setLocale($originalLocale);
});
it('stays database-local and preserves conservative wording without mutating downstream review output state', function (): void {
bindFailHardGraphClient();
[, , $exception] = spec354AdapterFixture();
assertNoOutboundHttp(function () use ($exception): void {
$guidance = app(AcceptedRiskResolutionAdapter::class)->forDetail($exception);
expect($guidance['primary_action']['label'])->toBe(__('localization.accepted_risk_guidance.next_step_ready'));
});
});