99 lines
3.7 KiB
JavaScript
99 lines
3.7 KiB
JavaScript
// 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);
|
|
});
|
|
});
|