Files
vyndr/tests/unit/tank01MlbAdapter.test.js
T

147 lines
6.3 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');
});
});
});