import { RateLimit, Store } from 'tapimo' describe('memory', () => { test('tracks fixed-minute quota in a store', async () => { const store = RateLimit.memory({ now: () => new Date('2026-05-25T00:00:30.000Z'), }) const limit = { limit: 2, period: 'minute' } as const const first = await store.consume({ key: 'public:anonymous', limit }) const second = await store.consume({ key: 'public:anonymous', limit }) const third = await store.consume({ key: 'public:anonymous', limit }) expect(first).toMatchInlineSnapshot(` { "allowed": true, "limit": 2, "remaining": 1, "reset": 1779667260, } `) expect(second).toMatchInlineSnapshot(` { "allowed": true, "limit": 2, "remaining": 0, "reset": 1779667260, } `) expect(third).toMatchInlineSnapshot(` { "allowed": false, "limit": 2, "remaining": 0, "reset": 1779667260, } `) }) test('restarts the count when the next second opens', async () => { // The window reset rides on counter expiry, so the store clock and the // consume clock must agree. vi.useFakeTimers() try { vi.setSystemTime(new Date('2026-05-25T00:00:30.250Z')) const store = RateLimit.memory() const limit = { limit: 2, period: 'second' } as const await store.consume({ key: 'public:anonymous', limit }) await store.consume({ key: 'public:anonymous', limit }) vi.setSystemTime(new Date('2026-05-25T00:00:31.001Z')) const next = await store.consume({ key: 'public:anonymous', limit }) expect(next).toMatchInlineSnapshot(` { "allowed": true, "limit": 2, "remaining": 1, "reset": 1779667232, } `) } finally { vi.useRealTimers() } }) test('consumes with one increment that expires at the end of the window', async () => { const increments: { key: string; options: Store.Store.PutOptions | undefined }[] = [] const backing = Store.memory() const recording = Store.from({ ...backing, async increment(key, options) { increments.push({ key, options }) return Store.increment(backing, key, options) }, }) const store = RateLimit.memory({ // 250ms into the second, so 750ms of the window remains. now: () => new Date('2026-05-25T00:00:30.250Z'), store: recording, }) await store.consume({ key: 'public:anonymous', limit: { limit: 2, period: 'second' }, }) expect(increments).toMatchInlineSnapshot(` [ { "key": "ratelimit:second:1779667230:public:anonymous", "options": { "ttl": 750, }, }, ] `) }) }) describe('edge', () => { function binding(success = true) { return { limit: vi.fn(async (_: { key: string }) => ({ success })) } } function request(options: { asn?: number | undefined; ip?: string | undefined } = {}) { const value = new Request('https://api.tempo.xyz/v1/health', { headers: options.ip ? { 'cf-connecting-ip': options.ip } : {}, }) if (options.asn) Object.assign(value, { cf: { asn: options.asn } }) return value } test('allows when every layer is under quota', async () => { const asn = binding() const global = binding() const ip = binding() const response = await RateLimit.edge(request({ asn: 64512, ip: '203.0.113.7' }), { asn, global, ip, }) expect(response).toBeUndefined() expect(ip.limit).toHaveBeenCalledWith({ key: '203.0.113.7' }) expect(asn.limit).toHaveBeenCalledWith({ key: 'asn:64512' }) expect(global.limit).toHaveBeenCalledWith({ key: 'global' }) }) test('buckets IPv6 clients by /64 prefix', async () => { const ip = binding() await RateLimit.edge(request({ ip: '2001:db8:abcd:12::1' }), { ip }) expect(ip.limit).toHaveBeenCalledWith({ key: '2001:db8:abcd:12' }) }) test('normalizes equivalent IPv6 addresses to the same /64 prefix', () => { expect([ RateLimit.normalizeIp('2001:0db8:abcd:0012::1'), RateLimit.normalizeIp('2001:db8:abcd:12:ffff::2'), ]).toMatchInlineSnapshot(` [ "2001:db8:abcd:12", "2001:db8:abcd:12", ] `) }) test('returns 429 when any layer denies', async () => { const response = await RateLimit.edge(request({ asn: 64512, ip: '203.0.113.7' }), { asn: binding(false), global: binding(), ip: binding(), }) expect(response?.status).toBe(429) expect(response?.headers.get('retry-after')).toBe('10') }) test('skips the asn layer when the request has no cf metadata', async () => { const asn = binding(false) const response = await RateLimit.edge(request({ ip: '203.0.113.7' }), { asn }) expect(response).toBeUndefined() expect(asn.limit).not.toHaveBeenCalled() }) test('keys unknown clients together when the ip header is missing', async () => { const ip = binding() await RateLimit.edge(request(), { ip }) expect(ip.limit).toHaveBeenCalledWith({ key: 'unknown' }) }) test('fails open when a binding errors', async () => { const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) try { const response = await RateLimit.edge(request({ ip: '203.0.113.7' }), { ip: { limit() { return Promise.reject(new Error('binding unavailable')) }, }, }) expect(response).toBeUndefined() } finally { consoleError.mockRestore() } }) })