TenantAtlas/apps/platform/tests/Feature/Findings/Spec354FindingExceptionDetailGuidanceTest.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

201 lines
9.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\FindingExceptionResource;
use App\Filament\Resources\FindingExceptionResource\Pages\ViewFindingException;
use App\Filament\Pages\Governance\DecisionRegister;
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\FindingExceptionDecision;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
function spec354DetailFixture(
string $role = 'owner',
array $findingAttributes = [],
array $exceptionAttributes = [],
?string $decisionType = FindingExceptionDecision::TYPE_APPROVED,
): array {
[$user, $tenant] = createUserWithTenant(role: $role);
$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) $user->getKey(),
'owner_user_id' => (int) $user->getKey(),
'approved_by_user_id' => (int) $approver->getKey(),
'status' => FindingException::STATUS_ACTIVE,
'current_validity_state' => FindingException::VALIDITY_VALID,
'request_reason' => 'Spec354 detail request',
'approval_reason' => 'Spec354 detail approval',
'requested_at' => now()->subDays(6),
'approved_at' => now()->subDays(5),
'effective_from' => now()->subDays(5),
'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) $approver->getKey(),
'decision_type' => $decisionType,
'reason' => 'Spec354 detail decision',
'metadata' => [],
'decided_at' => now()->subDays(5),
]);
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
}
test()->actingAs($user);
$tenant->makeCurrent();
Filament::setTenant($tenant, true);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
return [$user, $tenant, $finding->fresh(), $exception->fresh(['finding', 'tenant', 'owner', 'currentDecision', 'decisions.actor', 'evidenceReferences'])];
}
it('renders dominant guidance before decision history and keeps incomplete governance visible first', function (): void {
[, , , $exception] = spec354DetailFixture(exceptionAttributes: [
'owner_user_id' => null,
'request_reason' => '',
'review_due_at' => null,
]);
$component = Livewire::test(ViewFindingException::class, ['record' => $exception->getKey()])
->assertOk()
->assertSee(__('localization.accepted_risk_guidance.title_incomplete_governance'))
->assertSee(__('localization.accepted_risk_guidance.detail_missing_fields_label'))
->assertActionVisible('renew_exception')
->assertActionVisible('revoke_exception');
$html = $component->html();
expect(strpos($html, __('localization.accepted_risk_guidance.title_incomplete_governance')))
->toBeLessThan(strpos($html, 'Decision history'));
});
it('keeps readonly detail semantics aligned while hiding manage actions', function (): void {
[$user, $tenant, , $exception] = spec354DetailFixture(role: 'readonly', exceptionAttributes: [
'status' => FindingException::STATUS_EXPIRING,
'current_validity_state' => FindingException::VALIDITY_EXPIRING,
'review_due_at' => now()->addDay(),
'expires_at' => now()->addDays(2),
]);
$this->actingAs($user)
->withSession([
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
]);
$tenant->makeCurrent();
Filament::setTenant($tenant, true);
Livewire::test(ViewFindingException::class, ['record' => $exception->getKey()])
->assertOk()
->assertSee(__('localization.accepted_risk_guidance.title_expiring'))
->assertActionHidden('renew_exception')
->assertActionHidden('revoke_exception')
->assertDontSee(__('localization.accepted_risk_guidance.action_open_queue'));
});
it('keeps existing related context scoped and renders without outbound http', function (): void {
bindFailHardGraphClient();
[, $tenant, , $exception] = spec354DetailFixture();
assertNoOutboundHttp(function () use ($tenant, $exception): void {
$response = $this->get(FindingExceptionResource::getUrl('view', ['record' => $exception], tenant: $tenant));
$response->assertSuccessful()
->assertSee(__('localization.accepted_risk_guidance.title_ready'))
->assertSee(__('localization.accepted_risk_guidance.action_open_finding'))
->assertSee(__('localization.accepted_risk_guidance.action_open_queue'));
});
});
it('routes detail guidance and related-context queue links through the explicit queue filter contract', function (): void {
[, $tenant, , $exception] = spec354DetailFixture();
$originalQuery = request()->query();
$context = CanonicalNavigationContext::forDecisionRegister(
canonicalRouteName: DecisionRegister::getRouteName(),
tenantId: (int) $tenant->getKey(),
backLinkUrl: DecisionRegister::getUrl(panel: 'admin', parameters: [
'managed_environment_id' => (string) $tenant->getKey(),
]),
);
request()->query->replace($context->toQuery());
$guidance = FindingExceptionResource::acceptedRiskGuidance($exception);
$relatedContext = FindingExceptionResource::relatedContextEntries($exception);
$guidanceQueueAction = collect($guidance['secondary_actions'])->firstWhere('key', 'accepted_risk.ready.open_queue');
$relatedQueueEntry = collect($relatedContext)->firstWhere('key', 'approval_queue');
expect($guidanceQueueAction)->toBeArray()
->and($relatedQueueEntry)->toBeArray()
->and((string) $guidanceQueueAction['url'])->toContain('environment_id='.(string) $tenant->getKey())
->and((string) $guidanceQueueAction['url'])->toContain('exception='.(string) $exception->getKey())
->and((string) $guidanceQueueAction['url'])->toContain('nav%5Bsource_surface%5D=governance.decision_register')
->and((string) $guidanceQueueAction['url'])->not->toContain('tenant=')
->and((string) $relatedQueueEntry['targetUrl'])->toContain('environment_id='.(string) $tenant->getKey())
->and((string) $relatedQueueEntry['targetUrl'])->toContain('exception='.(string) $exception->getKey())
->and((string) $relatedQueueEntry['targetUrl'])->not->toContain('tenant=');
parse_str((string) parse_url((string) $guidanceQueueAction['url'], PHP_URL_QUERY), $guidanceQuery);
parse_str((string) parse_url((string) $relatedQueueEntry['targetUrl'], PHP_URL_QUERY), $relatedQuery);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
Livewire::withQueryParams($guidanceQuery)
->test(FindingExceptionsQueue::class)
->assertActionVisible('view_tenant_register')
->assertSet('selectedFindingExceptionId', (int) $exception->getKey());
Livewire::withQueryParams($relatedQuery)
->test(FindingExceptionsQueue::class)
->assertActionVisible('view_tenant_register')
->assertSet('selectedFindingExceptionId', (int) $exception->getKey());
request()->query->replace($originalQuery);
});
it('promotes pending detail guidance to a real approval-queue affordance', function (): void {
[, $tenant, , $exception] = spec354DetailFixture(
exceptionAttributes: [
'status' => FindingException::STATUS_PENDING,
'current_validity_state' => FindingException::VALIDITY_VALID,
],
decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED,
);
$guidance = FindingExceptionResource::acceptedRiskGuidance($exception);
expect($guidance['title'])->toBe(__('localization.accepted_risk_guidance.title_pending_renewal'))
->and($guidance['primary_action']['url'])->toBeString()
->and((string) $guidance['primary_action']['url'])->toContain('environment_id='.(string) $tenant->getKey())
->and((string) $guidance['primary_action']['url'])->toContain('exception='.(string) $exception->getKey())
->and($guidance['primary_action']['type'])->toBe('navigation');
});