// Wave 6 — combatAdapter defensive parse. Fixtures modeled on the REAL ESPN // MMA scoreboard shape (site.api.espn.com/.../mma/ufc/scoreboard) captured // live during the build. NO network — fetchImpl + cache are injected. const combat = require('../../src/services/adapters/combatAdapter'); // A trimmed but structurally-faithful ESPN MMA scoreboard payload: one UFC // event ("card") with two bouts. Athlete ids live in the player-card link href. const ESPN_FIXTURE = { events: [ { id: '600059599', name: 'UFC Fight Night: Du Plessis vs. Usman', shortName: 'UFC Fight Night', date: '2026-07-18T21:00Z', competitions: [ { id: '1', type: { id: '1007', abbreviation: 'W Flyweight', text: "Women's Flyweight" }, format: { regulation: { periods: 5 } }, venue: { fullName: 'UFC APEX' }, status: { type: { state: 'pre', completed: false } }, competitors: [ { id: '10', order: 0, winner: false, athlete: { fullName: 'Dricus du Plessis', displayName: 'Dricus du Plessis', shortName: 'D. du Plessis', links: [{ href: 'https://www.espn.com/mma/fighter/_/id/4801725/dricus-du-plessis' }], }, records: [{ name: 'overall', type: 'total', summary: '22-2-0' }], }, { id: '11', order: 1, winner: false, athlete: { fullName: 'Kamaru Usman', displayName: 'Kamaru Usman', shortName: 'K. Usman', links: [{ href: 'https://www.espn.com/mma/fighter/_/id/3088843/kamaru-usman' }], }, records: [{ name: 'overall', type: 'total', summary: '20-4-0' }], }, ], }, { id: '2', type: { abbreviation: 'Lightweight' }, format: { regulation: { periods: 3 } }, competitors: [ { id: '20', order: 0, athlete: { displayName: 'Fighter A', links: [] }, records: [{ type: 'total', summary: '10-0' }] }, { id: '21', order: 1, athlete: { displayName: 'Fighter B', links: [] }, records: [{ type: 'total', summary: '8-3' }] }, ], }, ], }, ], }; describe('combatAdapter.normalizeScoreboard — ESPN shape → fight cards', () => { it('normalizes a real-shaped payload into a card with bouts + tale-of-tape', () => { const { events } = combat.normalizeScoreboard(ESPN_FIXTURE); expect(events).toHaveLength(1); const card = events[0]; expect(card.id).toBe('600059599'); expect(card.name).toMatch(/Du Plessis/); expect(card.bouts).toHaveLength(2); const bout = card.bouts[0]; expect(bout.weightClass).toBe("Women's Flyweight"); expect(bout.rounds).toBe(5); expect(bout.fighters).toHaveLength(2); const a = bout.fighters[0]; expect(a.name).toBe('Dricus du Plessis'); expect(a.id).toBe('4801725'); // parsed from the link href expect(a.record.wins).toBe(22); expect(a.record.losses).toBe(2); expect(a.record.display).toBe('22–2'); // en-dash display form // Physicals absent from the free feed → null, NEVER fabricated 0. expect(a.stance).toBeNull(); expect(a.reach).toBeNull(); }); it('date-pins defensively — an off-date event is dropped', () => { const onDate = combat.normalizeScoreboard(ESPN_FIXTURE, { date: '2026-07-18' }); expect(onDate.events).toHaveLength(1); const offDate = combat.normalizeScoreboard(ESPN_FIXTURE, { date: '2026-01-01' }); expect(offDate.events).toHaveLength(0); }); it('DEFENSIVE: unrecognized/garbage shapes return empty, never throw', () => { expect(() => combat.normalizeScoreboard(null)).not.toThrow(); expect(combat.normalizeScoreboard(null).events).toEqual([]); expect(combat.normalizeScoreboard({}).events).toEqual([]); expect(combat.normalizeScoreboard({ events: 'nope' }).events).toEqual([]); // An identifiable event with only unusable bouts is kept with empty bouts. expect(combat.normalizeScoreboard({ events: [{ id: 'x', competitions: [{ competitors: [{}] }] }] }).events[0].bouts).toEqual([]); // An event with no id AND no usable bouts is dropped entirely. expect(combat.normalizeScoreboard({ events: [{ competitions: [{ competitors: [{}] }] }] }).events).toEqual([]); }); it('numOrNull never coerces null/empty to 0 (the fabrication trap)', () => { expect(combat.numOrNull(null)).toBeNull(); expect(combat.numOrNull('')).toBeNull(); expect(combat.numOrNull(undefined)).toBeNull(); expect(combat.numOrNull('5')).toBe(5); expect(combat.numOrNull(0)).toBe(0); }); it('parseAthleteId is defensive on bad links', () => { expect(combat.parseAthleteId(null)).toBeNull(); expect(combat.parseAthleteId([{ href: 'no-id-here' }])).toBeNull(); expect(combat.parseAthleteId([{ href: '/mma/fighter/_/id/999/x' }])).toBe('999'); }); }); describe('combatAdapter.getFightCards — injectable, no network', () => { it('fetches via injected fetchImpl + normalizes (cache stubbed)', async () => { const calls = []; const res = await combat.getFightCards('2026-07-18', { fetchImpl: async (url) => { calls.push(url); return ESPN_FIXTURE; }, cacheGet: async () => null, cacheSet: async () => true, }); expect(res.events).toHaveLength(1); expect(calls[0]).toMatch(/dates=20260718/); }); it('returns honest empty on a fetch error, never throws', async () => { const res = await combat.getFightCards('2026-07-18', { fetchImpl: async () => { throw new Error('network'); }, cacheGet: async () => null, cacheSet: async () => true, }); expect(res.events).toEqual([]); }); }); describe('combatAdapter.normalizeCombatOdds — odds-api MMA → ML + round total', () => { const ODDS_FIXTURE = [ { id: 'evt1', commence_time: '2026-07-18T21:00Z', home_team: 'Dricus du Plessis', away_team: 'Kamaru Usman', bookmakers: [ { key: 'draftkings', markets: [ { key: 'h2h', outcomes: [{ name: 'Dricus du Plessis', price: -230 }, { name: 'Kamaru Usman', price: 190 }] }, { key: 'totals', outcomes: [{ name: 'Over', price: -110, point: 2.5 }, { name: 'Under', price: -110, point: 2.5 }] }, ], }, { key: 'fanduel', markets: [{ key: 'h2h', outcomes: [{ name: 'Dricus du Plessis', price: -215 }, { name: 'Kamaru Usman', price: 200 }] }], }, // A non-allow-listed book must be ignored. { key: 'bovada', markets: [{ key: 'h2h', outcomes: [{ name: 'Dricus du Plessis', price: -999 }] }] }, ], }, ]; it('maps h2h to per-fighter moneyline (best price) + totals to round total', () => { const map = combat.normalizeCombatOdds(ODDS_FIXTURE); const rec = map['dricus du plessis|kamaru usman']; expect(rec).toBeDefined(); expect(rec.moneyline.home).toBe(-215); // best (higher) of -230 / -215 expect(rec.moneyline.away).toBe(200); // best of 190 / 200 expect(rec.roundTotal.line).toBe(2.5); expect(rec.roundTotal.over).toBe(-110); }); it('ignores non-allow-listed books (bovada never leaks a price)', () => { const map = combat.normalizeCombatOdds(ODDS_FIXTURE); expect(map['dricus du plessis|kamaru usman'].moneyline.home).not.toBe(-999); }); it('matchBoutOdds joins in either name orientation', () => { const map = combat.normalizeCombatOdds(ODDS_FIXTURE); const bout = { fighters: [{ name: 'Kamaru Usman' }, { name: 'Dricus du Plessis' }] }; expect(combat.matchBoutOdds(bout, map)).toBeTruthy(); }); it('DEFENSIVE: garbage odds input returns {}, never throws', () => { expect(() => combat.normalizeCombatOdds(null)).not.toThrow(); expect(combat.normalizeCombatOdds(null)).toEqual({}); expect(combat.normalizeCombatOdds([{ bookmakers: 'x' }])).toEqual({}); }); });