2026-06-22 14:00:14 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace MintyPHP\Tests\Module\Security\Service;
|
|
|
|
|
|
|
|
|
|
use MintyPHP\Module\Security\Service\SecurityReportTemplateService;
|
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
|
|
|
|
|
|
class SecurityReportTemplateServiceTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* @return array<string, mixed>
|
|
|
|
|
*/
|
|
|
|
|
private function context(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'title' => 'Security check report',
|
|
|
|
|
'customer_name' => '<b>Acme</b>',
|
|
|
|
|
'customer_no' => 'D-100',
|
|
|
|
|
'domain' => 'acme.de',
|
|
|
|
|
'product' => 'Intranet',
|
|
|
|
|
'owner' => 'Jane Doe',
|
|
|
|
|
'status_label' => 'In progress',
|
|
|
|
|
'generated_at' => '2026-06-22 10:00',
|
|
|
|
|
'app_name' => 'CoreCore',
|
2026-06-22 14:13:08 +02:00
|
|
|
'customer_report' => "Line one\nLine two",
|
2026-06-22 14:00:14 +02:00
|
|
|
'logo_data_uri' => 'data:image/png;base64,AAAA',
|
|
|
|
|
'summary' => ['percent' => 72, 'tech_done' => 2, 'tech_total' => 3],
|
|
|
|
|
'tech_sections' => [
|
|
|
|
|
['label' => 'Transport', 'items' => [
|
|
|
|
|
['label' => 'TLS enforced', 'done' => true, 'note' => 'TLS 1.3'],
|
|
|
|
|
['label' => 'HSTS header', 'done' => false, 'note' => ''],
|
|
|
|
|
]],
|
|
|
|
|
],
|
|
|
|
|
'steps' => [
|
|
|
|
|
['number' => 1, 'label' => 'Kickoff', 'done' => true, 'completed_at' => '2026-06-18'],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testRenderSubstitutesScalarsAndEscapesData(): void
|
|
|
|
|
{
|
|
|
|
|
$service = new SecurityReportTemplateService();
|
|
|
|
|
|
|
|
|
|
$out = $service->render(
|
|
|
|
|
'<h1>{{title}}</h1> who={{customer_name}} dom={{domain}} owner={{owner}} p={{percent}}% src={{logo_src}}',
|
|
|
|
|
$this->context()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('<h1>Security check report</h1>', $out);
|
|
|
|
|
$this->assertStringContainsString('dom=acme.de', $out);
|
|
|
|
|
$this->assertStringContainsString('owner=Jane Doe', $out);
|
|
|
|
|
$this->assertStringContainsString('p=72%', $out);
|
|
|
|
|
$this->assertStringContainsString('src=data:image/png;base64,AAAA', $out);
|
|
|
|
|
// Customer data is HTML-escaped so it cannot break out of the template markup.
|
|
|
|
|
$this->assertStringContainsString('who=<b>Acme</b>', $out);
|
|
|
|
|
$this->assertStringNotContainsString('<b>Acme</b>', $out);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 14:13:08 +02:00
|
|
|
public function testRenderCustomerReportEscapesAndPreservesLineBreaks(): void
|
|
|
|
|
{
|
|
|
|
|
$service = new SecurityReportTemplateService();
|
|
|
|
|
|
|
|
|
|
$context = $this->context();
|
|
|
|
|
$context['customer_report'] = "First line <script>\nSecond line";
|
|
|
|
|
|
|
|
|
|
$out = $service->render('<section>{{customer_report}}</section>', $context);
|
|
|
|
|
|
|
|
|
|
// HTML in the owner's text is escaped...
|
|
|
|
|
$this->assertStringContainsString('First line <script>', $out);
|
|
|
|
|
$this->assertStringNotContainsString('<script>', $out);
|
|
|
|
|
// ...and author line breaks survive as <br>.
|
|
|
|
|
$this->assertMatchesRegularExpression('/First line <script><br\s*\/?>\s*Second line/', $out);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 14:00:14 +02:00
|
|
|
public function testRenderBuildsChecklistAndProcessHtml(): void
|
|
|
|
|
{
|
|
|
|
|
$service = new SecurityReportTemplateService();
|
|
|
|
|
|
|
|
|
|
$out = $service->render('{{checklist}}||{{process}}', $this->context());
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('Transport', $out);
|
|
|
|
|
$this->assertStringContainsString('TLS enforced', $out);
|
|
|
|
|
$this->assertStringContainsString('TLS 1.3', $out);
|
|
|
|
|
$this->assertStringContainsString('Kickoff', $out);
|
|
|
|
|
$this->assertStringContainsString('<table class="checklist">', $out);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testRenderIsSinglePassAndDoesNotReExpandInjectedTokens(): void
|
|
|
|
|
{
|
|
|
|
|
$service = new SecurityReportTemplateService();
|
|
|
|
|
|
|
|
|
|
$context = $this->context();
|
|
|
|
|
$context['customer_name'] = '{{title}}';
|
|
|
|
|
$context['title'] = 'INJECTED';
|
|
|
|
|
|
|
|
|
|
$out = $service->render('{{customer_name}}', $context);
|
|
|
|
|
|
|
|
|
|
// The value substituted for {{customer_name}} must not be re-scanned,
|
|
|
|
|
// so the literal token survives and {{title}} is never expanded here.
|
|
|
|
|
$this->assertStringContainsString('{{title}}', $out);
|
|
|
|
|
$this->assertStringNotContainsString('INJECTED', $out);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testRenderHandlesEmptyContextSafely(): void
|
|
|
|
|
{
|
|
|
|
|
$service = new SecurityReportTemplateService();
|
|
|
|
|
|
|
|
|
|
$out = $service->render('[{{title}}]({{checklist}})', []);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('[]', $out);
|
|
|
|
|
// No sections -> the "no items" notice instead of a table.
|
|
|
|
|
$this->assertStringContainsString('class="muted"', $out);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDefaultTemplateContainsCoreVariables(): void
|
|
|
|
|
{
|
|
|
|
|
$service = new SecurityReportTemplateService();
|
|
|
|
|
|
|
|
|
|
$html = $service->defaultTemplateHtml();
|
|
|
|
|
|
|
|
|
|
$this->assertNotSame('', $html);
|
|
|
|
|
$this->assertStringContainsString('{{logo_src}}', $html);
|
|
|
|
|
$this->assertStringContainsString('{{checklist}}', $html);
|
|
|
|
|
$this->assertStringContainsString('{{process}}', $html);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAvailableVariablesExposeStructuralTokens(): void
|
|
|
|
|
{
|
|
|
|
|
$variables = (new SecurityReportTemplateService())->availableVariables();
|
|
|
|
|
|
2026-06-22 14:13:08 +02:00
|
|
|
$this->assertArrayHasKey('customer_report', $variables);
|
2026-06-22 14:00:14 +02:00
|
|
|
$this->assertArrayHasKey('logo_src', $variables);
|
|
|
|
|
$this->assertArrayHasKey('checklist', $variables);
|
|
|
|
|
$this->assertArrayHasKey('process', $variables);
|
|
|
|
|
}
|
|
|
|
|
}
|