Session 9: api-football + FootApi + Tank01 adapters, grace period middleware, cookie consent, /pricing page, OOM fix documented (1240 tests)

This commit is contained in:
Kev
2026-06-10 19:41:37 -04:00
parent 4db1c1c539
commit b55dcbd614
25 changed files with 2463 additions and 22 deletions
@@ -0,0 +1,122 @@
// Source-cascade tests for soccerFeatureExtractor (Session 9). The
// pre-existing soccerFeatureExtractor.test.js covers the legacy
// football-data path; this suite verifies that:
// - api-football data wins when the prefetch alias exists
// - footapi wins when api-football is missing but footapi alias exists
// - football-data is still served when only the legacy key is set
// - The `meta.sources` map attributes correctly per lookup
//
// We mock cacheGet to inspect which key the cascade asked for.
const mockCacheStore = new Map();
jest.mock('../../src/utils/redis', () => ({
cacheGet: async (k) => (mockCacheStore.has(k) ? mockCacheStore.get(k) : null),
cacheSet: async (k, v) => { mockCacheStore.set(k, v); return true; },
cacheDel: async (k) => { mockCacheStore.delete(k); return true; },
isDegraded: () => false,
}));
const { normalizeName } = require('../../src/utils/normalize');
const extractor = require('../../src/services/intelligence/soccerFeatureExtractor');
beforeEach(() => { mockCacheStore.clear(); });
describe('soccerFeatureExtractor — source cascade (Session 9)', () => {
test('api-football wins when its alias is populated', async () => {
const n = normalizeName('Lionel Messi');
// Only the apifootball alias is populated — others should not be consulted.
mockCacheStore.set(`apifootball:player_by_name:${n}`, {
team: 'Argentina', goals_per_90: 0.92, minutes_per_game: 88,
});
const r = await extractor.extractSoccerFeatures({
player: 'Lionel Messi', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.goals_per_90).toBe(0.92);
expect(r.meta.sources.player).toBe('api-football');
});
test('footapi wins when api-football is empty but footapi is populated', async () => {
const n = normalizeName('Harry Kane');
mockCacheStore.set(`footapi:player_by_name:${n}`, {
team: 'England', goals_per_90: 0.81,
});
const r = await extractor.extractSoccerFeatures({
player: 'Harry Kane', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.goals_per_90).toBe(0.81);
expect(r.meta.sources.player).toBe('footapi');
});
test('football-data legacy key is the final fallback', async () => {
const n = normalizeName('Bukayo Saka');
mockCacheStore.set(`soccer:player:${n}`, {
team: 'England', goals_per_90: 0.4,
});
const r = await extractor.extractSoccerFeatures({
player: 'Bukayo Saka', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.features.goals_per_90).toBe(0.4);
expect(r.meta.sources.player).toBe('football-data');
});
test('all-miss case → null source + errors populated', async () => {
const r = await extractor.extractSoccerFeatures({
player: 'Unknown', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.meta.sources.player).toBeNull();
expect(r.meta.errors).toContain('player_not_found_in_cache');
});
test('nextMatch cascade — api-football preferred', async () => {
const n = normalizeName('Vinicius Junior');
mockCacheStore.set(`apifootball:player_by_name:${n}`, { team: 'Brazil' });
mockCacheStore.set('apifootball:nextmatch:Brazil', {
opponent: 'Argentina', venue: 'MetLife Stadium', isHome: true, referee: 'A. Taylor',
});
mockCacheStore.set('soccer:nextmatch:Brazil', {
opponent: 'STALE', venue: 'old', isHome: false, referee: 'STALE',
});
const r = await extractor.extractSoccerFeatures({
player: 'Vinicius Junior', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.meta.opponentAbbr).toBe('Argentina'); // api-football won
expect(r.meta.sources.nextMatch).toBe('api-football');
});
test('referee cascade falls through to legacy key when richer sources empty', async () => {
const n = normalizeName('Anyone');
mockCacheStore.set(`apifootball:player_by_name:${n}`, { team: 'X' });
mockCacheStore.set('apifootball:nextmatch:X', {
opponent: 'Y', venue: 'V', isHome: true, referee: 'Bjorn',
});
mockCacheStore.set('soccer:referee:Bjorn', {
cards_per_game: 4.2, penalties_per_game: 0.3,
});
const r = await extractor.extractSoccerFeatures({
player: 'Anyone', stat_type: 'cards', line: 0.5, direction: 'over',
});
expect(r.features.referee_cards_per_game).toBe(4.2);
expect(r.meta.sources.referee).toBe('football-data');
});
test('multiple sources active → independent attribution per lookup', async () => {
const n = normalizeName('Mixed');
mockCacheStore.set(`apifootball:player_by_name:${n}`, { team: 'France', goals_per_90: 1.1 });
// Match data only in legacy.
mockCacheStore.set('soccer:nextmatch:France', {
opponent: 'Italy', venue: 'AT&T Stadium', isHome: false, referee: 'X',
});
// Referee only in footapi.
mockCacheStore.set('footapi:referee_by_name:X', { cards_per_game: 5.5 });
const r = await extractor.extractSoccerFeatures({
player: 'Mixed', stat_type: 'goals', line: 0.5, direction: 'over',
});
expect(r.meta.sources).toEqual({
player: 'api-football',
nextMatch: 'football-data',
lastFixture: null,
referee: 'footapi',
});
});
});