const mockRedis = { get: jest.fn(), set: jest.fn() }; jest.mock('../../src/utils/redis', () => ({ getRedisClient: () => mockRedis })); const mockSupabaseFrom = jest.fn(); jest.mock('../../src/utils/supabase', () => ({ getSupabaseServiceClient: () => ({ from: mockSupabaseFrom }), })); const { detectScratches } = require('../../src/services/cascadeService'); beforeEach(() => { jest.clearAllMocks(); mockRedis.get.mockResolvedValue(null); mockRedis.set.mockResolvedValue('OK'); }); function makeProps(players) { const props = []; for (const player of players) { // Add from 2 books to qualify props.push({ player, stat_type: 'points', book: 'draftkings', line: 26.5 }); props.push({ player, stat_type: 'points', book: 'fanduel', line: 27.0 }); } return props; } describe('cascadeService', () => { test('first fetch: no previous data, no scratches', async () => { mockRedis.get.mockResolvedValue(null); const result = await detectScratches('nba', makeProps(['Jokic', 'LeBron'])); expect(result.scratchedPlayers).toEqual([]); }); test('player present in previous but absent in current: flagged as scratch', async () => { // Previous fetch had Jokic + LeBron mockRedis.get.mockResolvedValue(JSON.stringify(['Jokic', 'LeBron'])); // Mock DB queries for processScratches mockSupabaseFrom.mockImplementation((table) => { if (table === 'picks') { return { select: () => ({ eq: (col, val) => ({ gte: () => Promise.resolve({ data: [], error: null }), }), }), }; } return { select: () => ({ eq: () => ({ gte: () => Promise.resolve({ data: [] }) }) }), }; }); // Current fetch only has Jokic const result = await detectScratches('nba', makeProps(['Jokic'])); expect(result.scratchedPlayers).toContain('LeBron'); }); test('player in only 1 book: not flagged (data gap)', async () => { mockRedis.get.mockResolvedValue(JSON.stringify(['Jokic', 'LeBron'])); // Current: Jokic from 2 books, LeBron from only 1 book const props = [ { player: 'Jokic', stat_type: 'points', book: 'draftkings', line: 26.5 }, { player: 'Jokic', stat_type: 'points', book: 'fanduel', line: 27.0 }, { player: 'LeBron', stat_type: 'points', book: 'draftkings', line: 25.5 }, // LeBron only from 1 book — doesn't qualify as "having props from 2+ books" ]; mockSupabaseFrom.mockReturnValue({ select: () => ({ eq: () => ({ gte: () => Promise.resolve({ data: [] }) }) }), }); const result = await detectScratches('nba', props); // LeBron doesn't qualify for currentPlayers (only 1 book), so flagged as scratch expect(result.scratchedPlayers).toContain('LeBron'); }); test('no players disappear: empty scratches', async () => { mockRedis.get.mockResolvedValue(JSON.stringify(['Jokic', 'LeBron'])); const result = await detectScratches('nba', makeProps(['Jokic', 'LeBron'])); expect(result.scratchedPlayers).toEqual([]); }); });