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 */ 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");