124 lines
4.7 KiB
JavaScript
124 lines
4.7 KiB
JavaScript
process.env.SHARPAPI_KEY = 'test-key';
|
|
process.env.SHARPAPI_BASE_URL = 'https://api.sharpapi.test/v1';
|
|
|
|
const mockAxiosGet = jest.fn();
|
|
jest.mock('axios', () => ({
|
|
get: (...args) => mockAxiosGet(...args),
|
|
}));
|
|
|
|
const mockCache = { current: new Map() };
|
|
jest.mock('../../src/utils/redis', () => ({
|
|
cacheGet: async (k) => mockCache.current.get(k) ?? null,
|
|
cacheSet: async (k, v) => { mockCache.current.set(k, v); return true; },
|
|
cacheDel: async (k) => { mockCache.current.delete(k); return true; },
|
|
}));
|
|
|
|
const adapter = require('../../src/services/adapters/sharpApiAdapter');
|
|
|
|
beforeEach(() => {
|
|
mockAxiosGet.mockReset();
|
|
mockCache.current.clear();
|
|
});
|
|
|
|
describe('sharpApiAdapter.configured', () => {
|
|
test('reflects SHARPAPI_KEY env presence', () => {
|
|
expect(adapter.configured()).toBe(true);
|
|
delete process.env.SHARPAPI_KEY;
|
|
expect(adapter.configured()).toBe(false);
|
|
process.env.SHARPAPI_KEY = 'test-key';
|
|
});
|
|
});
|
|
|
|
describe('getPlayerProps', () => {
|
|
test('throws on unsupported sport', async () => {
|
|
await expect(adapter.getPlayerProps('curling', 'g1')).rejects.toThrow(/Unsupported sport/);
|
|
});
|
|
|
|
test('normalizes the response and computes fair probabilities', async () => {
|
|
mockAxiosGet.mockResolvedValue({
|
|
status: 200,
|
|
data: {
|
|
props: [
|
|
{ book: 'dk', player: 'LeBron James', stat_type: 'points', line: 25.5, over_price: -110, under_price: -110 },
|
|
],
|
|
},
|
|
});
|
|
const result = await adapter.getPlayerProps('nba', 'game-1');
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({ book: 'dk', player: 'LeBron James', statType: 'points', line: 25.5 });
|
|
expect(result[0].fairOver).toBeCloseTo(0.5, 5);
|
|
expect(result[0].fairUnder).toBeCloseTo(0.5, 5);
|
|
});
|
|
|
|
test('cache hit on second call avoids a second request', async () => {
|
|
mockAxiosGet.mockResolvedValue({ status: 200, data: { props: [] } });
|
|
await adapter.getPlayerProps('nba', 'game-cache');
|
|
await adapter.getPlayerProps('nba', 'game-cache');
|
|
expect(mockAxiosGet).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('429 response serves prior stale cache marked stale:true', async () => {
|
|
// Simulate "cache exists but is already stale" — in production this is
|
|
// what an expired-EX Redis entry plus a previous 429 retention path
|
|
// would look like. The mock cache doesn't expire so we mark it stale
|
|
// directly to force the refresh branch.
|
|
mockCache.current.set('odds:nba:game-stale:player_props', {
|
|
props: [{ book: 'dk', player: 'Old', stat_type: 'points', line: 20, over_price: -110, under_price: -110 }],
|
|
stale: true,
|
|
});
|
|
mockAxiosGet.mockResolvedValue({ status: 429, data: {} });
|
|
const result = await adapter.getPlayerProps('nba', 'game-stale');
|
|
expect(result.stale).toBe(true);
|
|
expect(result).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('getGameOdds', () => {
|
|
test('returns spread/total/moneyline shape', async () => {
|
|
mockAxiosGet.mockResolvedValue({
|
|
status: 200,
|
|
data: { spread: { home: -3.5 }, total: { line: 220.5 }, h2h: { home: -150 } },
|
|
});
|
|
const result = await adapter.getGameOdds('nba', 'g99');
|
|
expect(result).toMatchObject({
|
|
spread: { home: -3.5 },
|
|
total: { line: 220.5 },
|
|
moneyline: { home: -150 },
|
|
});
|
|
});
|
|
|
|
test('returns null when adapter is unconfigured', async () => {
|
|
delete process.env.SHARPAPI_KEY;
|
|
const result = await adapter.getGameOdds('nba', 'g99');
|
|
expect(result).toBeNull();
|
|
process.env.SHARPAPI_KEY = 'test-key';
|
|
});
|
|
});
|
|
|
|
describe('getConsensusLine', () => {
|
|
test('returns median/min/max across books, ignoring unrelated props', async () => {
|
|
mockAxiosGet.mockResolvedValue({
|
|
status: 200,
|
|
data: {
|
|
props: [
|
|
{ book: 'dk', player: 'Anthony Edwards', stat_type: 'points', line: 27.5, over_price: -115, under_price: -105 },
|
|
{ book: 'fd', player: 'Anthony Edwards', stat_type: 'points', line: 28.5, over_price: -110, under_price: -110 },
|
|
{ book: 'mgm', player: 'Anthony Edwards', stat_type: 'points', line: 27.0, over_price: -120, under_price: +100 },
|
|
{ book: 'dk', player: 'Different Player', stat_type: 'points', line: 99.0 }, // ignored
|
|
],
|
|
},
|
|
});
|
|
const consensus = await adapter.getConsensusLine('nba', 'g-c', 'Anthony Edwards', 'points');
|
|
expect(consensus.bookCount).toBe(3);
|
|
expect(consensus.median).toBe(27.5);
|
|
expect(consensus.min).toBe(27.0);
|
|
expect(consensus.max).toBe(28.5);
|
|
});
|
|
|
|
test('returns null when no matching prop', async () => {
|
|
mockAxiosGet.mockResolvedValue({ status: 200, data: { props: [] } });
|
|
const result = await adapter.getConsensusLine('nba', 'g-x', 'Ghost', 'points');
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|