import { Request } from 'express'; import { keyByCallerAndEmail } from '../../src/rateLimit'; // The limiter's allowance is deliberately not asserted anywhere. Its store is // in-memory and process-wide, so a test that exhausts it leaks that state into // every later test keyed on the same address and produces an order-dependent // failure somewhere else entirely. The key function is pure, and the IPv6 // bypass in #84 lived here rather than in the allowance, so this is both the // safe thing to test and the thing worth testing. function requestFrom(ip: string | undefined, email?: unknown): Request { return { ip, body: email === undefined ? {} : { email } } as unknown as Request; } describe('keyByCallerAndEmail', () => { it('leaves an IPv4 caller as it is', () => { expect(keyByCallerAndEmail(requestFrom('203.0.113.5', 'someone@example.com'))) .toBe('203.0.113.5:someone@example.com'); }); // The bug #84 fixed. Before this, these two produced different keys, so an // IPv6 caller got a fresh allowance for every address in their own prefix — // which is to say, no limit at all. it('gives two addresses in one IPv6 delegation the same key', () => { const first = keyByCallerAndEmail(requestFrom('2001:db8:abcd:0100::1', 'target@example.com')); const second = keyByCallerAndEmail(requestFrom('2001:db8:abcd:01ff::9', 'target@example.com')); expect(first).toBe(second); expect(first).toBe('2001:db8:abcd:100::/56:target@example.com'); }); // The other half of the same claim: grouping must not be so coarse that // unrelated callers share one allowance. it('keeps separate IPv6 delegations apart', () => { const inside = keyByCallerAndEmail(requestFrom('2001:db8:abcd:0100::1', 'target@example.com')); const elsewhere = keyByCallerAndEmail(requestFrom('2001:db8:abcd:0200::1', 'target@example.com')); expect(inside).not.toBe(elsewhere); }); // Node reports an IPv4 client as an IPv4-mapped IPv6 address in some // configurations. It must key the same either way, or the same caller would // get two allowances depending on how the socket happened to be opened. it('treats an IPv4-mapped address as the IPv4 caller it is', () => { expect(keyByCallerAndEmail(requestFrom('::ffff:203.0.113.5', 'someone@example.com'))) .toBe(keyByCallerAndEmail(requestFrom('203.0.113.5', 'someone@example.com'))); }); it('normalizes the email so case and padding cannot buy a second allowance', () => { expect(keyByCallerAndEmail(requestFrom('203.0.113.5', ' SomeOne@Example.COM '))) .toBe('203.0.113.5:someone@example.com'); }); it('uses an empty email rather than stringifying a non-string one', () => { expect(keyByCallerAndEmail(requestFrom('203.0.113.5', { not: 'a string' }))) .toBe('203.0.113.5:'); expect(keyByCallerAndEmail(requestFrom('203.0.113.5'))) .toBe('203.0.113.5:'); }); // Only reachable if `trust proxy` were misconfigured, but it must not throw // from inside the limiter if it ever happens. it('survives a request with no address at all', () => { expect(keyByCallerAndEmail(requestFrom(undefined, 'someone@example.com'))) .toBe(':someone@example.com'); }); });