Sessions 29-30: Content templates + PropLine 3-key adapter + MLB Stats API + ESPN summary (1694 tests)
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// Unit: ESPN summary enrichment (Session 30).
|
||||
|
||||
const mockAxiosGet = jest.fn();
|
||||
jest.mock('axios', () => ({ get: (...a) => mockAxiosGet(...a) }));
|
||||
|
||||
const mockStore = {};
|
||||
jest.mock('../../src/utils/redis', () => ({
|
||||
cacheGet: async (k) => (k in mockStore ? mockStore[k] : null),
|
||||
cacheSet: async (k, v) => { mockStore[k] = v; return true; },
|
||||
}));
|
||||
|
||||
const { getGameSummary, __internals } = require('../../src/services/scheduleService');
|
||||
|
||||
beforeEach(() => {
|
||||
mockAxiosGet.mockReset();
|
||||
for (const k of Object.keys(mockStore)) delete mockStore[k];
|
||||
});
|
||||
|
||||
describe('getGameSummary', () => {
|
||||
test('extracts enriched fields for a valid sport + eventId', async () => {
|
||||
mockAxiosGet.mockResolvedValue({
|
||||
data: {
|
||||
injuries: [{ team: 'CIN', injuries: [{ athlete: { displayName: 'Player X' }, status: 'OUT' }] }],
|
||||
odds: [{ provider: { name: 'ESPN BET' }, spread: -1.5, overUnder: 9.5 }],
|
||||
againstTheSpread: [{ team: { abbreviation: 'CIN' }, records: [] }],
|
||||
leaders: [{ name: 'hits' }],
|
||||
boxscore: { teams: [] },
|
||||
},
|
||||
});
|
||||
const out = await getGameSummary('mlb', '401815722');
|
||||
expect(out.injuries).toHaveLength(1);
|
||||
expect(out.odds[0].overUnder).toBe(9.5);
|
||||
expect(out.ats).not.toBeNull();
|
||||
expect(out.boxscore).not.toBeNull();
|
||||
const url = mockAxiosGet.mock.calls[0][0];
|
||||
expect(url).toBe('https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/summary?event=401815722');
|
||||
});
|
||||
|
||||
test('missing sections → empty defaults (no crash)', async () => {
|
||||
mockAxiosGet.mockResolvedValue({ data: {} });
|
||||
const out = await getGameSummary('nba', '999');
|
||||
expect(out).toEqual({ injuries: [], odds: [], ats: null, leaders: [], boxscore: null });
|
||||
});
|
||||
|
||||
test('invalid sport → empty defaults without axios', async () => {
|
||||
const out = await getGameSummary('cricket', '1');
|
||||
expect(out.injuries).toEqual([]);
|
||||
expect(mockAxiosGet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('missing eventId → empty defaults without axios', async () => {
|
||||
await getGameSummary('mlb', null);
|
||||
expect(mockAxiosGet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('network error → empty defaults, not a throw', async () => {
|
||||
mockAxiosGet.mockRejectedValue(new Error('espn down'));
|
||||
const out = await getGameSummary('nba', '1');
|
||||
expect(out.injuries).toEqual([]);
|
||||
});
|
||||
|
||||
test('sport path mapping is correct', () => {
|
||||
expect(__internals.ESPN_SPORT_PATHS.nba).toBe('basketball/nba');
|
||||
expect(__internals.ESPN_SPORT_PATHS.mlb).toBe('baseball/mlb');
|
||||
expect(__internals.ESPN_SPORT_PATHS.wnba).toBe('basketball/wnba');
|
||||
expect(__internals.ESPN_SPORT_PATHS.nfl).toBe('football/nfl');
|
||||
expect(__internals.ESPN_SPORT_PATHS.nhl).toBe('hockey/nhl');
|
||||
});
|
||||
|
||||
test('caches — second call does not re-fetch', async () => {
|
||||
mockAxiosGet.mockResolvedValue({ data: {} });
|
||||
await getGameSummary('mlb', '5');
|
||||
await getGameSummary('mlb', '5');
|
||||
expect(mockAxiosGet).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
// Unit: MLB Stats API adapter (Session 30). No auth; cached; defensive.
|
||||
|
||||
const mockAxiosGet = jest.fn();
|
||||
jest.mock('axios', () => ({ get: (...a) => mockAxiosGet(...a) }));
|
||||
|
||||
const mockStore = new Map();
|
||||
const mockTtls = new Map();
|
||||
jest.mock('../../src/utils/redis', () => ({
|
||||
cacheGet: async (k) => (mockStore.has(k) ? mockStore.get(k) : null),
|
||||
cacheSet: async (k, v, ttl) => { mockStore.set(k, v); mockTtls.set(k, ttl); return true; },
|
||||
}));
|
||||
|
||||
const adapter = require('../../src/services/adapters/mlbStatsAdapter');
|
||||
const { TTL } = adapter.__internals;
|
||||
|
||||
beforeEach(() => {
|
||||
mockAxiosGet.mockReset();
|
||||
mockStore.clear();
|
||||
mockTtls.clear();
|
||||
});
|
||||
|
||||
describe('mlbStatsAdapter — no auth', () => {
|
||||
test('sends NO auth headers (free API)', async () => {
|
||||
mockAxiosGet.mockResolvedValue({ data: { dates: [] } });
|
||||
await adapter.getScheduleWithPitchers('2026-06-14');
|
||||
const [, opts] = mockAxiosGet.mock.calls[0];
|
||||
expect(opts.headers).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mlbStatsAdapter — schedule', () => {
|
||||
test('returns normalized games with probable pitchers', async () => {
|
||||
mockAxiosGet.mockResolvedValue({
|
||||
data: { totalGames: 1, dates: [{ games: [{
|
||||
gamePk: 777, gameDate: '2026-06-14T17:10:00Z',
|
||||
status: { abstractGameState: 'Preview' },
|
||||
venue: { name: 'GABP' },
|
||||
teams: {
|
||||
home: { team: { name: 'Reds', id: 17 }, probablePitcher: { id: 1, fullName: 'Hunter Greene' } },
|
||||
away: { team: { name: 'D-backs', id: 29 }, probablePitcher: { id: 2, fullName: 'Zac Gallen' } },
|
||||
},
|
||||
}] }] },
|
||||
});
|
||||
const games = await adapter.getScheduleWithPitchers('2026-06-14');
|
||||
expect(games).toHaveLength(1);
|
||||
expect(games[0].gamePk).toBe(777);
|
||||
expect(games[0].home.probablePitcher).toEqual({ id: 1, name: 'Hunter Greene' });
|
||||
expect(games[0].away.team).toBe('D-backs');
|
||||
expect(games[0].venue).toBe('GABP');
|
||||
const url = mockAxiosGet.mock.calls[0][0];
|
||||
expect(url).toContain('/schedule?sportId=1&date=2026-06-14');
|
||||
expect(url).toContain('hydrate=probablePitcher');
|
||||
expect(mockTtls.get('mlbstats:schedule:2026-06-14')).toBe(TTL.schedule);
|
||||
});
|
||||
|
||||
test('missing date → [] without axios', async () => {
|
||||
expect(await adapter.getScheduleWithPitchers()).toEqual([]);
|
||||
expect(mockAxiosGet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('error → [] (stale fallback empty)', async () => {
|
||||
mockAxiosGet.mockRejectedValue(new Error('mlb down'));
|
||||
expect(await adapter.getScheduleWithPitchers('2026-06-14')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mlbStatsAdapter — game log', () => {
|
||||
test('returns per-game splits', async () => {
|
||||
mockAxiosGet.mockResolvedValue({
|
||||
data: { stats: [{ splits: [
|
||||
{ date: '2026-06-13', opponent: { name: 'Mets' }, isHome: true, stat: { hits: 2, homeRuns: 1 } },
|
||||
{ date: '2026-06-12', opponent: { name: 'Mets' }, isHome: true, stat: { hits: 0 } },
|
||||
] }] },
|
||||
});
|
||||
const log = await adapter.getPlayerGameLog(592450, 2026, 'hitting');
|
||||
expect(log).toHaveLength(2);
|
||||
expect(log[0].stat.hits).toBe(2);
|
||||
expect(log[0].opponent).toBe('Mets');
|
||||
const url = mockAxiosGet.mock.calls[0][0];
|
||||
expect(url).toContain('/people/592450/stats?stats=gameLog&season=2026&group=hitting');
|
||||
expect(mockTtls.get('mlbstats:gamelog:592450:2026:hitting')).toBe(TTL.gameLog);
|
||||
});
|
||||
|
||||
test('no playerId → []', async () => {
|
||||
expect(await adapter.getPlayerGameLog()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mlbStatsAdapter — season averages', () => {
|
||||
test('returns the season stat object', async () => {
|
||||
mockAxiosGet.mockResolvedValue({ data: { stats: [{ splits: [{ stat: { avg: '.312', obp: '.401', slg: '.589', ops: '.990', homeRuns: 22, rbi: 55 } }] }] } });
|
||||
const s = await adapter.getSeasonAverages(592450);
|
||||
expect(s.avg).toBe('.312');
|
||||
expect(s.slg).toBe('.589');
|
||||
expect(mockTtls.get('mlbstats:season:592450:2026:hitting')).toBe(TTL.season);
|
||||
});
|
||||
|
||||
test('empty splits → null', async () => {
|
||||
mockAxiosGet.mockResolvedValue({ data: { stats: [] } });
|
||||
expect(await adapter.getSeasonAverages(592450)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mlbStatsAdapter — batter vs pitcher', () => {
|
||||
test('returns matchup stat object', async () => {
|
||||
mockAxiosGet.mockResolvedValue({ data: { stats: [{ splits: [{ stat: { atBats: 14, hits: 5, homeRuns: 2, avg: '.357' } }] }] } });
|
||||
const bvp = await adapter.getBatterVsPitcher(592450, 12345);
|
||||
expect(bvp.hits).toBe(5);
|
||||
expect(bvp.homeRuns).toBe(2);
|
||||
const url = mockAxiosGet.mock.calls[0][0];
|
||||
expect(url).toContain('stats=vsPlayer&opposingPlayerId=12345');
|
||||
expect(mockTtls.get('mlbstats:bvp:592450:12345:hitting')).toBe(TTL.bvp);
|
||||
});
|
||||
|
||||
test('missing ids → null without axios', async () => {
|
||||
expect(await adapter.getBatterVsPitcher(null, 1)).toBeNull();
|
||||
expect(await adapter.getBatterVsPitcher(1, null)).toBeNull();
|
||||
expect(mockAxiosGet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('cache hit on repeat call', async () => {
|
||||
mockAxiosGet.mockResolvedValue({ data: { stats: [{ splits: [{ stat: { hits: 1 } }] }] } });
|
||||
await adapter.getBatterVsPitcher(1, 2);
|
||||
await adapter.getBatterVsPitcher(1, 2);
|
||||
expect(mockAxiosGet).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
// Unit: provider preference + source tracking in getOdds (Session 30).
|
||||
|
||||
const mockRedis = { get: jest.fn(), set: jest.fn(), hset: jest.fn(), hgetall: jest.fn(), expire: jest.fn() };
|
||||
jest.mock('../../src/utils/redis', () => ({
|
||||
getRedisClient: () => mockRedis,
|
||||
cacheGet: jest.fn(async () => null),
|
||||
cacheSet: jest.fn(async () => true),
|
||||
cacheDel: jest.fn(async () => true),
|
||||
isDegraded: jest.fn(() => true), // gateway/quota fail open in tests
|
||||
}));
|
||||
|
||||
jest.mock('axios');
|
||||
const axios = require('axios');
|
||||
|
||||
// PropLine adapter mock — we drive hasKeys + getProps per test.
|
||||
jest.mock('../../src/services/adapters/proplineAdapter', () => ({
|
||||
hasKeys: jest.fn(),
|
||||
getProps: jest.fn(),
|
||||
}));
|
||||
const propline = require('../../src/services/adapters/proplineAdapter');
|
||||
|
||||
process.env.ODDS_API_KEY = 'test-api-key';
|
||||
const { getOdds } = require('../../src/services/oddsService');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRedis.get.mockResolvedValue(null); // cache miss
|
||||
mockRedis.set.mockResolvedValue('OK');
|
||||
mockRedis.hgetall.mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe('getOdds — PropLine preferred', () => {
|
||||
test('serves from PropLine when it returns props (provider=propline, no odds-api call)', async () => {
|
||||
propline.hasKeys.mockReturnValue(true);
|
||||
propline.getProps.mockResolvedValue({
|
||||
props: [{ player: 'Acuna', stat_type: 'hits', line: 0.5, over_odds: -200, under_odds: 160, book: 'betmgm' }],
|
||||
spreads: [],
|
||||
source: 'propline',
|
||||
});
|
||||
|
||||
const res = await getOdds('mlb');
|
||||
expect(res.source).toBe('live');
|
||||
expect(res.provider).toBe('propline');
|
||||
expect(res.props).toHaveLength(1);
|
||||
expect(axios.get).not.toHaveBeenCalled(); // odds-api never touched
|
||||
// Cached payload carries the provider tag.
|
||||
const cachedArg = JSON.parse(mockRedis.set.mock.calls[0][1]);
|
||||
expect(cachedArg.provider).toBe('propline');
|
||||
});
|
||||
|
||||
test('falls back to odds-api when PropLine returns empty (provider=odds-api)', async () => {
|
||||
propline.hasKeys.mockReturnValue(true);
|
||||
propline.getProps.mockResolvedValue({ props: [], spreads: [], source: 'propline' });
|
||||
|
||||
// odds-api fallback: events list, then per-event odds.
|
||||
axios.get
|
||||
.mockResolvedValueOnce({ data: [{ id: 'e1', home_team: 'Denver Nuggets', away_team: 'Los Angeles Lakers', commence_time: '2026-03-21T19:00:00Z' }], headers: {} })
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
id: 'e1', home_team: 'Denver Nuggets', away_team: 'Los Angeles Lakers', commence_time: '2026-03-21T19:00:00Z',
|
||||
bookmakers: [{ key: 'draftkings', title: 'DK', markets: [{ key: 'player_points', last_update: 't', outcomes: [
|
||||
{ name: 'Over', description: 'Jokic', price: -110, point: 26.5 },
|
||||
{ name: 'Under', description: 'Jokic', price: -110, point: 26.5 },
|
||||
] }] }],
|
||||
},
|
||||
headers: { 'x-requests-remaining': '400' },
|
||||
});
|
||||
|
||||
const res = await getOdds('nba');
|
||||
expect(res.provider).toBe('odds-api');
|
||||
expect(axios.get).toHaveBeenCalled();
|
||||
expect(res.props.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('PropLine skipped entirely when no keys (provider=odds-api)', async () => {
|
||||
propline.hasKeys.mockReturnValue(false);
|
||||
axios.get
|
||||
.mockResolvedValueOnce({ data: [{ id: 'e1', home_team: 'Denver Nuggets', away_team: 'Los Angeles Lakers', commence_time: '2026-03-21T19:00:00Z' }], headers: {} })
|
||||
.mockResolvedValueOnce({
|
||||
data: { id: 'e1', home_team: 'Denver Nuggets', away_team: 'Los Angeles Lakers', commence_time: '2026-03-21T19:00:00Z',
|
||||
bookmakers: [{ key: 'draftkings', title: 'DK', markets: [{ key: 'player_points', last_update: 't', outcomes: [
|
||||
{ name: 'Over', description: 'Jokic', price: -110, point: 26.5 },
|
||||
{ name: 'Under', description: 'Jokic', price: -110, point: 26.5 },
|
||||
] }] }] },
|
||||
headers: {},
|
||||
});
|
||||
const res = await getOdds('nba');
|
||||
expect(propline.getProps).not.toHaveBeenCalled();
|
||||
expect(res.provider).toBe('odds-api');
|
||||
});
|
||||
|
||||
test('PropLine error → graceful fallback to odds-api', async () => {
|
||||
propline.hasKeys.mockReturnValue(true);
|
||||
propline.getProps.mockRejectedValue(new Error('propline down'));
|
||||
axios.get
|
||||
.mockResolvedValueOnce({ data: [], headers: {} }); // empty events → empty props, but provider tagged
|
||||
const res = await getOdds('nba');
|
||||
expect(res.provider).toBe('odds-api');
|
||||
});
|
||||
|
||||
test('cached response surfaces the stored provider', async () => {
|
||||
mockRedis.get.mockResolvedValue(JSON.stringify({ updated_at: 't', props: [{ player: 'X' }], spreads: [], provider: 'propline' }));
|
||||
const res = await getOdds('mlb');
|
||||
expect(res.source).toBe('cache');
|
||||
expect(res.provider).toBe('propline');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
// Unit: PropLine adapter (Session 30). 3-key rotation + normalization.
|
||||
|
||||
const mockAxiosGet = jest.fn();
|
||||
jest.mock('axios', () => ({ get: (...a) => mockAxiosGet(...a) }));
|
||||
|
||||
// Gateway is a pass-through in tests (quota allowed).
|
||||
jest.mock('../../src/services/providerGateway', () => ({
|
||||
fetch: jest.fn(async (_id, cb) => cb('propline')),
|
||||
}));
|
||||
|
||||
const mockRedisStore = {};
|
||||
jest.mock('../../src/utils/redis', () => ({
|
||||
getRedisClient: () => ({
|
||||
get: async (k) => (k in mockRedisStore ? String(mockRedisStore[k]) : null),
|
||||
incr: async (k) => { mockRedisStore[k] = (mockRedisStore[k] || 0) + 1; return mockRedisStore[k]; },
|
||||
expire: async () => 1,
|
||||
}),
|
||||
isDegraded: () => false,
|
||||
}));
|
||||
|
||||
const adapter = require('../../src/services/adapters/proplineAdapter');
|
||||
|
||||
const KEYS = { PROPLINE_API_KEY_1: 'k1', PROPLINE_API_KEY_2: 'k2', PROPLINE_API_KEY_3: 'k3' };
|
||||
|
||||
beforeEach(() => {
|
||||
mockAxiosGet.mockReset();
|
||||
for (const k of Object.keys(mockRedisStore)) delete mockRedisStore[k];
|
||||
for (const k of Object.keys(adapter.__internals.memUsage)) delete adapter.__internals.memUsage[k];
|
||||
Object.assign(process.env, KEYS);
|
||||
});
|
||||
afterAll(() => {
|
||||
delete process.env.PROPLINE_API_KEY_1;
|
||||
delete process.env.PROPLINE_API_KEY_2;
|
||||
delete process.env.PROPLINE_API_KEY_3;
|
||||
});
|
||||
|
||||
const SAMPLE = [{
|
||||
id: '43866',
|
||||
sport_key: 'baseball_mlb',
|
||||
home_team: 'Cincinnati Reds',
|
||||
away_team: 'Arizona Diamondbacks',
|
||||
commence_time: '2026-06-14T02:05:00Z',
|
||||
bookmakers: [{
|
||||
key: 'betmgm', title: 'BetMGM', last_update: '2026-06-14T00:45:30Z',
|
||||
markets: [{
|
||||
key: 'batter_hits', last_update: '2026-06-13T15:12:46Z',
|
||||
outcomes: [
|
||||
{ name: 'Over', description: 'Braxton Fulford', price: -200, point: 0.5 },
|
||||
{ name: 'Under', description: 'Braxton Fulford', price: 160, point: 0.5 },
|
||||
],
|
||||
}],
|
||||
}],
|
||||
}];
|
||||
|
||||
describe('proplineAdapter — config + URL', () => {
|
||||
test('hasKeys true when any key set', () => {
|
||||
expect(adapter.hasKeys()).toBe(true);
|
||||
});
|
||||
|
||||
test('builds the odds URL with apiKey query param + markets', async () => {
|
||||
mockAxiosGet.mockResolvedValue({ data: SAMPLE });
|
||||
await adapter.fetchRaw('mlb');
|
||||
const [url, opts] = mockAxiosGet.mock.calls[0];
|
||||
expect(url).toBe('https://api.prop-line.com/v1/sports/baseball_mlb/odds');
|
||||
expect(opts.params.apiKey).toMatch(/^k[123]$/);
|
||||
expect(opts.params.markets).toContain('batter_hits');
|
||||
});
|
||||
|
||||
test('unsupported sport → null without calling axios', async () => {
|
||||
expect(await adapter.fetchRaw('cricket')).toBeNull();
|
||||
expect(mockAxiosGet).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('proplineAdapter — normalization (Odds-API-compatible)', () => {
|
||||
test('getProps normalizes into VYNDR prop shape (MLB market mapped)', async () => {
|
||||
mockAxiosGet.mockResolvedValue({ data: SAMPLE });
|
||||
const out = await adapter.getProps('mlb');
|
||||
expect(out.source).toBe('propline');
|
||||
expect(out.props).toHaveLength(1);
|
||||
const p = out.props[0];
|
||||
expect(p.player).toBe('Braxton Fulford');
|
||||
expect(p.stat_type).toBe('hits'); // batter_hits → hits via MARKET_MAP
|
||||
expect(p.line).toBe(0.5);
|
||||
expect(p.over_odds).toBe(-200);
|
||||
expect(p.under_odds).toBe(160);
|
||||
expect(p.book).toBe('betmgm');
|
||||
});
|
||||
|
||||
test('tolerates { data: [...] } wrapper', async () => {
|
||||
mockAxiosGet.mockResolvedValue({ data: { data: SAMPLE } });
|
||||
const raw = await adapter.fetchRaw('mlb');
|
||||
expect(raw).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('error → getProps returns null (caller falls back)', async () => {
|
||||
mockAxiosGet.mockRejectedValue(new Error('upstream 500'));
|
||||
expect(await adapter.getProps('mlb')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('proplineAdapter — 3-key rotation', () => {
|
||||
test('picks the least-used key', async () => {
|
||||
const { incrUsage, usageKey } = adapter.__internals;
|
||||
// Make key 0 heavily used, key 1 lightly, key 2 medium.
|
||||
mockRedisStore[usageKey(0)] = 500;
|
||||
mockRedisStore[usageKey(2)] = 100;
|
||||
const picked = await adapter.pickKey(adapter.__internals.getKeys());
|
||||
expect(picked.index).toBe(1); // unused → most remaining
|
||||
});
|
||||
|
||||
test('rotates OFF a key once it crosses the 900 threshold', async () => {
|
||||
const { usageKey } = adapter.__internals;
|
||||
mockRedisStore[usageKey(0)] = 950; // over threshold
|
||||
mockRedisStore[usageKey(1)] = 950; // over threshold
|
||||
mockRedisStore[usageKey(2)] = 10; // healthy
|
||||
const picked = await adapter.pickKey(adapter.__internals.getKeys());
|
||||
expect(picked.index).toBe(2);
|
||||
});
|
||||
|
||||
test('all keys exhausted (>=1000) → pickKey null, fetchRaw null', async () => {
|
||||
const { usageKey } = adapter.__internals;
|
||||
mockRedisStore[usageKey(0)] = 1000;
|
||||
mockRedisStore[usageKey(1)] = 1000;
|
||||
mockRedisStore[usageKey(2)] = 1000;
|
||||
expect(await adapter.pickKey(adapter.__internals.getKeys())).toBeNull();
|
||||
mockAxiosGet.mockResolvedValue({ data: SAMPLE });
|
||||
expect(await adapter.fetchRaw('mlb')).toBeNull();
|
||||
expect(mockAxiosGet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('increments usage on a successful call', async () => {
|
||||
mockAxiosGet.mockResolvedValue({ data: SAMPLE });
|
||||
await adapter.fetchRaw('mlb');
|
||||
const total = Object.entries(mockRedisStore)
|
||||
.filter(([k]) => k.startsWith('propline:usage:'))
|
||||
.reduce((s, [, v]) => s + v, 0);
|
||||
expect(total).toBe(1);
|
||||
});
|
||||
|
||||
test('no keys configured → fetchRaw null', async () => {
|
||||
delete process.env.PROPLINE_API_KEY_1;
|
||||
delete process.env.PROPLINE_API_KEY_2;
|
||||
delete process.env.PROPLINE_API_KEY_3;
|
||||
expect(adapter.hasKeys()).toBe(false);
|
||||
expect(await adapter.fetchRaw('mlb')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user