106 lines
4.2 KiB
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");
|