119 lines
4.3 KiB
JavaScript
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 });
|
|
});
|
|
});
|