Files
vyndr/tests/unit/oddsPapiAdapter.test.js

119 lines
4.3 KiB
JavaScript

process.env.ODDSPAPI_KEY = 'test-key';
process.env.ODDSPAPI_BASE_URL = 'https://api.oddspapi.test/v1';
const mockAxiosGet = jest.fn();
jest.mock('axios', () => ({
get: (...args) => mockAxiosGet(...args),
}));
// Fluent supabase fake. Records every upsert; from('grade_history') returns
// configurable rows; from('closing_lines') accepts upserts.
const mockGradeRows = { current: [] };
const mockUpserts = { current: [] };
jest.mock('../../src/utils/supabase', () => ({
getSupabaseServiceClient: () => ({
from(table) {
const ctx = { table };
const proxy = {
select() { return proxy; },
eq() { return proxy; },
is() { return Promise.resolve({ data: mockGradeRows.current, error: null }); },
upsert(row) {
mockUpserts.current.push({ table, row });
return Promise.resolve({ error: null });
},
};
return proxy;
},
}),
}));
const adapter = require('../../src/services/adapters/oddsPapiAdapter');
beforeEach(() => {
mockAxiosGet.mockReset();
mockGradeRows.current = [];
mockUpserts.current = [];
});
describe('oddsPapiAdapter.configured', () => {
test('reflects ODDSPAPI_KEY presence', () => {
expect(adapter.configured()).toBe(true);
delete process.env.ODDSPAPI_KEY;
expect(adapter.configured()).toBe(false);
process.env.ODDSPAPI_KEY = 'test-key';
});
});
describe('getPinnacleClosingLine', () => {
test('devigs Pinnacle prop into fair probabilities', async () => {
mockAxiosGet.mockResolvedValue({
data: {
props: [
{ player: 'Jalen Brunson', stat_type: 'points', line: 26.5, over_price: -110, under_price: -110 },
],
},
});
const closing = await adapter.getPinnacleClosingLine('nba', 'g-1', '3934672', 'points', 'Jalen Brunson');
expect(closing).toMatchObject({ line: 26.5, overOdds: -110, underOdds: -110 });
expect(closing.fairOver).toBeCloseTo(0.5, 5);
expect(typeof closing.capturedAt).toBe('string');
});
test('returns null when no matching prop', async () => {
mockAxiosGet.mockResolvedValue({ data: { props: [] } });
const result = await adapter.getPinnacleClosingLine('nba', 'g-1', '3934672', 'points', 'Ghost');
expect(result).toBeNull();
});
test('returns null when unconfigured', async () => {
delete process.env.ODDSPAPI_KEY;
const result = await adapter.getPinnacleClosingLine('nba', 'g-1', 'x', 'points', 'Anyone');
expect(result).toBeNull();
process.env.ODDSPAPI_KEY = 'test-key';
});
});
describe('batchCapture', () => {
test('skips when no graded props exist for the game', async () => {
mockGradeRows.current = [];
const summary = await adapter.batchCapture('nba', 'empty-game');
expect(summary).toMatchObject({ captured: 0, reason: 'no_graded_props' });
expect(mockUpserts.current).toHaveLength(0);
});
test('upserts closing_lines for each unique (player, stat) pair', async () => {
mockGradeRows.current = [
{ player_id: '1', player_name: 'A. Player', stat_type: 'points' },
{ player_id: '1', player_name: 'A. Player', stat_type: 'points' }, // duplicate
{ player_id: '2', player_name: 'B. Player', stat_type: 'rebounds' },
];
mockAxiosGet.mockResolvedValue({
data: {
props: [
{ player: 'A. Player', stat_type: 'points', line: 22.5, over_price: -110, under_price: -110 },
{ player: 'B. Player', stat_type: 'rebounds', line: 8.5, over_price: -105, under_price: -115 },
],
},
});
const summary = await adapter.batchCapture('nba', 'multi-game');
expect(summary.captured).toBe(2);
expect(summary.total).toBe(2); // duplicates collapsed
expect(mockUpserts.current).toHaveLength(2);
expect(mockUpserts.current[0].table).toBe('closing_lines');
expect(mockUpserts.current[0].row).toMatchObject({
game_id: 'multi-game',
sport: 'nba',
stat_type: 'points',
pinnacle_line: 22.5,
});
});
test('marks props skipped when Pinnacle has no line for them', async () => {
mockGradeRows.current = [{ player_id: '99', player_name: 'No Line', stat_type: 'points' }];
mockAxiosGet.mockResolvedValue({ data: { props: [] } });
const summary = await adapter.batchCapture('nba', 'no-pin');
expect(summary).toMatchObject({ captured: 0, skipped: 1, total: 1 });
});
});