'use strict'; const { getFutures, priceMove, attachMoves, linkNewsToMoves, } = require('../../src/services/futuresService'); const outrightEvents = [ { id: 'evt-ws', sport_key: 'baseball_mlb_world_series_winner', sport_title: 'MLB World Series Winner', bookmakers: [ { key: 'draftkings', markets: [{ key: 'outrights', outcomes: [ { name: 'Los Angeles Dodgers', price: 300 }, { name: 'New York Yankees', price: 500 }, ] }], }, ], }, ]; describe('priceMove (pure)', () => { test('shortening when payout drops (+400 → +300)', () => { expect(priceMove(400, 300)).toBe('shortening'); }); test('drifting when payout rises (+300 → +450)', () => { expect(priceMove(300, 450)).toBe('drifting'); }); test('flat within epsilon / unusable prices', () => { expect(priceMove(300, 300)).toBe('flat'); expect(priceMove(null, 300)).toBe('flat'); expect(priceMove(300, null)).toBe('flat'); }); test('sign correct across the +/- american boundary', () => { // +120 (dec 2.2) → -110 (dec 1.909): payout dropped → shortening expect(priceMove(120, -110)).toBe('shortening'); // -110 → +120: payout rose → drifting expect(priceMove(-110, 120)).toBe('drifting'); }); }); describe('attachMoves (pure)', () => { const cur = [{ key: 'k', title: 'K', selections: [ { name: 'A', price: 300 }, { name: 'B', price: 500 }, ] }]; test('first fetch (no prev) → move:flat, no prevPrice', () => { const out = attachMoves(cur, undefined); expect(out[0].selections[0]).toEqual({ name: 'A', price: 300, move: 'flat' }); }); test('diffs against prev cached markets → prevPrice + move', () => { const prev = [{ key: 'k', selections: [{ name: 'A', price: 400 }, { name: 'B', price: 500 }] }]; const out = attachMoves(cur, prev); const a = out[0].selections.find((s) => s.name === 'A'); expect(a.prevPrice).toBe(400); expect(a.move).toBe('shortening'); // 400 → 300 const b = out[0].selections.find((s) => s.name === 'B'); expect(b.prevPrice).toBe(500); expect(b.move).toBe('flat'); // unchanged }); }); describe('linkNewsToMoves (pure causal tie)', () => { const news = [ { headline: 'Dodgers acquire ace at deadline', published: '2026-07-30T12:00:00Z' }, { headline: 'Unrelated older story', published: '2026-06-01T12:00:00Z' }, ]; test('matches a move to the most-recent PRECEDING headline in window', () => { const moves = [{ key: 'k', name: 'Los Angeles Dodgers', at: '2026-07-31T00:00:00Z' }]; const links = linkNewsToMoves(news, moves, 48); expect(links).toEqual([{ moveKey: 'k|Los Angeles Dodgers', headline: 'Dodgers acquire ace at deadline' }]); }); test('no headline within window → no cause (never invents)', () => { const moves = [{ key: 'k', name: 'X', at: '2026-08-15T00:00:00Z' }]; expect(linkNewsToMoves(news, moves, 48)).toEqual([]); }); test('headline AFTER the move is not a cause', () => { const moves = [{ key: 'k', name: 'X', at: '2026-07-29T00:00:00Z' }]; expect(linkNewsToMoves(news, moves, 48)).toEqual([]); }); }); describe('getFutures (injectable, offline)', () => { const baseDeps = { apiKey: 'test', cacheGet: async () => null, cacheSet: async () => {} }; test('unknown sport → { markets: [] }, no fetch', async () => { const axios = { get: jest.fn() }; const out = await getFutures('cricket', { ...baseDeps, axios }); expect(out.markets).toEqual([]); expect(out.sport).toBe('cricket'); expect(axios.get).not.toHaveBeenCalled(); }); test('quota gate OFF (enabled:false) → { markets: [] }, no fetch', async () => { const axios = { get: jest.fn() }; const out = await getFutures('mlb', { ...baseDeps, axios, enabled: false }); expect(out.markets).toEqual([]); expect(axios.get).not.toHaveBeenCalled(); }); test('cold cache + enabled → fetches once, normalizes markets', async () => { const axios = { get: jest.fn().mockResolvedValue({ data: outrightEvents, headers: {} }) }; const cacheSet = jest.fn(async () => {}); const out = await getFutures('mlb', { ...baseDeps, axios, enabled: true, ttl: 3600, cacheSet }); expect(axios.get).toHaveBeenCalledTimes(1); // single outrights call expect(axios.get.mock.calls[0][1].params.markets).toBe('outrights'); expect(out.markets).toHaveLength(1); expect(out.markets[0].key).toBe('baseball_mlb_world_series_winner'); const dodgers = out.markets[0].selections.find((s) => s.name === 'Los Angeles Dodgers'); expect(dodgers.price).toBe(300); expect(dodgers.move).toBe('flat'); // no prev expect(cacheSet).toHaveBeenCalled(); }); test('price move shortening from a prev-price cache', async () => { const prevCache = { updated_at: '2000-01-01T00:00:00Z', // ancient → stale → refetch markets: [{ key: 'baseball_mlb_world_series_winner', selections: [ { name: 'Los Angeles Dodgers', price: 450 }, // was +450, now +300 → shortening ] }], }; const axios = { get: jest.fn().mockResolvedValue({ data: outrightEvents, headers: {} }) }; const out = await getFutures('mlb', { apiKey: 'test', axios, enabled: true, ttl: 3600, cacheGet: async () => prevCache, cacheSet: async () => {}, }); const dodgers = out.markets[0].selections.find((s) => s.name === 'Los Angeles Dodgers'); expect(dodgers.prevPrice).toBe(450); expect(dodgers.move).toBe('shortening'); }); test('fresh cache → served without fetching', async () => { const fresh = { updated_at: new Date().toISOString(), markets: [{ key: 'baseball_mlb_world_series_winner', title: 'WS', selections: [{ name: 'A', price: 100 }] }], }; const axios = { get: jest.fn() }; const out = await getFutures('mlb', { apiKey: 'test', axios, enabled: true, ttl: 3600, cacheGet: async () => fresh, cacheSet: async () => {}, }); expect(axios.get).not.toHaveBeenCalled(); expect(out.markets).toEqual(fresh.markets); }); test('fetch failure → empty (never throws, never fabricates)', async () => { const axios = { get: jest.fn().mockRejectedValue(new Error('boom')) }; const out = await getFutures('mlb', { ...baseDeps, axios, enabled: true, ttl: 3600 }); expect(out.markets).toEqual([]); }); test('empty board with no prior cache → { markets: [] }', async () => { const axios = { get: jest.fn().mockResolvedValue({ data: [], headers: {} }) }; const out = await getFutures('mlb', { ...baseDeps, axios, enabled: true, ttl: 3600 }); expect(out.markets).toEqual([]); }); });