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:
72
src/mcpd/tests/error-handler.test.ts
Normal file
72
src/mcpd/tests/error-handler.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import Fastify from 'fastify';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { ZodError, z } from 'zod';
|
||||
import { errorHandler } from '../src/middleware/error-handler.js';
|
||||
|
||||
let app: FastifyInstance;
|
||||
|
||||
afterEach(async () => {
|
||||
if (app) await app.close();
|
||||
});
|
||||
|
||||
function setupApp() {
|
||||
app = Fastify({ logger: false });
|
||||
app.setErrorHandler(errorHandler);
|
||||
return app;
|
||||
}
|
||||
|
||||
describe('errorHandler', () => {
|
||||
it('returns 400 for ZodError', async () => {
|
||||
const a = setupApp();
|
||||
a.get('/test', async () => {
|
||||
z.object({ name: z.string() }).parse({});
|
||||
});
|
||||
await a.ready();
|
||||
|
||||
const res = await a.inject({ method: 'GET', url: '/test' });
|
||||
expect(res.statusCode).toBe(400);
|
||||
const body = res.json<{ error: string; details: unknown[] }>();
|
||||
expect(body.error).toBe('Validation error');
|
||||
expect(body.details).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns 500 for unknown errors and hides details', async () => {
|
||||
const a = setupApp();
|
||||
a.get('/test', async () => {
|
||||
throw new Error('secret database password leaked');
|
||||
});
|
||||
await a.ready();
|
||||
|
||||
const res = await a.inject({ method: 'GET', url: '/test' });
|
||||
expect(res.statusCode).toBe(500);
|
||||
const body = res.json<{ error: string }>();
|
||||
expect(body.error).toBe('Internal server error');
|
||||
expect(JSON.stringify(body)).not.toContain('secret');
|
||||
});
|
||||
|
||||
it('returns correct status for HTTP errors', async () => {
|
||||
const a = setupApp();
|
||||
a.get('/test', async (_req, reply) => {
|
||||
reply.code(404).send({ error: 'Not found', statusCode: 404 });
|
||||
});
|
||||
await a.ready();
|
||||
|
||||
const res = await a.inject({ method: 'GET', url: '/test' });
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('returns 429 for rate limit errors', async () => {
|
||||
const a = setupApp();
|
||||
a.get('/test', async () => {
|
||||
const err = new Error('Rate limit') as Error & { statusCode: number };
|
||||
err.statusCode = 429;
|
||||
throw err;
|
||||
});
|
||||
await a.ready();
|
||||
|
||||
const res = await a.inject({ method: 'GET', url: '/test' });
|
||||
expect(res.statusCode).toBe(429);
|
||||
expect(res.json<{ error: string }>().error).toBe('Rate limit exceeded');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user