const request = require('supertest'); const express = require('express'); const statsRoutes = require('../../src/routes/stats'); const propsRoutes = require('../../src/routes/props'); // Mock supabase jest.mock('../../src/utils/supabase', () => ({ getSupabaseServiceClient: () => mockSupabase, })); // Mock auth middleware jest.mock('../../src/middleware/auth', () => ({ requireAuth: (req, res, next) => { req.user = req._mockUser || { id: '123', tier: 'analyst' }; next(); }, })); let mockSupabase; beforeEach(() => { mockSupabase = { from: jest.fn().mockReturnThis(), select: jest.fn().mockReturnThis(), order: jest.fn().mockReturnThis(), limit: jest.fn().mockReturnThis(), not: jest.fn().mockReturnThis(), eq: jest.fn().mockReturnThis(), single: jest.fn(), }; }); function buildStatsApp() { const app = express(); app.use(express.json()); app.use('/stats', statsRoutes); return app; } function buildPropsApp(mockUser) { const app = express(); app.use(express.json()); // Inject mock user before routes app.use((req, res, next) => { if (mockUser) req._mockUser = mockUser; next(); }); app.use('/props', propsRoutes); return app; } describe('GET /stats/parlays-graded', () => { it('returns count from scan_sessions', async () => { // Mock the select to return count via head:true pattern // supabase .from().select('*', { count: 'exact', head: true }) returns { count, error } mockSupabase.select.mockReturnValue({ count: 42, error: null }); const app = buildStatsApp(); const res = await request(app).get('/stats/parlays-graded'); expect(res.status).toBe(200); expect(res.body).toEqual({ count: 42 }); expect(res.headers['x-vyndr-mission']).toBeDefined(); }); it('returns 503 on supabase error', async () => { mockSupabase.select.mockReturnValue({ count: null, error: new Error('db down') }); const app = buildStatsApp(); const res = await request(app).get('/stats/parlays-graded'); expect(res.status).toBe(503); expect(res.headers['x-vyndr-mission']).toBeDefined(); }); }); describe('GET /stats/public', () => { it('returns all 4 fields', async () => { // Chain: first call is for count, second for grades, third for kill_conditions let callCount = 0; mockSupabase.from.mockImplementation(() => { callCount++; return mockSupabase; }); mockSupabase.select.mockImplementation((fields, opts) => { if (opts && opts.count === 'exact') { // parlays count return { count: 100, error: null }; } if (fields === 'final_grade') { // grades return { data: [ { final_grade: 'A' }, { final_grade: 'B' }, { final_grade: 'A' }, { final_grade: 'A' }, ], error: null, }; } if (fields === 'kill_conditions') { // return this for the .not() chain return mockSupabase; } return mockSupabase; }); mockSupabase.not.mockReturnValue({ data: [ { kill_conditions: ['back-to-back'] }, { kill_conditions: ['injury'] }, { kill_conditions: [] }, ], error: null, }); const app = buildStatsApp(); const res = await request(app).get('/stats/public'); expect(res.status).toBe(200); expect(res.body).toHaveProperty('parlays_graded'); expect(res.body).toHaveProperty('avg_grade'); expect(res.body).toHaveProperty('kill_conditions_caught'); expect(res.body).toHaveProperty('sports_covered'); expect(res.body.parlays_graded).toBe(100); expect(res.body.avg_grade).toBe('A'); expect(res.body.kill_conditions_caught).toBe(2); expect(res.body.sports_covered).toEqual(['NBA', 'MLB']); expect(res.headers['x-vyndr-mission']).toBeDefined(); }); }); describe('GET /stats/live', () => { it('returns array of max 3 items', async () => { const mockPicks = [ { player: 'LeBron', stat_type: 'points', line: 25.5, direction: 'over', grade: 'A', confidence: 0.85, created_at: '2026-03-28T12:00:00Z' }, { player: 'Curry', stat_type: 'threes', line: 3.5, direction: 'over', grade: 'B', confidence: 0.72, created_at: '2026-03-28T11:00:00Z' }, { player: 'Jokic', stat_type: 'rebounds', line: 10.5, direction: 'under', grade: 'A', confidence: 0.9, created_at: '2026-03-28T10:00:00Z' }, ]; mockSupabase.limit.mockReturnValue({ data: mockPicks, error: null }); const app = buildStatsApp(); const res = await request(app).get('/stats/live'); expect(res.status).toBe(200); expect(Array.isArray(res.body)).toBe(true); expect(res.body.length).toBeLessThanOrEqual(3); expect(res.body[0]).toHaveProperty('player'); expect(res.body[0]).toHaveProperty('stat'); expect(res.body[0]).toHaveProperty('sport', 'NBA'); expect(res.body[0]).toHaveProperty('graded_at'); expect(res.headers['x-vyndr-mission']).toBeDefined(); }); it('returns 503 on supabase error', async () => { mockSupabase.limit.mockReturnValue({ data: null, error: new Error('db down') }); const app = buildStatsApp(); const res = await request(app).get('/stats/live'); expect(res.status).toBe(503); expect(res.headers['x-vyndr-mission']).toBeDefined(); }); }); describe('GET /props/joint-history', () => { it('blocks free tier with 403', async () => { const app = buildPropsApp({ id: '123', tier: 'free' }); const res = await request(app) .get('/props/joint-history') .query({ player_a: 'LeBron', stat_a: 'points', player_b: 'AD', stat_b: 'rebounds' }); expect(res.status).toBe(403); expect(res.body.error).toMatch(/Analyst or Desk/); }); it('returns sample_size 0 when no data', async () => { let eqCalls = 0; mockSupabase.eq.mockImplementation(() => { eqCalls++; if (eqCalls >= 4) return { data: [], error: null }; return mockSupabase; }); const app = buildPropsApp({ id: '123', tier: 'analyst' }); const res = await request(app) .get('/props/joint-history') .query({ player_a: 'LeBron', stat_a: 'points', player_b: 'AD', stat_b: 'rebounds' }); expect(res.status).toBe(200); expect(res.body.sample_size).toBe(0); }); it('returns 400 when missing query params', async () => { const app = buildPropsApp({ id: '123', tier: 'analyst' }); const res = await request(app).get('/props/joint-history'); expect(res.status).toBe(400); }); });