// Unit: NFL + NHL sport-key wiring (Session 32). Closes the silent-zero // trap before NFL season — sport keys, per-sport markets, PropLine markets, // and end-to-end MARKET_MAP normalization. const oddsService = require('../../src/services/oddsService'); const propline = require('../../src/services/adapters/proplineAdapter'); const { normalizeProps } = require('../../src/utils/oddsNormalizer'); describe('oddsService sport keys', () => { test('nfl/nhl map to the correct the-odds-api keys', () => { // the-odds-api uses full-name prefixes (basketball_nba, baseball_mlb); // NFL/NHL follow the same convention — NOT football_nfl/hockey_nhl. expect(oddsService.SPORT_KEYS.nfl).toBe('americanfootball_nfl'); expect(oddsService.SPORT_KEYS.nhl).toBe('icehockey_nhl'); }); test('getMarketsForSport(nfl) requests NFL markets + spreads (not the NBA fallback)', () => { const markets = oddsService.getMarketsForSport('nfl'); expect(markets).toContain('player_pass_yds'); expect(markets).toContain('player_anytime_td'); expect(markets).toContain('spreads'); expect(markets).not.toContain('player_points'); // would mean nba fallback }); test('getMarketsForSport(nhl) requests NHL markets', () => { const markets = oddsService.getMarketsForSport('nhl'); expect(markets).toContain('player_shots_on_goal'); expect(markets).toContain('goalie_saves'); expect(markets).not.toContain('player_points'); }); }); describe('proplineAdapter NFL/NHL markets', () => { test('nfl/nhl markets are populated and sport keys resolve', () => { const { MARKETS, SPORT_KEYS } = propline.__internals; expect(MARKETS.nfl.length).toBeGreaterThan(0); expect(MARKETS.nhl.length).toBeGreaterThan(0); expect(MARKETS.nfl).toContain('player_pass_yds'); expect(MARKETS.nhl).toContain('player_shots_on_goal'); expect(SPORT_KEYS.nfl).toBe('football_nfl'); expect(SPORT_KEYS.nhl).toBe('hockey_nhl'); }); }); describe('MARKET_MAP end-to-end normalization', () => { function eventWith(marketKey, player, point) { return [{ home_team: 'Kansas City Chiefs', away_team: 'Buffalo Bills', commence_time: '2026-09-10T00:00:00Z', bookmakers: [{ key: 'draftkings', markets: [{ key: marketKey, last_update: '2026-09-09T20:00:00Z', outcomes: [ { description: player, point, name: 'Over', price: -110 }, { description: player, point, name: 'Under', price: -110 }, ], }], }], }]; } test('NFL passing-yards prop normalizes to passing_yards', () => { const props = normalizeProps(eventWith('player_pass_yds', 'Patrick Mahomes', 274.5)); expect(props).toHaveLength(1); expect(props[0].stat_type).toBe('passing_yards'); expect(props[0].player).toBe('Patrick Mahomes'); expect(props[0].line).toBe(274.5); }); test('NFL anytime-TD prop normalizes (does not drop to zero)', () => { const props = normalizeProps(eventWith('player_anytime_td', 'Travis Kelce', 0.5)); expect(props).toHaveLength(1); expect(props[0].stat_type).toBe('anytime_td'); }); test('NHL shots-on-goal + goalie-saves normalize', () => { expect(normalizeProps(eventWith('player_shots_on_goal', 'Connor McDavid', 3.5))[0].stat_type).toBe('shots_on_goal'); expect(normalizeProps(eventWith('goalie_saves', 'Igor Shesterkin', 28.5))[0].stat_type).toBe('saves'); }); test('off-season / empty response normalizes gracefully (no crash)', () => { expect(normalizeProps([])).toEqual([]); // Unknown market key is skipped, not crashed on. expect(normalizeProps(eventWith('player_unknown_stat', 'Nobody', 1.5))).toEqual([]); }); });