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
308 lines
13 KiB
PHP
308 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
|
|
use App\Filament\Resources\FindingExceptionResource;
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\FindingExceptionDecision;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\User;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
pest()->browser()->timeout(30_000);
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function spec354BrowserScreenshot(string $name): string
|
|
{
|
|
return 'spec354-'.$name;
|
|
}
|
|
|
|
function spec354CopyBrowserScreenshot(string $name): void
|
|
{
|
|
$filename = spec354BrowserScreenshot($name).'.png';
|
|
$source = base_path('tests/Browser/Screenshots/'.$filename);
|
|
$targetDirectory = repo_path('specs/354-finding-exceptions-accepted-risk-resolution-guidance-v1/artifacts/screenshots');
|
|
|
|
if (! is_dir($targetDirectory)) {
|
|
@mkdir($targetDirectory, 0755, true);
|
|
}
|
|
|
|
if (! is_file($source)) {
|
|
$source = \Pest\Browser\Support\Screenshot::path($filename);
|
|
}
|
|
|
|
for ($attempt = 0; $attempt < 10 && ! is_file($source); $attempt++) {
|
|
usleep(100_000);
|
|
clearstatcache(true, $source);
|
|
}
|
|
|
|
if (is_file($source) && is_dir($targetDirectory) && is_writable($targetDirectory)) {
|
|
@copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$filename);
|
|
}
|
|
}
|
|
|
|
function spec354AuthenticateBrowser(mixed $test, User $user, ManagedEnvironment $tenant): void
|
|
{
|
|
$workspaceId = (int) $tenant->workspace_id;
|
|
|
|
$test->actingAs($user)->withSession([
|
|
WorkspaceContext::SESSION_KEY => $workspaceId,
|
|
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
|
(string) $workspaceId => (int) $tenant->getKey(),
|
|
],
|
|
]);
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
|
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
|
(string) $workspaceId => (int) $tenant->getKey(),
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
}
|
|
|
|
function spec354BrowserException(
|
|
ManagedEnvironment $tenant,
|
|
User $user,
|
|
array $findingAttributes = [],
|
|
array $exceptionAttributes = [],
|
|
?string $decisionType = FindingExceptionDecision::TYPE_APPROVED,
|
|
): FindingException {
|
|
$decisionMetadata = is_array($exceptionAttributes['decision_metadata'] ?? null)
|
|
? $exceptionAttributes['decision_metadata']
|
|
: [];
|
|
|
|
unset($exceptionAttributes['decision_metadata']);
|
|
|
|
$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) $user->getKey(),
|
|
'owner_user_id' => (int) $user->getKey(),
|
|
'approved_by_user_id' => (int) $user->getKey(),
|
|
'status' => FindingException::STATUS_ACTIVE,
|
|
'current_validity_state' => FindingException::VALIDITY_VALID,
|
|
'request_reason' => 'Spec354 browser accepted-risk guidance',
|
|
'approval_reason' => 'Spec354 browser approval',
|
|
'requested_at' => now()->subDays(5),
|
|
'approved_at' => now()->subDays(4),
|
|
'effective_from' => now()->subDays(4),
|
|
'review_due_at' => now()->addDays(10),
|
|
'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) $user->getKey(),
|
|
'decision_type' => $decisionType,
|
|
'reason' => 'Spec354 browser decision',
|
|
'metadata' => $decisionMetadata,
|
|
'decided_at' => now()->subDays(4),
|
|
]);
|
|
|
|
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
|
|
}
|
|
|
|
return $exception->fresh(['finding', 'tenant', 'owner', 'currentDecision', 'decisions.actor', 'evidenceReferences']);
|
|
}
|
|
|
|
it('smokes expiring queue guidance', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$expiring = spec354BrowserException($tenant, $user, exceptionAttributes: [
|
|
'review_due_at' => now()->addDay(),
|
|
'expires_at' => now()->addDays(2),
|
|
]);
|
|
|
|
spec354AuthenticateBrowser($this, $user, $tenant);
|
|
|
|
visit(FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [
|
|
'exception' => (int) $expiring->getKey(),
|
|
]))
|
|
->resize(1440, 1100)
|
|
->waitForText(__('localization.accepted_risk_guidance.title_expiring'))
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee(__('localization.accepted_risk_guidance.review_focus_label'))
|
|
->assertSee(__('localization.accepted_risk_guidance.next_step_expiring'))
|
|
->assertSee(__('localization.accepted_risk_guidance.action_open_exception'))
|
|
->screenshot(true, spec354BrowserScreenshot('ui-026-finding-exceptions-queue-guidance'));
|
|
|
|
spec354CopyBrowserScreenshot('ui-026-finding-exceptions-queue-guidance');
|
|
});
|
|
|
|
it('smokes expired queue guidance', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$expired = spec354BrowserException($tenant, $user, exceptionAttributes: [
|
|
'review_due_at' => now()->subDays(2),
|
|
'expires_at' => now()->subDay(),
|
|
]);
|
|
|
|
spec354AuthenticateBrowser($this, $user, $tenant);
|
|
|
|
visit(FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [
|
|
'exception' => (int) $expired->getKey(),
|
|
]))
|
|
->waitForText(__('localization.accepted_risk_guidance.title_expired'))
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee(__('localization.accepted_risk_guidance.impact_expired'));
|
|
});
|
|
|
|
it('smokes pending-renewal queue guidance while governance remains non-lapsed', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$pendingRenewalValid = spec354BrowserException($tenant, $user, exceptionAttributes: [
|
|
'status' => FindingException::STATUS_PENDING,
|
|
'current_validity_state' => FindingException::VALIDITY_VALID,
|
|
'decision_metadata' => [
|
|
'previous_review_due_at' => now()->addDays(10)->toIso8601String(),
|
|
'previous_expires_at' => now()->addDays(30)->toIso8601String(),
|
|
],
|
|
], decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED);
|
|
|
|
spec354AuthenticateBrowser($this, $user, $tenant);
|
|
|
|
visit(FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [
|
|
'exception' => (int) $pendingRenewalValid->getKey(),
|
|
]))
|
|
->waitForText(__('localization.accepted_risk_guidance.title_pending_renewal'))
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee(__('localization.accepted_risk_guidance.next_step_pending_renewal'));
|
|
});
|
|
|
|
it('smokes pending-renewal queue guidance when expired governance stays dominant', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$pendingRenewalExpired = spec354BrowserException($tenant, $user, exceptionAttributes: [
|
|
'status' => FindingException::STATUS_PENDING,
|
|
'current_validity_state' => FindingException::VALIDITY_VALID,
|
|
'decision_metadata' => [
|
|
'previous_review_due_at' => now()->subDays(2)->toIso8601String(),
|
|
'previous_expires_at' => now()->subDay()->toIso8601String(),
|
|
],
|
|
], decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED);
|
|
|
|
spec354AuthenticateBrowser($this, $user, $tenant);
|
|
|
|
visit(FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [
|
|
'exception' => (int) $pendingRenewalExpired->getKey(),
|
|
]))
|
|
->waitForText(__('localization.accepted_risk_guidance.title_expired'))
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertDontSee(__('localization.accepted_risk_guidance.title_pending_renewal'));
|
|
});
|
|
|
|
it('smokes german queue localization without fake remediation copy', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$expiring = spec354BrowserException($tenant, $user, exceptionAttributes: [
|
|
'review_due_at' => now()->addDay(),
|
|
'expires_at' => now()->addDays(2),
|
|
]);
|
|
|
|
spec354AuthenticateBrowser($this, $user, $tenant);
|
|
|
|
visit(FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [
|
|
'exception' => (int) $expiring->getKey(),
|
|
'locale' => 'de',
|
|
]))
|
|
->waitForText('Was zu prüfen ist')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee('Das aktuelle Accepted-Risk-Governance-Fenster ist noch aktiv, läuft aber bald ab und muss geprüft werden.')
|
|
->assertDontSee('The current accepted-risk governance window is still active, but it is nearing expiry and needs review.')
|
|
->assertDontSee('Fix accepted risk');
|
|
});
|
|
|
|
it('smokes detail guidance hierarchy and pending-renewal queue continuity', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$incomplete = spec354BrowserException($tenant, $user, exceptionAttributes: [
|
|
'owner_user_id' => null,
|
|
'request_reason' => '',
|
|
'review_due_at' => null,
|
|
]);
|
|
|
|
$pendingRenewalValid = spec354BrowserException($tenant, $user, exceptionAttributes: [
|
|
'status' => FindingException::STATUS_PENDING,
|
|
'current_validity_state' => FindingException::VALIDITY_VALID,
|
|
'decision_metadata' => [
|
|
'previous_review_due_at' => now()->addDays(10)->toIso8601String(),
|
|
'previous_expires_at' => now()->addDays(30)->toIso8601String(),
|
|
],
|
|
], decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED);
|
|
|
|
spec354AuthenticateBrowser($this, $user, $tenant);
|
|
|
|
visit(FindingExceptionResource::getUrl('view', ['record' => $incomplete], tenant: $tenant))
|
|
->resize(1440, 1100)
|
|
->waitForText(__('localization.accepted_risk_guidance.title_incomplete_governance'))
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee(__('localization.accepted_risk_guidance.detail_missing_fields_label'))
|
|
->assertSee('Renew exception')
|
|
->screenshot(true, spec354BrowserScreenshot('ui-036-exception-detail-guidance'));
|
|
|
|
spec354CopyBrowserScreenshot('ui-036-exception-detail-guidance');
|
|
|
|
visit(FindingExceptionResource::getUrl('view', ['record' => $pendingRenewalValid], tenant: $tenant))
|
|
->waitForText(__('localization.accepted_risk_guidance.title_pending_renewal'))
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee(__('localization.accepted_risk_guidance.primary_action_label'))
|
|
->assertSee(__('localization.accepted_risk_guidance.next_step_pending_renewal'))
|
|
->click(__('localization.accepted_risk_guidance.next_step_pending_renewal'))
|
|
->waitForText(__('localization.accepted_risk_guidance.title_pending_renewal'))
|
|
->assertSee($tenant->name);
|
|
});
|
|
|
|
it('smokes ready and missing-support detail semantics without fake remediation', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$ready = spec354BrowserException($tenant, $user);
|
|
|
|
$missingSupport = spec354BrowserException($tenant, $user, exceptionAttributes: [
|
|
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
|
]);
|
|
|
|
spec354AuthenticateBrowser($this, $user, $tenant);
|
|
|
|
visit(FindingExceptionResource::getUrl('view', ['record' => $ready], tenant: $tenant))
|
|
->resize(1440, 1100)
|
|
->waitForText(__('localization.accepted_risk_guidance.title_ready'))
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee(__('localization.accepted_risk_guidance.review_focus_label'))
|
|
->assertSee(__('localization.accepted_risk_guidance.impact_ready'))
|
|
->assertDontSee(__('localization.accepted_risk_guidance.primary_action_label'))
|
|
->assertDontSee(__('localization.accepted_risk_guidance.title_expired'));
|
|
|
|
visit(FindingExceptionResource::getUrl('view', ['record' => $missingSupport], tenant: $tenant))
|
|
->waitForText(__('localization.accepted_risk_guidance.title_missing_support'))
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee(__('localization.accepted_risk_guidance.review_focus_label'))
|
|
->assertSee(__('localization.accepted_risk_guidance.next_step_missing_support'))
|
|
->assertDontSee(__('localization.accepted_risk_guidance.primary_action_label'))
|
|
->assertDontSee('Fix accepted risk')
|
|
->assertDontSee('Resolve risk');
|
|
});
|