TenantAtlas/apps/platform/tests/Feature/ReviewPack/Spec347ReviewPackReadinessSemanticsTest.php
ahmido dd7139ebe3 Spec392 customer output gating (#463)
Implements Spec392 customer output gating for review pack downloads, rendered reports, management PDFs, and customer workspace CTAs.

Validation:
- php vendor/bin/pest --filter=Spec392: 12 passed / 58 assertions
- php vendor/bin/pest --filter='ReviewPack|CustomerReviewWorkspace|StoredReport': 283 passed / 1 skipped / 2053 assertions
- affected browser matrix: 12 passed / 420 assertions
- php vendor/bin/pint --dirty: pass
- git diff --check: pass

Notes:
- Deprecated limited-download semantics remain removed.
- Unsafe customer-facing output returns 403/no output.
- Internal preview/report access is operator-only.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #463
2026-06-20 20:54:50 +00:00

111 lines
4.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\GenerateReviewPackJob;
use App\Models\ReviewPack;
use App\Services\ReviewPackService;
use App\Support\ReviewPackStatus;
use Illuminate\Support\Facades\Storage;
beforeEach(function (): void {
Storage::fake('exports');
});
it('keeps section appendix files even when readiness stays limitation-aware', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
'include_pii' => false,
'include_operations' => true,
]);
app()->call([new GenerateReviewPackJob(
reviewPackId: (int) $pack->getKey(),
operationRunId: (int) $pack->operation_run_id,
), 'handle']);
$pack->refresh();
[$zip, $tempFile, $filenames] = spec347ReadinessZip($pack);
$summary = json_decode((string) $zip->getFromName('summary.json'), true, 512, JSON_THROW_ON_ERROR);
$sections = collect(json_decode((string) $zip->getFromName('sections.json'), true, 512, JSON_THROW_ON_ERROR));
$executive = (string) $zip->getFromName(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME);
$limitedSection = $sections->first(fn (array $section): bool => (string) $section['completeness_state'] !== 'complete');
expect($pack->status)->toBe(ReviewPackStatus::Ready->value)
->and(data_get($summary, 'has_ready_export'))->toBeTrue()
->and(data_get($summary, 'output_readiness.readiness_label'))->toBe('Published with limitations')
->and(data_get($summary, 'output_readiness.evidence_completeness_state'))->toBe((string) $snapshot->completeness_state)
->and((int) data_get($summary, 'output_readiness.section_summary.required_limited'))->toBeGreaterThan(0)
->and($limitedSection)->not->toBeNull();
$limitedSectionFilename = sprintf(
'sections/%02d-%s.json',
(int) $limitedSection['sort_order'],
(string) $limitedSection['section_key'],
);
expect($filenames)->toContain($limitedSectionFilename)
->and($executive)->toContain('## Limitations')
->and($executive)->toContain('incomplete evidence basis')
->and($executive)->toContain('structured appendices but are marked missing');
$zip->close();
unlink($tempFile);
});
it('distinguishes ready export from customer-safe readiness when pii is included', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$review = composeEnvironmentReviewForTest($tenant, $user);
$review = markEnvironmentReviewCustomerSafeReady($review);
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
'include_pii' => true,
'include_operations' => true,
]);
app()->call([new GenerateReviewPackJob(
reviewPackId: (int) $pack->getKey(),
operationRunId: (int) $pack->operation_run_id,
), 'handle']);
$pack->refresh();
[$zip, $tempFile] = spec347ReadinessZip($pack);
$summary = json_decode((string) $zip->getFromName('summary.json'), true, 512, JSON_THROW_ON_ERROR);
$executive = (string) $zip->getFromName(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME);
expect(data_get($summary, 'has_ready_export'))->toBeTrue()
->and(data_get($summary, 'output_readiness.readiness_state'))->toBe('internal_review_package_available')
->and(data_get($summary, 'output_readiness.customer_safe_state'))->toBe('internal_only')
->and(data_get($summary, 'output_readiness.contains_pii'))->toBeTrue()
->and($executive)->toContain('PII is included in this package');
$zip->close();
unlink($tempFile);
});
/**
* @return array{0: ZipArchive, 1: string, 2: list<string>}
*/
function spec347ReadinessZip(ReviewPack $pack): array
{
$zipContent = Storage::disk('exports')->get((string) $pack->file_path);
$tempFile = tempnam(sys_get_temp_dir(), 'spec347-readiness-pack-');
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();
return [$zip, $tempFile, $filenames];
}