TenantAtlas/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php
ahmido 9cd06e8b66 feat: review pack pdf and html renderer v1 (spec 356) (#427)
Implemented the first version of the PDF and HTML renderer for review packs. Added ReviewPackRenderedReportController and related blade views to render reports. Updated EnvironmentReviewResource, ReviewPackResource, ReviewPackService, and routing. Added new tests for the renderer and download actions, and updated UI documentation.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #427
2026-06-05 20:39:13 +00:00

234 lines
12 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\EnvironmentReviewResource;
use App\Filament\Widgets\ManagedEnvironment\ManagedEnvironmentReviewPackCard;
use App\Jobs\GenerateReviewPackJob;
use App\Models\Finding;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Findings\FindingExceptionService;
use App\Services\Findings\FindingRiskGovernanceResolver;
use App\Services\ReviewPackService;
use App\Support\EnvironmentReviewStatus;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
beforeEach(function (): void {
Storage::fake('exports');
});
function spec308SeedExpiredDecisionFinding(ManagedEnvironment $tenant, User $requester, string $title): Finding
{
$approver = User::factory()->create(['name' => 'Spec 308 Approver']);
createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager');
/** @var FindingExceptionService $exceptionService */
$exceptionService = app(FindingExceptionService::class);
$finding = Finding::factory()->for($tenant)->riskAccepted()->create([
'fingerprint' => 'spec-308-raw-fingerprint-'.$title,
'evidence_jsonb' => [
'display_name' => $title,
'internal_url' => 'https://tenantpilot.test/admin/operations/raw-run',
],
]);
$requested = $exceptionService->request($finding, $tenant, $requester, [
'owner_user_id' => (int) $requester->getKey(),
'request_reason' => 'Temporary exception for staged remediation.',
'review_due_at' => now()->addDays(5)->toDateTimeString(),
'expires_at' => now()->addDays(14)->toDateTimeString(),
]);
$exceptionService->approve($requested, $approver, [
'effective_from' => now()->subDays(10)->toDateTimeString(),
'expires_at' => now()->subDay()->toDateTimeString(),
'approval_reason' => 'Approved with customer controls.',
]);
app(FindingRiskGovernanceResolver::class)->syncExceptionState($finding->findingException()->firstOrFail());
return $finding->refresh();
}
it('renders an executive-ready environment review and exports a pack with matching section order and summary truth', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$review = composeEnvironmentReviewForTest($tenant, $user);
$this->actingAs($user)
->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant))
->assertOk()
->assertSee('Executive posture')
->assertSee('Executive summary')
->assertSee('Open risk highlights')
->assertSee('Permission posture')
->assertSee('Publication readiness');
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
'include_pii' => true,
'include_operations' => true,
]);
$job = new GenerateReviewPackJob(
reviewPackId: (int) $pack->getKey(),
operationRunId: (int) $pack->operation_run_id,
);
app()->call([$job, 'handle']);
$pack->refresh();
$review->refresh()->load(['sections', 'evidenceSnapshot']);
$zipContent = Storage::disk('exports')->get((string) $pack->file_path);
$tempFile = tempnam(sys_get_temp_dir(), 'environment-review-pack-');
file_put_contents($tempFile, $zipContent);
$zip = new ZipArchive;
$zip->open($tempFile);
$metadata = json_decode((string) $zip->getFromName('metadata.json'), true, 512, JSON_THROW_ON_ERROR);
$summary = json_decode((string) $zip->getFromName('summary.json'), true, 512, JSON_THROW_ON_ERROR);
$sections = json_decode((string) $zip->getFromName('sections.json'), true, 512, JSON_THROW_ON_ERROR);
$executiveEntrypoint = (string) $zip->getFromName(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME);
$filenames = collect(range(0, $zip->numFiles - 1))
->map(fn (int $index): string => (string) $zip->getNameIndex($index))
->values()
->all();
expect(array_column($sections, 'section_key'))
->toBe($review->sections->pluck('section_key')->values()->all())
->and($summary['highlights'] ?? null)->toBe($review->summary['highlights'] ?? [])
->and($summary['recommended_next_actions'] ?? null)->toBe($review->summary['recommended_next_actions'] ?? [])
->and($summary['delivery_bundle']['contract'] ?? null)->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT)
->and($summary['delivery_bundle']['executive_entrypoint_file'] ?? null)->toBe(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME)
->and($metadata['delivery_bundle']['contract'] ?? null)->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT)
->and($metadata['delivery_bundle']['review_pack_id'] ?? null)->toBe((int) $pack->getKey())
->and($metadata['delivery_bundle']['released_review']['id'] ?? null)->toBe((int) $review->getKey())
->and($metadata['delivery_bundle']['interpretation_version'] ?? null)->toBe($review->controlInterpretationVersion())
->and($metadata['delivery_bundle']['entrypoint']['file'] ?? null)->toBe(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME)
->and(array_slice(collect($metadata['delivery_bundle']['appendix'] ?? [])->pluck('file')->all(), 0, 3))->toBe(['metadata.json', 'summary.json', 'sections.json'])
->and(collect($metadata['delivery_bundle']['appendix'] ?? [])->pluck('file')->all())->toContain('sections/10-executive_summary.json')
->and($filenames)->toContain('metadata.json', 'summary.json', 'sections.json', ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME)
->and(collect($filenames)->filter(fn (string $filename): bool => str_starts_with($filename, 'executive-'))->values()->all())->toBe([ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME])
->and($executiveEntrypoint)->toContain('# Executive summary')
->and($executiveEntrypoint)->toContain('## Executive story')
->and($executiveEntrypoint)->toContain('## Structured auditor appendix')
->and($executiveEntrypoint)->toContain('metadata.json, summary.json, and sections.json')
->and($executiveEntrypoint)->not->toContain((string) $review->fingerprint)
->and($executiveEntrypoint)->not->toContain((string) $review->evidenceSnapshot?->fingerprint)
->and($executiveEntrypoint)->not->toContain('Reason owner')
->and($executiveEntrypoint)->not->toContain('Platform reason family');
$zip->close();
unlink($tempFile);
setAdminEnvironmentContext($tenant);
Livewire::actingAs($user)
->test(ManagedEnvironmentReviewPackCard::class, ['record' => $tenant])
->assertSee('View review');
$this->actingAs($user)
->get(app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
'source_surface' => 'environment_review_detail',
'review_id' => (int) $review->getKey(),
'interpretation_version' => $review->controlInterpretationVersion(),
]))
->assertOk()
->assertSee('Rendered review report')
->assertSee((string) data_get($summary, 'governance_package.executive_summary'))
->assertSee('Structured auditor appendix')
->assertDontSee('Platform reason family');
});
it('builds an explicit customer-safe decision summary for released review consumption', function (): void {
[$user, $tenant] = createUserWithTenant(
tenant: ManagedEnvironment::factory()->create(['name' => 'Visible Customer Environment']),
role: 'owner',
);
spec308SeedExpiredDecisionFinding($tenant, $user, 'Privileged role exception');
$hiddenTenant = ManagedEnvironment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'name' => 'Hidden Customer Environment',
]);
[$hiddenUser, $hiddenTenant] = createUserWithTenant(tenant: $hiddenTenant, role: 'owner');
spec308SeedExpiredDecisionFinding($hiddenTenant, $hiddenUser, 'Hidden tenant exception');
$snapshot = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
$decisionSummary = data_get($review->fresh(), 'summary.governance_package.decision_summary');
expect($decisionSummary)->toBeArray()
->and($decisionSummary['status'] ?? null)->toBe('requires_awareness')
->and($decisionSummary['total_count'] ?? null)->toBe(1)
->and($decisionSummary['summary'] ?? null)->toContain('1 governance decision requires customer awareness')
->and($decisionSummary['next_action'] ?? null)->toBe('Review the accepted-risk decision basis before customer delivery.')
->and(data_get($decisionSummary, 'entries.0.title'))->toBe('Privileged role exception')
->and(data_get($decisionSummary, 'entries.0.awareness_reason'))->toContain('expired')
->and(data_get($decisionSummary, 'entries.0.next_action'))->toBe('Confirm whether this accepted risk should be renewed, remediated, or removed before relying on the review.')
->and(json_encode($decisionSummary, JSON_THROW_ON_ERROR))->not->toContain(
'Hidden tenant exception',
'spec-308-raw-fingerprint',
'raw-run',
'OperationRun',
'Reason owner',
'Platform reason family',
);
$this->actingAs($user)
->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant).'?customer_workspace=1&source_surface=customer_review_workspace')
->assertOk()
->assertSee('Governance decisions requiring awareness')
->assertSee('Privileged role exception')
->assertSee('Review the accepted-risk decision basis before customer delivery.')
->assertDontSee('Hidden tenant exception')
->assertDontSee('raw-run')
->assertDontSeeText('Approve exception')
->assertDontSeeText('Reject exception')
->assertDontSeeText('Renew exception')
->assertDontSeeText('Revoke exception');
});
it('distinguishes no decision awareness from incomplete decision evidence', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$reviewWithNoDecisions = composeEnvironmentReviewForTest(
tenant: $tenant,
user: $user,
snapshot: seedEnvironmentReviewEvidence($tenant, findingCount: 2, driftCount: 0),
);
$noDecisionSummary = data_get($reviewWithNoDecisions, 'summary.governance_package.decision_summary');
expect($noDecisionSummary)->toBeArray()
->and($noDecisionSummary['status'] ?? null)->toBe('none')
->and($noDecisionSummary['total_count'] ?? null)->toBe(0)
->and($noDecisionSummary['summary'] ?? null)->toBe('No governance decisions require customer awareness in this released review.');
$partialTenant = ManagedEnvironment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'name' => 'Partial Evidence Environment',
]);
createUserWithTenant(tenant: $partialTenant, user: $user, role: 'owner');
$partialReview = composeEnvironmentReviewForTest(
tenant: $partialTenant,
user: $user,
snapshot: seedPartialEnvironmentReviewEvidence($partialTenant, findingCount: 0, driftCount: 0),
);
$unavailableSummary = data_get($partialReview, 'summary.governance_package.decision_summary');
expect($unavailableSummary)->toBeArray()
->and($unavailableSummary['status'] ?? null)->toBe('unavailable')
->and($unavailableSummary['total_count'] ?? null)->toBe(0)
->and($unavailableSummary['summary'] ?? null)->toContain('Decision evidence is incomplete');
});