connection = $container->get(Connection::class); /* @var AuditLogger $auditLogger */ $this->auditLogger = $container->get(AuditLogger::class); } #[Test] public function logAuthenticationWritesEntryToAuditLogTable(): void { $userId = Uuid::uuid4(); $this->auditLogger->logAuthentication( eventType: 'ConnexionReussie', userId: $userId, payload: [ 'email_hash' => hash('sha256', 'test@example.com'), 'result' => 'success', 'method' => 'password', ], ); $entry = $this->connection->fetchAssociative( 'SELECT * FROM audit_log WHERE aggregate_id = ? AND event_type = ? ORDER BY occurred_at DESC LIMIT 1', [$userId->toString(), 'ConnexionReussie'], ); self::assertNotFalse($entry, 'Audit log entry should exist after logAuthentication'); self::assertSame('User', $entry['aggregate_type']); self::assertSame($userId->toString(), $entry['aggregate_id']); self::assertSame('ConnexionReussie', $entry['event_type']); $payload = json_decode($entry['payload'], true, 512, JSON_THROW_ON_ERROR); self::assertSame('success', $payload['result']); self::assertSame('password', $payload['method']); self::assertArrayHasKey('email_hash', $payload); } #[Test] public function logAuthenticationIncludesMetadataWithTimestamp(): void { $userId = Uuid::uuid4(); $this->auditLogger->logAuthentication( eventType: 'ConnexionReussie', userId: $userId, payload: ['result' => 'success'], ); $entry = $this->connection->fetchAssociative( 'SELECT * FROM audit_log WHERE aggregate_id = ? ORDER BY occurred_at DESC LIMIT 1', [$userId->toString()], ); self::assertNotFalse($entry); self::assertNotEmpty($entry['occurred_at'], 'Audit entry must have a timestamp'); $metadata = json_decode($entry['metadata'], true, 512, JSON_THROW_ON_ERROR); self::assertIsArray($metadata); } #[Test] public function logFailedAuthenticationWritesWithNullUserId(): void { $this->auditLogger->logAuthentication( eventType: 'ConnexionEchouee', userId: null, payload: [ 'email_hash' => hash('sha256', 'unknown@example.com'), 'result' => 'failure', 'reason' => 'invalid_credentials', ], ); $entry = $this->connection->fetchAssociative( "SELECT * FROM audit_log WHERE event_type = 'ConnexionEchouee' ORDER BY occurred_at DESC LIMIT 1", ); self::assertNotFalse($entry, 'Failed login audit entry should exist'); self::assertNull($entry['aggregate_id'], 'Failed login should have null user ID'); self::assertSame('User', $entry['aggregate_type']); $payload = json_decode($entry['payload'], true, 512, JSON_THROW_ON_ERROR); self::assertSame('failure', $payload['result']); self::assertSame('invalid_credentials', $payload['reason']); } #[Test] public function logDataChangeWritesOldAndNewValues(): void { $aggregateId = Uuid::uuid4(); $this->auditLogger->logDataChange( aggregateType: 'Grade', aggregateId: $aggregateId, eventType: 'GradeModified', oldValues: ['value' => 14.0], newValues: ['value' => 16.0], reason: 'Correction erreur de saisie', ); $entry = $this->connection->fetchAssociative( 'SELECT * FROM audit_log WHERE aggregate_id = ? AND event_type = ? ORDER BY occurred_at DESC LIMIT 1', [$aggregateId->toString(), 'GradeModified'], ); self::assertNotFalse($entry); self::assertSame('Grade', $entry['aggregate_type']); $payload = json_decode($entry['payload'], true, 512, JSON_THROW_ON_ERROR); self::assertSame(['value' => 14.0], $payload['old_values']); self::assertSame(['value' => 16.0], $payload['new_values']); self::assertSame('Correction erreur de saisie', $payload['reason']); } #[Test] public function auditLogEntriesAreAppendOnly(): void { $userId = Uuid::uuid4(); $this->auditLogger->logAuthentication( eventType: 'ConnexionReussie', userId: $userId, payload: ['result' => 'success'], ); $countBefore = (int) $this->connection->fetchOne( 'SELECT COUNT(*) FROM audit_log WHERE aggregate_id = ?', [$userId->toString()], ); self::assertSame(1, $countBefore); // Log a second event for the same user $this->auditLogger->logAuthentication( eventType: 'ConnexionReussie', userId: $userId, payload: ['result' => 'success'], ); $countAfter = (int) $this->connection->fetchOne( 'SELECT COUNT(*) FROM audit_log WHERE aggregate_id = ?', [$userId->toString()], ); // Both entries should exist (append-only, no overwrite) self::assertSame(2, $countAfter, 'Audit log must be append-only — both entries should exist'); } }