process.env.VYNDR_INTERNAL_KEY = 'health-test-key'; const mockPing = jest.fn(); jest.mock('../../src/utils/redis', () => ({ getRedisClient: () => ({ ping: mockPing }), isDegraded: () => false, cacheGet: async () => null, cacheSet: async () => true, cacheDel: async () => true, })); const mockSupabaseLimit = jest.fn(); jest.mock('../../src/utils/supabase', () => ({ getSupabaseServiceClient: () => ({ from() { const proxy = { select() { return proxy; }, limit() { return mockSupabaseLimit(); }, }; return proxy; }, }), })); const mockAxiosGet = jest.fn(); jest.mock('axios', () => ({ get: (...args) => mockAxiosGet(...args), post: jest.fn() })); // Adapter configured() helpers — fixed boolean returns are enough for the // detailed branch assertions. jest.mock('../../src/services/adapters/sharpApiAdapter', () => ({ configured: () => true })); jest.mock('../../src/services/adapters/propOddsAdapter', () => ({ configured: () => false })); jest.mock('../../src/services/adapters/parlayApiAdapter', () => ({ configured: () => true })); jest.mock('../../src/services/adapters/oddsPapiAdapter', () => ({ configured: () => false })); jest.mock('../../src/services/adapters/cfbdAdapter', () => ({ configured: () => false })); jest.mock('../../src/services/adapters/openRouterAdapter', () => ({ configured: () => true })); const app = require('../../src/app'); const http = require('http'); function call(path, headers = {}) { return new Promise((resolve) => { const server = app.listen(0, '127.0.0.1', () => { const port = server.address().port; const req = http.request({ host: '127.0.0.1', port, path, method: 'GET', headers }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { server.close(); const raw = Buffer.concat(chunks).toString('utf8'); let parsed; try { parsed = JSON.parse(raw); } catch { parsed = raw; } resolve({ status: res.statusCode, body: parsed }); }); }); req.end(); }); }); } beforeEach(() => { mockPing.mockReset(); mockSupabaseLimit.mockReset(); mockAxiosGet.mockReset(); }); describe('GET /api/health', () => { test('public response is minimal when no internal key', async () => { mockPing.mockResolvedValue('PONG'); mockSupabaseLimit.mockResolvedValue({ error: null }); const res = await call('/api/health'); expect(res.status).toBe(200); expect(res.body).toEqual({ status: 'healthy' }); }); test('reports 503 when Redis is down', async () => { mockPing.mockRejectedValue(new Error('econnrefused')); mockSupabaseLimit.mockResolvedValue({ error: null }); const res = await call('/api/health'); expect(res.status).toBe(503); expect(res.body.status).toBe('degraded'); }); test('reports 503 when Supabase errors', async () => { mockPing.mockResolvedValue('PONG'); mockSupabaseLimit.mockResolvedValue({ error: { message: 'bad' } }); const res = await call('/api/health'); expect(res.status).toBe(503); }); test('detailed branch with valid internal key includes adapters + python', async () => { mockPing.mockResolvedValue('PONG'); mockSupabaseLimit.mockResolvedValue({ error: null }); mockAxiosGet.mockResolvedValue({ status: 200, data: { status: 'ok' } }); const res = await call('/api/health', { 'x-vyndr-internal-key': 'health-test-key' }); expect(res.status).toBe(200); expect(res.body.checks.python).toBe('ok'); expect(res.body.checks.adapters).toMatchObject({ sharpapi: true, propodds: false, openrouter: true, }); expect(typeof res.body.uptime).toBe('number'); }); test('wrong internal key falls through to public response', async () => { mockPing.mockResolvedValue('PONG'); mockSupabaseLimit.mockResolvedValue({ error: null }); const res = await call('/api/health', { 'x-vyndr-internal-key': 'wrong' }); expect(res.body).toEqual({ status: 'healthy' }); expect(res.body.checks).toBeUndefined(); }); test('python down does not flip overall status (Python is optional)', async () => { mockPing.mockResolvedValue('PONG'); mockSupabaseLimit.mockResolvedValue({ error: null }); mockAxiosGet.mockRejectedValue(new Error('refused')); const res = await call('/api/health', { 'x-vyndr-internal-key': 'health-test-key' }); expect(res.status).toBe(200); expect(res.body.checks.python).toBe('down'); }); });