, 2: array} * * Capture de l'appel `insert()` : [$table, $data, $types] */ private ?array $capturedInsert = null; private Connection $connection; private RequestStack $requestStack; private RequestIdProvider $requestIdProvider; protected function setUp(): void { $this->capturedInsert = null; $this->connection = $this->createMock(Connection::class); $this->connection ->method('insert') ->willReturnCallback(function (string $table, array $data, array $types = []): int { $this->capturedInsert = [$table, $data, $types]; return 1; }) ; $this->requestStack = new RequestStack(); $this->requestIdProvider = new RequestIdProvider(); } public function testLogsCreateWithAuthenticatedUser(): void { $security = $this->buildSecurityWithUser('alice'); $writer = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider); $writer->log('core.User', '42', 'create', ['username' => 'alice']); $this->assertNotNull($this->capturedInsert); [$table, $data] = $this->capturedInsert; $this->assertSame('audit_log', $table); $this->assertSame('core.User', $data['entity_type']); $this->assertSame('42', $data['entity_id']); $this->assertSame('create', $data['action']); $this->assertSame(['username' => 'alice'], $data['changes']); $this->assertSame('alice', $data['performed_by']); } public function testUsesSystemWhenNoAuthenticatedUser(): void { $security = $this->buildSecurityWithUser(null); $writer = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider); $writer->log('core.User', '1', 'update', ['isAdmin' => ['old' => false, 'new' => true]]); $this->assertSame('system', $this->capturedInsert[1]['performed_by']); } public function testStripsSensitiveKeys(): void { $security = $this->buildSecurityWithUser('alice'); $writer = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider); $writer->log('core.User', '1', 'create', [ 'username' => 'bob', 'password' => 'topsecrethash', 'plainPassword' => 'clear', 'token' => 'abc', 'secret' => 'xyz', 'email' => 'bob@example.com', ]); $changes = $this->capturedInsert[1]['changes']; $this->assertArrayNotHasKey('password', $changes); $this->assertArrayNotHasKey('plainPassword', $changes); $this->assertArrayNotHasKey('token', $changes); $this->assertArrayNotHasKey('secret', $changes); $this->assertSame('bob', $changes['username']); $this->assertSame('bob@example.com', $changes['email']); } public function testCapturesIpAddressWhenRequestPresent(): void { $request = Request::create('/api/users', 'POST'); $request->server->set('REMOTE_ADDR', '203.0.113.42'); $this->requestStack->push($request); $security = $this->buildSecurityWithUser('alice'); $writer = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider); $writer->log('core.User', '1', 'create', []); $this->assertSame('203.0.113.42', $this->capturedInsert[1]['ip_address']); } public function testIpAddressNullInCli(): void { $security = $this->buildSecurityWithUser(null); $writer = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider); $writer->log('core.User', '1', 'create', []); $this->assertNull($this->capturedInsert[1]['ip_address']); $this->assertNull($this->capturedInsert[1]['request_id']); } public function testGeneratesUuidV7PrimaryKey(): void { $security = $this->buildSecurityWithUser('alice'); $writer = new AuditLogWriter($this->connection, $security, $this->requestStack, $this->requestIdProvider); $writer->log('core.User', '1', 'create', []); $id = $this->capturedInsert[1]['id']; // UUID v7 : le 13e caractere (apres les tirets) vaut "7". // Format : xxxxxxxx-xxxx-7xxx-xxxx-xxxxxxxxxxxx $this->assertMatchesRegularExpression( '/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $id ); } private function buildSecurityWithUser(?string $username): Security { $security = $this->createMock(Security::class); $user = null !== $username ? new InMemoryUser($username, 'pwd') : null; $security->method('getUser')->willReturn($user); return $security; } }