import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock mongoose to prevent real DB connections; provide class-based models vi.mock('mongoose', () => { class FakeSchema { paths: Record = {}; constructor(def: Record, _opts?: unknown) { for (const key of Object.keys(def)) { this.paths[key] = { path: key }; } this.paths['createdAt'] = { path: 'createdAt' }; this.paths['updatedAt'] = { path: 'updatedAt' }; } index() { return this; } } const models: Record = {}; function createFakeModel(name: string) { const mockExec = vi.fn().mockResolvedValue(null); const mockLean = vi.fn(() => ({ exec: mockExec })); class Model { _data: Record; constructor(data: Record) { this._data = data; Object.assign(this, data); } save() { return Promise.resolve(this); } toObject() { return { _id: `${name}-id`, ...this._data }; } static modelName = name; static schema = new FakeSchema({}); static findOne = vi.fn(() => ({ lean: mockLean })); static findById = vi.fn(() => ({ lean: mockLean })); static findOneAndUpdate = vi.fn(() => ({ exec: mockExec })); static findByIdAndUpdate = vi.fn(() => ({ exec: mockExec })); } return Model; } return { default: { Schema: FakeSchema, model: vi.fn((name: string, _schema?: unknown) => { if (!models[name]) models[name] = createFakeModel(name); return models[name]; }), connect: vi.fn().mockResolvedValue(undefined), disconnect: vi.fn().mockResolvedValue(undefined), }, }; }); vi.mock('jose', () => ({ createRemoteJWKSet: vi.fn(() => 'mock-jwks'), jwtVerify: vi.fn(), })); import { buildApp } from '../src/main.js'; import * as jose from 'jose'; import { NotFoundError } from '../src/common/errors.js'; describe('buildApp', () => { beforeEach(() => { vi.clearAllMocks(); }); it('creates a Fastify app that is ready', async () => { const app = await buildApp({ logger: false }); await app.ready(); expect(app).toBeDefined(); await app.close(); }); it('has the health route available', async () => { const app = await buildApp({ logger: false }); await app.ready(); const res = await app.inject({ method: 'GET', url: '/api/v1/health' }); expect(res.statusCode).toBe(200); expect(res.json().status).toBe('ok'); await app.close(); }); }); describe('error handler', () => { async function getApp() { // Set up a valid JWT mock for authenticated routes vi.mocked(jose.jwtVerify).mockResolvedValue({ payload: { sub: 'kc-1', email: 'test@example.com', preferred_username: 'testuser', realm_access: { roles: ['member'] }, householdIds: ['hh1'], }, protectedHeader: { alg: 'RS256' }, key: {} as never, } as never); const app = await buildApp({ logger: false }); // Register test routes that throw various errors app.get('/test/app-error', { config: { public: true } as never }, async () => { throw new NotFoundError('Test not found'); }); app.get('/test/generic-error', { config: { public: true } as never }, async () => { const err = new Error('Something broke'); (err as unknown as Record).statusCode = 422; throw err; }); app.get('/test/unknown-error', { config: { public: true } as never }, async () => { throw new Error('Unexpected'); }); await app.ready(); return app; } beforeEach(() => { vi.clearAllMocks(); }); it('handles AppError with correct status and body', async () => { const app = await getApp(); const res = await app.inject({ method: 'GET', url: '/test/app-error' }); expect(res.statusCode).toBe(404); const body = res.json(); expect(body.error).toBe('Not Found'); expect(body.message).toBe('Test not found'); expect(body.timestamp).toBeDefined(); expect(body.path).toBe('/test/app-error'); await app.close(); }); it('handles generic errors with statusCode', async () => { const app = await getApp(); const res = await app.inject({ method: 'GET', url: '/test/generic-error' }); expect(res.statusCode).toBe(422); const body = res.json(); expect(body.error).toBe('Error'); expect(body.message).toBe('Something broke'); await app.close(); }); it('handles unknown 500 errors without leaking messages', async () => { const app = await getApp(); const res = await app.inject({ method: 'GET', url: '/test/unknown-error' }); expect(res.statusCode).toBe(500); const body = res.json(); expect(body.error).toBe('Internal Server Error'); expect(body.message).toBe('An unexpected error occurred'); await app.close(); }); it('returns 404 for unknown routes', async () => { const app = await getApp(); const res = await app.inject({ method: 'GET', url: '/nonexistent', headers: { authorization: 'Bearer test-token', 'x-household-id': 'hh1', }, }); expect(res.statusCode).toBe(404); await app.close(); }); });