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'); const app = require('../../src/app'); beforeEach(() => { jest.clearAllMocks(); mockRedis.get.mockResolvedValue(null); mockRedis.set.mockResolvedValue('OK'); mockRedis.hset.mockResolvedValue(1); mockRedis.hgetall.mockResolvedValue({}); mockRedis.expire.mockResolvedValue(1); }); describe('GET /api/movements', () => { test('returns movements for today', async () => { const movements = [ { player: 'Nikola Jokic', stat_type: 'points', book: 'draftkings', baseline_line: 26.5, current_line: 27.5, movement: 1.0, direction: 'up', sharp_indicator: 'sharp_action' }, ]; mockSupabaseFrom.mockReturnValue({ select: () => ({ eq: () => ({ ilike: () => ({ eq: () => Promise.resolve({ data: movements }), }), }), }), }); // Simple mock: just return movements from Supabase mockSupabaseFrom.mockImplementation((table) => { if (table === 'line_movements') { return { select: () => ({ eq: () => Promise.resolve({ data: movements }), }), }; } return { select: () => ({ eq: () => Promise.resolve({ data: [] }) }) }; }); const res = await request(app).get('/api/movements').expect(200); expect(res.body.game_date).toBeDefined(); expect(Array.isArray(res.body.movements)).toBe(true); }); }); describe('GET /api/alerts', () => { test('returns 401 without auth', async () => { const res = await request(app).get('/api/alerts').expect(401); expect(res.body.error).toContain('Authentication'); }); test('returns 403 for free tier', async () => { mockSupabaseAuth.getUser.mockResolvedValue({ data: { user: { id: 'u1', email: 'free@test.com' } }, error: null, }); mockSupabaseFrom.mockImplementation((table) => { if (table === 'users') { return { select: () => ({ eq: () => ({ single: () => Promise.resolve({ data: { id: 'u1', email: 'free@test.com', tier: 'free', scan_count: 0 }, error: null, }), }), }), }; } return { select: () => ({ eq: () => Promise.resolve({ data: [] }) }) }; }); const res = await request(app) .get('/api/alerts') .set('Authorization', 'Bearer valid-token') .expect(403); expect(res.body.error).toContain('Analyst and Desk'); }); test('returns alerts for paid tier user', async () => { const alerts = [ { id: 'a1', alert_type: 'player_scratched', player: 'X', detail: 'scratched', is_read: false, created_at: '2026-03-21T18:00:00Z' }, ]; mockSupabaseAuth.getUser.mockResolvedValue({ data: { user: { id: 'u2', email: 'paid@test.com' } }, error: null, }); mockSupabaseFrom.mockImplementation((table) => { if (table === 'users') { return { select: () => ({ eq: () => ({ single: () => Promise.resolve({ data: { id: 'u2', email: 'paid@test.com', tier: 'analyst', scan_count: 10 }, error: null, }), }), }), }; } if (table === 'cascade_alerts') { return { select: (sel, opts) => { if (opts?.head) { return { eq: () => ({ eq: () => Promise.resolve({ count: 1 }) }) }; } return { eq: () => ({ eq: () => ({ order: () => Promise.resolve({ data: alerts, error: null }), }), }), }; }, }; } return { select: () => ({ eq: () => Promise.resolve({ data: [] }) }) }; }); const res = await request(app) .get('/api/alerts') .set('Authorization', 'Bearer valid-token') .expect(200); expect(res.body.alerts).toHaveLength(1); expect(res.body.alerts[0].alert_type).toBe('player_scratched'); }); }); describe('PATCH /api/alerts/:id/read', () => { test('marks alert as read', async () => { mockSupabaseAuth.getUser.mockResolvedValue({ data: { user: { id: 'u2', email: 'paid@test.com' } }, error: null, }); mockSupabaseFrom.mockImplementation((table) => { if (table === 'users') { return { select: () => ({ eq: () => ({ single: () => Promise.resolve({ data: { id: 'u2', email: 'paid@test.com', tier: 'desk', scan_count: 0 }, error: null, }), }), }), }; } if (table === 'cascade_alerts') { return { update: () => ({ eq: () => ({ eq: () => ({ select: () => ({ single: () => Promise.resolve({ data: { id: 'a1', is_read: true }, error: null }), }), }), }), }), }; } return {}; }); const res = await request(app) .patch('/api/alerts/a1/read') .set('Authorization', 'Bearer valid-token') .expect(200); expect(res.body.is_read).toBe(true); }); test('returns 404 for nonexistent alert', async () => { mockSupabaseAuth.getUser.mockResolvedValue({ data: { user: { id: 'u2', email: 'paid@test.com' } }, error: null, }); mockSupabaseFrom.mockImplementation((table) => { if (table === 'users') { return { select: () => ({ eq: () => ({ single: () => Promise.resolve({ data: { id: 'u2', email: 'paid@test.com', tier: 'analyst', scan_count: 0 }, error: null, }), }), }), }; } if (table === 'cascade_alerts') { return { update: () => ({ eq: () => ({ eq: () => ({ select: () => ({ single: () => Promise.resolve({ data: null, error: { message: 'not found' } }), }), }), }), }), }; } return {}; }); const res = await request(app) .patch('/api/alerts/nonexistent/read') .set('Authorization', 'Bearer valid-token') .expect(404); expect(res.body.error).toContain('not found'); }); });