/** * Unit tests for src/services/AuditService.ts */ import { v4 as uuidv4 } from 'uuid'; import { AuditService } from '../../../src/services/AuditService'; import { AuditRepository } from '../../../src/repositories/AuditRepository'; import { AuditEventNotFoundError, RetentionWindowError, ValidationError, } from '../../../src/utils/errors'; import { IAuditEvent } from '../../../src/types/index'; jest.mock('../../../src/repositories/AuditRepository'); const MockAuditRepo = AuditRepository as jest.MockedClass; const MOCK_EVENT: IAuditEvent = { eventId: uuidv4(), agentId: uuidv4(), action: 'token.issued', outcome: 'success', ipAddress: '127.0.0.1', userAgent: 'test/1.0', metadata: { scope: 'agents:read' }, timestamp: new Date(), // recent timestamp }; describe('AuditService', () => { let service: AuditService; let auditRepo: jest.Mocked; beforeEach(() => { jest.clearAllMocks(); auditRepo = new MockAuditRepo({} as never) as jest.Mocked; service = new AuditService(auditRepo); }); // ──────────────────────────────────────────────────────────────── // logEvent // ──────────────────────────────────────────────────────────────── describe('logEvent()', () => { it('should create an audit event', async () => { auditRepo.create.mockResolvedValue(MOCK_EVENT); const result = await service.logEvent( MOCK_EVENT.agentId, 'token.issued', 'success', '127.0.0.1', 'test/1.0', { scope: 'agents:read' }, ); expect(result).toEqual(MOCK_EVENT); expect(auditRepo.create).toHaveBeenCalledTimes(1); }); }); // ──────────────────────────────────────────────────────────────── // queryEvents // ──────────────────────────────────────────────────────────────── describe('queryEvents()', () => { it('should return paginated events', async () => { auditRepo.findAll.mockResolvedValue({ events: [MOCK_EVENT], total: 1 }); const result = await service.queryEvents({ page: 1, limit: 50 }); expect(result.data).toHaveLength(1); expect(result.total).toBe(1); }); it('should throw RetentionWindowError for fromDate before 90-day cutoff', async () => { const oldDate = new Date(); oldDate.setDate(oldDate.getDate() - 100); await expect( service.queryEvents({ page: 1, limit: 50, fromDate: oldDate.toISOString() }), ).rejects.toThrow(RetentionWindowError); }); it('should throw ValidationError when fromDate is after toDate', async () => { const future = new Date(); future.setDate(future.getDate() + 5); const past = new Date(); past.setDate(past.getDate() - 1); await expect( service.queryEvents({ page: 1, limit: 50, fromDate: future.toISOString(), toDate: past.toISOString(), }), ).rejects.toThrow(ValidationError); }); it('should not throw for valid date range within retention window', async () => { auditRepo.findAll.mockResolvedValue({ events: [], total: 0 }); const recentDate = new Date(); recentDate.setDate(recentDate.getDate() - 30); await expect( service.queryEvents({ page: 1, limit: 50, fromDate: recentDate.toISOString() }), ).resolves.toBeDefined(); }); }); // ──────────────────────────────────────────────────────────────── // getEventById // ──────────────────────────────────────────────────────────────── describe('getEventById()', () => { it('should return the event when found within retention window', async () => { auditRepo.findById.mockResolvedValue(MOCK_EVENT); const result = await service.getEventById(MOCK_EVENT.eventId); expect(result).toEqual(MOCK_EVENT); }); it('should throw AuditEventNotFoundError when not found', async () => { auditRepo.findById.mockResolvedValue(null); await expect(service.getEventById('nonexistent')).rejects.toThrow(AuditEventNotFoundError); }); it('should throw AuditEventNotFoundError for event outside retention window', async () => { const oldEvent: IAuditEvent = { ...MOCK_EVENT, timestamp: new Date('2020-01-01T00:00:00Z'), }; auditRepo.findById.mockResolvedValue(oldEvent); await expect(service.getEventById(oldEvent.eventId)).rejects.toThrow( AuditEventNotFoundError, ); }); }); });