b5d3fd14bb
MLB statsapi + WNBA ESPN live boxscores -> per-player current values
(live:{sport}:{date} TTL 90s, /api/live/:sport + Next proxy). Pure
propState math (HIT / ON PACE / NEEDS N / HOLDS / LINE PASSED — never
red in-progress), attachLiveProgress strip join on nameKey+statType,
proximity-to-hit slate float, StatStrip LiveTracker in the ROW-GRAMMAR
outcome slot (spec amended + lock test updated). Grades never change
in-game — tracking, labeled as such. 2698 -> 2757 tests, web build 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
70 lines
2.7 KiB
JavaScript
70 lines
2.7 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* GET /api/live/:sport (A1 Session 11) — route wiring over the cache-aside
|
|
* service. The service's cache behavior is unit-tested with injected deps
|
|
* (liveTrackingService.test.js); here we lock the route contract: sport
|
|
* gating BEFORE the service, envelope pass-through, and fail-open 200s.
|
|
*/
|
|
|
|
const express = require('express');
|
|
const request = require('supertest');
|
|
|
|
jest.mock('../../src/services/liveTrackingService', () => ({
|
|
getLiveTracking: jest.fn(),
|
|
}));
|
|
const { getLiveTracking } = require('../../src/services/liveTrackingService');
|
|
|
|
function mountLive() {
|
|
delete require.cache[require.resolve('../../src/routes/live')];
|
|
const liveRoutes = require('../../src/routes/live');
|
|
const app = express();
|
|
app.use('/api/live', liveRoutes);
|
|
return app;
|
|
}
|
|
|
|
beforeEach(() => jest.clearAllMocks());
|
|
|
|
describe('GET /api/live/:sport', () => {
|
|
it('returns the service envelope for a wired sport (mlb)', async () => {
|
|
const envelope = {
|
|
sport: 'mlb', date: '2026-07-11', hasLive: true, updated_at: 'x',
|
|
games: [{ id: '824249', progress: { label: '▼8th', fraction: 0.83 }, players: { 'bryce harper': { name: 'Bryce Harper', values: { total_bases: 3 } } } }],
|
|
};
|
|
getLiveTracking.mockResolvedValue(envelope);
|
|
const res = await request(mountLive()).get('/api/live/mlb');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual(envelope);
|
|
expect(getLiveTracking).toHaveBeenCalledWith('mlb');
|
|
expect(res.headers['cache-control']).toContain('max-age=30');
|
|
});
|
|
|
|
it('wnba is wired', async () => {
|
|
getLiveTracking.mockResolvedValue({ sport: 'wnba', hasLive: false, games: [] });
|
|
const res = await request(mountLive()).get('/api/live/wnba');
|
|
expect(res.status).toBe(200);
|
|
expect(getLiveTracking).toHaveBeenCalledWith('wnba');
|
|
});
|
|
|
|
it('an unwired sport short-circuits WITHOUT touching the service (no quota risk)', async () => {
|
|
const res = await request(mountLive()).get('/api/live/nba');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual({ sport: 'nba', hasLive: false, games: [] });
|
|
expect(getLiveTracking).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('a service failure fails OPEN — 200 with an honest empty envelope', async () => {
|
|
getLiveTracking.mockRejectedValue(new Error('redis down'));
|
|
const res = await request(mountLive()).get('/api/live/mlb');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual({ sport: 'mlb', hasLive: false, games: [] });
|
|
});
|
|
|
|
it('sport param is case-insensitive', async () => {
|
|
getLiveTracking.mockResolvedValue({ sport: 'mlb', hasLive: false, games: [] });
|
|
const res = await request(mountLive()).get('/api/live/MLB');
|
|
expect(res.status).toBe(200);
|
|
expect(getLiveTracking).toHaveBeenCalledWith('mlb');
|
|
});
|
|
});
|