TenantAtlas/apps/platform/tests/Feature/ReviewPack/Spec392CustomerOutputRouteGateTest.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

190 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AuditLog;
use App\Models\ReviewPack;
use App\Models\StoredReport;
use App\Services\ReviewPacks\ManagementReportPdfService;
use App\Services\ReviewPackService;
use App\Support\Audit\AuditActionId;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\ReviewPacks\CustomerOutputGate;
use App\Support\ReviewPacks\ReportProfileRegistry;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
uses(RefreshDatabase::class);
beforeEach(function (): void {
Storage::fake('exports');
});
function spec392RouteCurrentReviewPack(bool $customerSafeReady = true): array
{
[$user, $tenant] = createUserWithTenant(role: 'owner', clearCapabilityCaches: true);
$snapshot = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
$review->forceFill([
'status' => 'published',
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
$review = markEnvironmentReviewCustomerSafeReady($review);
if (! $customerSafeReady) {
restateEnvironmentReviewEvidenceSnapshot($review->evidenceSnapshot, EvidenceCompletenessState::Partial);
$review = $review->fresh(['sections', 'evidenceSnapshot']);
}
$zipBytes = 'spec392 review pack bytes';
$filePath = sprintf('review-packs/%s/spec392-current.zip', $tenant->external_id);
Storage::disk('exports')->put($filePath, $zipBytes);
$pack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'environment_review_id' => (int) $review->getKey(),
'evidence_snapshot_id' => (int) $review->evidence_snapshot_id,
'initiated_by_user_id' => (int) $user->getKey(),
'options' => [
'include_pii' => false,
'include_operations' => true,
],
'summary' => [
'governance_package' => [
'executive_summary' => 'Spec392 management-ready review pack.',
'top_findings' => [],
'accepted_risks' => [],
'decision_summary' => [
'entries' => [],
],
],
'control_interpretation' => [
'non_certification_disclosure' => 'TenantPilot summarizes available service-delivery evidence for governance review. This report is not a certification, legal attestation, audit opinion, or compliance guarantee.',
],
'delivery_bundle' => [
'executive_entrypoint_file' => 'executive-summary.md',
'appendix_files' => ['metadata.json'],
],
],
'file_disk' => 'exports',
'file_path' => $filePath,
'file_size' => strlen($zipBytes),
'sha256' => hash('sha256', $zipBytes),
'expires_at' => now()->addDay(),
]);
$review->forceFill([
'current_export_review_pack_id' => (int) $pack->getKey(),
])->save();
return [$user, $tenant, $review->fresh(['sections', 'evidenceSnapshot', 'currentExportReviewPack']), $pack->fresh(['tenant', 'environmentReview'])];
}
function spec392ReadyManagementPdf(ReviewPack $pack): StoredReport
{
$pdfBytes = '%PDF-1.7 Spec392 management report';
$filePath = sprintf('management-reports/%s/spec392-%d.pdf', $pack->tenant->external_id, (int) $pack->getKey());
Storage::disk('exports')->put($filePath, $pdfBytes);
return StoredReport::factory()->managementReportPdf([
'title' => 'Spec392 Management Report',
])->create([
'workspace_id' => (int) $pack->workspace_id,
'managed_environment_id' => (int) $pack->managed_environment_id,
'source_environment_review_id' => (int) $pack->environment_review_id,
'source_review_pack_id' => (int) $pack->getKey(),
'profile' => ReportProfileRegistry::CUSTOMER_EXECUTIVE,
'file_disk' => 'exports',
'file_path' => $filePath,
'file_size' => strlen($pdfBytes),
'sha256' => hash('sha256', $pdfBytes),
'generated_at' => now(),
]);
}
it('Spec392 blocks unsafe customer-output downloads but allows authorized internal preview', function (): void {
[$owner, $tenant, $review, $pack] = spec392RouteCurrentReviewPack(customerSafeReady: false);
[$readonly] = createUserWithTenant(
tenant: $tenant,
user: \App\Models\User::factory()->create(),
role: 'readonly',
clearCapabilityCaches: true,
);
$customerUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
'source_surface' => 'customer_review_workspace',
'review_id' => (int) $review->getKey(),
]);
$internalPreviewUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
'source_surface' => 'review_pack',
CustomerOutputGate::INTERNAL_PREVIEW_QUERY_KEY => 1,
]);
$this->actingAs($owner)->get($customerUrl)->assertForbidden();
$this->actingAs($readonly)->get($internalPreviewUrl)->assertForbidden();
$this->actingAs($owner)
->get($internalPreviewUrl)
->assertOk()
->assertHeader('X-Review-Pack-SHA256', $pack->sha256)
->assertDownload();
$audit = AuditLog::query()
->where('action', AuditActionId::ReviewPackDownloaded->value)
->latest('id')
->first();
expect($audit)->not->toBeNull()
->and(data_get($audit?->metadata, 'download_mode'))->toBe('internal_preview')
->and(data_get($audit?->metadata, 'customer_output_gate_state'))->toBe(CustomerOutputGate::STATE_NEEDS_ATTENTION);
});
it('Spec392 renders unsafe reports only on internal profile routes without customer download copy', function (): void {
[$owner, $tenant, $review, $pack] = spec392RouteCurrentReviewPack(customerSafeReady: false);
[$readonly] = createUserWithTenant(
tenant: $tenant,
user: \App\Models\User::factory()->create(),
role: 'readonly',
clearCapabilityCaches: true,
);
$customerReportUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
'source_surface' => 'customer_review_workspace',
\App\Filament\Pages\Reviews\CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
'review_id' => (int) $review->getKey(),
ReportProfileRegistry::QUERY_PARAMETER => ReportProfileRegistry::CUSTOMER_EXECUTIVE,
]);
$internalReportUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
'source_surface' => 'review_pack',
'review_id' => (int) $review->getKey(),
]);
$this->actingAs($owner)->get($customerReportUrl)->assertForbidden();
$this->actingAs($readonly)->get($internalReportUrl)->assertForbidden();
$this->actingAs($owner)
->get($internalReportUrl)
->assertOk()
->assertSee('Report with limitations')
->assertSee('Download internal preview')
->assertDontSee('Customer-safe report ready');
});
it('Spec392 blocks management PDF downloads when the source review pack is not customer safe', function (): void {
[$owner, , , $pack] = spec392RouteCurrentReviewPack(customerSafeReady: false);
$report = spec392ReadyManagementPdf($pack);
$url = app(ManagementReportPdfService::class)->generateDownloadUrl($report, [
'source_surface' => 'review_pack',
]);
$this->actingAs($owner)
->get($url)
->assertForbidden();
expect(AuditLog::query()
->where('action', AuditActionId::ManagementReportPdfDownloaded->value)
->count())->toBe(0);
});