146 lines
6.7 KiB
PHP
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);
|
|
});
|