// 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); }); });