- B1: Next.js 15 + Payload CMS 3.0 + Postgres 16, ESLint, Prettier, Husky, Vitest - B2: 9 collections, 6 globals, 12 Page Builder blocks, access control, slugify/revalidate hooks - B3: ezy.com.ua payments, Binotel HMAC webhook, leads API, Telegram bot, Resend email, rate limiting - B4: Tariffs collection with ezy API sync (cron + manual), dynamic pricing source-of-truth - B5: 13 test files covering unit libs and all API routes - B6: Dockerfile multi-stage, docker-compose.prod.yml, nginx.conf SSL, GitHub Actions CI/CD, health endpoint - B7: docs/admin-guide-ua.md (marketer guide), docs/deploy.md (VPS instructions), README quickstart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
70 lines
2.2 KiB
TypeScript
70 lines
2.2 KiB
TypeScript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
|
|
|
// Reset module between tests so the buckets Map is fresh
|
|
let checkRateLimit: (ip: string) => boolean
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
const mod = await import('@/lib/rateLimit')
|
|
checkRateLimit = mod.checkRateLimit
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
describe('checkRateLimit', () => {
|
|
it('allows the first request from a new IP', () => {
|
|
expect(checkRateLimit('1.2.3.4')).toBe(true)
|
|
})
|
|
|
|
it('allows up to 5 requests from the same IP (MAX_TOKENS)', () => {
|
|
const ip = '10.0.0.1'
|
|
// first request seeds bucket with MAX_TOKENS-1 (4) tokens consumed
|
|
expect(checkRateLimit(ip)).toBe(true) // creates bucket, tokens = 4
|
|
expect(checkRateLimit(ip)).toBe(true) // tokens = 3
|
|
expect(checkRateLimit(ip)).toBe(true) // tokens = 2
|
|
expect(checkRateLimit(ip)).toBe(true) // tokens = 1
|
|
expect(checkRateLimit(ip)).toBe(true) // tokens = 0
|
|
})
|
|
|
|
it('blocks the 6th request within the refill window', () => {
|
|
const ip = '10.0.0.2'
|
|
for (let i = 0; i < 5; i++) checkRateLimit(ip)
|
|
expect(checkRateLimit(ip)).toBe(false)
|
|
})
|
|
|
|
it('tracks different IPs independently', () => {
|
|
const ip1 = '192.168.1.1'
|
|
const ip2 = '192.168.1.2'
|
|
for (let i = 0; i < 5; i++) checkRateLimit(ip1)
|
|
// ip1 exhausted, ip2 should still be allowed
|
|
expect(checkRateLimit(ip1)).toBe(false)
|
|
expect(checkRateLimit(ip2)).toBe(true)
|
|
})
|
|
|
|
it('refills tokens after the 15-minute window passes', () => {
|
|
vi.useFakeTimers()
|
|
const ip = '172.16.0.1'
|
|
for (let i = 0; i < 5; i++) checkRateLimit(ip)
|
|
expect(checkRateLimit(ip)).toBe(false)
|
|
|
|
// Advance past REFILL_INTERVAL_MS (15 min)
|
|
vi.advanceTimersByTime(15 * 60 * 1_000 + 1)
|
|
expect(checkRateLimit(ip)).toBe(true)
|
|
})
|
|
|
|
it('does NOT refill before the 15-minute window elapses', () => {
|
|
vi.useFakeTimers()
|
|
const ip = '172.16.0.2'
|
|
for (let i = 0; i < 5; i++) checkRateLimit(ip)
|
|
expect(checkRateLimit(ip)).toBe(false)
|
|
|
|
vi.advanceTimersByTime(14 * 60 * 1_000)
|
|
expect(checkRateLimit(ip)).toBe(false)
|
|
})
|
|
|
|
it('handles "unknown" as a valid IP key', () => {
|
|
expect(checkRateLimit('unknown')).toBe(true)
|
|
})
|
|
})
|