// Session 16 — sport-specific market scoping in oddsService. // Replaces a runtime axios interceptor (NODE_OPTIONS --require) that // had been deployed to filter out cross-sport markets the upstream // odds-api 422s on. These tests pin the contract so the runtime hack // can be retired safely. const { SPORT_MARKETS, getMarketsForSport } = require('../../src/services/oddsService'); describe('SPORT_MARKETS — isolation', () => { test('NBA market list contains no soccer markets', () => { expect(SPORT_MARKETS.nba).not.toMatch(/player_goals\b/); expect(SPORT_MARKETS.nba).not.toMatch(/player_shots_on_target/); expect(SPORT_MARKETS.nba).not.toMatch(/player_tackles/); expect(SPORT_MARKETS.nba).not.toMatch(/player_cards/); expect(SPORT_MARKETS.nba).not.toMatch(/team_clean_sheet/); }); test('NBA market list contains no MLB markets', () => { expect(SPORT_MARKETS.nba).not.toMatch(/batter_/); expect(SPORT_MARKETS.nba).not.toMatch(/pitcher_/); }); test('NBA market list does contain canonical NBA markets', () => { expect(SPORT_MARKETS.nba).toMatch(/player_points/); expect(SPORT_MARKETS.nba).toMatch(/player_rebounds/); expect(SPORT_MARKETS.nba).toMatch(/player_assists/); expect(SPORT_MARKETS.nba).toMatch(/player_threes/); expect(SPORT_MARKETS.nba).toMatch(/spreads/); }); test('WNBA market list is NBA-shaped minus PRA combo', () => { expect(SPORT_MARKETS.wnba).toMatch(/player_points/); expect(SPORT_MARKETS.wnba).toMatch(/player_rebounds/); // WNBA odds-api doesn't expose the PRA combo today. expect(SPORT_MARKETS.wnba).not.toMatch(/points_rebounds_assists/); expect(SPORT_MARKETS.wnba).not.toMatch(/batter_/); expect(SPORT_MARKETS.wnba).not.toMatch(/player_goals\b/); }); test('MLB market list contains batter + pitcher markets, no basketball', () => { expect(SPORT_MARKETS.mlb).toMatch(/batter_home_runs/); expect(SPORT_MARKETS.mlb).toMatch(/batter_hits/); expect(SPORT_MARKETS.mlb).toMatch(/pitcher_strikeouts/); expect(SPORT_MARKETS.mlb).not.toMatch(/player_points/); expect(SPORT_MARKETS.mlb).not.toMatch(/player_goals\b/); }); test('every soccer league shares the same market list', () => { const soccerKeys = Object.keys(SPORT_MARKETS).filter((k) => k.startsWith('soccer_')); expect(soccerKeys.length).toBeGreaterThanOrEqual(9); const first = SPORT_MARKETS[soccerKeys[0]]; for (const k of soccerKeys) { expect(SPORT_MARKETS[k]).toBe(first); } }); test('soccer market list contains soccer-only markets, no basketball/baseball', () => { const wc = SPORT_MARKETS.soccer_wc; expect(wc).toMatch(/player_goals/); expect(wc).toMatch(/player_shots_on_target/); expect(wc).toMatch(/player_cards/); expect(wc).toMatch(/team_clean_sheet/); expect(wc).not.toMatch(/player_points/); expect(wc).not.toMatch(/batter_/); }); test('every market list ends with `spreads`', () => { for (const list of Object.values(SPORT_MARKETS)) { // We don't require spreads to be the literal final segment, // only that it's present in the comma-separated list. expect(list.split(',')).toContain('spreads'); } }); test('SPORT_MARKETS is frozen at the top level', () => { expect(Object.isFrozen(SPORT_MARKETS)).toBe(true); }); }); describe('getMarketsForSport', () => { test('returns the NBA list for nba', () => { expect(getMarketsForSport('nba')).toBe(SPORT_MARKETS.nba); }); test('returns the soccer_wc list for soccer_wc', () => { expect(getMarketsForSport('soccer_wc')).toBe(SPORT_MARKETS.soccer_wc); }); test('unknown sport falls back to NBA (safe default)', () => { expect(getMarketsForSport('cricket')).toBe(SPORT_MARKETS.nba); }); test('null / undefined / empty fall back to NBA', () => { expect(getMarketsForSport(null)).toBe(SPORT_MARKETS.nba); expect(getMarketsForSport(undefined)).toBe(SPORT_MARKETS.nba); expect(getMarketsForSport('')).toBe(SPORT_MARKETS.nba); }); }); describe('fetchEventOddsFromApi uses sport-scoped markets', () => { // Mock axios so the test doesn't hit the network. jest.resetModules(); const mockGet = jest.fn(() => Promise.resolve({ data: {}, headers: {} })); jest.doMock('axios', () => ({ get: mockGet })); const { fetchEventOddsFromApi, SPORT_MARKETS: SM } = require('../../src/services/oddsService'); beforeEach(() => mockGet.mockClear()); test('NBA fetch sends NBA markets only', async () => { await fetchEventOddsFromApi('basketball_nba', 'evt1', 'key', 'nba'); const [, opts] = mockGet.mock.calls[0]; expect(opts.params.markets).toBe(SM.nba); }); test('MLB fetch sends MLB markets only', async () => { await fetchEventOddsFromApi('baseball_mlb', 'evt1', 'key', 'mlb'); const [, opts] = mockGet.mock.calls[0]; expect(opts.params.markets).toBe(SM.mlb); }); test('soccer fetch sends soccer markets only', async () => { await fetchEventOddsFromApi('soccer_fifa_world_cup', 'evt1', 'key', 'soccer_wc'); const [, opts] = mockGet.mock.calls[0]; expect(opts.params.markets).toBe(SM.soccer_wc); }); test('omitted sport arg falls back to NBA markets (legacy callers)', async () => { await fetchEventOddsFromApi('basketball_nba', 'evt1', 'key'); const [, opts] = mockGet.mock.calls[0]; expect(opts.params.markets).toBe(SM.nba); }); });