Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 2m1s
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.
199 lines
9.8 KiB
PHP
199 lines
9.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\ReviewPackResource;
|
|
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 App\Support\ReviewPackStatus;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
beforeEach(function (): void {
|
|
Storage::fake('exports');
|
|
});
|
|
|
|
function spec308SeedPackDecisionFinding(ManagedEnvironment $tenant, User $requester, string $title): Finding
|
|
{
|
|
$approver = User::factory()->create(['name' => 'Risk Owner']);
|
|
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-pack-fingerprint',
|
|
'evidence_jsonb' => [
|
|
'display_name' => $title,
|
|
'internal_url' => 'https://tenantpilot.test/admin/operations/raw-review-pack-run',
|
|
],
|
|
]);
|
|
|
|
$requested = $exceptionService->request($finding, $tenant, $requester, [
|
|
'owner_user_id' => (int) $approver->getKey(),
|
|
'request_reason' => 'Customer owner approved temporary exception.',
|
|
'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('generates a review-derived executive pack with environment-review metadata and filtered sections', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
$review = composeEnvironmentReviewForTest($tenant, $user);
|
|
|
|
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
|
|
'include_pii' => false,
|
|
'include_operations' => false,
|
|
]);
|
|
|
|
$job = new GenerateReviewPackJob(
|
|
reviewPackId: (int) $pack->getKey(),
|
|
operationRunId: (int) $pack->operation_run_id,
|
|
);
|
|
app()->call([$job, 'handle']);
|
|
|
|
$pack->refresh();
|
|
$review->refresh()->load('evidenceSnapshot');
|
|
|
|
expect($pack->environment_review_id)->toBe((int) $review->getKey())
|
|
->and($pack->status)->toBe(ReviewPackStatus::Ready->value)
|
|
->and($pack->summary['environment_review_id'] ?? null)->toBe((int) $review->getKey())
|
|
->and($pack->summary['review_status'] ?? null)->toBe((string) $review->status)
|
|
->and($review->current_export_review_pack_id)->toBe((int) $pack->getKey())
|
|
->and(data_get($review->summary, 'has_ready_export'))->toBeTrue();
|
|
|
|
$zipContent = Storage::disk('exports')->get((string) $pack->file_path);
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'review-derived-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);
|
|
|
|
expect(data_get($metadata, 'tenant_name'))->toBe('[REDACTED]')
|
|
->and(data_get($metadata, 'delivery_bundle.entrypoint.file'))->toBe(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME)
|
|
->and(data_get($metadata, 'delivery_bundle.entrypoint.role'))->toBe('executive_entrypoint')
|
|
->and(data_get($metadata, 'delivery_bundle.appendix.0.file'))->toBe('metadata.json')
|
|
->and(data_get($metadata, 'delivery_bundle.appendix.1.file'))->toBe('summary.json')
|
|
->and(data_get($metadata, 'delivery_bundle.appendix.2.file'))->toBe('sections.json')
|
|
->and(data_get($metadata, 'options.include_operations'))->toBeFalse()
|
|
->and(data_get($summary, 'delivery_bundle.executive_entrypoint_file'))->toBe(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME)
|
|
->and(data_get($summary, 'environment_review_id'))->toBe((int) $review->getKey())
|
|
->and(collect($sections)->pluck('section_key')->all())->not->toContain('operations_health')
|
|
->and($executiveEntrypoint)->toContain('ManagedEnvironment: [REDACTED]')
|
|
->and($executiveEntrypoint)->toContain('This executive entrypoint is the first file to read')
|
|
->and($executiveEntrypoint)->not->toContain((string) $review->fingerprint)
|
|
->and($executiveEntrypoint)->not->toContain((string) $review->evidenceSnapshot?->fingerprint);
|
|
|
|
$zip->close();
|
|
unlink($tempFile);
|
|
|
|
$this->actingAs($user)
|
|
->get(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant, panel: 'admin'))
|
|
->assertOk()
|
|
->assertSee('Outcome summary')
|
|
->assertDontSee('Artifact truth')
|
|
->assertSee('#'.$review->getKey())
|
|
->assertSee('Review status');
|
|
|
|
$this->actingAs($user)
|
|
->get(app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
|
'source_surface' => 'review_pack',
|
|
'review_id' => (int) $review->getKey(),
|
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
|
]))
|
|
->assertOk()
|
|
->assertSee('Rendered review report')
|
|
->assertSee('Executive summary')
|
|
->assertSee('Evidence basis')
|
|
->assertSee('Output limitations')
|
|
->assertSee('Supporting appendix')
|
|
->assertDontSee((string) data_get($summary, 'governance_package.evidence_basis_summary'))
|
|
->assertDontSee('localization.');
|
|
});
|
|
|
|
it('includes the customer-safe decision summary in review-derived pack JSON and markdown', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(
|
|
tenant: ManagedEnvironment::factory()->create(['name' => 'Contoso Decision Tenant']),
|
|
role: 'owner',
|
|
);
|
|
spec308SeedPackDecisionFinding($tenant, $user, 'Privileged access accepted risk');
|
|
|
|
$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();
|
|
|
|
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
|
|
'include_pii' => false,
|
|
'include_operations' => false,
|
|
]);
|
|
|
|
$job = new GenerateReviewPackJob(
|
|
reviewPackId: (int) $pack->getKey(),
|
|
operationRunId: (int) $pack->operation_run_id,
|
|
);
|
|
app()->call([$job, 'handle']);
|
|
|
|
$pack->refresh();
|
|
|
|
expect(data_get($pack->summary, 'governance_package.decision_summary.status'))->toBe('requires_awareness')
|
|
->and(data_get($pack->summary, 'governance_package.decision_summary.total_count'))->toBe(1)
|
|
->and(data_get($pack->summary, 'delivery_bundle.contract'))->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT);
|
|
|
|
$zipContent = Storage::disk('exports')->get((string) $pack->file_path);
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'review-derived-pack-decisions-');
|
|
file_put_contents($tempFile, $zipContent);
|
|
|
|
$zip = new ZipArchive;
|
|
$zip->open($tempFile);
|
|
|
|
$filenames = collect(range(0, $zip->numFiles - 1))
|
|
->map(fn (int $index): string => (string) $zip->getNameIndex($index))
|
|
->values()
|
|
->all();
|
|
$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);
|
|
$executiveEntrypoint = (string) $zip->getFromName(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME);
|
|
$summaryJson = json_encode($summary, JSON_THROW_ON_ERROR);
|
|
|
|
expect($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(data_get($metadata, 'delivery_bundle.contract'))->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT)
|
|
->and(data_get($summary, 'delivery_bundle.contract'))->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT)
|
|
->and(data_get($summary, 'governance_package.decision_summary.status'))->toBe('requires_awareness')
|
|
->and(data_get($summary, 'governance_package.decision_summary.total_count'))->toBe(1)
|
|
->and(data_get($summary, 'governance_package.decision_summary.entries.0.title'))->toBe('Privileged access accepted risk')
|
|
->and($executiveEntrypoint)->toContain('## Governance decisions requiring awareness')
|
|
->and($executiveEntrypoint)->toContain('1 governance decision requires customer awareness')
|
|
->and($executiveEntrypoint)->toContain('Privileged access accepted risk')
|
|
->and($executiveEntrypoint)->toContain('Review the accepted-risk decision basis before customer delivery.')
|
|
->and($summaryJson)->not->toContain('Contoso Decision Tenant', 'Risk Owner', 'spec-308-pack-fingerprint', 'raw-review-pack-run')
|
|
->and($executiveEntrypoint)->not->toContain('Contoso Decision Tenant', 'Risk Owner', 'spec-308-pack-fingerprint', 'raw-review-pack-run', 'OperationRun URL');
|
|
|
|
$zip->close();
|
|
unlink($tempFile);
|
|
});
|