Session 23: All-day intelligence layer — schedule, game lines, streaks, hot lists, stat filtering, ParlayAPI dead (1567 tests)
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
// Integration: /api/gamelines/:sport (Session 23).
|
||||
//
|
||||
// The Tank01 adapters are mocked — we assert the route normalizes the
|
||||
// book-by-book body, parses teams from the gameID, handles the missing
|
||||
// API-key path gracefully, and never 500s on adapter failure.
|
||||
|
||||
const express = require('express');
|
||||
const request = require('supertest');
|
||||
|
||||
jest.mock('../../src/services/adapters/tank01NbaAdapter', () => ({
|
||||
getNBABettingOdds: jest.fn(),
|
||||
hasApiKey: jest.fn(() => true),
|
||||
}));
|
||||
jest.mock('../../src/services/adapters/tank01MlbAdapter', () => ({
|
||||
getMLBBettingOdds: jest.fn(),
|
||||
hasApiKey: jest.fn(() => true),
|
||||
}));
|
||||
jest.mock('../../src/services/scheduleService', () => ({
|
||||
todayET: () => '2026-06-12',
|
||||
}));
|
||||
|
||||
const nbaAdapter = require('../../src/services/adapters/tank01NbaAdapter');
|
||||
const mlbAdapter = require('../../src/services/adapters/tank01MlbAdapter');
|
||||
|
||||
function mountApp() {
|
||||
delete require.cache[require.resolve('../../src/routes/gameLines')];
|
||||
const routes = require('../../src/routes/gameLines');
|
||||
const app = express();
|
||||
app.use('/api/gamelines', routes);
|
||||
return app;
|
||||
}
|
||||
|
||||
const MLB_BODY = {
|
||||
'20260612_ARI@CIN': {
|
||||
gameID: '20260612_ARI@CIN',
|
||||
sportsBooks: [
|
||||
{ sportsBook: 'bet365', odds: { homeTeamMLOdds: '-110', awayTeamMLOdds: '+100', totalOver: '9.5', totalOverOdds: '-105', totalUnderOdds: '-115' } },
|
||||
{ sportsBook: 'betmgm', odds: { homeTeamMLOdds: '-115', awayTeamMLOdds: '-105', totalOver: '9' } },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// clearAllMocks wipes call history but not custom implementations set via
|
||||
// mockReturnValue in a prior test — restore the configured-key default.
|
||||
nbaAdapter.hasApiKey.mockReturnValue(true);
|
||||
mlbAdapter.hasApiKey.mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe('GET /api/gamelines/:sport', () => {
|
||||
test('mlb returns book-by-book odds with teams parsed from gameID', async () => {
|
||||
mlbAdapter.getMLBBettingOdds.mockResolvedValue(MLB_BODY);
|
||||
const res = await request(mountApp()).get('/api/gamelines/mlb');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.source).toBe('tank01');
|
||||
const game = res.body.games['20260612_ARI@CIN'];
|
||||
expect(game.homeTeam).toBe('CIN');
|
||||
expect(game.awayTeam).toBe('ARI');
|
||||
expect(game.books.bet365.homeML).toBe('-110');
|
||||
expect(game.books.bet365.total).toBe('9.5');
|
||||
expect(game.books.betmgm.homeML).toBe('-115');
|
||||
});
|
||||
|
||||
test('nba returns empty games object off-season (not an error)', async () => {
|
||||
nbaAdapter.getNBABettingOdds.mockResolvedValue({});
|
||||
const res = await request(mountApp()).get('/api/gamelines/nba');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.games).toEqual({});
|
||||
});
|
||||
|
||||
test('missing RAPID_API_KEY → graceful, configured:false, not a crash', async () => {
|
||||
mlbAdapter.hasApiKey.mockReturnValue(false);
|
||||
const res = await request(mountApp()).get('/api/gamelines/mlb');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.configured).toBe(false);
|
||||
expect(mlbAdapter.getMLBBettingOdds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('adapter throwing → empty games, 200 not 500', async () => {
|
||||
mlbAdapter.getMLBBettingOdds.mockRejectedValue(new Error('rapidapi 429'));
|
||||
const res = await request(mountApp()).get('/api/gamelines/mlb');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.games).toEqual({});
|
||||
});
|
||||
|
||||
test('unsupported sport → 404', async () => {
|
||||
const res = await request(mountApp()).get('/api/gamelines/soccer');
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
test('cache works — adapter called once per request (adapter owns TTL)', async () => {
|
||||
mlbAdapter.getMLBBettingOdds.mockResolvedValue(MLB_BODY);
|
||||
const app = mountApp();
|
||||
await request(app).get('/api/gamelines/mlb');
|
||||
expect(mlbAdapter.getMLBBettingOdds).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user