Files
JobcardSystem/tests/TechnicalInformationRepositoryTest.php

106 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../app/Domain/Credential/TechnicalInformation.php';
require_once __DIR__ . '/../app/Domain/Credential/TechnicalInformationRepository.php';
use App\Domain\Credential\TechnicalInformationRepository;
final class TechnicalInformationFakeStatement extends PDOStatement
{
/** @var callable */
private $executor;
private mixed $result = null;
public function __construct(callable $executor)
{
$this->executor = $executor;
}
public function execute(?array $params = null): bool
{
$this->result = ($this->executor)($params ?? []);
return true;
}
public function fetch(int $mode = PDO::FETCH_DEFAULT, int ...$args): mixed
{
return $this->result;
}
public function fetchAll(int $mode = PDO::FETCH_DEFAULT, mixed ...$args): array
{
return $this->result ?? [];
}
}
final class TechnicalInformationFakePdo extends PDO
{
/** @var list<array{sql:string,params:array}> */
public array $calls = [];
public ?array $row = null;
public function __construct()
{
}
public function prepare(string $query, array $options = []): PDOStatement|false
{
return new TechnicalInformationFakeStatement(function (array $params) use ($query): mixed {
$this->calls[] = ['sql' => $query, 'params' => $params];
if (str_starts_with($query, 'INSERT')) {
$this->row = [
'id' => $this->row['id'] ?? 41,
'client_id' => $params['client_id'],
'category' => $params['category'],
'data_json' => $params['data_json'],
'updated_by' => $params['updated_by'],
'created_at' => '2026-09-01 10:00:00',
'updated_at' => '2026-09-01 10:05:00',
];
return null;
}
return $this->row;
});
}
}
function technical_repository_assert_same(mixed $expected, mixed $actual, string $message): void
{
if ($expected !== $actual) {
throw new RuntimeException($message . "\nExpected: " . var_export($expected, true) . "\nActual: " . var_export($actual, true));
}
}
$pdo = new TechnicalInformationFakePdo();
$repository = new TechnicalInformationRepository($pdo);
$result = $repository->upsert(7, ' VPN ', ['label' => ' Office VPN ', 'username' => ' alice ', 'notes' => ' MFA enabled '], 12);
technical_repository_assert_same(41, $result['id'], 'Upsert must return the persisted record id.');
technical_repository_assert_same('vpn', $result['category'], 'Upsert must normalize the category before persistence.');
technical_repository_assert_same(['label' => 'Office VPN', 'username' => 'alice', 'notes' => 'MFA enabled'], $result['data'], 'Upsert must return normalized JSON data.');
technical_repository_assert_same(['id' => 41, 'client_id' => 7, 'category' => 'vpn', 'label' => 'Office VPN', 'username' => 'alice', 'notes' => 'MFA enabled'], $result['display'], 'Display projection must be allow-listed and safe.');
technical_repository_assert_same(['event' => 'technical_information.upserted', 'entity_type' => 'technical_information', 'entity_id' => 41, 'client_id' => 7, 'category' => 'vpn', 'updated_by' => 12], $result['audit'], 'Return value must include audit-ready identifiers and actor.');
$call = $pdo->calls[0] ?? [];
if (!str_contains($call['sql'] ?? '', 'ON DUPLICATE KEY UPDATE') || ($call['params']['data_json'] ?? '') !== '{"label":"Office VPN","username":"alice","notes":"MFA enabled"}') {
throw new RuntimeException('Repository must use a parameterized client/category upsert with canonical JSON.');
}
$found = $repository->find(7, 'vpn');
technical_repository_assert_same($result['id'], $found['id'], 'Find must read back the upserted row by client and category.');
try {
$repository->upsert(7, 'unknown', ['label' => 'Bad']);
throw new RuntimeException('Invalid technical-information categories must be rejected.');
} catch (InvalidArgumentException $expected) {
}
try {
$repository->upsert(0, 'vpn', ['label' => 'Bad']);
throw new RuntimeException('Invalid client ids must be rejected.');
} catch (InvalidArgumentException $expected) {
}
printf("Technical information repository tests: 5 passed\n");