createMock(BrandingLogoService::class); $branding->method('hasLogo')->willReturn(false); // Null tenant-logo service: render falls back to the static brand asset. return new SecurityReportPdfService(new SecurityCheckProcess(), $branding, null); } /** * @return array */ private function enrichedCheck(): array { $process = new SecurityCheckProcess(); $processState = []; foreach ($process->manualStepKeys() as $key) { $processState[$key] = ['done' => true, 'at' => '2026-06-18 10:00:00']; } $snapshot = ['version' => 1, 'items' => [ ['type' => 'section', 'label' => 'Transport'], ['type' => 'check', 'key' => 'tls', 'label' => 'TLS enforced'], ['type' => 'check', 'key' => 'hsts', 'label' => 'HSTS header'], ['type' => 'section', 'label' => 'Application'], ['type' => 'check', 'key' => 'csrf', 'label' => 'CSRF protection'], ]]; $techState = [ 'tls' => ['done' => true, 'note' => 'TLS 1.3'], 'hsts' => ['done' => true, 'note' => ''], 'csrf' => ['done' => false, 'note' => 'pending review'], ]; return [ 'debitor_name' => 'Acme GmbH', 'debitor_no' => 'D-100', 'domain_url' => 'acme.de', 'domain_no' => 'DNS-1', 'product_name' => 'Intranet', 'product_code' => 'intranet', 'owner_name' => 'Jane Doe', 'status' => SecurityCheckProcess::STATUS_IN_PROGRESS, 'process_state' => $processState, 'tech_schema_snapshot' => $snapshot, 'tech_state' => $techState, 'progress' => $process->computeProgress($processState, $snapshot, $techState), ]; } public function testBuildReportContextMapsCheckDataIntoRenderShape(): void { $context = $this->makeService()->buildReportContext($this->enrichedCheck()); $this->assertSame('Acme GmbH', $context['customer_name']); $this->assertSame('D-100', $context['customer_no']); $this->assertSame('acme.de', $context['domain']); $this->assertSame('Intranet', $context['product']); $this->assertSame('Jane Doe', $context['owner']); $this->assertNotSame('', $context['title']); // Summary mirrors the progress math (3 tech items, 2 done). $this->assertSame(3, $context['summary']['tech_total']); $this->assertSame(2, $context['summary']['tech_done']); // Five manual milestones, numbered with the technical step skipped. $this->assertCount(5, $context['steps']); $this->assertSame(1, $context['steps'][0]['number']); $this->assertTrue($context['steps'][0]['done']); $this->assertSame('2026-06-18 10:00:00', $context['steps'][0]['completed_at']); // 'report' is step 6 (the tech checklist step 5 is skipped from the milestone list). $this->assertSame(6, $context['steps'][4]['number']); // Tech items grouped under their sections, carrying state + notes. $this->assertCount(2, $context['tech_sections']); $this->assertSame('Transport', $context['tech_sections'][0]['label']); $this->assertCount(2, $context['tech_sections'][0]['items']); $this->assertSame('TLS enforced', $context['tech_sections'][0]['items'][0]['label']); $this->assertTrue($context['tech_sections'][0]['items'][0]['done']); $this->assertSame('TLS 1.3', $context['tech_sections'][0]['items'][0]['note']); $this->assertSame('Application', $context['tech_sections'][1]['label']); $this->assertFalse($context['tech_sections'][1]['items'][0]['done']); } public function testBuildReportContextHandlesEmptyCheckSafely(): void { $context = $this->makeService()->buildReportContext([]); $this->assertSame('', $context['customer_name']); $this->assertSame('', $context['domain']); $this->assertSame([], $context['tech_sections']); $this->assertSame(0, $context['summary']['total']); $this->assertSame(0, $context['summary']['percent']); // The fixed process always yields its five manual milestones, all open. $this->assertCount(5, $context['steps']); $this->assertFalse($context['steps'][0]['done']); } public function testBuildReportFilenameSlugifiesCustomerAndDomain(): void { $name = $this->makeService()->buildReportFilename($this->enrichedCheck()); $this->assertSame('security-check-report-acme-gmbh-acme-de.pdf', $name); } public function testBuildReportFilenameFallsBackWhenNoIdentifiers(): void { $name = $this->makeService()->buildReportFilename([]); $this->assertSame('security-check-report.pdf', $name); } public function testRenderCustomerReportPdfProducesPdfBytes(): void { $pdf = $this->makeService()->renderCustomerReportPdf($this->enrichedCheck(), [ 'app_name' => 'CoreCore', ]); $this->assertNotSame('', $pdf); $this->assertStringStartsWith('%PDF-', $pdf); } public function testRenderCustomerReportPdfUsesConfiguredCustomTemplate(): void { $branding = $this->createMock(BrandingLogoService::class); $branding->method('hasLogo')->willReturn(false); $settings = $this->createMock(SecurityReportSettingsGateway::class); $settings->method('getTemplateHtml')->willReturn( '

Custom {{customer_name}}

{{checklist}}' ); $service = new SecurityReportPdfService( new SecurityCheckProcess(), $branding, null, $settings, new SecurityReportTemplateService(), null ); $pdf = $service->renderCustomerReportPdf($this->enrichedCheck(), ['app_name' => 'CoreCore']); $this->assertStringStartsWith('%PDF-', $pdf); } }