Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user