/** * Unit tests for src/routes/health.ts * * Tests the GET /health endpoint via the createHealthRouter factory. * PostgreSQL and Redis dependencies are fully mocked — no live services required. */ import express, { Application } from 'express'; import request from 'supertest'; import { Pool, PoolClient } from 'pg'; import { RedisClientType } from 'redis'; import { createHealthRouter } from '../../../src/routes/health'; // ── Mock helpers ────────────────────────────────────────────────────────────── /** Builds a mock pg PoolClient with controllable query/release. */ function makePoolClient(queryError?: Error): jest.Mocked> { return { query: queryError ? jest.fn().mockRejectedValue(queryError) : jest.fn().mockResolvedValue({ rows: [{ '?column?': 1 }], rowCount: 1 }), release: jest.fn(), } as unknown as jest.Mocked>; } /** Builds a mock pg Pool whose connect() resolves or rejects on demand. */ function makePool(connectError?: Error, queryError?: Error): jest.Mocked { return { connect: connectError ? jest.fn().mockRejectedValue(connectError) : jest.fn().mockResolvedValue(makePoolClient(queryError)), } as unknown as jest.Mocked; } /** Builds a mock Redis client whose ping() resolves or rejects on demand. */ function makeRedis(pingError?: Error): jest.Mocked { return { ping: pingError ? jest.fn().mockRejectedValue(pingError) : jest.fn().mockResolvedValue('PONG'), } as unknown as jest.Mocked; } /** Creates a minimal Express app with the health router mounted at /health. */ function buildApp(pool: jest.Mocked, redis: jest.Mocked): Application { const app = express(); app.use('/health', createHealthRouter(pool as unknown as Pool, redis as unknown as RedisClientType)); return app; } // ── Tests ───────────────────────────────────────────────────────────────────── describe('GET /health', () => { describe('when both PostgreSQL and Redis are healthy', () => { it('returns 200 with status ok and both services connected', async () => { const app = buildApp(makePool(), makeRedis()); const response = await request(app).get('/health'); expect(response.status).toBe(200); expect(response.body).toMatchObject({ status: 'ok', services: { postgres: 'connected', redis: 'connected', }, }); }); it('includes version and uptime fields in the response', async () => { const app = buildApp(makePool(), makeRedis()); const response = await request(app).get('/health'); expect(typeof response.body.version).toBe('string'); expect(typeof response.body.uptime).toBe('number'); }); }); describe('when PostgreSQL connect() throws', () => { it('returns 503 with status degraded and postgres disconnected, redis connected', async () => { const pool = makePool(new Error('PG connection refused')); const app = buildApp(pool, makeRedis()); const response = await request(app).get('/health'); expect(response.status).toBe(503); expect(response.body).toMatchObject({ status: 'degraded', services: { postgres: 'disconnected', redis: 'connected', }, }); }); }); describe('when Redis ping() throws', () => { it('returns 503 with status degraded and postgres connected, redis disconnected', async () => { const app = buildApp(makePool(), makeRedis(new Error('Redis ECONNREFUSED'))); const response = await request(app).get('/health'); expect(response.status).toBe(503); expect(response.body).toMatchObject({ status: 'degraded', services: { postgres: 'connected', redis: 'disconnected', }, }); }); }); describe('when both PostgreSQL and Redis fail', () => { it('returns 503 with status degraded and both services disconnected', async () => { const pool = makePool(new Error('PG down')); const app = buildApp(pool, makeRedis(new Error('Redis down'))); const response = await request(app).get('/health'); expect(response.status).toBe(503); expect(response.body).toMatchObject({ status: 'degraded', services: { postgres: 'disconnected', redis: 'disconnected', }, }); }); }); describe('when PostgreSQL query() throws (connect succeeds but query fails)', () => { it('returns 503 with postgres disconnected', async () => { const pool = makePool(undefined, new Error('PG query error')); const app = buildApp(pool, makeRedis()); const response = await request(app).get('/health'); expect(response.status).toBe(503); expect(response.body).toMatchObject({ status: 'degraded', services: { postgres: 'disconnected', redis: 'connected', }, }); }); }); });