Shumiland/tests/unit/lib/rateLimit.test.ts
Vadym Samoilenko 9b41fa447a
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions
feat: complete backend B1-B7 — Payload CMS, ezy payments, leads, deploy
- 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>
2026-05-09 19:14:54 +01:00

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)
})
})