HuggingClaw-MissionControl / src /lib /__tests__ /rate-limit.test.ts
nyk
fix: reduce server memory footprint (#405)
c963447 unverified
Raw
History Blame Contribute Delete
3.42 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { createRateLimiter } from '@/lib/rate-limit'
describe('createRateLimiter', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
function makeRequest(ip: string = '127.0.0.1'): Request {
return new Request('http://localhost/api/test', {
headers: new Headers({ 'x-real-ip': ip }),
})
}
it('allows first request within limit (returns null)', () => {
const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 5 })
const result = limiter(makeRequest())
expect(result).toBeNull()
})
it('allows requests up to the max limit', () => {
const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 3 })
expect(limiter(makeRequest())).toBeNull()
expect(limiter(makeRequest())).toBeNull()
expect(limiter(makeRequest())).toBeNull()
})
it('blocks request exceeding the limit with 429', () => {
const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 2 })
limiter(makeRequest())
limiter(makeRequest())
const blocked = limiter(makeRequest())
expect(blocked).not.toBeNull()
expect(blocked!.status).toBe(429)
})
it('uses custom message in 429 response', async () => {
const limiter = createRateLimiter({
windowMs: 60_000,
maxRequests: 1,
message: 'Slow down!',
})
limiter(makeRequest())
const blocked = limiter(makeRequest())
expect(blocked).not.toBeNull()
const body = await blocked!.json()
expect(body.error).toBe('Slow down!')
})
it('resets after the window expires', () => {
const limiter = createRateLimiter({ windowMs: 10_000, maxRequests: 1 })
expect(limiter(makeRequest())).toBeNull()
expect(limiter(makeRequest())).not.toBeNull()
// Advance past the window
vi.advanceTimersByTime(11_000)
// Should be allowed again
expect(limiter(makeRequest())).toBeNull()
})
it('tracks different IPs independently', () => {
const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 1 })
expect(limiter(makeRequest('10.0.0.1'))).toBeNull()
expect(limiter(makeRequest('10.0.0.2'))).toBeNull()
// First IP now blocked
expect(limiter(makeRequest('10.0.0.1'))).not.toBeNull()
// Second IP now blocked
expect(limiter(makeRequest('10.0.0.2'))).not.toBeNull()
})
it('evicts oldest entry when maxEntries is reached', () => {
const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 1, maxEntries: 3 })
// Fill to capacity and exhaust 10.0.0.1's quota
limiter(makeRequest('10.0.0.1'))
vi.advanceTimersByTime(1)
expect(limiter(makeRequest('10.0.0.1'))).not.toBeNull() // blocked: quota consumed
limiter(makeRequest('10.0.0.2'))
vi.advanceTimersByTime(1)
limiter(makeRequest('10.0.0.3'))
vi.advanceTimersByTime(1)
// Store has 3 entries (A blocked, B, C). Adding D evicts A (oldest resetAt)
limiter(makeRequest('10.0.0.4'))
// 10.0.0.1 was evicted — counter is gone, this is allowed (fresh entry)
expect(limiter(makeRequest('10.0.0.1'))).toBeNull()
// Now 10.0.0.1 is back at count=1 and blocked
expect(limiter(makeRequest('10.0.0.1'))).not.toBeNull()
// 10.0.0.3 should still be tracked (not evicted — it had the newest resetAt)
expect(limiter(makeRequest('10.0.0.3'))).not.toBeNull()
})
})