197 lines
8.1 KiB
JavaScript
197 lines
8.1 KiB
JavaScript
// Soccer poller — tests the tick() function (the unit of work). Run
|
|
// loop intentionally not exercised: it's a sleep+repeat shape that
|
|
// would only test setTimeout.
|
|
|
|
const mockAxiosGet = jest.fn();
|
|
jest.mock('axios', () => ({ get: (...args) => mockAxiosGet(...args) }));
|
|
|
|
const mockCacheSets = new Map();
|
|
jest.mock('../../src/utils/redis', () => ({
|
|
cacheGet: async () => null,
|
|
cacheSet: async (k, v, ttl) => { mockCacheSets.set(k, { value: v, ttl }); return true; },
|
|
cacheDel: async () => true,
|
|
isDegraded: () => false,
|
|
}));
|
|
|
|
// Stub the football-data adapter so soccer poller's "non-WC league"
|
|
// branch is exercised without hitting the real API.
|
|
const mockFbdGetLeagueFixtures = jest.fn();
|
|
const mockFbdGetWorldCupFixtures = jest.fn();
|
|
jest.mock('../../src/services/adapters/footballDataAdapter', () => ({
|
|
getLeagueFixtures: (...a) => mockFbdGetLeagueFixtures(...a),
|
|
getWorldCupFixtures: (...a) => mockFbdGetWorldCupFixtures(...a),
|
|
hasApiKey: () => false,
|
|
}));
|
|
|
|
const soccerPoller = require('../../poller/soccer');
|
|
|
|
beforeEach(() => {
|
|
mockAxiosGet.mockReset();
|
|
mockFbdGetLeagueFixtures.mockReset();
|
|
mockFbdGetWorldCupFixtures.mockReset();
|
|
mockCacheSets.clear();
|
|
});
|
|
|
|
describe('soccer poller', () => {
|
|
describe('parseLeagues', () => {
|
|
test('defaults to WC when env var unset', () => {
|
|
const original = process.env.SOCCER_LEAGUES;
|
|
delete process.env.SOCCER_LEAGUES;
|
|
expect(soccerPoller.__internals.parseLeagues()).toEqual(['WC']);
|
|
if (original !== undefined) process.env.SOCCER_LEAGUES = original;
|
|
});
|
|
|
|
test('parses comma-separated list, uppercases, trims', () => {
|
|
const original = process.env.SOCCER_LEAGUES;
|
|
process.env.SOCCER_LEAGUES = 'wc, pl ,PD,bl1';
|
|
expect(soccerPoller.__internals.parseLeagues()).toEqual(['WC', 'PL', 'PD', 'BL1']);
|
|
if (original !== undefined) process.env.SOCCER_LEAGUES = original;
|
|
else delete process.env.SOCCER_LEAGUES;
|
|
});
|
|
});
|
|
|
|
describe('classifyStatus', () => {
|
|
const { classifyStatus } = soccerPoller.__internals;
|
|
test.each([
|
|
['IN_PLAY', 'live'],
|
|
['PAUSED', 'live'],
|
|
['LIVE', 'live'],
|
|
['FINISHED', 'finished'],
|
|
['FINAL', 'finished'],
|
|
['COMPLETED', 'finished'],
|
|
['SCHEDULED', 'scheduled'],
|
|
['TIMED', 'scheduled'],
|
|
['', 'scheduled'],
|
|
[null, 'scheduled'],
|
|
])('classifies %s → %s', (input, expected) => {
|
|
expect(classifyStatus(input)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe('fetchWorldCupFixtures via OSS API', () => {
|
|
test('projects the OSS API response to the unified shape', async () => {
|
|
mockAxiosGet.mockResolvedValueOnce({
|
|
data: [
|
|
{
|
|
id: 1, home_team: 'England', away_team: 'Brazil',
|
|
utc_date: '2026-06-15T20:00:00Z', status: 'SCHEDULED',
|
|
matchday: 1, venue: 'MetLife Stadium',
|
|
},
|
|
],
|
|
});
|
|
const fixtures = await soccerPoller.__internals.fetchWorldCupFixtures();
|
|
expect(Array.isArray(fixtures)).toBe(true);
|
|
expect(fixtures[0]).toMatchObject({
|
|
id: 1, homeTeam: 'England', awayTeam: 'Brazil',
|
|
venue: 'MetLife Stadium', competition: 'WC',
|
|
});
|
|
});
|
|
|
|
test('axios throw → returns null (graceful)', async () => {
|
|
mockAxiosGet.mockRejectedValueOnce(new Error('OSS down'));
|
|
const fixtures = await soccerPoller.__internals.fetchWorldCupFixtures();
|
|
expect(fixtures).toBeNull();
|
|
});
|
|
|
|
test('handles both top-level array and {matches: [...]} envelopes', async () => {
|
|
mockAxiosGet.mockResolvedValueOnce({ data: { matches: [{ id: 9, homeTeam: 'X', awayTeam: 'Y' }] } });
|
|
const fixtures = await soccerPoller.__internals.fetchWorldCupFixtures();
|
|
expect(fixtures).toHaveLength(1);
|
|
expect(fixtures[0].id).toBe(9);
|
|
});
|
|
});
|
|
|
|
describe('fetchLeagueFixtures dispatch', () => {
|
|
test('WC prefers OSS API, falls back to football-data when OSS dies', async () => {
|
|
mockAxiosGet.mockRejectedValueOnce(new Error('OSS unreachable'));
|
|
mockFbdGetWorldCupFixtures.mockResolvedValueOnce([{ id: 1, homeTeam: 'A', awayTeam: 'B', competition: 'WC' }]);
|
|
const fixtures = await soccerPoller.__internals.fetchLeagueFixtures('WC');
|
|
expect(fixtures).toHaveLength(1);
|
|
expect(mockFbdGetWorldCupFixtures).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('non-WC leagues use the football-data adapter', async () => {
|
|
mockFbdGetLeagueFixtures.mockResolvedValueOnce([{ id: 7, homeTeam: 'X', awayTeam: 'Y', competition: 'PL' }]);
|
|
const fixtures = await soccerPoller.__internals.fetchLeagueFixtures('PL');
|
|
expect(fixtures).toHaveLength(1);
|
|
expect(mockFbdGetLeagueFixtures).toHaveBeenCalledWith('PL');
|
|
});
|
|
});
|
|
|
|
describe('indexFixturesForLeague', () => {
|
|
test('writes per-team nextmatch + lastfixture keys', async () => {
|
|
const inFuture = new Date(Date.now() + 5 * 86_400_000).toISOString();
|
|
const inPast = new Date(Date.now() - 2 * 86_400_000).toISOString();
|
|
const counts = await soccerPoller.__internals.indexFixturesForLeague('WC', [
|
|
{ homeTeam: 'England', awayTeam: 'Brazil', utcDate: inFuture, status: 'SCHEDULED', venue: 'MetLife Stadium' },
|
|
{ homeTeam: 'USA', awayTeam: 'Mexico', utcDate: inPast, status: 'FINISHED', venue: 'AT&T Stadium', score: { fullTime: { home: 2, away: 1 } } },
|
|
]);
|
|
expect(counts.scheduled).toBe(1);
|
|
expect(counts.finished).toBe(1);
|
|
|
|
// Future fixture → next match for both teams.
|
|
expect(mockCacheSets.has('soccer:nextmatch:England')).toBe(true);
|
|
expect(mockCacheSets.has('soccer:nextmatch:Brazil')).toBe(true);
|
|
expect(mockCacheSets.get('soccer:nextmatch:England').value).toMatchObject({
|
|
opponent: 'Brazil', isHome: true, venue: 'MetLife Stadium',
|
|
});
|
|
expect(mockCacheSets.get('soccer:nextmatch:Brazil').value).toMatchObject({
|
|
opponent: 'England', isHome: false,
|
|
});
|
|
|
|
// Past finished → last fixture for both teams.
|
|
expect(mockCacheSets.has('soccer:lastfixture:USA')).toBe(true);
|
|
expect(mockCacheSets.has('soccer:lastfixture:Mexico')).toBe(true);
|
|
});
|
|
|
|
test('returns zero counts on empty input', async () => {
|
|
const counts = await soccerPoller.__internals.indexFixturesForLeague('WC', []);
|
|
expect(counts).toEqual({ scheduled: 0, live: 0, finished: 0 });
|
|
expect(mockCacheSets.size).toBe(0);
|
|
});
|
|
|
|
test('returns zero counts on null input (graceful)', async () => {
|
|
const counts = await soccerPoller.__internals.indexFixturesForLeague('WC', null);
|
|
expect(counts).toEqual({ scheduled: 0, live: 0, finished: 0 });
|
|
});
|
|
});
|
|
|
|
describe('tick', () => {
|
|
test('tick polls each configured league and reports live status', async () => {
|
|
const original = process.env.SOCCER_LEAGUES;
|
|
process.env.SOCCER_LEAGUES = 'WC,PL';
|
|
const inFuture = new Date(Date.now() + 1 * 86_400_000).toISOString();
|
|
|
|
// WC: OSS returns one live match.
|
|
mockAxiosGet.mockResolvedValueOnce({
|
|
data: [{ id: 1, home_team: 'A', away_team: 'B', utc_date: inFuture, status: 'IN_PLAY' }],
|
|
});
|
|
// PL: football-data adapter returns one scheduled.
|
|
mockFbdGetLeagueFixtures.mockResolvedValueOnce([
|
|
{ id: 2, homeTeam: 'X', awayTeam: 'Y', utcDate: inFuture, status: 'SCHEDULED' },
|
|
]);
|
|
|
|
const result = await soccerPoller.tick();
|
|
expect(result.liveSeen).toBe(true);
|
|
expect(result.summary.some((s) => s.startsWith('WC:'))).toBe(true);
|
|
expect(result.summary.some((s) => s.startsWith('PL:'))).toBe(true);
|
|
|
|
if (original !== undefined) process.env.SOCCER_LEAGUES = original;
|
|
else delete process.env.SOCCER_LEAGUES;
|
|
});
|
|
|
|
test('tick survives a league with no_data', async () => {
|
|
const original = process.env.SOCCER_LEAGUES;
|
|
process.env.SOCCER_LEAGUES = 'WC';
|
|
mockAxiosGet.mockRejectedValueOnce(new Error('OSS down'));
|
|
mockFbdGetWorldCupFixtures.mockResolvedValueOnce(null);
|
|
|
|
const result = await soccerPoller.tick();
|
|
expect(result.summary[0]).toMatch(/WC: no_data/);
|
|
|
|
if (original !== undefined) process.env.SOCCER_LEAGUES = original;
|
|
else delete process.env.SOCCER_LEAGUES;
|
|
});
|
|
});
|
|
});
|