feat: add administration reporting and correction services
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<?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");
|
||||
Reference in New Issue
Block a user