instance(GraphClientInterface::class, new FailHardGraphClient); }); function spec379ConfigurePdfRenderer(bool $runtimeValidated = true, array $overrides = []): void { config([ 'tenantpilot.pdf_renderer' => array_merge([ 'enabled' => true, 'runtime_validated' => $runtimeValidated, 'driver' => 'gotenberg', 'base_url' => 'http://gotenberg.test', 'health_path' => '/health', 'html_route' => '/forms/chromium/convert/html', 'timeout_seconds' => 30, 'connect_timeout_seconds' => 5, 'max_html_bytes' => 1024 * 1024, 'max_asset_bytes' => 512 * 1024, 'max_output_bytes' => 2 * 1024 * 1024, 'correlation_header' => 'Gotenberg-Trace', 'output_filename' => 'tenantpilot-management-report', ], $overrides), ]); } /** * @param array $files */ function spec379ZipContents(array $files = []): string { $tempFile = tempnam(sys_get_temp_dir(), 'spec379-review-pack-'); if ($tempFile === false) { throw new RuntimeException('Failed to allocate Spec379 archive.'); } try { $zip = new ZipArchive; $result = $zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE); if ($result !== true) { throw new RuntimeException("Failed to create Spec379 archive: {$result}"); } foreach (array_replace([ 'metadata.json' => json_encode(['fixture' => 'spec379'], JSON_THROW_ON_ERROR), 'executive-summary.md' => 'Spec379 current review pack.', ], $files) as $filename => $contents) { $zip->addFromString($filename, $contents); } $zip->close(); $contents = file_get_contents($tempFile); if (! is_string($contents) || $contents === '') { throw new RuntimeException('Spec379 archive is empty.'); } return $contents; } finally { if (file_exists($tempFile)) { unlink($tempFile); } } } /** * @param array $summaryOverrides * @return array{0: \App\Models\User, 1: \App\Models\ManagedEnvironment, 2: \App\Models\EnvironmentReview, 3: ReviewPack} */ function spec379CurrentReadyPack(array $summaryOverrides = []): 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); $zipContents = spec379ZipContents(); $filePath = sprintf('review-packs/%s/spec379-current.zip', $tenant->external_id); Storage::disk('exports')->put($filePath, $zipContents); $summary = array_replace_recursive([ 'governance_package' => [ 'executive_summary' => 'The released review is ready for a customer management handoff.', ], 'decision_summary' => [ 'entries' => [ [ 'title' => 'Proceed with governed handoff', 'summary' => 'Customer-safe evidence is ready.', 'rationale' => 'Current Review Pack is complete.', ], ], ], 'top_findings' => [ [ 'title' => 'Privileged access review', 'severity' => 'medium', 'summary' => 'Review privileged role assignment cadence.', ], ], 'risk_acceptance' => [ [ 'title' => 'Known exception', 'summary' => 'Accepted until the next operating review.', ], ], 'recommended_next_actions' => [ [ 'title' => 'Schedule review', 'summary' => 'Review the management report with customer stakeholders.', ], ], '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.', ], ], $summaryOverrides); $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) $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(), ]); $review->forceFill([ 'current_export_review_pack_id' => (int) $pack->getKey(), ])->save(); return [$user, $tenant, $review->fresh(['sections', 'evidenceSnapshot', 'currentExportReviewPack']), $pack->fresh(['tenant', 'environmentReview'])]; } function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport { $pdfBytes = '%PDF-1.7 Spec379 ready management report'; $filePath = sprintf('management-reports/%s/ready-%d.pdf', $pack->tenant->external_id, (int) $pack->getKey()); Storage::disk('exports')->put($filePath, $pdfBytes); return StoredReport::factory()->managementReportPdf([ 'title' => 'TenantPilot 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('Spec379 blocks generation until the PDF runtime is validated', function (): void { spec379ConfigurePdfRenderer(runtimeValidated: false); [$user, $tenant, , $pack] = spec379CurrentReadyPack(); Queue::fake(); $runCount = OperationRun::query()->count(); $reportCount = StoredReport::query() ->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF) ->count(); $service = app(ManagementReportPdfService::class); $decision = $service->generationDecision($pack); $result = $service->startGeneration($pack, $user); expect($decision['is_blocked'])->toBeTrue() ->and($decision['reason_code'])->toBe('runtime_validation_missing') ->and($result['mode'])->toBe('blocked') ->and(OperationRun::query()->count())->toBe($runCount) ->and(StoredReport::query()->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF)->count())->toBe($reportCount); Queue::assertNothingPushed(); $audit = AuditLog::query() ->where('action', AuditActionId::ManagementReportPdfGenerationBlocked->value) ->latest('id') ->first(); expect($audit)->not->toBeNull() ->and($audit?->workspace_id)->toBe((int) $tenant->workspace_id) ->and($audit?->resource_type)->toBe('stored_report') ->and(data_get($audit?->metadata, 'decision.reason_code'))->toBe('runtime_validation_missing'); }); it('Spec379 queues generation, renders through Gotenberg, stores a private PDF, and records operation proof', function (): void { spec379ConfigurePdfRenderer(); [$user, $tenant, $review, $pack] = spec379CurrentReadyPack([ 'top_findings' => [ [ 'title' => 'Secret-shaped source value', 'summary' => 'SQLSTATE raw error with access_token should never appear.', ], ], ]); Queue::fake(); $service = app(ManagementReportPdfService::class); $result = $service->startGeneration($pack, $user); expect($result['mode'])->toBe('queued') ->and($result['report'])->toBeInstanceOf(StoredReport::class) ->and($result['operation_run'])->toBeInstanceOf(OperationRun::class); Queue::assertPushed(GenerateManagementReportPdfJob::class); /** @var StoredReport $report */ $report = $result['report']; /** @var OperationRun $run */ $run = $result['operation_run']; expect($run->type)->toBe(OperationRunType::ManagementReportGenerate->value) ->and($report->status)->toBe(StoredReport::STATUS_QUEUED) ->and($report->source_review_pack_id)->toBe((int) $pack->getKey()) ->and($report->source_environment_review_id)->toBe((int) $review->getKey()); Http::fake([ 'gotenberg.test/forms/chromium/convert/html' => Http::response('%PDF-1.7 rendered spec379', 200, [ 'Content-Type' => 'application/pdf', 'Gotenberg-Trace' => 'spec379-render', ]), ]); app()->call([new GenerateManagementReportPdfJob( storedReportId: (int) $report->getKey(), operationRunId: (int) $run->getKey(), ), 'handle']); $report->refresh(); $run->refresh(); expect($report->status)->toBe(StoredReport::STATUS_READY) ->and($report->file_disk)->toBe('exports') ->and(Storage::disk('exports')->exists((string) $report->file_path))->toBeTrue() ->and($report->sha256)->toBe(hash('sha256', '%PDF-1.7 rendered spec379')) ->and(json_encode($report->payload, JSON_THROW_ON_ERROR))->not->toContain('SQLSTATE', 'access_token') ->and($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value) ->and($run->summary_counts)->toMatchArray([ 'total' => 1, 'processed' => 1, 'succeeded' => 1, 'report_created' => 1, ]); Http::assertSent(fn (Request $request): bool => $request->url() === 'http://gotenberg.test/forms/chromium/convert/html' && $request->method() === 'POST'); $audit = AuditLog::query() ->where('action', AuditActionId::ManagementReportPdfGenerated->value) ->latest('id') ->first(); expect($audit)->not->toBeNull() ->and($audit?->workspace_id)->toBe((int) $tenant->workspace_id) ->and($audit?->resource_type)->toBe('stored_report') ->and(data_get($audit?->metadata, 'stored_report_id'))->toBe((int) $report->getKey()); }); it('Spec379 builds a customer-executive payload without profile fallback or sensitive source strings', function (): void { [$user, , , $pack] = spec379CurrentReadyPack([ 'decision_summary' => [ 'entries' => [ [ 'title' => 'Token shaped value', 'summary' => 'Bearer abc.def access_token=secret should be redacted.', ], ], ], ]); $payload = app(ManagementReportPdfPayloadBuilder::class)->build($pack); $encoded = json_encode($payload, JSON_THROW_ON_ERROR); expect($payload['profile'])->toBe(ReportProfileRegistry::CUSTOMER_EXECUTIVE) ->and($encoded)->toContain('Executive summary') ->and($encoded)->toContain('Method summary') ->and($encoded)->not->toContain('Bearer abc.def', 'access_token=secret', 'Internal MSP review') ->and($user)->not->toBeNull(); }); it('Spec379 maps source and disclosure blockers before rendering', function (): void { spec379ConfigurePdfRenderer(); [$user, , $review, $pack] = spec379CurrentReadyPack(); Queue::fake(); $service = app(ManagementReportPdfService::class); Storage::disk('exports')->delete((string) $pack->file_path); expect($service->generationDecision($pack)['reason_code'])->toBe('source_artifact_missing'); Storage::disk('exports')->put((string) $pack->file_path, spec379ZipContents()); $pack->forceFill(['expires_at' => now()->subMinute()])->save(); expect($service->generationDecision($pack->fresh())['reason_code'])->toBe('review_pack_expired'); $pack->forceFill(['expires_at' => now()->addDay()])->save(); $review->forceFill(['current_export_review_pack_id' => null])->save(); expect($service->generationDecision($pack->fresh(['environmentReview']))['reason_code'])->toBe('review_pack_not_current'); $review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save(); $pack->forceFill([ 'options' => [ 'include_pii' => true, 'include_operations' => true, ], ])->save(); $runCount = OperationRun::query()->count(); $reportCount = StoredReport::query() ->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF) ->count(); $disclosureDecision = $service->generationDecision($pack->fresh(['environmentReview'])); $result = $service->startGeneration($pack->fresh(['tenant', 'environmentReview']), $user); expect($disclosureDecision['is_blocked'])->toBeTrue() ->and($disclosureDecision['reason_code'])->toBe('customer_profile_internal_only') ->and($result['mode'])->toBe('blocked') ->and(OperationRun::query()->count())->toBe($runCount) ->and(StoredReport::query()->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF)->count())->toBe($reportCount); Queue::assertNotPushed(GenerateManagementReportPdfJob::class); expect(fn () => app(ManagementReportPdfPayloadBuilder::class)->build($pack->fresh(['environmentReview']))) ->toThrow(InvalidArgumentException::class); }); it('Spec379 marks queued generation as blocked when disclosure changes before the job runs', function (): void { Queue::fake(); spec379ConfigurePdfRenderer(); [$user, , , $pack] = spec379CurrentReadyPack(); $result = app(ManagementReportPdfService::class)->startGeneration($pack, $user); /** @var StoredReport $report */ $report = $result['report']; /** @var OperationRun $run */ $run = $result['operation_run']; $pack->forceFill([ 'options' => [ 'include_pii' => true, 'include_operations' => true, ], ])->save(); Http::fake(); app()->call([new GenerateManagementReportPdfJob( storedReportId: (int) $report->getKey(), operationRunId: (int) $run->getKey(), ), 'handle']); $report->refresh(); $run->refresh(); expect($report->status)->toBe(StoredReport::STATUS_FAILED) ->and(data_get($report->payload, 'blocked'))->toBeTrue() ->and($report->file_path)->toBeNull() ->and($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value) ->and($run->summary_counts)->toMatchArray([ 'total' => 1, 'processed' => 1, 'succeeded' => 0, 'failed' => 0, ]) ->and(data_get($run->failure_summary, '0.code'))->toBe('management_report_pdf.customer_profile_internal_only'); Http::assertNothingSent(); $audit = AuditLog::query() ->where('action', AuditActionId::ManagementReportPdfGenerationBlocked->value) ->latest('id') ->first(); expect($audit)->not->toBeNull() ->and(data_get($audit?->metadata, 'reason_code'))->toBe('customer_profile_internal_only'); }); it('Spec379 records renderer failure without exposing a ready artifact', function (): void { Queue::fake(); spec379ConfigurePdfRenderer(); [$user, , , $pack] = spec379CurrentReadyPack(); $result = app(ManagementReportPdfService::class)->startGeneration($pack, $user); /** @var StoredReport $report */ $report = $result['report']; /** @var OperationRun $run */ $run = $result['operation_run']; Http::fake([ 'gotenberg.test/forms/chromium/convert/html' => Http::sequence() ->push('renderer failed', 500, [ 'Content-Type' => 'text/plain', ]) ->push('%PDF-1.7 rendered spec379 retry', 200, [ 'Content-Type' => 'application/pdf', 'Gotenberg-Trace' => 'spec379-renderer-retry', ]), ]); app()->call([new GenerateManagementReportPdfJob( storedReportId: (int) $report->getKey(), operationRunId: (int) $run->getKey(), ), 'handle']); $report->refresh(); $run->refresh(); expect($report->status)->toBe(StoredReport::STATUS_FAILED) ->and($report->file_path)->toBeNull() ->and($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Failed->value) ->and(data_get($run->failure_summary, '0.code'))->toBe('management_report_pdf.renderer_failed'); Queue::fake(); $retry = app(ManagementReportPdfService::class)->startGeneration($pack->fresh(['tenant', 'environmentReview']), $user); /** @var StoredReport $retryReport */ $retryReport = $retry['report']; /** @var OperationRun $retryRun */ $retryRun = $retry['operation_run']; expect($retry['mode'])->toBe('queued') ->and($retryReport->getKey())->toBe($report->getKey()) ->and($retryReport->status)->toBe(StoredReport::STATUS_QUEUED) ->and($retryReport->operation_run_id)->toBe((int) $retryRun->getKey()) ->and($retryRun->getKey())->not->toBe($run->getKey()); Queue::assertPushed(GenerateManagementReportPdfJob::class); app()->call([new GenerateManagementReportPdfJob( storedReportId: (int) $retryReport->getKey(), operationRunId: (int) $retryRun->getKey(), ), 'handle']); $retryReport->refresh(); $retryRun->refresh(); expect($retryReport->status)->toBe(StoredReport::STATUS_READY) ->and($retryReport->file_path)->not->toBeNull() ->and($retryRun->status)->toBe(OperationRunStatus::Completed->value) ->and($retryRun->outcome)->toBe(OperationRunOutcome::Succeeded->value); Http::assertSentCount(2); }); it('Spec379 records storage failure without exposing a ready artifact', function (): void { Queue::fake(); spec379ConfigurePdfRenderer(); [$user, , , $pack] = spec379CurrentReadyPack(); $result = app(ManagementReportPdfService::class)->startGeneration($pack, $user); /** @var StoredReport $report */ $report = $result['report']; /** @var OperationRun $run */ $run = $result['operation_run']; Http::fake([ 'gotenberg.test/forms/chromium/convert/html' => Http::response('%PDF-1.7 rendered spec379', 200, [ 'Content-Type' => 'application/pdf', 'Gotenberg-Trace' => 'spec379-storage-failure', ]), ]); $fakeDisk = \Mockery::mock(\Illuminate\Contracts\Filesystem\Filesystem::class); $fakeDisk->shouldReceive('put') ->once() ->andReturnFalse(); Storage::shouldReceive('disk') ->with('exports') ->andReturn($fakeDisk); app()->call([new GenerateManagementReportPdfJob( storedReportId: (int) $report->getKey(), operationRunId: (int) $run->getKey(), ), 'handle']); $report->refresh(); $run->refresh(); expect($report->status)->toBe(StoredReport::STATUS_FAILED) ->and($report->file_path)->toBeNull() ->and($report->sha256)->toBeNull() ->and($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Failed->value) ->and(data_get($run->failure_summary, '0.code'))->toBe('management_report_pdf.storage_failed'); }); it('Spec379 downloads a ready management PDF only through the signed tenant-authorized route', function (): void { [$user, $tenant, , $pack] = spec379CurrentReadyPack(); $report = spec379ReadyManagementPdf($pack); $url = app(ManagementReportPdfService::class)->generateDownloadUrl($report, [ 'source_surface' => 'spec379', ]); $this->actingAs($user) ->get($url) ->assertOk() ->assertHeader('X-Management-Report-PDF-SHA256', $report->sha256) ->assertDownload(); $audit = AuditLog::query() ->where('action', AuditActionId::ManagementReportPdfDownloaded->value) ->latest('id') ->first(); expect($audit)->not->toBeNull() ->and($audit?->resource_type)->toBe('stored_report') ->and(data_get($audit?->metadata, 'stored_report_id'))->toBe((int) $report->getKey()) ->and(data_get($audit?->metadata, 'source_surface'))->toBe('spec379'); [$otherUser] = createUserWithTenant(role: 'owner'); $this->actingAs($otherUser) ->get($url) ->assertNotFound(); expect($tenant)->not->toBeNull(); }); it('Spec379 enforces scoped authorization for generation and download', function (): void { spec379ConfigurePdfRenderer(); [$owner, , , $pack] = spec379CurrentReadyPack(); [$readonly] = createUserWithTenant(tenant: $pack->tenant, user: \App\Models\User::factory()->create(), role: 'readonly', clearCapabilityCaches: true); $report = spec379ReadyManagementPdf($pack); $url = app(ManagementReportPdfService::class)->generateDownloadUrl($report); try { app(ManagementReportPdfService::class)->startGeneration($pack, $readonly); $this->fail('Scoped readonly actor should not start management PDF generation.'); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) { expect($exception->getStatusCode())->toBe(403); } Gate::define( Capabilities::REVIEW_PACK_VIEW, fn (User $actor, ManagedEnvironment $tenant): bool => (int) $actor->getKey() !== (int) $readonly->getKey() || (int) $tenant->getKey() !== (int) $pack->tenant->getKey(), ); $this->actingAs($readonly) ->get($url) ->assertForbidden(); [$outsideUser] = createUserWithTenant(role: 'owner', clearCapabilityCaches: true); $this->actingAs($outsideUser) ->get($url) ->assertNotFound(); expect($owner)->not->toBeNull(); }); it('Spec379 exposes the confirmed Filament action only when generation is available', function (): void { spec379ConfigurePdfRenderer(runtimeValidated: false); [$user, $tenant, , $pack] = spec379CurrentReadyPack(); setAdminEnvironmentContext($tenant); Livewire::actingAs($user) ->test(ViewReviewPack::class, ['record' => $pack->getKey()]) ->assertActionVisible('generate_management_report_pdf') ->assertActionDisabled('generate_management_report_pdf'); spec379ConfigurePdfRenderer(runtimeValidated: true); Livewire::actingAs($user) ->test(ViewReviewPack::class, ['record' => $pack->getKey()]) ->assertActionVisible('generate_management_report_pdf') ->assertActionEnabled('generate_management_report_pdf') ->assertActionExists('generate_management_report_pdf', fn (Action $action): bool => $action->isConfirmationRequired()) ->mountAction('generate_management_report_pdf') ->assertActionMounted('generate_management_report_pdf'); }); it('Spec379 exposes an unavailable Filament action when disclosure blocks customer PDF generation', function (): void { spec379ConfigurePdfRenderer(); [$user, $tenant, , $pack] = spec379CurrentReadyPack(); $pack->forceFill([ 'options' => [ 'include_pii' => true, 'include_operations' => true, ], ])->save(); setAdminEnvironmentContext($tenant); Livewire::actingAs($user) ->test(ViewReviewPack::class, ['record' => $pack->getKey()]) ->assertActionVisible('generate_management_report_pdf') ->assertActionDisabled('generate_management_report_pdf') ->assertActionExists('generate_management_report_pdf', fn (Action $action): bool => $action->getLabel() === __('localization.review.management_report_pdf_blocked')); }); it('Spec379 regenerates a review-bound pack from the Review Pack detail action', function (): void { Queue::fake(); [$user, $tenant, $review, $pack] = spec379CurrentReadyPack(); $tenantWidePackCount = ReviewPack::query() ->where('managed_environment_id', (int) $tenant->getKey()) ->whereNull('environment_review_id') ->count(); setAdminEnvironmentContext($tenant); Livewire::actingAs($user) ->test(ViewReviewPack::class, ['record' => $pack->getKey()]) ->callAction('regenerate', data: [ 'include_pii' => true, 'include_operations' => true, ]) ->assertNotified(); $regeneratedPack = ReviewPack::query() ->where('environment_review_id', (int) $review->getKey()) ->whereKeyNot((int) $pack->getKey()) ->latest('id') ->firstOrFail(); expect($regeneratedPack->options)->toMatchArray([ 'include_pii' => true, 'include_operations' => true, ]) ->and($regeneratedPack->managed_environment_id)->toBe((int) $tenant->getKey()) ->and($regeneratedPack->evidence_snapshot_id)->toBe((int) $review->evidence_snapshot_id) ->and(ReviewPack::query() ->where('managed_environment_id', (int) $tenant->getKey()) ->whereNull('environment_review_id') ->count())->toBe($tenantWidePackCount); Queue::assertPushed(GenerateReviewPackJob::class); }); it('Spec379 swaps the generate action for the download action once a PDF exists', function (): void { [$user, $tenant, , $pack] = spec379CurrentReadyPack(); spec379ReadyManagementPdf($pack); setAdminEnvironmentContext($tenant); Livewire::actingAs($user) ->test(ViewReviewPack::class, ['record' => $pack->getKey()]) ->assertActionVisible('download_management_report_pdf') ->assertActionDoesNotExist('generate_management_report_pdf'); });