170 lines
7.4 KiB
JavaScript
170 lines
7.4 KiB
JavaScript
const mockAxiosGet = jest.fn();
|
|
jest.mock('axios', () => ({ get: (...args) => mockAxiosGet(...args) }));
|
|
|
|
const mockCacheStore = new Map();
|
|
const mockCacheTtls = new Map();
|
|
jest.mock('../../src/utils/redis', () => ({
|
|
cacheGet: async (k) => (mockCacheStore.has(k) ? mockCacheStore.get(k) : null),
|
|
cacheSet: async (k, v, ttl) => { mockCacheStore.set(k, v); mockCacheTtls.set(k, ttl); return true; },
|
|
cacheDel: async (k) => { mockCacheStore.delete(k); return true; },
|
|
isDegraded: () => false,
|
|
}));
|
|
|
|
const adapter = require('../../src/services/adapters/tank01MlbAdapter');
|
|
|
|
beforeEach(() => {
|
|
mockAxiosGet.mockReset();
|
|
mockCacheStore.clear();
|
|
mockCacheTtls.clear();
|
|
});
|
|
|
|
describe('tank01MlbAdapter', () => {
|
|
describe('graceful degradation (no RAPID_API_KEY)', () => {
|
|
const original = process.env.RAPID_API_KEY;
|
|
beforeAll(() => { delete process.env.RAPID_API_KEY; });
|
|
afterAll(() => { if (original !== undefined) process.env.RAPID_API_KEY = original; });
|
|
|
|
test('hasApiKey false', () => { expect(adapter.hasApiKey()).toBe(false); });
|
|
|
|
test('all endpoints return null without touching axios', async () => {
|
|
expect(await adapter.getMLBBoxScore('20260611_ATL_NYM')).toBeNull();
|
|
expect(await adapter.getMLBBatterVsPitcher('B1', 'P1')).toBeNull();
|
|
expect(await adapter.getMLBDailyScoreboard('20260611')).toBeNull();
|
|
expect(mockAxiosGet).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('with key configured', () => {
|
|
beforeAll(() => { process.env.RAPID_API_KEY = 'test-rapid-key'; });
|
|
|
|
test('RapidAPI host header is the MLB-specific Tank01 host', async () => {
|
|
mockAxiosGet.mockResolvedValueOnce({ data: { body: [] } });
|
|
await adapter.getMLBDailyScoreboard('20260611');
|
|
const [url, opts] = mockAxiosGet.mock.calls[0];
|
|
expect(url).toMatch(/^https:\/\/tank01-mlb-live-in-game-real-time-statistics\.p\.rapidapi\.com/);
|
|
expect(opts.headers['x-rapidapi-host']).toBe('tank01-mlb-live-in-game-real-time-statistics.p.rapidapi.com');
|
|
});
|
|
|
|
test('TANK01_MLB_HOST override is honored', async () => {
|
|
const original = process.env.TANK01_MLB_HOST;
|
|
process.env.TANK01_MLB_HOST = 'alt-mlb.rapidapi.com';
|
|
mockAxiosGet.mockResolvedValueOnce({ data: { body: [] } });
|
|
await adapter.getMLBDailyScoreboard('20260611');
|
|
const [url] = mockAxiosGet.mock.calls[0];
|
|
expect(url).toMatch(/^https:\/\/alt-mlb\.rapidapi\.com/);
|
|
if (original !== undefined) process.env.TANK01_MLB_HOST = original;
|
|
else delete process.env.TANK01_MLB_HOST;
|
|
});
|
|
|
|
test('getMLBBoxScore tags batters and pitchers with role', async () => {
|
|
mockAxiosGet.mockResolvedValueOnce({
|
|
data: {
|
|
body: {
|
|
gameStatus: 'Final',
|
|
playerStats: {
|
|
batting: { 'B1': { longName: 'Ronald Acuña', teamAbv: 'ATL' } },
|
|
pitching: { 'P1': { longName: 'Spencer Strider', teamAbv: 'ATL' } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const stats = await adapter.getMLBBoxScore('GAME-1');
|
|
expect(stats.find((s) => s.role === 'batter').name).toBe('Ronald Acuña');
|
|
expect(stats.find((s) => s.role === 'pitcher').name).toBe('Spencer Strider');
|
|
// Final → 24h TTL.
|
|
expect(mockCacheTtls.get('tank01:mlb:boxscore:GAME-1')).toBe(adapter.__internals.TTL.boxScoreFinal);
|
|
});
|
|
|
|
test('In-progress game keeps 5-min TTL on the box score', async () => {
|
|
mockAxiosGet.mockResolvedValueOnce({
|
|
data: { body: { gameStatus: 'InProgress', playerStats: { batting: {}, pitching: {} } } },
|
|
});
|
|
await adapter.getMLBBoxScore('GAME-2');
|
|
expect(mockCacheTtls.get('tank01:mlb:boxscore:GAME-2')).toBe(adapter.__internals.TTL.boxScoreLive);
|
|
});
|
|
|
|
test('getMLBBatterVsPitcher projects single-object payload', async () => {
|
|
mockAxiosGet.mockResolvedValueOnce({
|
|
data: {
|
|
body: { batterID: 'B1', pitcherID: 'P1', PA: 18, AB: 16, H: 5, HR: 1, RBI: 3, SO: 4, AVG: '.313', OPS: '.857' },
|
|
},
|
|
});
|
|
const bvp = await adapter.getMLBBatterVsPitcher('B1', 'P1');
|
|
expect(bvp).toMatchObject({
|
|
batterId: 'B1', pitcherId: 'P1',
|
|
plateAppearances: 18, atBats: 16, hits: 5, homeRuns: 1, rbi: 3, strikeouts: 4,
|
|
avg: '.313', ops: '.857',
|
|
});
|
|
});
|
|
|
|
test('getMLBBatterVsPitcher handles array of matchups', async () => {
|
|
mockAxiosGet.mockResolvedValueOnce({
|
|
data: {
|
|
body: [
|
|
{ batterID: 'B1', pitcherID: 'P1', PA: 12, H: 3, SO: 5 },
|
|
{ batterID: 'B1', pitcherID: 'P1', PA: 6, H: 2, SO: 1 },
|
|
],
|
|
},
|
|
});
|
|
const bvp = await adapter.getMLBBatterVsPitcher('B1', 'P1');
|
|
expect(Array.isArray(bvp)).toBe(true);
|
|
expect(bvp).toHaveLength(2);
|
|
expect(bvp[0].plateAppearances).toBe(12);
|
|
});
|
|
|
|
test('null IDs return null without touching axios', async () => {
|
|
expect(await adapter.getMLBBoxScore(null)).toBeNull();
|
|
expect(await adapter.getMLBBatterVsPitcher(null, 'P1')).toBeNull();
|
|
expect(await adapter.getMLBBatterVsPitcher('B1', null)).toBeNull();
|
|
expect(await adapter.getMLBDailyScoreboard(null)).toBeNull();
|
|
expect(mockAxiosGet).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('getMLBDailyScoreboard projects both array + map shapes', async () => {
|
|
mockAxiosGet.mockResolvedValueOnce({
|
|
data: { body: { 'GAME-X': { gameID: 'GAME-X', home: 'NYY', away: 'BOS', homePts: 5, awayPts: 4, gameStatus: 'Final' } } },
|
|
});
|
|
const games = await adapter.getMLBDailyScoreboard('20260611');
|
|
expect(games).toHaveLength(1);
|
|
expect(games[0]).toMatchObject({ gameId: 'GAME-X', homeTeam: 'NYY', awayTeam: 'BOS', homeScore: 5, awayScore: 4 });
|
|
});
|
|
|
|
test('cache hit on repeat call', async () => {
|
|
mockAxiosGet.mockResolvedValueOnce({ data: { body: { batterID: 'B', PA: 1 } } });
|
|
await adapter.getMLBBatterVsPitcher('B', 'P');
|
|
await adapter.getMLBBatterVsPitcher('B', 'P');
|
|
expect(mockAxiosGet).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('axios throw → stale fallback', async () => {
|
|
mockCacheStore.set('tank01:mlb:scoreboard:20260611:stale', { body: [{ gameID: 'STALE' }] });
|
|
mockAxiosGet.mockRejectedValueOnce(new Error('upstream 500'));
|
|
const games = await adapter.getMLBDailyScoreboard('20260611');
|
|
expect(games[0].gameId).toBe('STALE');
|
|
});
|
|
|
|
// Session 23 — game-level book-by-book betting odds.
|
|
test('getMLBBettingOdds returns the raw body and caches at the odds TTL', async () => {
|
|
mockAxiosGet.mockResolvedValueOnce({
|
|
data: { body: { '20260612_ARI@CIN': { sportsBooks: [{ sportsBook: 'bet365', odds: {} }] } } },
|
|
});
|
|
const body = await adapter.getMLBBettingOdds('2026-06-12');
|
|
expect(body['20260612_ARI@CIN']).toBeDefined();
|
|
const [url] = mockAxiosGet.mock.calls[0];
|
|
expect(url).toMatch(/getMLBBettingOdds\?gameDate=20260612/);
|
|
expect(mockCacheTtls.get('tank01:mlb:odds:20260612')).toBe(adapter.__internals.TTL.odds);
|
|
});
|
|
|
|
test('getMLBBettingOdds second call within TTL does not hit Tank01', async () => {
|
|
mockAxiosGet.mockResolvedValueOnce({ data: { body: { g: {} } } });
|
|
await adapter.getMLBBettingOdds('2026-06-12');
|
|
await adapter.getMLBBettingOdds('2026-06-12');
|
|
expect(mockAxiosGet).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('getMLBBettingOdds null date returns null without axios', async () => {
|
|
expect(await adapter.getMLBBettingOdds(null)).toBeNull();
|
|
});
|
|
});
|
|
});
|