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