Implements IAuditLogRepository with Prisma, AuditLogService with configurable retention policy and purge, and REST routes for querying/filtering audit logs at /api/v1/audit-logs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
112 lines
3.3 KiB
TypeScript
112 lines
3.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import Fastify from 'fastify';
|
|
import type { FastifyInstance } from 'fastify';
|
|
import { registerAuditLogRoutes } from '../src/routes/audit-logs.js';
|
|
import { AuditLogService } from '../src/services/audit-log.service.js';
|
|
import type { IAuditLogRepository } from '../src/repositories/interfaces.js';
|
|
import { errorHandler } from '../src/middleware/error-handler.js';
|
|
|
|
function makeLog(id: string) {
|
|
return {
|
|
id,
|
|
userId: 'user-1',
|
|
action: 'CREATE',
|
|
resource: 'servers',
|
|
resourceId: null,
|
|
details: {},
|
|
createdAt: new Date('2025-01-15T00:00:00Z'),
|
|
};
|
|
}
|
|
|
|
function mockRepo(): IAuditLogRepository {
|
|
return {
|
|
findAll: vi.fn(async () => [makeLog('log-1'), makeLog('log-2')]),
|
|
findById: vi.fn(async () => null),
|
|
create: vi.fn(async () => makeLog('new-log')),
|
|
count: vi.fn(async () => 2),
|
|
deleteOlderThan: vi.fn(async () => 3),
|
|
};
|
|
}
|
|
|
|
describe('Audit Log Routes', () => {
|
|
let app: FastifyInstance;
|
|
let repo: ReturnType<typeof mockRepo>;
|
|
let service: AuditLogService;
|
|
|
|
beforeEach(async () => {
|
|
app = Fastify();
|
|
app.setErrorHandler(errorHandler);
|
|
repo = mockRepo();
|
|
service = new AuditLogService(repo, 90);
|
|
registerAuditLogRoutes(app, service);
|
|
await app.ready();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
describe('GET /api/v1/audit-logs', () => {
|
|
it('returns paginated audit logs', async () => {
|
|
const res = await app.inject({ method: 'GET', url: '/api/v1/audit-logs' });
|
|
expect(res.statusCode).toBe(200);
|
|
const body = res.json();
|
|
expect(body.logs).toHaveLength(2);
|
|
expect(body.total).toBe(2);
|
|
});
|
|
|
|
it('passes query filters to service', async () => {
|
|
await app.inject({
|
|
method: 'GET',
|
|
url: '/api/v1/audit-logs?userId=user-1&action=CREATE&resource=servers&limit=10&offset=5',
|
|
});
|
|
|
|
expect(repo.findAll).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
userId: 'user-1',
|
|
action: 'CREATE',
|
|
resource: 'servers',
|
|
limit: 10,
|
|
offset: 5,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('passes date range filters', async () => {
|
|
await app.inject({
|
|
method: 'GET',
|
|
url: '/api/v1/audit-logs?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('GET /api/v1/audit-logs/:id', () => {
|
|
it('returns a specific log entry', async () => {
|
|
vi.mocked(repo.findById).mockResolvedValue(makeLog('log-1'));
|
|
const res = await app.inject({ method: 'GET', url: '/api/v1/audit-logs/log-1' });
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.json().id).toBe('log-1');
|
|
});
|
|
|
|
it('returns 404 for missing log', async () => {
|
|
const res = await app.inject({ method: 'GET', url: '/api/v1/audit-logs/nonexistent' });
|
|
expect(res.statusCode).toBe(404);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/v1/audit-logs/purge', () => {
|
|
it('purges expired logs and returns count', async () => {
|
|
const res = await app.inject({ method: 'POST', url: '/api/v1/audit-logs/purge' });
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.json().deleted).toBe(3);
|
|
});
|
|
});
|
|
});
|