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>
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
import { describe, it, expect, afterEach } from 'vitest';
|
|
import Fastify from 'fastify';
|
|
import type { FastifyInstance } from 'fastify';
|
|
import { registerHealthRoutes } from '../src/routes/health.js';
|
|
|
|
let app: FastifyInstance;
|
|
|
|
afterEach(async () => {
|
|
if (app) await app.close();
|
|
});
|
|
|
|
describe('GET /health', () => {
|
|
it('returns healthy when DB is up', async () => {
|
|
app = Fastify({ logger: false });
|
|
registerHealthRoutes(app, { checkDb: async () => true });
|
|
await app.ready();
|
|
|
|
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
expect(res.statusCode).toBe(200);
|
|
const body = res.json<{ status: string; version: string; checks: { database: string } }>();
|
|
expect(body.status).toBe('healthy');
|
|
expect(body.version).toBeDefined();
|
|
expect(body.checks.database).toBe('ok');
|
|
});
|
|
|
|
it('returns degraded when DB is down', async () => {
|
|
app = Fastify({ logger: false });
|
|
registerHealthRoutes(app, { checkDb: async () => false });
|
|
await app.ready();
|
|
|
|
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
expect(res.statusCode).toBe(503);
|
|
const body = res.json<{ status: string; checks: { database: string } }>();
|
|
expect(body.status).toBe('degraded');
|
|
expect(body.checks.database).toBe('error');
|
|
});
|
|
|
|
it('returns degraded when DB check throws', async () => {
|
|
app = Fastify({ logger: false });
|
|
registerHealthRoutes(app, {
|
|
checkDb: async () => { throw new Error('connection refused'); },
|
|
});
|
|
await app.ready();
|
|
|
|
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
expect(res.statusCode).toBe(503);
|
|
});
|
|
|
|
it('includes uptime and timestamp', async () => {
|
|
app = Fastify({ logger: false });
|
|
registerHealthRoutes(app, { checkDb: async () => true });
|
|
await app.ready();
|
|
|
|
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
const body = res.json<{ uptime: number; timestamp: string }>();
|
|
expect(body.uptime).toBeGreaterThan(0);
|
|
expect(body.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
});
|
|
});
|
|
|
|
describe('GET /healthz', () => {
|
|
it('returns ok (liveness probe)', async () => {
|
|
app = Fastify({ logger: false });
|
|
registerHealthRoutes(app, { checkDb: async () => true });
|
|
await app.ready();
|
|
|
|
const res = await app.inject({ method: 'GET', url: '/healthz' });
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.json<{ status: string }>().status).toBe('ok');
|
|
});
|
|
});
|