const request = require('supertest'); // Mock Redis const mockRedis = { get: jest.fn(), set: jest.fn(), hset: jest.fn(), hgetall: jest.fn(), expire: jest.fn() }; jest.mock('../../src/utils/redis', () => ({ getRedisClient: () => mockRedis })); // Mock Supabase const mockSupabaseFrom = jest.fn(); const mockSupabaseAuth = { getUser: jest.fn() }; jest.mock('../../src/utils/supabase', () => ({ getSupabaseClient: () => ({ auth: mockSupabaseAuth, from: mockSupabaseFrom }), getSupabaseServiceClient: () => ({ auth: mockSupabaseAuth, from: mockSupabaseFrom }), })); jest.mock('axios'); process.env.ODDS_API_KEY = 'test-key'; const app = require('../../src/app'); const MOCK_USER = { id: 'user-1', email: 'test@test.com', tier: 'analyst', scan_count: 0, scan_reset_date: '2026-04-01', }; const VALID_QUICKSLIP = { legs: [ { player: 'Nikola Jokic', stat_type: 'points', line: 26.5, direction: 'over', odds: -110 }, ], amount: 20, book: 'draftkings', bet_type: 'straight', }; const VALID_PARLAY_SLIP = { legs: [ { player: 'Nikola Jokic', stat_type: 'points', line: 26.5, direction: 'over', odds: -110 }, { player: 'LeBron James', stat_type: 'rebounds', line: 8.5, direction: 'over', odds: -120 }, ], amount: 10, book: 'fanduel', bet_type: 'parlay', }; function setupAuthMock() { mockSupabaseAuth.getUser.mockResolvedValue({ data: { user: { id: MOCK_USER.id, email: MOCK_USER.email } }, error: null, }); } function setupFullMocks() { setupAuthMock(); mockSupabaseFrom.mockImplementation((table) => { if (table === 'users') { return { select: () => ({ eq: () => ({ single: () => Promise.resolve({ data: MOCK_USER, error: null }), }), }), }; } if (table === 'bets') { return { insert: () => ({ select: () => ({ single: () => Promise.resolve({ data: { id: 'bet-1', status: 'pending', amount: 20, potential_payout: 38.18, bet_type: 'straight', book: 'draftkings', created_at: '2026-03-22T00:00:00Z' }, error: null, }), }), }), select: (sel, opts) => ({ eq: (col, val) => ({ eq: () => ({ single: () => Promise.resolve({ data: { id: 'bet-1', user_id: 'user-1', amount: 20, potential_payout: 38.18, status: 'pending', book: 'draftkings' }, error: null, }), }), order: () => ({ range: () => Promise.resolve({ data: [], count: 0, error: null }), }), in: () => Promise.resolve({ data: [], error: null }), }), }), update: () => ({ eq: () => Promise.resolve({ error: null }), }), }; } if (table === 'scan_sessions') { return { select: () => ({ eq: () => ({ eq: () => ({ single: () => Promise.resolve({ data: { id: 'session-1' }, error: null }), }), }), }), }; } if (table === 'performance') { return { upsert: () => Promise.resolve({ error: null }), }; } return { select: () => ({ eq: () => ({ in: () => Promise.resolve({ data: [], error: null }) }) }), }; }); } beforeEach(() => { jest.clearAllMocks(); mockRedis.get.mockResolvedValue(null); mockRedis.set.mockResolvedValue('OK'); mockRedis.hset.mockResolvedValue(1); mockRedis.hgetall.mockResolvedValue({}); mockRedis.expire.mockResolvedValue(1); }); describe('POST /api/bets/quickslip', () => { test('creates straight bet with correct payout', async () => { setupFullMocks(); const res = await request(app) .post('/api/bets/quickslip') .set('Authorization', 'Bearer valid-token') .send(VALID_QUICKSLIP) .expect(201); expect(res.body.bet_id).toBeDefined(); expect(res.body.status).toBe('pending'); expect(res.body.bet_type).toBe('straight'); }); test('creates parlay bet', async () => { setupFullMocks(); // Override bets insert to return parlay data const res = await request(app) .post('/api/bets/quickslip') .set('Authorization', 'Bearer valid-token') .send(VALID_PARLAY_SLIP) .expect(201); expect(res.body.bet_id).toBeDefined(); }); test('returns 400 for missing legs', async () => { setupFullMocks(); const res = await request(app) .post('/api/bets/quickslip') .set('Authorization', 'Bearer valid-token') .send({ amount: 20, book: 'draftkings', bet_type: 'straight' }) .expect(400); expect(res.body.error).toContain('legs'); }); test('returns 400 for invalid bet_type', async () => { setupFullMocks(); const res = await request(app) .post('/api/bets/quickslip') .set('Authorization', 'Bearer valid-token') .send({ ...VALID_QUICKSLIP, bet_type: 'invalid' }) .expect(400); expect(res.body.error).toContain('bet_type'); }); test('returns 401 without auth', async () => { const res = await request(app) .post('/api/bets/quickslip') .send(VALID_QUICKSLIP) .expect(401); expect(res.body.error).toContain('Authentication'); }); }); describe('POST /api/bets/screenshot', () => { test('returns needs_confirmation with stub extraction', async () => { setupFullMocks(); const res = await request(app) .post('/api/bets/screenshot') .set('Authorization', 'Bearer valid-token') .send({ book: 'draftkings' }) .expect(201); expect(res.body.needs_confirmation).toBe(true); expect(res.body.extracted).toBeDefined(); expect(res.body.extracted.book).toBe('draftkings'); }); }); describe('POST /api/bets/screenshot/confirm', () => { test('saves confirmed bet', async () => { setupFullMocks(); const res = await request(app) .post('/api/bets/screenshot/confirm') .set('Authorization', 'Bearer valid-token') .send(VALID_QUICKSLIP) .expect(201); expect(res.body.bet_id).toBeDefined(); expect(res.body.status).toBe('pending'); }); }); describe('POST /api/bets/sync', () => { test('returns coming_soon stub', async () => { setupFullMocks(); const res = await request(app) .post('/api/bets/sync') .set('Authorization', 'Bearer valid-token') .expect(200); expect(res.body.status).toBe('coming_soon'); expect(res.body.supported_books).toContain('draftkings'); }); }); describe('PATCH /api/bets/:id/settle', () => { test('settles pending bet as won', async () => { setupFullMocks(); const res = await request(app) .patch('/api/bets/bet-1/settle') .set('Authorization', 'Bearer valid-token') .send({ status: 'won' }) .expect(200); expect(res.body.status).toBe('won'); expect(res.body.settled_at).toBeDefined(); expect(typeof res.body.profit).toBe('number'); }); test('rejects settling already-settled bet', async () => { setupFullMocks(); // Override bet fetch to return already-settled mockSupabaseFrom.mockImplementation((table) => { if (table === 'users') { return { select: () => ({ eq: () => ({ single: () => Promise.resolve({ data: MOCK_USER, error: null }) }) }) }; } if (table === 'bets') { return { select: () => ({ eq: () => ({ eq: () => ({ single: () => Promise.resolve({ data: { id: 'bet-1', user_id: 'user-1', amount: 20, potential_payout: 38, status: 'won' }, error: null, }), }), }), }), }; } return { select: () => ({ eq: () => Promise.resolve({ data: [] }) }) }; }); const res = await request(app) .patch('/api/bets/bet-1/settle') .set('Authorization', 'Bearer valid-token') .send({ status: 'lost' }) .expect(422); expect(res.body.error).toContain('already settled'); }); test('returns 400 for invalid status', async () => { setupFullMocks(); const res = await request(app) .patch('/api/bets/bet-1/settle') .set('Authorization', 'Bearer valid-token') .send({ status: 'invalid' }) .expect(400); expect(res.body.error).toContain('Invalid status'); }); }); describe('GET /api/bets', () => { test('returns user bets with pagination', async () => { setupFullMocks(); const res = await request(app) .get('/api/bets') .set('Authorization', 'Bearer valid-token') .expect(200); expect(Array.isArray(res.body.bets)).toBe(true); expect(typeof res.body.total).toBe('number'); expect(res.body.limit).toBeDefined(); expect(res.body.offset).toBeDefined(); }); }); describe('GET /api/bets/performance', () => { test('returns performance stats', async () => { setupFullMocks(); const res = await request(app) .get('/api/bets/performance') .set('Authorization', 'Bearer valid-token') .expect(200); // Performance service returns results for weekly/monthly/all_time expect(res.body).toBeDefined(); }); });