Added security check report settings and templating
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Module\Security\Service;
|
||||
|
||||
use MintyPHP\Module\Security\Service\SecurityReportLogoService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Service-behaviour test. As with TenantLogoServiceTest, the successful upload
|
||||
* path is asserted indirectly (MIME/size guards, no directory creation on
|
||||
* rejection) because move_uploaded_file() needs a real HTTP-uploaded file and
|
||||
* cannot run under phpunit. The move step is covered by manual smoke testing.
|
||||
*/
|
||||
class SecurityReportLogoServiceTest extends TestCase
|
||||
{
|
||||
private SecurityReportLogoService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->service = new SecurityReportLogoService();
|
||||
}
|
||||
|
||||
public function testReportLogoDirIsScopedToSecurityReportLogo(): void
|
||||
{
|
||||
$this->assertStringEndsWith('/security/report-logo', $this->service->reportLogoDir());
|
||||
}
|
||||
|
||||
public function testSaveUploadRejectsMissingFile(): void
|
||||
{
|
||||
$this->assertFalse($this->service->saveUpload([])['ok']);
|
||||
}
|
||||
|
||||
public function testSaveUploadRejectsUploadError(): void
|
||||
{
|
||||
$result = $this->service->saveUpload([
|
||||
'tmp_name' => '/tmp/whatever',
|
||||
'error' => UPLOAD_ERR_NO_FILE,
|
||||
'size' => 100,
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testSaveUploadRejectsOversizedFile(): void
|
||||
{
|
||||
$result = $this->service->saveUpload([
|
||||
'tmp_name' => '/tmp/whatever',
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
'size' => 6 * 1024 * 1024,
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testSaveUploadRejectsNonImageContent(): void
|
||||
{
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'sec-logo-test');
|
||||
$this->assertNotFalse($tmp);
|
||||
file_put_contents($tmp, "just some plain text, not an image\n");
|
||||
|
||||
try {
|
||||
$result = $this->service->saveUpload([
|
||||
'tmp_name' => $tmp,
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
'size' => filesize($tmp),
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
// Rejection must happen before any directory/file is created.
|
||||
$this->assertFalse(is_dir($this->service->reportLogoDir() . '/__never__'));
|
||||
} finally {
|
||||
@unlink($tmp);
|
||||
}
|
||||
}
|
||||
|
||||
public function testDeleteIsIdempotentWhenNothingStored(): void
|
||||
{
|
||||
// delete() is safe to call even when the directory does not exist.
|
||||
$this->assertTrue($this->service->delete());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ namespace MintyPHP\Tests\Module\Security\Service;
|
||||
|
||||
use MintyPHP\Module\Security\Service\SecurityCheckProcess;
|
||||
use MintyPHP\Module\Security\Service\SecurityReportPdfService;
|
||||
use MintyPHP\Module\Security\Service\SecurityReportSettingsGateway;
|
||||
use MintyPHP\Module\Security\Service\SecurityReportTemplateService;
|
||||
use MintyPHP\Service\Branding\BrandingLogoService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -48,6 +50,7 @@ class SecurityReportPdfServiceTest extends TestCase
|
||||
'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,
|
||||
@@ -64,6 +67,7 @@ class SecurityReportPdfServiceTest extends TestCase
|
||||
$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).
|
||||
@@ -126,4 +130,28 @@ class SecurityReportPdfServiceTest extends TestCase
|
||||
$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(
|
||||
'<!doctype html><html><body><h1>Custom {{customer_name}}</h1>{{checklist}}</body></html>'
|
||||
);
|
||||
|
||||
$service = new SecurityReportPdfService(
|
||||
new SecurityCheckProcess(),
|
||||
$branding,
|
||||
null,
|
||||
$settings,
|
||||
new SecurityReportTemplateService(),
|
||||
null
|
||||
);
|
||||
|
||||
$pdf = $service->renderCustomerReportPdf($this->enrichedCheck(), ['app_name' => 'CoreCore']);
|
||||
|
||||
$this->assertStringStartsWith('%PDF-', $pdf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Module\Security\Service;
|
||||
|
||||
use MintyPHP\Module\Security\Service\SecurityReportSettingsGateway;
|
||||
use MintyPHP\Service\Settings\SettingsMetadataGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SecurityReportSettingsGatewayTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $values
|
||||
*/
|
||||
private function makeGateway(array $values = []): SecurityReportSettingsGateway
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getValue')->willReturnCallback(static fn (string $key): ?string => $values[$key] ?? null);
|
||||
$metadata->method('set')->willReturn(true);
|
||||
|
||||
return new SecurityReportSettingsGateway($metadata);
|
||||
}
|
||||
|
||||
public function testTemplateDefaultsToEmpty(): void
|
||||
{
|
||||
$gateway = $this->makeGateway();
|
||||
|
||||
$this->assertSame('', $gateway->getTemplateHtml());
|
||||
$this->assertFalse($gateway->hasCustomTemplate());
|
||||
}
|
||||
|
||||
public function testGetTemplateHtmlReturnsTrimmedStoredValue(): void
|
||||
{
|
||||
$gateway = $this->makeGateway([
|
||||
SecurityReportSettingsGateway::KEY_TEMPLATE_HTML => " <h1>{{title}}</h1> ",
|
||||
]);
|
||||
|
||||
$this->assertSame('<h1>{{title}}</h1>', $gateway->getTemplateHtml());
|
||||
$this->assertTrue($gateway->hasCustomTemplate());
|
||||
}
|
||||
|
||||
public function testSetTemplateHtmlRejectsOversizedTemplate(): void
|
||||
{
|
||||
$gateway = $this->makeGateway();
|
||||
|
||||
$tooLong = str_repeat('a', SecurityReportSettingsGateway::MAX_TEMPLATE_LENGTH + 1);
|
||||
$this->assertFalse($gateway->setTemplateHtml($tooLong));
|
||||
}
|
||||
|
||||
public function testSetTemplateHtmlPersistsValueAndClearsOnEmpty(): void
|
||||
{
|
||||
$writes = [];
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getValue')->willReturn(null);
|
||||
$metadata->method('set')->willReturnCallback(static function (string $key, ?string $value, ?string $description = null) use (&$writes): bool {
|
||||
$writes[$key] = $value;
|
||||
|
||||
return true;
|
||||
});
|
||||
$gateway = new SecurityReportSettingsGateway($metadata);
|
||||
|
||||
$this->assertTrue($gateway->setTemplateHtml('<p>x</p>'));
|
||||
$this->assertSame('<p>x</p>', $writes[SecurityReportSettingsGateway::KEY_TEMPLATE_HTML]);
|
||||
|
||||
// Blank input clears the setting (restores the built-in default).
|
||||
$this->assertTrue($gateway->setTemplateHtml(' '));
|
||||
$this->assertNull($writes[SecurityReportSettingsGateway::KEY_TEMPLATE_HTML]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?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',
|
||||
'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);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
$this->assertArrayHasKey('logo_src', $variables);
|
||||
$this->assertArrayHasKey('checklist', $variables);
|
||||
$this->assertArrayHasKey('process', $variables);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user