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:
@@ -141,7 +141,9 @@ describe('pipeline body-parser regression (Session 7b)', () => {
|
||||
// void: true path returns 200 with the empty resolution summary.
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveProperty('resolved');
|
||||
});
|
||||
// Parsing a 5MB body is CPU-bound; under full-suite parallel load it can
|
||||
// exceed Jest's 5s default. Give it headroom so the gate stays reliable.
|
||||
}, 20000);
|
||||
|
||||
test('rejects payloads larger than the 10MB limit with 413 (sanity check)', async () => {
|
||||
// Use a tiny app limit so the test doesn't allocate 11MB just to
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user