const { scanLimit, __internals } = require('../../src/middleware/scanLimit'); function mockReqRes({ ip = '1.1.1.1', tier, userId } = {}) { const req = { ip, socket: { remoteAddress: ip }, headers: {}, user: userId ? { id: userId, tier } : (tier ? { tier } : null), }; 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 }; } beforeEach(() => __internals.resetForTests()); describe('scanLimit middleware', () => { const mw = scanLimit(); test('free user allowed up to 3 scans per day', () => { const next = jest.fn(); for (let i = 0; i < 3; i += 1) { const { req, res } = mockReqRes({ tier: 'free', userId: 'u-free' }); mw(req, res, next); } expect(next).toHaveBeenCalledTimes(3); }); test('free user 4th scan in the window returns 429', () => { const next = jest.fn(); let lastRes; for (let i = 0; i < 4; i += 1) { const { req, res } = mockReqRes({ tier: 'free', userId: 'u-free-block' }); mw(req, res, next); lastRes = res; } expect(next).toHaveBeenCalledTimes(3); expect(lastRes.statusCode).toBe(429); expect(lastRes.body.error).toMatch(/scan limit/i); expect(lastRes.body.scans_used).toBe(3); expect(lastRes.body.scans_limit).toBe(3); expect(lastRes.body.tier).toBe('free'); }); test('429 response includes Retry-After + X-Scans-* headers', () => { const next = jest.fn(); let lastHeaders; for (let i = 0; i < 4; i += 1) { const { req, res, headers } = mockReqRes({ tier: 'free', userId: 'u-hdr' }); mw(req, res, next); lastHeaders = headers; } expect(parseInt(lastHeaders['Retry-After'], 10)).toBeGreaterThan(0); expect(lastHeaders['X-Scans-Used']).toBe('3'); expect(lastHeaders['X-Scans-Limit']).toBe('3'); }); test('analyst tier gets 15 scans/day', () => { const next = jest.fn(); let blockedAt = null; for (let i = 0; i < 16; i += 1) { const { req, res } = mockReqRes({ tier: 'analyst', userId: 'u-analyst' }); mw(req, res, next); if (res.statusCode === 429 && blockedAt == null) blockedAt = i; } expect(blockedAt).toBe(15); expect(next).toHaveBeenCalledTimes(15); }); test('desk tier is unlimited — never blocks', () => { const next = jest.fn(); for (let i = 0; i < 100; i += 1) { const { req, res } = mockReqRes({ tier: 'desk', userId: 'u-desk' }); mw(req, res, next); expect(res.statusCode).toBe(200); } expect(next).toHaveBeenCalledTimes(100); }); test('anonymous user (no req.user) falls back to free quota keyed by IP', () => { const next = jest.fn(); let lastRes; for (let i = 0; i < 4; i += 1) { const { req, res } = mockReqRes({ ip: '7.7.7.7' }); mw(req, res, next); lastRes = res; } expect(next).toHaveBeenCalledTimes(3); expect(lastRes.statusCode).toBe(429); }); test('different IPs are independent (anonymous bucket)', () => { const next = jest.fn(); const a = mockReqRes({ ip: '1.1.1.1' }); const b = mockReqRes({ ip: '2.2.2.2' }); mw(a.req, a.res, next); mw(b.req, b.res, next); expect(next).toHaveBeenCalledTimes(2); expect(a.res.statusCode).toBe(200); expect(b.res.statusCode).toBe(200); }); test('authenticated users in the same IP get independent quotas', () => { const next = jest.fn(); // Same IP, different user IDs — should not interfere. for (let i = 0; i < 3; i += 1) { const { req, res } = mockReqRes({ tier: 'free', userId: 'u-A', ip: '5.5.5.5' }); mw(req, res, next); } // User A exhausted; user B starts clean. const { req, res } = mockReqRes({ tier: 'free', userId: 'u-B', ip: '5.5.5.5' }); mw(req, res, next); expect(res.statusCode).toBe(200); expect(next).toHaveBeenCalledTimes(4); }); });