// Integration: parlay / lines / books routes (Session 28). const express = require('express'); const request = require('supertest'); // Redis-backed services are mocked at the redis layer. const mockStore = {}; const mockScan = jest.fn(async () => ['0', []]); jest.mock('../../src/utils/redis', () => ({ cacheGet: jest.fn(async (k) => (k in mockStore ? mockStore[k] : null)), getRedisClient: () => ({ scan: mockScan, lrange: async () => [], rpush: async () => 1, ltrim: async () => 'OK', expire: async () => 1 }), isDegraded: () => false, })); function mount(routePath, file) { delete require.cache[require.resolve(file)]; const app = express(); app.use(express.json()); app.use(routePath, require(file)); return app; } beforeEach(() => { for (const k of Object.keys(mockStore)) delete mockStore[k]; jest.clearAllMocks(); }); describe('POST /api/parlay/calculate', () => { const app = () => mount('/api/parlay', '../../src/routes/parlay'); test('returns combined odds + grade for valid legs', async () => { const res = await request(app()).post('/api/parlay/calculate').send({ legs: [ { player: 'A', stat: 'points', odds: 100, grade: 'A', gameId: 'g1' }, { player: 'B', stat: 'hits', odds: 100, grade: 'A', gameId: 'g2' }, ], }); expect(res.status).toBe(200); expect(res.body.combinedOdds).toBe(300); // 2×2 = 4.0 → +300 expect(res.body.combinedGrade).toBeDefined(); }); test('empty legs → 400', async () => { const res = await request(app()).post('/api/parlay/calculate').send({ legs: [] }); expect(res.status).toBe(400); }); test('suggestions endpoint returns combos', async () => { const props = [ { player: 'A', stat: 'points', odds: -110, grade: 'A', gameId: 'g1' }, { player: 'B', stat: 'hits', odds: -110, grade: 'A', gameId: 'g2' }, { player: 'C', stat: 'goals', odds: -110, grade: 'B', gameId: 'g3' }, ]; const res = await request(app()).post('/api/parlay/suggestions').send({ props, legs: 3, max: 1 }); expect(res.status).toBe(200); expect(res.body.suggestions).toHaveLength(1); }); }); describe('GET /api/lines/:sport/movers', () => { const app = () => mount('/api/lines', '../../src/routes/lineMovement'); test('empty when no snapshots cached', async () => { const res = await request(app()).get('/api/lines/mlb/movers'); expect(res.status).toBe(200); expect(res.body.movers).toEqual([]); }); test('unsupported sport → 404', async () => { const res = await request(app()).get('/api/lines/cricket/movers'); expect(res.status).toBe(404); }); }); describe('GET /api/books/:sport', () => { const app = () => mount('/api/books', '../../src/routes/bookComparison'); test('returns best lines from cached props', async () => { mockStore[`odds:nba:${new Date().toISOString().split('T')[0]}`] = { props: [ { player: 'Wemby', stat_type: 'points', lines: [ { book: 'dk', over_odds: -110 }, { book: 'fd', over_odds: -105 }, ], }, ], }; const res = await request(app()).get('/api/books/nba'); expect(res.status).toBe(200); expect(res.body.bestLines).toHaveLength(1); expect(res.body.bestLines[0].bestBook).toBe('fd'); }); test('empty when no cached props', async () => { const res = await request(app()).get('/api/books/nba'); expect(res.status).toBe(200); expect(res.body.bestLines).toEqual([]); }); test('unsupported sport → 404', async () => { const res = await request(app()).get('/api/books/cricket'); expect(res.status).toBe(404); }); });