284 lines
10 KiB
PHP
284 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\GenerateManagementReportPdfJob;
|
|
use App\Models\AuditLog;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\StoredReport;
|
|
use App\Models\User;
|
|
use App\Services\Graph\GraphClientInterface;
|
|
use App\Services\ReviewPacks\ManagementReportPdfService;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\ReviewPacks\ReportProfileRegistry;
|
|
use Illuminate\Http\Client\Request;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\Support\FailHardGraphClient;
|
|
|
|
beforeEach(function (): void {
|
|
Storage::fake('exports');
|
|
Storage::fake('public');
|
|
app()->instance(GraphClientInterface::class, new FailHardGraphClient);
|
|
});
|
|
|
|
function spec404ConfigurePdfRenderer(): void
|
|
{
|
|
config([
|
|
'tenantpilot.pdf_renderer' => [
|
|
'enabled' => true,
|
|
'runtime_validated' => true,
|
|
'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',
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array{0: User, 1: ManagedEnvironment, 2: ReviewPack}
|
|
*/
|
|
function spec404CurrentReadyPack(): 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 = spec404ZipContents();
|
|
$filePath = sprintf('review-packs/%s/spec404-current.zip', $tenant->external_id);
|
|
Storage::disk('exports')->put($filePath, $zipContents);
|
|
|
|
$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' => [
|
|
'governance_package' => [
|
|
'executive_summary' => 'Spec404 validates the management report PDF runtime path.',
|
|
],
|
|
'control_interpretation' => [
|
|
'non_certification_disclosure' => 'TenantPilot summarizes available evidence and does not certify compliance.',
|
|
],
|
|
],
|
|
'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, $pack->fresh(['tenant', 'environmentReview'])];
|
|
}
|
|
|
|
function spec404ZipContents(): string
|
|
{
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'spec404-review-pack-');
|
|
|
|
if ($tempFile === false) {
|
|
throw new RuntimeException('Failed to allocate Spec404 archive.');
|
|
}
|
|
|
|
try {
|
|
$zip = new ZipArchive;
|
|
$result = $zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
|
|
|
if ($result !== true) {
|
|
throw new RuntimeException("Failed to create Spec404 archive: {$result}");
|
|
}
|
|
|
|
$zip->addFromString('metadata.json', json_encode(['fixture' => 'spec404'], JSON_THROW_ON_ERROR));
|
|
$zip->addFromString('executive-summary.md', 'Spec404 current review pack.');
|
|
$zip->close();
|
|
|
|
$contents = file_get_contents($tempFile);
|
|
|
|
if (! is_string($contents) || $contents === '') {
|
|
throw new RuntimeException('Spec404 archive is empty.');
|
|
}
|
|
|
|
return $contents;
|
|
} finally {
|
|
if (file_exists($tempFile)) {
|
|
unlink($tempFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
function spec404PdfBytes(string $label): string
|
|
{
|
|
return "%PDF-1.7\n1 0 obj\n<< /Type /Catalog /Title ({$label}) >>\nendobj\nstartxref\n0\n%%EOF";
|
|
}
|
|
|
|
function spec404TruncatedPdfBytes(): string
|
|
{
|
|
return "%PDF-1.7\n1 0 obj\n<< /Type /Catalog >>\nendobj\n";
|
|
}
|
|
|
|
function spec404ManagementPdfWithStoredBytes(
|
|
ReviewPack $pack,
|
|
string $bytes,
|
|
?string $sha256 = null,
|
|
?int $fileSize = null,
|
|
string $fileDisk = 'exports',
|
|
): StoredReport
|
|
{
|
|
$filePath = sprintf('management-reports/%s/spec404-%d.pdf', $pack->tenant->external_id, (int) $pack->getKey());
|
|
Storage::disk($fileDisk)->put($filePath, $bytes);
|
|
|
|
return StoredReport::factory()->managementReportPdf([
|
|
'title' => 'Spec404 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' => $fileDisk,
|
|
'file_path' => $filePath,
|
|
'file_size' => $fileSize ?? max(1, strlen($bytes)),
|
|
'sha256' => $sha256 ?? hash('sha256', $bytes),
|
|
'generated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
it('Spec404 rejects renderer success responses that are not valid PDF bytes', function (string $rendererBytes): void {
|
|
Queue::fake();
|
|
spec404ConfigurePdfRenderer();
|
|
[$user, , $pack] = spec404CurrentReadyPack();
|
|
|
|
$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($rendererBytes, 200, [
|
|
'Content-Type' => 'application/pdf',
|
|
'Gotenberg-Trace' => 'spec404-corrupt-render',
|
|
]),
|
|
]);
|
|
|
|
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.invalid_response');
|
|
|
|
Http::assertSent(fn (Request $request): bool => $request->url() === 'http://gotenberg.test/forms/chromium/convert/html');
|
|
})->with([
|
|
'non-pdf bytes' => ['not a pdf'],
|
|
'truncated pdf bytes' => [spec404TruncatedPdfBytes()],
|
|
]);
|
|
|
|
it('Spec404 refuses signed management PDF downloads when stored bytes are invalid', function (string $bytes, ?string $sha256, ?int $fileSize = null): void {
|
|
[$user, , $pack] = spec404CurrentReadyPack();
|
|
$report = spec404ManagementPdfWithStoredBytes($pack, $bytes, $sha256, $fileSize);
|
|
$url = app(ManagementReportPdfService::class)->generateDownloadUrl($report, [
|
|
'source_surface' => 'spec404',
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get($url)
|
|
->assertNotFound();
|
|
|
|
expect(AuditLog::query()
|
|
->where('action', AuditActionId::ManagementReportPdfDownloaded->value)
|
|
->count())->toBe(0);
|
|
})->with([
|
|
'zero-byte file' => ['', hash('sha256', '')],
|
|
'non-pdf file' => ['plain text report', hash('sha256', 'plain text report')],
|
|
'truncated pdf file' => [spec404TruncatedPdfBytes(), hash('sha256', spec404TruncatedPdfBytes())],
|
|
'sha mismatch' => [spec404PdfBytes('Spec404 valid-looking report'), str_repeat('0', 64)],
|
|
'file-size mismatch' => [spec404PdfBytes('Spec404 valid-looking report'), hash('sha256', spec404PdfBytes('Spec404 valid-looking report')), 999],
|
|
]);
|
|
|
|
it('Spec404 refuses ready management PDF downloads stored outside the private exports disk', function (): void {
|
|
[$user, , $pack] = spec404CurrentReadyPack();
|
|
$pdfBytes = spec404PdfBytes('Spec404 wrong disk report');
|
|
$report = spec404ManagementPdfWithStoredBytes(
|
|
pack: $pack,
|
|
bytes: $pdfBytes,
|
|
sha256: hash('sha256', $pdfBytes),
|
|
fileSize: strlen($pdfBytes),
|
|
fileDisk: 'public',
|
|
);
|
|
$url = app(ManagementReportPdfService::class)->generateDownloadUrl($report, [
|
|
'source_surface' => 'spec404',
|
|
]);
|
|
|
|
expect($report->isReadyManagementPdf())->toBeFalse()
|
|
->and(app(ManagementReportPdfService::class)->findReadyReport($pack))->toBeNull();
|
|
|
|
$this->actingAs($user)
|
|
->get($url)
|
|
->assertNotFound();
|
|
|
|
expect(AuditLog::query()
|
|
->where('action', AuditActionId::ManagementReportPdfDownloaded->value)
|
|
->count())->toBe(0);
|
|
});
|
|
|
|
it('Spec404 does not treat ready metadata as downloadable when the private file is missing', function (): void {
|
|
[$user, , $pack] = spec404CurrentReadyPack();
|
|
$pdfBytes = spec404PdfBytes('Spec404 missing private file');
|
|
$report = spec404ManagementPdfWithStoredBytes($pack, $pdfBytes);
|
|
Storage::disk('exports')->delete((string) $report->file_path);
|
|
|
|
$service = app(ManagementReportPdfService::class);
|
|
$url = $service->generateDownloadUrl($report, [
|
|
'source_surface' => 'spec404',
|
|
]);
|
|
|
|
expect($report->isReadyManagementPdf())->toBeTrue()
|
|
->and($service->findReadyReport($pack))->toBeNull()
|
|
->and($service->generationDecision($pack)['state'])->not->toBe('ready');
|
|
|
|
$this->actingAs($user)
|
|
->get($url)
|
|
->assertNotFound();
|
|
|
|
expect(AuditLog::query()
|
|
->where('action', AuditActionId::ManagementReportPdfDownloaded->value)
|
|
->count())->toBe(0);
|
|
});
|