Sessions 29-30: Content templates + PropLine 3-key adapter + MLB Stats API + ESPN summary (1694 tests)

This commit is contained in:
Kev
2026-06-14 22:29:01 -04:00
parent 927c4a5c65
commit a3351e2135
14 changed files with 1091 additions and 27 deletions
+107
View File
@@ -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');
});
});