feat: implement mcpd core server framework with Fastify
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>
This commit is contained in:
102
src/mcpd/tests/audit.test.ts
Normal file
102
src/mcpd/tests/audit.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
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',
|
||||
}));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user