Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user