First version of the security module
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
<?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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user