Add Fastify server with config validation (Zod), health/healthz endpoints, auth middleware (Bearer token + session lookup), security plugins (CORS, Helmet, rate limiting), error handler, audit logging, and graceful shutdown. 36 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import Fastify from 'fastify';
|
|
import type { FastifyInstance } from 'fastify';
|
|
import { registerAuditHook } from '../src/middleware/audit.js';
|
|
|
|
let app: FastifyInstance;
|
|
|
|
afterEach(async () => {
|
|
if (app) await app.close();
|
|
});
|
|
|
|
describe('audit middleware', () => {
|
|
it('logs mutating requests from authenticated users', async () => {
|
|
const createAuditLog = vi.fn(async () => {});
|
|
app = Fastify({ logger: false });
|
|
|
|
// Simulate authenticated request
|
|
app.addHook('preHandler', async (request) => {
|
|
request.userId = 'user-1';
|
|
});
|
|
|
|
registerAuditHook(app, { createAuditLog });
|
|
|
|
app.post('/api/v1/servers', async () => ({ ok: true }));
|
|
await app.ready();
|
|
|
|
await app.inject({ method: 'POST', url: '/api/v1/servers', payload: {} });
|
|
|
|
expect(createAuditLog).toHaveBeenCalledOnce();
|
|
expect(createAuditLog).toHaveBeenCalledWith(expect.objectContaining({
|
|
userId: 'user-1',
|
|
action: 'CREATE',
|
|
resource: 'servers',
|
|
}));
|
|
});
|
|
|
|
it('does not log GET requests', async () => {
|
|
const createAuditLog = vi.fn(async () => {});
|
|
app = Fastify({ logger: false });
|
|
|
|
app.addHook('preHandler', async (request) => {
|
|
request.userId = 'user-1';
|
|
});
|
|
|
|
registerAuditHook(app, { createAuditLog });
|
|
app.get('/api/v1/servers', async () => []);
|
|
await app.ready();
|
|
|
|
await app.inject({ method: 'GET', url: '/api/v1/servers' });
|
|
expect(createAuditLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not log unauthenticated requests', async () => {
|
|
const createAuditLog = vi.fn(async () => {});
|
|
app = Fastify({ logger: false });
|
|
|
|
registerAuditHook(app, { createAuditLog });
|
|
app.post('/api/v1/servers', async () => ({ ok: true }));
|
|
await app.ready();
|
|
|
|
await app.inject({ method: 'POST', url: '/api/v1/servers', payload: {} });
|
|
expect(createAuditLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('maps DELETE method to DELETE action', async () => {
|
|
const createAuditLog = vi.fn(async () => {});
|
|
app = Fastify({ logger: false });
|
|
|
|
app.addHook('preHandler', async (request) => {
|
|
request.userId = 'user-1';
|
|
});
|
|
|
|
registerAuditHook(app, { createAuditLog });
|
|
app.delete('/api/v1/servers/:id', async () => ({ ok: true }));
|
|
await app.ready();
|
|
|
|
await app.inject({ method: 'DELETE', url: '/api/v1/servers/srv-123' });
|
|
expect(createAuditLog).toHaveBeenCalledWith(expect.objectContaining({
|
|
action: 'DELETE',
|
|
resource: 'servers',
|
|
resourceId: 'srv-123',
|
|
}));
|
|
});
|
|
|
|
it('maps PUT/PATCH to UPDATE action', async () => {
|
|
const createAuditLog = vi.fn(async () => {});
|
|
app = Fastify({ logger: false });
|
|
|
|
app.addHook('preHandler', async (request) => {
|
|
request.userId = 'user-1';
|
|
});
|
|
|
|
registerAuditHook(app, { createAuditLog });
|
|
app.put('/api/v1/servers/:id', async () => ({ ok: true }));
|
|
await app.ready();
|
|
|
|
await app.inject({ method: 'PUT', url: '/api/v1/servers/srv-1', payload: {} });
|
|
expect(createAuditLog).toHaveBeenCalledWith(expect.objectContaining({
|
|
action: 'UPDATE',
|
|
}));
|
|
});
|
|
});
|