111 lines
4.4 KiB
JavaScript
111 lines
4.4 KiB
JavaScript
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; },
|
|
}));
|
|
|
|
jest.mock('../../src/utils/rateLimiter', () => ({
|
|
createLimiter: () => ({ waitForToken: async () => true, snapshot: () => ({}) }),
|
|
createCircuitBreaker: () => ({ call: async (fn) => fn(), snapshot: () => ({}) }),
|
|
}));
|
|
|
|
const cache = require('../../src/services/intelligence/teamStatsCache');
|
|
|
|
beforeEach(() => {
|
|
mockAxiosGet.mockReset();
|
|
mockCache.current.clear();
|
|
});
|
|
|
|
describe('teamStatsCache', () => {
|
|
test('refreshTeamStats walks team list and writes cache per team', async () => {
|
|
mockAxiosGet
|
|
// teams list
|
|
.mockResolvedValueOnce({
|
|
status: 200,
|
|
data: {
|
|
sports: [{ leagues: [{ teams: [
|
|
{ team: { id: 1, abbreviation: 'NYK', displayName: 'Knicks' } },
|
|
{ team: { id: 2, abbreviation: 'BOS', displayName: 'Celtics' } },
|
|
] }] }],
|
|
},
|
|
})
|
|
// team 1 stats
|
|
.mockResolvedValueOnce({
|
|
status: 200,
|
|
data: { results: { stats: [{ stats: [
|
|
{ name: 'offensiveRating', value: 118.5 },
|
|
{ name: 'defensiveRating', value: 110.2 },
|
|
{ name: 'pace', value: 100.4 },
|
|
] }] } },
|
|
})
|
|
// team 2 stats
|
|
.mockResolvedValueOnce({
|
|
status: 200,
|
|
data: { results: { stats: [{ stats: [
|
|
{ name: 'offensiveRating', value: 120.0 },
|
|
{ name: 'defensiveRating', value: 108.0 },
|
|
] }] } },
|
|
});
|
|
|
|
const summary = await cache.refreshTeamStats('nba');
|
|
expect(summary.captured).toBe(2);
|
|
expect(summary.total).toBe(2);
|
|
|
|
const nyk = await cache.getTeamStats('nba', 'NYK');
|
|
expect(nyk).toMatchObject({ offensive_rating: 118.5, defensive_rating: 110.2, pace: 100.4 });
|
|
|
|
const bos = await cache.getTeamStats('nba', 'BOS');
|
|
expect(bos).toMatchObject({ offensive_rating: 120.0, defensive_rating: 108.0 });
|
|
});
|
|
|
|
test('getOpponentRank returns the normalized 0..1 rank baked at refresh time', async () => {
|
|
// The normalized value is set during refreshTeamStats; reads use it
|
|
// directly. A solo-team cache entry without the field returns null.
|
|
mockCache.current.set('team_stats:nba:NYK', {
|
|
defensive_rating: 110.2,
|
|
defensive_rank_normalized: 0.45,
|
|
});
|
|
expect(await cache.getOpponentRank('nba', 'NYK', 'points')).toBe(0.45);
|
|
});
|
|
|
|
test('getOpponentRank returns null when cache predates the normalization upgrade', async () => {
|
|
mockCache.current.set('team_stats:nba:OLD', { defensive_rating: 110.2 });
|
|
expect(await cache.getOpponentRank('nba', 'OLD', 'points')).toBeNull();
|
|
});
|
|
|
|
test('refreshTeamStats normalizes defensive_rank_normalized across the league', async () => {
|
|
mockAxiosGet
|
|
.mockResolvedValueOnce({
|
|
status: 200,
|
|
data: {
|
|
sports: [{ leagues: [{ teams: [
|
|
{ team: { id: 1, abbreviation: 'BEST', displayName: 'Best D' } },
|
|
{ team: { id: 2, abbreviation: 'MID', displayName: 'Middle' } },
|
|
{ team: { id: 3, abbreviation: 'WORST', displayName: 'Worst D' } },
|
|
] }] }],
|
|
},
|
|
})
|
|
.mockResolvedValueOnce({ status: 200, data: { results: { stats: [{ stats: [{ name: 'defensiveRating', value: 105 }] }] } } })
|
|
.mockResolvedValueOnce({ status: 200, data: { results: { stats: [{ stats: [{ name: 'defensiveRating', value: 112 }] }] } } })
|
|
.mockResolvedValueOnce({ status: 200, data: { results: { stats: [{ stats: [{ name: 'defensiveRating', value: 118 }] }] } } });
|
|
await cache.refreshTeamStats('nba');
|
|
expect(await cache.getOpponentRank('nba', 'BEST', 'points')).toBe(0);
|
|
expect(await cache.getOpponentRank('nba', 'MID', 'points')).toBeCloseTo(0.5, 5);
|
|
expect(await cache.getOpponentRank('nba', 'WORST', 'points')).toBe(1);
|
|
});
|
|
|
|
test('getTeamStats returns null when nothing cached', async () => {
|
|
expect(await cache.getTeamStats('nba', 'GHOST')).toBeNull();
|
|
});
|
|
|
|
test('refreshTeamStats skips unsupported sport gracefully', async () => {
|
|
const summary = await cache.refreshTeamStats('curling');
|
|
// listTeams returns [] for unsupported sport, so total = 0.
|
|
expect(summary).toMatchObject({ captured: 0, errored: 0, total: 0 });
|
|
});
|
|
});
|