TenantAtlas/apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php
Ahmed Darrazi 58c0064cb0
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m1s
feat: implement management report layout branded report themes
2026-06-08 05:05:36 +02:00

607 lines
25 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\ReviewPack;
use App\Models\AuditLog;
use App\Models\OperationRun;
use App\Models\PlatformUser;
use App\Services\Graph\GraphClientInterface;
use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver;
use App\Services\ReviewPackService;
use App\Services\Settings\SettingsWriter;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Audit\AuditActionId;
use App\Support\Auth\PlatformCapabilities;
use App\Support\ReviewPackStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Tests\Support\FailHardGraphClient;
uses(RefreshDatabase::class);
beforeEach(function (): void {
Storage::fake('exports');
});
// ─── Helper ──────────────────────────────────────────────────
function createReadyPackWithFile(?array $packOverrides = []): array
{
[$user, $tenant] = createUserWithTenant();
$filePath = 'review-packs/'.$tenant->external_id.'/test.zip';
$zipContents = reviewPackDownloadTestZipContents([
'executive-summary.md' => 'Ready review pack download fixture.',
]);
Storage::disk('exports')->put($filePath, $zipContents);
$pack = ReviewPack::factory()->ready()->create(array_merge([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'initiated_by_user_id' => (int) $user->getKey(),
'file_path' => $filePath,
'file_disk' => 'exports',
'file_size' => strlen($zipContents),
'sha256' => hash('sha256', $zipContents),
], $packOverrides));
return [$user, $tenant, $pack];
}
/**
* @param array<string, string> $files
*/
function reviewPackDownloadTestZipContents(array $files = []): string
{
$tempFile = tempnam(sys_get_temp_dir(), 'review-pack-download-test-');
if ($tempFile === false) {
throw new RuntimeException('Failed to allocate a temporary review pack test archive.');
}
try {
$zip = new \ZipArchive();
$result = $zip->open($tempFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
if ($result !== true) {
throw new RuntimeException("Failed to create review pack test archive: error code {$result}");
}
foreach (array_replace([
'metadata.json' => json_encode(['fixture' => 'review-pack-download-test'], JSON_THROW_ON_ERROR),
'summary.json' => json_encode(['status' => 'ready'], JSON_THROW_ON_ERROR),
], $files) as $filename => $contents) {
$zip->addFromString($filename, $contents);
}
if ($zip->close() !== true) {
throw new RuntimeException('Failed to finalize review pack test archive.');
}
$contents = file_get_contents($tempFile);
if (! is_string($contents) || $contents === '') {
throw new RuntimeException('Failed to read review pack test archive contents.');
}
return $contents;
} finally {
if (file_exists($tempFile)) {
unlink($tempFile);
}
}
}
function createCurrentReviewPackForRenderedReport(
?array $packOverrides = [],
bool $customerSafeReady = false,
?\App\Models\EvidenceSnapshot $snapshot = null,
): array
{
$packOverrides ??= [];
$tenant = \App\Models\ManagedEnvironment::factory()->create();
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$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();
if ($customerSafeReady) {
$review = markEnvironmentReviewCustomerSafeReady($review);
}
$filePath = 'review-packs/'.$tenant->external_id.'/rendered-report.zip';
$zipContents = reviewPackDownloadTestZipContents([
'executive-summary.md' => 'Rendered report download fixture.',
]);
Storage::disk('exports')->put($filePath, $zipContents);
$summary = array_replace_recursive([
'governance_package' => [
'executive_summary' => 'The released review is ready for management handoff.',
'evidence_basis_summary' => 'The report is anchored to the current released evidence snapshot.',
'top_findings' => [],
'accepted_risks' => [],
'decision_summary' => [
'status' => 'none',
'summary' => '',
'next_action' => '',
'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.',
],
'recommended_next_actions' => [],
'delivery_bundle' => [
'executive_entrypoint_file' => 'executive-summary.md',
'appendix_files' => ['metadata.json', 'summary.json', 'sections.json'],
],
], is_array($packOverrides['summary'] ?? null) ? $packOverrides['summary'] : []);
$packAttributes = array_merge([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'environment_review_id' => (int) $review->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'options' => [
'include_pii' => false,
'include_operations' => true,
],
'summary' => $summary,
'file_path' => $filePath,
'file_disk' => 'exports',
'file_size' => strlen($zipContents),
'sha256' => hash('sha256', $zipContents),
'expires_at' => now()->addDay(),
], $packOverrides);
$packAttributes['summary'] = $summary;
$pack = ReviewPack::factory()->ready()->create($packAttributes);
$review->forceFill([
'current_export_review_pack_id' => (int) $pack->getKey(),
])->save();
return [$user, $tenant, $review->fresh(['sections', 'evidenceSnapshot', 'currentExportReviewPack']), $pack->fresh()];
}
function suspendReadyPackWorkspaceForDownloadTest(ReviewPack $pack): void
{
app(SettingsWriter::class)->updateWorkspaceCommercialLifecycle(
actor: PlatformUser::factory()->create([
'capabilities' => [
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
PlatformCapabilities::DIRECTORY_VIEW,
PlatformCapabilities::COMMERCIAL_LIFECYCLE_MANAGE,
],
'is_active' => true,
]),
workspace: $pack->workspace,
state: WorkspaceCommercialLifecycleResolver::STATE_SUSPENDED_READ_ONLY,
reason: 'Download preservation test',
);
}
// ─── Happy Path: Signed URL → 200 ───────────────────────────
it('downloads a ready pack via signed URL with correct headers', function (): void {
[$user, $tenant, $pack] = createReadyPackWithFile();
$signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
'source_surface' => 'customer_review_workspace',
'review_id' => '789',
'tenant_filter_id' => (string) $tenant->getKey(),
'interpretation_version' => 'compliance_evidence_mapping.v1',
]);
$packCount = ReviewPack::query()->count();
$operationRunCount = OperationRun::query()->count();
$response = $this->actingAs($user)->get($signedUrl);
$response->assertOk();
$response->assertHeader('X-Review-Pack-SHA256', $pack->sha256);
$response->assertDownload();
$audit = AuditLog::query()
->where('action', AuditActionId::ReviewPackDownloaded->value)
->latest('id')
->first();
expect($audit)->not->toBeNull()
->and($audit?->resource_type)->toBe('review_pack')
->and(data_get($audit?->metadata, 'review_pack_id'))->toBe((int) $pack->getKey())
->and(data_get($audit?->metadata, 'source_surface'))->toBe('customer_review_workspace')
->and(data_get($audit?->metadata, 'review_id'))->toBe('789')
->and(data_get($audit?->metadata, 'tenant_filter_id'))->toBe((string) $tenant->getKey())
->and(data_get($audit?->metadata, 'interpretation_version'))->toBe('compliance_evidence_mapping.v1')
->and(ReviewPack::query()->count())->toBe($packCount)
->and(OperationRun::query()->count())->toBe($operationRunCount);
});
it('keeps ready pack downloads available while the workspace is suspended read-only', function (): void {
[$user, $tenant, $pack] = createReadyPackWithFile();
suspendReadyPackWorkspaceForDownloadTest($pack);
$signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
'source_surface' => 'suspended_read_only_check',
]);
$response = $this->actingAs($user)->get($signedUrl);
$response->assertOk();
$response->assertHeader('X-Review-Pack-SHA256', $pack->sha256);
$response->assertDownload();
});
it('renders the current review pack as a customer-safe management report via signed URL without creating a download audit event or provider calls', function (): void {
[$user, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport(customerSafeReady: true);
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
'source_surface' => 'review_pack',
'review_id' => (int) $review->getKey(),
'interpretation_version' => $review->controlInterpretationVersion(),
]);
$this->app->instance(GraphClientInterface::class, new FailHardGraphClient());
$packCount = ReviewPack::query()->count();
$operationRunCount = OperationRun::query()->count();
expect(\App\Filament\Resources\EnvironmentReviewResource::renderedReportActionLabelFor($review))->toBe('View customer-safe report')
->and(\App\Filament\Resources\ReviewPackResource::downloadActionLabelFor($pack))->toBe('Download customer-safe review pack');
$response = $this->actingAs($user)->get($signedUrl);
$content = (string) $response->getContent();
$toolbarPosition = strpos($content, 'data-testid="rendered-report-toolbar"');
$canvasPosition = strpos($content, 'data-testid="rendered-report-canvas"');
$response->assertOk()
->assertSee('Rendered review report')
->assertSee('Customer-safe report ready')
->assertSee('Executive summary')
->assertSee('Overall state')
->assertSee('Reason')
->assertSee('Impact')
->assertSee('Recommended next action')
->assertSee('Prepared by '.$tenant->workspace->name.' for '.$tenant->name)
->assertSee('Generated by TenantPilot')
->assertSee('Download customer-safe review pack')
->assertSee('No open risks are listed for this review.')
->assertSee('No accepted risks are listed for this review.')
->assertSee('No governance decisions require customer awareness in this released review.')
->assertSee('Evidence basis')
->assertSee('Supporting appendix')
->assertSee('Non-certification disclosure')
->assertSee('@media print', false)
->assertSee('.report-toolbar, .screen-only { display: none !important; }', false)
->assertSee((string) data_get($pack->summary, 'governance_package.executive_summary'))
->assertDontSee('Platform reason family')
->assertDontSee('localization.')
->assertDontSee('Evidence state:')
->assertDontSee('Section completeness:')
->assertDontSee('Total findings')
->assertDontSee('Certified report')
->assertDontSee('Approved compliance report')
->assertDontSee('Share with customer')
->assertDontSee('Do not share externally before review.')
->assertDontSee((string) $review->fingerprint);
expect($toolbarPosition)->not->toBeFalse()
->and($canvasPosition)->not->toBeFalse()
->and($toolbarPosition)->toBeLessThan($canvasPosition)
->and(AuditLog::query()->where('action', AuditActionId::ReviewPackDownloaded->value)->count())->toBe(0)
->and(ReviewPack::query()->count())->toBe($packCount)
->and(OperationRun::query()->count())->toBe($operationRunCount);
});
it('renders limitations near the top without claiming customer-safe readiness', function (): void {
[$user, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport(customerSafeReady: true);
restateEnvironmentReviewEvidenceSnapshot($review->evidenceSnapshot, EvidenceCompletenessState::Partial);
$review = $review->fresh(['sections', 'evidenceSnapshot', 'currentExportReviewPack']);
$pack = $pack->fresh(['environmentReview']);
expect(\App\Filament\Resources\EnvironmentReviewResource::renderedReportActionLabelFor($review))->toBe('View report with limitations')
->and(\App\Filament\Resources\ReviewPackResource::downloadActionLabelFor($pack))->toBe('Download review pack with limitations');
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
'source_surface' => 'review_pack',
'review_id' => (int) $review->getKey(),
'interpretation_version' => $review->controlInterpretationVersion(),
]);
$response = $this->actingAs($user)->get($signedUrl);
$content = (string) $response->getContent();
$response->assertOk()
->assertSee('Report with limitations')
->assertSee('Do not share externally before review.')
->assertSee('Output limitations')
->assertSee('Download review pack with limitations')
->assertSee('The evidence basis is incomplete, stale, or missing.')
->assertSee('Review or refresh the evidence basis before external sharing.')
->assertSee('This report is anchored to Evidence snapshot #')
->assertDontSee('Customer-safe report ready')
->assertDontSee('Customer-ready report')
->assertDontSee('Certified report')
->assertDontSee('Approved compliance report')
->assertDontSee('Share with customer')
->assertDontSee('localization.');
expect(strpos($content, 'data-testid="rendered-report-output-limitations"'))
->toBeLessThan(strpos($content, 'data-testid="rendered-report-evidence-basis"'));
});
it('renders internal pii reports with a visible internal warning and qualified labels', function (): void {
[$user, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport(
packOverrides: [
'options' => [
'include_pii' => true,
'include_operations' => true,
],
],
customerSafeReady: true,
);
expect(\App\Filament\Resources\EnvironmentReviewResource::renderedReportActionLabelFor($review))->toBe('View internal report')
->and(\App\Filament\Resources\ReviewPackResource::downloadActionLabelFor($pack))->toBe('Download internal review pack');
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
'source_surface' => 'review_pack',
'review_id' => (int) $review->getKey(),
'interpretation_version' => $review->controlInterpretationVersion(),
]);
$this->actingAs($user)
->get($signedUrl)
->assertOk()
->assertSee('Internal report with limitations')
->assertSee('Do not share externally before review.')
->assertSee('This output includes internal or PII-bearing detail.')
->assertSee('Download internal review pack')
->assertDontSee('Customer-safe report ready')
->assertDontSee('Customer-ready report')
->assertDontSee('Certified report')
->assertDontSee('Approved compliance report')
->assertDontSee('Share with customer')
->assertDontSee('localization.');
});
it('renders localized report chrome and copy without exposing localization keys', function (): void {
[$user, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport(customerSafeReady: true);
$user->forceFill(['preferred_locale' => 'de'])->save();
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
'source_surface' => 'review_pack',
'review_id' => (int) $review->getKey(),
'interpretation_version' => $review->controlInterpretationVersion(),
]);
$this->actingAs($user)
->get($signedUrl)
->assertOk()
->assertSee('Kundensicherer Bericht bereit')
->assertSee('Executive-Zusammenfassung')
->assertSee('Erstellt von '.$tenant->workspace->name.' für '.$tenant->name)
->assertSee('Unterstützender Anhang')
->assertSee('Nicht-Zertifizierungs-Offenlegung')
->assertDontSee('localization.');
});
it('renders accepted risks from customer-safe summaries without leaking internal rationale', function (): void {
[$user, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport(
packOverrides: [
'summary' => [
'governance_package' => [
'accepted_risks' => [
[
'title' => 'MFA exception accepted during migration',
'governance_state' => 'expiring_exception',
'customer_safe_summary' => 'The exception is time-bound and tracked for customer awareness.',
'summary' => 'Internal committee rationale must stay internal.',
'owner_label' => 'Customer Success Lead',
'expires_at' => '2026-07-15',
],
[
'title' => 'Legacy device exception',
'governance_state' => 'expired_exception',
'summary' => 'Internal rationale without customer-safe copy.',
'owner_label' => 'Security Operations',
'review_due_at' => '2026-06-01',
],
],
],
],
],
customerSafeReady: true,
);
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
'source_surface' => 'review_pack',
'review_id' => (int) $review->getKey(),
'interpretation_version' => $review->controlInterpretationVersion(),
]);
$this->actingAs($user)
->get($signedUrl)
->assertOk()
->assertSee('MFA exception accepted during migration')
->assertSee('Expiring soon')
->assertSee('Expires on 2026-07-15')
->assertSee('Owner: Customer Success Lead')
->assertSee('The exception is time-bound and tracked for customer awareness.')
->assertSee('Legacy device exception')
->assertSee('Expired')
->assertSee('Review due on 2026-06-01')
->assertSee('A customer-safe accepted-risk summary is not recorded.')
->assertDontSee('Internal committee rationale must stay internal.')
->assertDontSee('Internal rationale without customer-safe copy.')
->assertDontSee('Certified report')
->assertDontSee('Approved compliance report')
->assertDontSee('localization.');
});
it('returns 404 for a rendered report when the pack is no longer the current export', function (): void {
[$user, $tenant, $review] = createCurrentReviewPackForRenderedReport();
$oldPack = 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(),
'expires_at' => now()->addDay(),
]);
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($oldPack, [
'source_surface' => 'review_pack',
'review_id' => (int) $review->getKey(),
]);
$this->actingAs($user)
->get($signedUrl)
->assertNotFound();
});
it('returns 404 for a rendered report when the user is not a tenant member', function (): void {
[$owner, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport();
$otherTenant = \App\Models\ManagedEnvironment::factory()->create();
[$otherUser] = createUserWithTenant(tenant: $otherTenant, role: 'owner');
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
'source_surface' => 'review_pack',
'review_id' => (int) $review->getKey(),
]);
$this->actingAs($otherUser)
->get($signedUrl)
->assertNotFound();
});
// ─── Expired Signature → 403 ────────────────────────────────
it('rejects requests with an expired signature', function (): void {
[$user, $tenant, $pack] = createReadyPackWithFile();
// Generate a signed URL that expires immediately
$signedUrl = URL::signedRoute(
'admin.review-packs.download',
['reviewPack' => $pack->getKey()],
now()->subMinute(),
);
$response = $this->actingAs($user)->get($signedUrl);
$response->assertForbidden();
});
// ─── Expired Pack → 404 ─────────────────────────────────────
it('returns 404 for an expired pack', function (): void {
[$user, $tenant, $pack] = createReadyPackWithFile([
'status' => ReviewPackStatus::Expired->value,
]);
$signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack);
$response = $this->actingAs($user)->get($signedUrl);
$response->assertNotFound();
});
// ─── Non-Ready Pack → 404 ───────────────────────────────────
it('returns 404 for a queued pack', function (): void {
[$user, $tenant] = createUserWithTenant();
$pack = ReviewPack::factory()->queued()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'initiated_by_user_id' => (int) $user->getKey(),
]);
$signedUrl = URL::signedRoute(
'admin.review-packs.download',
['reviewPack' => $pack->getKey()],
now()->addHour(),
);
$response = $this->actingAs($user)->get($signedUrl);
$response->assertNotFound();
});
// ─── Non-Existent Pack → 404 ────────────────────────────────
it('returns 404 for a non-existent pack', function (): void {
[$user, $tenant] = createUserWithTenant();
$signedUrl = URL::signedRoute(
'admin.review-packs.download',
['reviewPack' => 99999],
now()->addHour(),
);
$response = $this->actingAs($user)->get($signedUrl);
$response->assertNotFound();
});
// ─── Past Expiry Date → 404 ─────────────────────────────────
it('returns 404 when pack status is ready but expires_at is in the past', function (): void {
[$user, $tenant, $pack] = createReadyPackWithFile([
'expires_at' => now()->subDay(),
]);
$signedUrl = URL::signedRoute(
'admin.review-packs.download',
['reviewPack' => $pack->getKey()],
now()->addHour(),
);
$response = $this->actingAs($user)->get($signedUrl);
$response->assertNotFound();
});
// ─── Missing File on Disk → 404 ─────────────────────────────
it('returns 404 when file does not exist on disk', function (): void {
[$user, $tenant] = createUserWithTenant();
$pack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'initiated_by_user_id' => (int) $user->getKey(),
'file_path' => 'review-packs/does-not-exist.zip',
'file_disk' => 'exports',
]);
$signedUrl = URL::signedRoute(
'admin.review-packs.download',
['reviewPack' => $pack->getKey()],
now()->addHour(),
);
$response = $this->actingAs($user)->get($signedUrl);
$response->assertNotFound();
});
// ─── Unsigned URL → 403 ─────────────────────────────────────
it('returns 403 for an unsigned URL', function (): void {
[$user, $tenant, $pack] = createReadyPackWithFile();
$response = $this->actingAs($user)->get(
route('admin.review-packs.download', ['reviewPack' => $pack->getKey()]),
);
$response->assertForbidden();
});