TenantAtlas/apps/platform/tests/Feature/Findings/FindingOutcomeSummaryReportingTest.php
Ahmed Darrazi 807578dd9c
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m16s
feat: implement finding outcome taxonomy
2026-04-23 09:24:59 +02:00

146 lines
6.7 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\Reviews\ReviewRegister;
use App\Filament\Resources\TenantReviewResource;
use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Services\Evidence\EvidenceSnapshotService;
use App\Services\Evidence\Sources\FindingsSummarySource;
use App\Services\ReviewPackService;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\Findings\FindingOutcomeSemantics;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
function seedFindingOutcomeMatrix(\App\Models\Tenant $tenant): array
{
return [
'pending_verification' => Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_RESOLVED,
'resolved_reason' => Finding::RESOLVE_REASON_REMEDIATED,
]),
'verified_cleared' => Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_RESOLVED,
'resolved_reason' => Finding::RESOLVE_REASON_NO_LONGER_DRIFTING,
]),
'closed_duplicate' => Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_CLOSED,
'closed_reason' => Finding::CLOSE_REASON_DUPLICATE,
]),
'risk_accepted' => Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_RISK_ACCEPTED,
'closed_reason' => Finding::CLOSE_REASON_ACCEPTED_RISK,
]),
];
}
function materializeFindingOutcomeSnapshot(\App\Models\Tenant $tenant): EvidenceSnapshot
{
$payload = app(EvidenceSnapshotService::class)->buildSnapshotPayload($tenant);
$snapshot = EvidenceSnapshot::query()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'status' => EvidenceSnapshotStatus::Active->value,
'fingerprint' => $payload['fingerprint'],
'completeness_state' => $payload['completeness'],
'summary' => $payload['summary'],
'generated_at' => now(),
]);
foreach ($payload['items'] as $item) {
$snapshot->items()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'dimension_key' => $item['dimension_key'],
'state' => $item['state'],
'required' => $item['required'],
'source_kind' => $item['source_kind'],
'source_record_type' => $item['source_record_type'],
'source_record_id' => $item['source_record_id'],
'source_fingerprint' => $item['source_fingerprint'],
'measured_at' => $item['measured_at'],
'freshness_at' => $item['freshness_at'],
'summary_payload' => $item['summary_payload'],
'sort_order' => $item['sort_order'],
]);
}
return $snapshot->load('items');
}
it('summarizes canonical terminal outcomes and report buckets from findings evidence', function (): void {
[, $tenant] = createUserWithTenant(role: 'owner');
$findings = seedFindingOutcomeMatrix($tenant);
$summary = app(FindingsSummarySource::class)->collect($tenant)['summary_payload'] ?? [];
expect(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_RESOLVED_PENDING_VERIFICATION))->toBe(1)
->and(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED))->toBe(1)
->and(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_CLOSED_DUPLICATE))->toBe(1)
->and(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_RISK_ACCEPTED))->toBe(1)
->and(data_get($summary, 'report_bucket_counts.remediation_pending_verification'))->toBe(1)
->and(data_get($summary, 'report_bucket_counts.remediation_verified'))->toBe(1)
->and(data_get($summary, 'report_bucket_counts.administrative_closure'))->toBe(1)
->and(data_get($summary, 'report_bucket_counts.accepted_risk'))->toBe(1);
$pendingEntry = collect($summary['entries'] ?? [])->firstWhere('id', (int) $findings['pending_verification']->getKey());
$verifiedEntry = collect($summary['entries'] ?? [])->firstWhere('id', (int) $findings['verified_cleared']->getKey());
expect(data_get($pendingEntry, 'terminal_outcome.key'))->toBe(FindingOutcomeSemantics::OUTCOME_RESOLVED_PENDING_VERIFICATION)
->and(data_get($pendingEntry, 'terminal_outcome.verification_state'))->toBe(FindingOutcomeSemantics::VERIFICATION_PENDING)
->and(data_get($verifiedEntry, 'terminal_outcome.key'))->toBe(FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED)
->and(data_get($verifiedEntry, 'terminal_outcome.verification_state'))->toBe(FindingOutcomeSemantics::VERIFICATION_VERIFIED);
});
it('propagates finding outcome summaries into evidence snapshots tenant reviews and review packs', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
seedFindingOutcomeMatrix($tenant);
$snapshot = materializeFindingOutcomeSnapshot($tenant);
expect(data_get($snapshot->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_RESOLVED_PENDING_VERIFICATION))->toBe(1)
->and(data_get($snapshot->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED))->toBe(1)
->and(data_get($snapshot->summary, 'finding_report_buckets.accepted_risk'))->toBe(1);
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
expect(data_get($review->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_CLOSED_DUPLICATE))->toBe(1)
->and(data_get($review->summary, 'finding_report_buckets.administrative_closure'))->toBe(1);
setTenantPanelContext($tenant);
$this->actingAs($user)
->get(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant))
->assertOk()
->assertSee('Terminal outcomes:')
->assertSee('resolved pending verification')
->assertSee('verified cleared')
->assertSee('closed as duplicate')
->assertSee('risk accepted');
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
Livewire::actingAs($user)
->test(ReviewRegister::class)
->assertCanSeeTableRecords([$review])
->assertSee('Terminal outcomes:')
->assertSee('resolved pending verification');
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
'include_pii' => false,
'include_operations' => false,
]);
expect(data_get($pack->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_RISK_ACCEPTED))->toBe(1)
->and(data_get($pack->summary, 'finding_report_buckets.accepted_risk'))->toBe(1);
});