feat: Feature 1.1 — Odds API integration complete, 28 tests passing
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
const { normalizeProps, MARKET_MAP, ALLOWED_BOOKS } = require('../../src/utils/oddsNormalizer');
|
||||
|
||||
function makeEvent(overrides = {}) {
|
||||
return {
|
||||
id: 'event-1',
|
||||
sport_key: 'basketball_nba',
|
||||
home_team: 'Denver Nuggets',
|
||||
away_team: 'Los Angeles Lakers',
|
||||
commence_time: '2026-03-21T19:00:00Z',
|
||||
bookmakers: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeBookmaker(key, markets) {
|
||||
return { key, title: key, markets };
|
||||
}
|
||||
|
||||
function makeMarket(marketKey, outcomes, lastUpdate = '2026-03-21T14:28:00Z') {
|
||||
return { key: marketKey, last_update: lastUpdate, outcomes };
|
||||
}
|
||||
|
||||
function makeOutcome(name, player, price, point) {
|
||||
return { name, description: player, price, point };
|
||||
}
|
||||
|
||||
describe('oddsNormalizer', () => {
|
||||
describe('normalizeProps', () => {
|
||||
it('normalizes a raw response with multiple books and markets', () => {
|
||||
const event = makeEvent({
|
||||
bookmakers: [
|
||||
makeBookmaker('draftkings', [
|
||||
makeMarket('player_points', [
|
||||
makeOutcome('Over', 'Nikola Jokic', -110, 26.5),
|
||||
makeOutcome('Under', 'Nikola Jokic', -110, 26.5),
|
||||
]),
|
||||
]),
|
||||
makeBookmaker('fanduel', [
|
||||
makeMarket('player_points', [
|
||||
makeOutcome('Over', 'Nikola Jokic', -105, 27.0),
|
||||
makeOutcome('Under', 'Nikola Jokic', -115, 27.0),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const result = normalizeProps([event]);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toMatchObject({
|
||||
player: 'Nikola Jokic',
|
||||
home_team: 'DEN',
|
||||
away_team: 'LAL',
|
||||
stat_type: 'points',
|
||||
book: 'draftkings',
|
||||
line: 26.5,
|
||||
over_odds: -110,
|
||||
under_odds: -110,
|
||||
});
|
||||
expect(result[1]).toMatchObject({
|
||||
player: 'Nikola Jokic',
|
||||
book: 'fanduel',
|
||||
line: 27.0,
|
||||
over_odds: -105,
|
||||
under_odds: -115,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out books not in the allowed set', () => {
|
||||
const event = makeEvent({
|
||||
bookmakers: [
|
||||
makeBookmaker('bovada', [
|
||||
makeMarket('player_points', [
|
||||
makeOutcome('Over', 'Jokic', -110, 26.5),
|
||||
makeOutcome('Under', 'Jokic', -110, 26.5),
|
||||
]),
|
||||
]),
|
||||
makeBookmaker('draftkings', [
|
||||
makeMarket('player_points', [
|
||||
makeOutcome('Over', 'Jokic', -110, 26.5),
|
||||
makeOutcome('Under', 'Jokic', -110, 26.5),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const result = normalizeProps([event]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].book).toBe('draftkings');
|
||||
});
|
||||
|
||||
it('maps all 8 market keys to correct internal stat_types', () => {
|
||||
const markets = Object.entries(MARKET_MAP);
|
||||
const bookmaker = makeBookmaker(
|
||||
'draftkings',
|
||||
markets.map(([key]) =>
|
||||
makeMarket(key, [
|
||||
makeOutcome('Over', 'Test Player', -110, 10.5),
|
||||
makeOutcome('Under', 'Test Player', -110, 10.5),
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
const event = makeEvent({ bookmakers: [bookmaker] });
|
||||
const result = normalizeProps([event]);
|
||||
|
||||
const statTypes = result.map((p) => p.stat_type);
|
||||
const expected = Object.values(MARKET_MAP);
|
||||
expect(statTypes).toEqual(expected);
|
||||
});
|
||||
|
||||
it('handles missing/null odds gracefully (skips incomplete outcomes)', () => {
|
||||
const event = makeEvent({
|
||||
bookmakers: [
|
||||
makeBookmaker('draftkings', [
|
||||
makeMarket('player_points', [
|
||||
// Missing description
|
||||
{ name: 'Over', description: null, price: -110, point: 26.5 },
|
||||
// Missing point
|
||||
{ name: 'Over', description: 'Jokic', price: -110, point: null },
|
||||
// Valid pair
|
||||
makeOutcome('Over', 'LeBron James', -110, 25.5),
|
||||
makeOutcome('Under', 'LeBron James', -110, 25.5),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const result = normalizeProps([event]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].player).toBe('LeBron James');
|
||||
});
|
||||
|
||||
it('returns empty array for empty input', () => {
|
||||
expect(normalizeProps([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array for events with no bookmakers', () => {
|
||||
const event = makeEvent({ bookmakers: undefined });
|
||||
expect(normalizeProps([event])).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles an outcome with only Over (no Under pair)', () => {
|
||||
const event = makeEvent({
|
||||
bookmakers: [
|
||||
makeBookmaker('fanduel', [
|
||||
makeMarket('player_points', [
|
||||
makeOutcome('Over', 'Solo Player', -110, 20.5),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const result = normalizeProps([event]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].over_odds).toBe(-110);
|
||||
expect(result[0].under_odds).toBeNull();
|
||||
});
|
||||
|
||||
it('uses UTC timestamps from the API as fetched_at', () => {
|
||||
const ts = '2026-03-21T18:00:00Z';
|
||||
const event = makeEvent({
|
||||
bookmakers: [
|
||||
makeBookmaker('betmgm', [
|
||||
makeMarket('player_points', [
|
||||
makeOutcome('Over', 'Player A', -110, 10.5),
|
||||
makeOutcome('Under', 'Player A', -110, 10.5),
|
||||
], ts),
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const result = normalizeProps([event]);
|
||||
expect(result[0].fetched_at).toBe(ts);
|
||||
});
|
||||
|
||||
it('maps team names to 3-letter abbreviations', () => {
|
||||
const event = makeEvent({
|
||||
home_team: 'Golden State Warriors',
|
||||
away_team: 'Phoenix Suns',
|
||||
bookmakers: [
|
||||
makeBookmaker('draftkings', [
|
||||
makeMarket('player_points', [
|
||||
makeOutcome('Over', 'Steph Curry', -110, 28.5),
|
||||
makeOutcome('Under', 'Steph Curry', -110, 28.5),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
const result = normalizeProps([event]);
|
||||
expect(result[0].home_team).toBe('GSW');
|
||||
expect(result[0].away_team).toBe('PHX');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user