Files
mcpctl/src/mcpd/tests/audit-log-routes.test.ts
Michal 7c07749580 feat: add audit logging repository, service, and query API
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>
2026-02-21 05:09:14 +00:00

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);
});
});
});