import Fastify, { FastifyInstance } from 'fastify'; import cookie from '@fastify/cookie'; import fp from 'fastify-plugin'; import { AppError } from '../../src/utils/errors.js'; import authPlugin from '../../src/plugins/auth.js'; import { authRoutes } from '../../src/routes/auth.js'; import type { MockDb } from '../test-utils.js'; import { createMockDb } from '../test-utils.js'; const mockDatabasePlugin = (db: MockDb) => fp(async (app) => { app.decorate('db', db); }, { name: 'database' }); /** Mock Redis for login lockout in auth tests. Implements ttl, setex, del, eval. */ const mockRedis = { async ttl(_key: string): Promise { return -2; // key does not exist -> not blocked }, async setex(_key: string, _ttl: number, _value: string): Promise<'OK'> { return 'OK'; }, async del(..._keys: string[]): Promise { return 0; }, async eval( _script: string, _keysCount: number, ..._keysAndArgs: string[] ): Promise { return [0, 0, 0]; // counters below threshold }, }; /** * Build a minimal Fastify app for auth route integration tests. * Uses mock db, mock Redis for login lockout, and rate limit options (no actual rate limiting). */ export async function buildAuthTestApp(mockDb?: MockDb): Promise { const db = mockDb ?? createMockDb(); const app = Fastify({ logger: false, requestIdHeader: 'x-request-id', requestIdLogLabel: 'requestId', }); app.setErrorHandler((err: unknown, request, reply) => { const error = err as Error & { statusCode?: number; validation?: unknown }; if (err instanceof AppError) { return reply.status(err.statusCode).send(err.toJSON()); } if (error.validation) { return reply.status(422).send({ error: { code: 'VALIDATION_ERROR', message: 'Validation failed', details: error.validation }, }); } return reply.status(500).send({ error: { code: 'INTERNAL_ERROR', message: error.message } }); }); app.decorate('redis', mockRedis); app.decorate('rateLimitOptions', { register: { max: 100, timeWindow: '1 hour' }, forgotPassword: { max: 100, timeWindow: '1 hour' }, verifyEmail: { max: 100, timeWindow: '15 minutes' }, apiAuthed: { max: 100, timeWindow: '1 minute' }, apiGuest: { max: 100, timeWindow: '1 minute' }, }); await app.register(mockDatabasePlugin(db)); await app.register(cookie, { secret: 'test-secret-at-least-32-characters-long' }); await app.register(authPlugin); await app.register(authRoutes, { prefix: '/api/v1/auth' }); return app; }