1
0
Files
breadcrumb-the-shire/modules/security/tests/Module/Security/Service/SecurityCheckServiceTest.php

345 lines
13 KiB
PHP

<?php
namespace MintyPHP\Tests\Module\Security\Service;
use MintyPHP\Module\Security\Repository\SecurityCheckRepository;
use MintyPHP\Module\Security\Service\SecurityCheckProcess;
use MintyPHP\Module\Security\Service\SecurityCheckService;
use MintyPHP\Module\Security\Service\SecurityCheckTemplateService;
use PHPUnit\Framework\TestCase;
class SecurityCheckServiceTest extends TestCase
{
private const TENANT_ID = 1;
/**
* @return array{0: SecurityCheckService, 1: SecurityCheckRepository, 2: SecurityCheckTemplateService}
*/
private function makeService(?SecurityCheckRepository $repo = null, ?SecurityCheckTemplateService $templates = null): array
{
$repo = $repo ?? $this->createMock(SecurityCheckRepository::class);
$templates = $templates ?? $this->createMock(SecurityCheckTemplateService::class);
$service = new SecurityCheckService($repo, $templates, new SecurityCheckProcess());
return [$service, $repo, $templates];
}
public function testCreateRequiresCustomerDomainAndProduct(): void
{
$repo = $this->createMock(SecurityCheckRepository::class);
$repo->expects($this->never())->method('insert');
[$service] = $this->makeService($repo);
$result = $service->create(self::TENANT_ID, [], 5);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('debitor_no', $result['errors']);
$this->assertArrayHasKey('domain_no', $result['errors']);
$this->assertArrayHasKey('product_code', $result['errors']);
}
public function testCreateFailsWhenTemplateMissing(): void
{
$templates = $this->createMock(SecurityCheckTemplateService::class);
$templates->method('findByCode')->willReturn(null);
[$service] = $this->makeService(null, $templates);
$result = $service->create(self::TENANT_ID, [
'debitor_no' => 'D1',
'domain_no' => 'DNS1',
'product_code' => 'intranet',
], 5);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('product_code', $result['errors']);
}
public function testCreateSucceedsAndSnapshotsSchema(): void
{
$templates = $this->createMock(SecurityCheckTemplateService::class);
$templates->method('findByCode')->willReturn(['id' => 7, 'product_name' => 'Intranet', 'active' => 1]);
$templates->method('decodeSchema')->willReturn(['version' => 2, 'items' => [['type' => 'check', 'key' => 'tls', 'label' => 'TLS']]]);
$captured = null;
$repo = $this->createMock(SecurityCheckRepository::class);
$repo->method('insert')->willReturnCallback(function (int $tenantId, array $data) use (&$captured) {
$captured = $data;
return 42;
});
[$service] = $this->makeService($repo, $templates);
$result = $service->create(self::TENANT_ID, [
'debitor_no' => 'D1',
'debitor_name' => 'Acme',
'domain_no' => 'DNS1',
'domain_url' => 'acme.de',
'product_code' => 'intranet',
'owner_user_id' => 9,
], 5);
$this->assertTrue($result['ok']);
$this->assertSame(42, $result['id']);
$this->assertSame(SecurityCheckProcess::STATUS_OPEN, $captured['status']);
$this->assertSame(7, $captured['template_id']);
$this->assertSame('Intranet', $captured['product_name']);
$snapshot = json_decode((string) $captured['tech_schema_snapshot_json'], true);
$this->assertSame('tls', $snapshot['items'][0]['key']);
}
public function testSaveStateStampsCompletionAndDerivesCompletedStatus(): void
{
$snapshot = json_encode(['version' => 1, 'items' => [['type' => 'check', 'key' => 'tls', 'label' => 'TLS']]]);
$repo = $this->createMock(SecurityCheckRepository::class);
$repo->method('findById')->willReturn([
'id' => 1,
'status' => SecurityCheckProcess::STATUS_OPEN,
'process_state_json' => '{}',
'tech_schema_snapshot_json' => $snapshot,
'tech_state_json' => '{}',
]);
$captured = null;
$repo->method('updateState')->willReturnCallback(function (int $tenantId, int $id, array $data) use (&$captured) {
$captured = $data;
return true;
});
$process = new SecurityCheckProcess();
[$service] = $this->makeService($repo);
$step = [];
foreach ($process->manualStepKeys() as $key) {
$step[$key] = ['done' => '1', 'note' => ''];
}
$step[SecurityCheckProcess::STEP_INFORMATION]['fields'] = [
'technical_contact' => 'Jane',
'deployment' => 'Cloud',
'system_info' => 'PHP 8.5',
'external_systems' => 'https://api.example.test',
];
$step['scheduling']['fields'] = [
'golive_start' => '2026-07-01T09:00',
'golive_end' => '2026-07-01T12:00',
];
$step['blocker_appointment']['fields'] = [
'blocker_start' => '2026-07-01T08:00',
'blocker_end' => '2026-07-02T18:00',
];
$input = [
'step' => $step,
'tech' => ['tls' => ['done' => '1', 'note' => 'verified']],
];
$result = $service->saveState(self::TENANT_ID, 1, $input, 77);
$this->assertTrue($result['ok']);
$this->assertSame(SecurityCheckProcess::STATUS_COMPLETED, $captured['status']);
$process_state = json_decode((string) $captured['process_state_json'], true);
$this->assertTrue($process_state['beauftragung']['done']);
$this->assertSame(77, $process_state['beauftragung']['by']);
$this->assertNotEmpty($process_state['beauftragung']['at']);
$this->assertSame('Jane', $process_state[SecurityCheckProcess::STEP_INFORMATION]['fields']['technical_contact']);
$tech_state = json_decode((string) $captured['tech_state_json'], true);
$this->assertTrue($tech_state['tls']['done']);
$this->assertSame('verified', $tech_state['tls']['note']);
}
public function testSaveStateGatesInformationStepUntilRequiredFieldsFilled(): void
{
$repo = $this->createMock(SecurityCheckRepository::class);
$repo->method('findById')->willReturn([
'id' => 1,
'status' => SecurityCheckProcess::STATUS_OPEN,
'process_state_json' => '{}',
'tech_schema_snapshot_json' => '{}',
'tech_state_json' => '{}',
]);
$captured = null;
$repo->method('updateState')->willReturnCallback(function (int $tenantId, int $id, array $data) use (&$captured) {
$captured = $data;
return true;
});
[$service] = $this->makeService($repo);
$result = $service->saveState(self::TENANT_ID, 1, [
'step' => [SecurityCheckProcess::STEP_INFORMATION => [
'done' => '1',
'note' => '',
'fields' => ['technical_contact' => 'Jane'], // deployment / system_info / external_systems missing
]],
'tech' => [],
], 77);
// Saved (values persisted) but the step is held open + a warning is returned.
$this->assertTrue($result['ok']);
$this->assertArrayHasKey('warning', $result);
$process_state = json_decode((string) $captured['process_state_json'], true);
$this->assertFalse($process_state[SecurityCheckProcess::STEP_INFORMATION]['done']);
$this->assertSame('Jane', $process_state[SecurityCheckProcess::STEP_INFORMATION]['fields']['technical_contact']);
$this->assertNotSame(SecurityCheckProcess::STATUS_COMPLETED, $captured['status']);
}
public function testSaveStateCompletesInformationStepWhenAllRequiredFieldsFilled(): void
{
$repo = $this->createMock(SecurityCheckRepository::class);
$repo->method('findById')->willReturn([
'id' => 1,
'status' => SecurityCheckProcess::STATUS_OPEN,
'process_state_json' => '{}',
'tech_schema_snapshot_json' => '{}',
'tech_state_json' => '{}',
]);
$captured = null;
$repo->method('updateState')->willReturnCallback(function (int $tenantId, int $id, array $data) use (&$captured) {
$captured = $data;
return true;
});
[$service] = $this->makeService($repo);
$result = $service->saveState(self::TENANT_ID, 1, [
'step' => [SecurityCheckProcess::STEP_INFORMATION => [
'done' => '1',
'note' => '',
'fields' => [
'technical_contact' => 'Jane',
'deployment' => 'Cloud',
'system_info' => 'PHP 8.5',
'external_systems' => 'https://api.example.test',
// special_notes intentionally left empty — it is optional
],
]],
'tech' => [],
], 77);
$this->assertTrue($result['ok']);
$this->assertArrayNotHasKey('warning', $result);
$process_state = json_decode((string) $captured['process_state_json'], true);
$this->assertTrue($process_state[SecurityCheckProcess::STEP_INFORMATION]['done']);
$this->assertSame(77, $process_state[SecurityCheckProcess::STEP_INFORMATION]['by']);
}
public function testSaveStateGatesDatetimeStepUntilBothFieldsFilled(): void
{
$repo = $this->createMock(SecurityCheckRepository::class);
$repo->method('findById')->willReturn([
'id' => 1,
'status' => SecurityCheckProcess::STATUS_OPEN,
'process_state_json' => '{}',
'tech_schema_snapshot_json' => '{}',
'tech_state_json' => '{}',
]);
$captured = null;
$repo->method('updateState')->willReturnCallback(function (int $tenantId, int $id, array $data) use (&$captured) {
$captured = $data;
return true;
});
[$service] = $this->makeService($repo);
// Scheduling needs both datetimes; with only one filled it stays open.
$result = $service->saveState(self::TENANT_ID, 1, [
'step' => ['scheduling' => [
'done' => '1',
'note' => '',
'fields' => ['golive_start' => '2026-07-01T09:00'], // golive_end missing
]],
'tech' => [],
], 77);
$this->assertTrue($result['ok']);
$this->assertArrayHasKey('warning', $result);
$process_state = json_decode((string) $captured['process_state_json'], true);
$this->assertFalse($process_state['scheduling']['done']);
$this->assertSame('2026-07-01T09:00', $process_state['scheduling']['fields']['golive_start']);
}
public function testSaveStateRejectsArchivedCheck(): void
{
$repo = $this->createMock(SecurityCheckRepository::class);
$repo->method('findById')->willReturn([
'id' => 1,
'status' => SecurityCheckProcess::STATUS_ARCHIVED,
'process_state_json' => '{}',
'tech_schema_snapshot_json' => '{}',
'tech_state_json' => '{}',
]);
$repo->expects($this->never())->method('updateState');
[$service] = $this->makeService($repo);
$result = $service->saveState(self::TENANT_ID, 1, ['step' => [], 'tech' => []], 5);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('general', $result['errors']);
}
public function testSetArchivedUpdatesStatusToArchived(): void
{
$repo = $this->createMock(SecurityCheckRepository::class);
$repo->method('findById')->willReturn([
'id' => 1,
'status' => SecurityCheckProcess::STATUS_IN_PROGRESS,
'process_state_json' => '{}',
'tech_schema_snapshot_json' => '{}',
'tech_state_json' => '{}',
]);
$capturedStatus = null;
$repo->method('updateStatus')->willReturnCallback(function (int $tenantId, int $id, string $status) use (&$capturedStatus) {
$capturedStatus = $status;
return true;
});
[$service] = $this->makeService($repo);
$result = $service->setArchived(self::TENANT_ID, 1, true, 5);
$this->assertTrue($result['ok']);
$this->assertSame(SecurityCheckProcess::STATUS_ARCHIVED, $capturedStatus);
}
public function testListPagedDelegatesToRepository(): void
{
$repo = $this->createMock(SecurityCheckRepository::class);
$repo->method('listPaged')->willReturn(['total' => 3, 'rows' => [['id' => 1]]]);
[$service] = $this->makeService($repo);
$result = $service->listPaged(self::TENANT_ID, ['search' => 'x']);
$this->assertSame(3, $result['total']);
$this->assertCount(1, $result['rows']);
}
public function testListAssignableUsersDelegatesToRepository(): void
{
$repo = $this->createMock(SecurityCheckRepository::class);
$repo->method('listTenantUsers')->willReturn([
['id' => 3, 'display_name' => 'Alice'],
['id' => 7, 'display_name' => 'Bob'],
]);
[$service] = $this->makeService($repo);
$users = $service->listAssignableUsers(self::TENANT_ID);
$this->assertCount(2, $users);
$this->assertSame('Alice', $users[0]['display_name']);
}
}