/** * @ai-summary Integration tests for AuditLogService * @ai-context Tests log creation, search, filtering, and cleanup */ import { Pool } from 'pg'; import { appConfig } from '../../../core/config/config-loader'; import { AuditLogService } from '../domain/audit-log.service'; import { AuditLogRepository } from '../data/audit-log.repository'; describe('AuditLogService', () => { let pool: Pool; let repository: AuditLogRepository; let service: AuditLogService; const createdIds: string[] = []; beforeAll(async () => { pool = new Pool({ connectionString: appConfig.getDatabaseUrl(), }); repository = new AuditLogRepository(pool); service = new AuditLogService(repository); }); afterAll(async () => { // Cleanup test data if (createdIds.length > 0) { await pool.query(`DELETE FROM audit_logs WHERE id = ANY($1::uuid[])`, [createdIds]); } await pool.end(); }); describe('log()', () => { it('should create log entry with all fields', async () => { const entry = await service.log( 'auth', 'info', 'user-123', 'User logged in', 'session', 'session-456', { ip: '192.168.1.1', browser: 'Chrome' } ); createdIds.push(entry.id); expect(entry.id).toBeDefined(); expect(entry.category).toBe('auth'); expect(entry.severity).toBe('info'); expect(entry.userId).toBe('user-123'); expect(entry.action).toBe('User logged in'); expect(entry.resourceType).toBe('session'); expect(entry.resourceId).toBe('session-456'); expect(entry.details).toEqual({ ip: '192.168.1.1', browser: 'Chrome' }); expect(entry.createdAt).toBeInstanceOf(Date); }); it('should create log entry with null userId for system actions', async () => { const entry = await service.log( 'system', 'info', null, 'Scheduled backup started' ); createdIds.push(entry.id); expect(entry.id).toBeDefined(); expect(entry.category).toBe('system'); expect(entry.userId).toBeNull(); }); it('should throw error for invalid category', async () => { await expect( service.log( 'invalid' as any, 'info', 'user-123', 'Test action' ) ).rejects.toThrow('Invalid audit log category'); }); it('should throw error for invalid severity', async () => { await expect( service.log( 'auth', 'invalid' as any, 'user-123', 'Test action' ) ).rejects.toThrow('Invalid audit log severity'); }); }); describe('convenience methods', () => { it('info() should create info-level log', async () => { const entry = await service.info('vehicle', 'user-123', 'Vehicle created'); createdIds.push(entry.id); expect(entry.severity).toBe('info'); }); it('warning() should create warning-level log', async () => { const entry = await service.warning('user', 'user-123', 'Password reset requested'); createdIds.push(entry.id); expect(entry.severity).toBe('warning'); }); it('error() should create error-level log', async () => { const entry = await service.error('admin', 'admin-123', 'Failed to revoke user'); createdIds.push(entry.id); expect(entry.severity).toBe('error'); }); }); describe('search()', () => { beforeAll(async () => { // Create test data for search const testLogs = [ { category: 'auth', severity: 'info', action: 'Login successful' }, { category: 'auth', severity: 'warning', action: 'Login failed' }, { category: 'vehicle', severity: 'info', action: 'Vehicle created' }, { category: 'vehicle', severity: 'info', action: 'Vehicle updated' }, { category: 'admin', severity: 'error', action: 'Admin action failed' }, ]; for (const log of testLogs) { const entry = await service.log( log.category as any, log.severity as any, 'test-user', log.action ); createdIds.push(entry.id); } }); it('should return paginated results', async () => { const result = await service.search({}, { limit: 10, offset: 0 }); expect(result.logs).toBeInstanceOf(Array); expect(result.total).toBeGreaterThan(0); expect(result.limit).toBe(10); expect(result.offset).toBe(0); }); it('should filter by category', async () => { const result = await service.search( { category: 'auth' }, { limit: 100, offset: 0 } ); expect(result.logs.length).toBeGreaterThan(0); expect(result.logs.every((log) => log.category === 'auth')).toBe(true); }); it('should filter by severity', async () => { const result = await service.search( { severity: 'error' }, { limit: 100, offset: 0 } ); expect(result.logs.every((log) => log.severity === 'error')).toBe(true); }); it('should search by action text', async () => { const result = await service.search( { search: 'Login' }, { limit: 100, offset: 0 } ); expect(result.logs.length).toBeGreaterThan(0); expect(result.logs.every((log) => log.action.includes('Login'))).toBe(true); }); }); describe('cleanup()', () => { it('should delete entries older than specified days', async () => { // Create an old entry by directly inserting await pool.query(` INSERT INTO audit_logs (category, severity, action, created_at) VALUES ('system', 'info', 'Old test entry', NOW() - INTERVAL '100 days') `); const deletedCount = await service.cleanup(90); expect(deletedCount).toBeGreaterThanOrEqual(1); }); it('should not delete recent entries', async () => { const entry = await service.log('system', 'info', null, 'Recent entry'); createdIds.push(entry.id); await service.cleanup(90); // Verify entry still exists const result = await pool.query( 'SELECT id FROM audit_logs WHERE id = $1', [entry.id] ); expect(result.rows.length).toBe(1); }); }); });