Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user