import { describe, it, expect, vi, beforeEach } from 'vitest'; import { AuditLogService } from '../src/services/audit-log.service.js'; import type { IAuditLogRepository } from '../src/repositories/interfaces.js'; import { NotFoundError } from '../src/services/mcp-server.service.js'; function makeLog(overrides: Partial<{ id: string; userId: string; action: string; resource: string; resourceId: string | null; createdAt: Date }> = {}) { return { id: overrides.id ?? 'log-1', userId: overrides.userId ?? 'user-1', action: overrides.action ?? 'CREATE', resource: overrides.resource ?? 'servers', resourceId: overrides.resourceId ?? null, details: {}, createdAt: overrides.createdAt ?? new Date('2025-01-15T00:00:00Z'), }; } function mockRepo(): IAuditLogRepository { return { findAll: vi.fn(async () => [makeLog()]), findById: vi.fn(async () => null), create: vi.fn(async (data) => ({ id: 'new-log', userId: data.userId, action: data.action, resource: data.resource, resourceId: data.resourceId ?? null, details: data.details ?? {}, createdAt: new Date(), })), count: vi.fn(async () => 1), deleteOlderThan: vi.fn(async () => 5), }; } describe('AuditLogService', () => { let repo: ReturnType; let service: AuditLogService; beforeEach(() => { repo = mockRepo(); service = new AuditLogService(repo, 90); }); describe('list', () => { it('returns logs with total count', async () => { const result = await service.list(); expect(result.logs).toHaveLength(1); expect(result.total).toBe(1); expect(repo.findAll).toHaveBeenCalled(); expect(repo.count).toHaveBeenCalled(); }); it('passes filter params to repository', async () => { await service.list({ userId: 'user-1', action: 'CREATE', resource: 'servers', limit: 50, offset: 10, }); expect(repo.findAll).toHaveBeenCalledWith( expect.objectContaining({ userId: 'user-1', action: 'CREATE', resource: 'servers', limit: 50, offset: 10, }), ); }); it('parses date strings in since/until', async () => { await service.list({ since: '2025-01-01T00:00:00Z', until: '2025-12-31T23:59:59Z', }); expect(repo.findAll).toHaveBeenCalledWith( expect.objectContaining({ since: expect.any(Date), until: expect.any(Date), }), ); }); }); describe('getById', () => { it('returns a log entry', async () => { vi.mocked(repo.findById).mockResolvedValue(makeLog({ id: 'log-1' })); const log = await service.getById('log-1'); expect(log.id).toBe('log-1'); }); it('throws NotFoundError when log does not exist', async () => { await expect(service.getById('nonexistent')).rejects.toThrow(NotFoundError); }); }); describe('create', () => { it('creates an audit log entry', async () => { const log = await service.create({ userId: 'user-1', action: 'CREATE', resource: 'servers', details: { method: 'POST' }, }); expect(log.id).toBe('new-log'); expect(repo.create).toHaveBeenCalledWith( expect.objectContaining({ userId: 'user-1', action: 'CREATE', resource: 'servers', }), ); }); it('creates log with optional resourceId', async () => { await service.create({ userId: 'user-1', action: 'DELETE', resource: 'servers', resourceId: 'srv-1', }); expect(repo.create).toHaveBeenCalledWith( expect.objectContaining({ resourceId: 'srv-1' }), ); }); }); describe('purgeExpired', () => { it('deletes logs older than retention period', async () => { const deleted = await service.purgeExpired(); expect(deleted).toBe(5); expect(repo.deleteOlderThan).toHaveBeenCalledWith(expect.any(Date)); }); it('uses configured retention days', async () => { const customService = new AuditLogService(repo, 30); await customService.purgeExpired(); const cutoff = vi.mocked(repo.deleteOlderThan).mock.calls[0]?.[0] as Date; const now = new Date(); const daysDiff = Math.round((now.getTime() - cutoff.getTime()) / (1000 * 60 * 60 * 24)); expect(daysDiff).toBe(30); }); }); });