Session 7d: Audit fixes - rate limiting, error leak, parallel parlays, analyze cache, bundle analyzer
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
const { createRateLimit } = require('../../src/middleware/rateLimit');
|
||||
|
||||
function mockReqRes(ip = '1.2.3.4') {
|
||||
const req = { ip, headers: {}, socket: { remoteAddress: ip } };
|
||||
const headers = {};
|
||||
const res = {
|
||||
statusCode: 200,
|
||||
body: null,
|
||||
set(k, v) { headers[k] = v; return res; },
|
||||
status(c) { res.statusCode = c; return res; },
|
||||
json(b) { res.body = b; return res; },
|
||||
};
|
||||
return { req, res, headers };
|
||||
}
|
||||
|
||||
describe('rateLimit middleware', () => {
|
||||
test('allows under-limit calls', () => {
|
||||
const mw = createRateLimit({ windowMs: 60_000, max: 3 });
|
||||
const next = jest.fn();
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const { req, res } = mockReqRes();
|
||||
mw(req, res, next);
|
||||
}
|
||||
expect(next).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('429s on the (max+1)th call within the window', () => {
|
||||
const mw = createRateLimit({ windowMs: 60_000, max: 2 });
|
||||
const next = jest.fn();
|
||||
let lastRes;
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const { req, res } = mockReqRes('5.5.5.5');
|
||||
mw(req, res, next);
|
||||
lastRes = res;
|
||||
}
|
||||
expect(next).toHaveBeenCalledTimes(2);
|
||||
expect(lastRes.statusCode).toBe(429);
|
||||
expect(lastRes.body).toEqual({ error: 'Too many requests' });
|
||||
});
|
||||
|
||||
test('429 response carries Retry-After header', () => {
|
||||
const mw = createRateLimit({ windowMs: 60_000, max: 1 });
|
||||
const next = jest.fn();
|
||||
let lastHeaders;
|
||||
let lastRes;
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
const { req, res, headers } = mockReqRes('6.6.6.6');
|
||||
mw(req, res, next);
|
||||
lastRes = res; lastHeaders = headers;
|
||||
}
|
||||
expect(lastRes.statusCode).toBe(429);
|
||||
expect(lastHeaders['Retry-After']).toBeDefined();
|
||||
expect(parseInt(lastHeaders['Retry-After'], 10)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('different IPs each get their own quota', () => {
|
||||
const mw = createRateLimit({ windowMs: 60_000, max: 1 });
|
||||
const next = jest.fn();
|
||||
const r1 = mockReqRes('7.7.7.7');
|
||||
const r2 = mockReqRes('8.8.8.8');
|
||||
mw(r1.req, r1.res, next);
|
||||
mw(r2.req, r2.res, next);
|
||||
expect(next).toHaveBeenCalledTimes(2);
|
||||
expect(r1.res.statusCode).toBe(200);
|
||||
expect(r2.res.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('window expiry releases the quota', async () => {
|
||||
const mw = createRateLimit({ windowMs: 60, max: 1 });
|
||||
const next = jest.fn();
|
||||
const a = mockReqRes('9.9.9.9');
|
||||
mw(a.req, a.res, next); // counts as 1
|
||||
const b = mockReqRes('9.9.9.9');
|
||||
mw(b.req, b.res, next); // 429
|
||||
expect(b.res.statusCode).toBe(429);
|
||||
await new Promise((r) => setTimeout(r, 90));
|
||||
const c = mockReqRes('9.9.9.9');
|
||||
mw(c.req, c.res, next); // back to OK
|
||||
expect(next).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('eviction trims the IP table when capacity exceeded', () => {
|
||||
const { __internals } = require('../../src/middleware/rateLimit');
|
||||
// We can't directly read the internal Map, so just confirm the
|
||||
// module exposes MAX_TRACKED_IPS as a sanity floor.
|
||||
expect(__internals.MAX_TRACKED_IPS).toBeGreaterThanOrEqual(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('analyze routes wired to the middleware', () => {
|
||||
test('hits a 429 on the 11th identical-IP call', async () => {
|
||||
const express = require('express');
|
||||
process.env.NBA_STATS_URL = 'http://localhost:9999'; // unreachable, but
|
||||
// the rate limit fires before we ever try to call upstream.
|
||||
|
||||
const analyze = require('../../src/routes/analyze');
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/analyze', analyze);
|
||||
|
||||
const http = require('http');
|
||||
const server = await new Promise((resolve) => {
|
||||
const s = app.listen(0, '127.0.0.1', () => resolve(s));
|
||||
});
|
||||
const port = server.address().port;
|
||||
|
||||
function postOnce(path = '/api/analyze/prop') {
|
||||
return new Promise((resolve) => {
|
||||
const req = http.request({
|
||||
host: '127.0.0.1', port, path, method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', (c) => chunks.push(c));
|
||||
res.on('end', () => resolve({ status: res.statusCode }));
|
||||
});
|
||||
// Empty body — validator will 400 within the window, but the
|
||||
// rate-limit middleware runs first and counts the hit.
|
||||
req.end('{}');
|
||||
});
|
||||
}
|
||||
|
||||
const statuses = [];
|
||||
for (let i = 0; i < 11; i += 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
statuses.push((await postOnce()).status);
|
||||
}
|
||||
server.close();
|
||||
|
||||
const tooMany = statuses.filter((s) => s === 429).length;
|
||||
expect(tooMany).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user