204 lines
6.4 KiB
JavaScript
204 lines
6.4 KiB
JavaScript
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);
|
|
});
|
|
});
|