Session 32: Grades pipeline + NFL/NHL wiring + rate limiting + audit cleanup (1718 tests)

- gradeSlateService writes grades:{sport} cache (closes content pipeline →
  dataLevel full); fire-and-forget from oddsService.recordDownstream, gated
  by shouldGradeSlate (off in test, GRADE_SLATE_ON_FETCH override)
- NFL/NHL wired: oddsService SPORT_KEYS/SPORT_MARKETS (correct the-odds-api
  keys americanfootball_nfl/icehockey_nhl), proplineAdapter MARKETS, NHL
  MARKET_MAP keys to avoid silent-zero
- rate limiting mounted on 8 public cached routers (odds/parlay 30/min,
  rest 60/min)
- jsonlLogger writes to temp under test (no more dirtied tracked artifact);
  5MB pipeline test given 20s timeout

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-15 18:21:32 -04:00
parent 2ba3958c7a
commit f0c8b4f29b
20 changed files with 667 additions and 9 deletions
@@ -0,0 +1,70 @@
// Integration: Session 32 mounted the existing rate-limit middleware on the
// public cached routers. These tests prove the wiring at the ROUTER level —
// the middleware's own behavior (429, window reset, per-IP keys) is covered
// in tests/unit/rateLimitMiddleware.test.js.
const express = require('express');
const request = require('supertest');
// Mock redis so any handler that does run never touches a real server.
jest.mock('../../src/utils/redis', () => ({
cacheGet: jest.fn(async () => null),
cacheSet: jest.fn(async () => {}),
getRedisClient: () => ({ get: async () => null, set: async () => 'OK', scan: async () => ['0', []], hgetall: async () => ({}) }),
isDegraded: () => false,
}));
// Fresh module registry → fresh per-router bucket (createRateLimit state is
// module-scoped). jest.resetModules() resets Jest's registry; deleting
// require.cache alone does NOT under Jest.
function mountFresh(routePath, file) {
jest.resetModules();
const app = express();
app.use(express.json());
app.use(routePath, require(file));
return app;
}
// A deep nonsense path enters the router (running router.use(rateLimit))
// but matches no route → 404. Lets us exhaust the limiter without invoking
// the real handlers or any upstream call.
const MISS = '/__rl__/__miss__/__deep__';
describe('public route rate limiting (Session 32 wiring)', () => {
test('schedule router 429s after exceeding its 60/min limit', async () => {
const app = mountFresh('/api/schedule', '../../src/routes/schedule');
// 60 allowed (each 404s but counts), 61st is throttled.
for (let i = 0; i < 60; i += 1) {
const r = await request(app).get(`/api/schedule${MISS}`);
expect(r.status).not.toBe(429);
}
const blocked = await request(app).get(`/api/schedule${MISS}`);
expect(blocked.status).toBe(429);
expect(blocked.body.error).toMatch(/too many/i);
});
test('odds router 429s after exceeding its tighter 30/min limit', async () => {
const app = mountFresh('/api/odds', '../../src/routes/odds');
for (let i = 0; i < 30; i += 1) {
const r = await request(app).get(`/api/odds${MISS}`);
expect(r.status).not.toBe(429);
}
expect((await request(app).get(`/api/odds${MISS}`)).status).toBe(429);
});
test('different route files have INDEPENDENT buckets (not a shared limit)', async () => {
// Mount two routers in one app; exhaust parlay, confirm odds is unaffected.
jest.resetModules();
const app = express();
app.use(express.json());
app.use('/api/parlay', require('../../src/routes/parlay'));
app.use('/api/odds', require('../../src/routes/odds'));
// Exhaust parlay (30/min).
for (let i = 0; i < 30; i += 1) await request(app).get(`/api/parlay${MISS}`);
expect((await request(app).get(`/api/parlay${MISS}`)).status).toBe(429);
// odds shares the same client IP but its own bucket — still open.
expect((await request(app).get(`/api/odds${MISS}`)).status).not.toBe(429);
});
});