121 lines
4.4 KiB
JavaScript
121 lines
4.4 KiB
JavaScript
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');
|
|
});
|
|
});
|