Sessions 29-30: Content templates + PropLine 3-key adapter + MLB Stats API + ESPN summary (1694 tests)
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
// Unit: provider preference + source tracking in getOdds (Session 30).
|
||||
|
||||
const mockRedis = { get: jest.fn(), set: jest.fn(), hset: jest.fn(), hgetall: jest.fn(), expire: jest.fn() };
|
||||
jest.mock('../../src/utils/redis', () => ({
|
||||
getRedisClient: () => mockRedis,
|
||||
cacheGet: jest.fn(async () => null),
|
||||
cacheSet: jest.fn(async () => true),
|
||||
cacheDel: jest.fn(async () => true),
|
||||
isDegraded: jest.fn(() => true), // gateway/quota fail open in tests
|
||||
}));
|
||||
|
||||
jest.mock('axios');
|
||||
const axios = require('axios');
|
||||
|
||||
// PropLine adapter mock — we drive hasKeys + getProps per test.
|
||||
jest.mock('../../src/services/adapters/proplineAdapter', () => ({
|
||||
hasKeys: jest.fn(),
|
||||
getProps: jest.fn(),
|
||||
}));
|
||||
const propline = require('../../src/services/adapters/proplineAdapter');
|
||||
|
||||
process.env.ODDS_API_KEY = 'test-api-key';
|
||||
const { getOdds } = require('../../src/services/oddsService');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRedis.get.mockResolvedValue(null); // cache miss
|
||||
mockRedis.set.mockResolvedValue('OK');
|
||||
mockRedis.hgetall.mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe('getOdds — PropLine preferred', () => {
|
||||
test('serves from PropLine when it returns props (provider=propline, no odds-api call)', async () => {
|
||||
propline.hasKeys.mockReturnValue(true);
|
||||
propline.getProps.mockResolvedValue({
|
||||
props: [{ player: 'Acuna', stat_type: 'hits', line: 0.5, over_odds: -200, under_odds: 160, book: 'betmgm' }],
|
||||
spreads: [],
|
||||
source: 'propline',
|
||||
});
|
||||
|
||||
const res = await getOdds('mlb');
|
||||
expect(res.source).toBe('live');
|
||||
expect(res.provider).toBe('propline');
|
||||
expect(res.props).toHaveLength(1);
|
||||
expect(axios.get).not.toHaveBeenCalled(); // odds-api never touched
|
||||
// Cached payload carries the provider tag.
|
||||
const cachedArg = JSON.parse(mockRedis.set.mock.calls[0][1]);
|
||||
expect(cachedArg.provider).toBe('propline');
|
||||
});
|
||||
|
||||
test('falls back to odds-api when PropLine returns empty (provider=odds-api)', async () => {
|
||||
propline.hasKeys.mockReturnValue(true);
|
||||
propline.getProps.mockResolvedValue({ props: [], spreads: [], source: 'propline' });
|
||||
|
||||
// odds-api fallback: events list, then per-event odds.
|
||||
axios.get
|
||||
.mockResolvedValueOnce({ data: [{ id: 'e1', home_team: 'Denver Nuggets', away_team: 'Los Angeles Lakers', commence_time: '2026-03-21T19:00:00Z' }], headers: {} })
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
id: 'e1', home_team: 'Denver Nuggets', away_team: 'Los Angeles Lakers', commence_time: '2026-03-21T19:00:00Z',
|
||||
bookmakers: [{ key: 'draftkings', title: 'DK', markets: [{ key: 'player_points', last_update: 't', outcomes: [
|
||||
{ name: 'Over', description: 'Jokic', price: -110, point: 26.5 },
|
||||
{ name: 'Under', description: 'Jokic', price: -110, point: 26.5 },
|
||||
] }] }],
|
||||
},
|
||||
headers: { 'x-requests-remaining': '400' },
|
||||
});
|
||||
|
||||
const res = await getOdds('nba');
|
||||
expect(res.provider).toBe('odds-api');
|
||||
expect(axios.get).toHaveBeenCalled();
|
||||
expect(res.props.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('PropLine skipped entirely when no keys (provider=odds-api)', async () => {
|
||||
propline.hasKeys.mockReturnValue(false);
|
||||
axios.get
|
||||
.mockResolvedValueOnce({ data: [{ id: 'e1', home_team: 'Denver Nuggets', away_team: 'Los Angeles Lakers', commence_time: '2026-03-21T19:00:00Z' }], headers: {} })
|
||||
.mockResolvedValueOnce({
|
||||
data: { id: 'e1', home_team: 'Denver Nuggets', away_team: 'Los Angeles Lakers', commence_time: '2026-03-21T19:00:00Z',
|
||||
bookmakers: [{ key: 'draftkings', title: 'DK', markets: [{ key: 'player_points', last_update: 't', outcomes: [
|
||||
{ name: 'Over', description: 'Jokic', price: -110, point: 26.5 },
|
||||
{ name: 'Under', description: 'Jokic', price: -110, point: 26.5 },
|
||||
] }] }] },
|
||||
headers: {},
|
||||
});
|
||||
const res = await getOdds('nba');
|
||||
expect(propline.getProps).not.toHaveBeenCalled();
|
||||
expect(res.provider).toBe('odds-api');
|
||||
});
|
||||
|
||||
test('PropLine error → graceful fallback to odds-api', async () => {
|
||||
propline.hasKeys.mockReturnValue(true);
|
||||
propline.getProps.mockRejectedValue(new Error('propline down'));
|
||||
axios.get
|
||||
.mockResolvedValueOnce({ data: [], headers: {} }); // empty events → empty props, but provider tagged
|
||||
const res = await getOdds('nba');
|
||||
expect(res.provider).toBe('odds-api');
|
||||
});
|
||||
|
||||
test('cached response surfaces the stored provider', async () => {
|
||||
mockRedis.get.mockResolvedValue(JSON.stringify({ updated_at: 't', props: [{ player: 'X' }], spreads: [], provider: 'propline' }));
|
||||
const res = await getOdds('mlb');
|
||||
expect(res.source).toBe('cache');
|
||||
expect(res.provider).toBe('propline');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user