Implemented the accepted risk resolution guidance, including the AcceptedRiskResolutionAdapter, guidance cards, and updated related Filament views. Added unit, feature, and browser tests. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #425
267 lines
11 KiB
PHP
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'));
|
|
});
|
|
});
|