import assert from "assert";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { RateLimiter } from "./index";
describe("RateLimiter", () => {
let rateLimiter: RateLimiter;
beforeEach(() => {
rateLimiter = new RateLimiter();
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
rateLimiter.clear();
});
describe("checkRateLimit", () => {
it("should allow requests within rate limit", () => {
const config = {
name: "test",
windowMs: 60000,
maxRequests: 3,
};
const result1 = rateLimiter.checkRateLimit(config, "user1");
const result2 = rateLimiter.checkRateLimit(config, "user1");
const result3 = rateLimiter.checkRateLimit(config, "user1");
expect(result1.allowed).toBe(true);
expect(result2.allowed).toBe(true);
expect(result3.allowed).toBe(true);
});
it("should block requests exceeding rate limit", () => {
const config = {
name: "test",
windowMs: 60000,
maxRequests: 2,
};
const result1 = rateLimiter.checkRateLimit(config, "user1");
const result2 = rateLimiter.checkRateLimit(config, "user1");
const result3 = rateLimiter.checkRateLimit(config, "user1");
expect(result1.allowed).toBe(true);
expect(result2.allowed).toBe(true);
expect(result3.allowed).toBe(false);
assert(!result3.allowed);
expect(result3.resetInSeconds).toBeGreaterThan(0);
});
it("should reset after window expires", () => {
const config = {
name: "test",
windowMs: 60000,
maxRequests: 2,
};
// First two requests allowed
const result1 = rateLimiter.checkRateLimit(config, "user1");
const result2 = rateLimiter.checkRateLimit(config, "user1");
expect(result1.allowed).toBe(true);
expect(result2.allowed).toBe(true);
// Third request blocked
const result3 = rateLimiter.checkRateLimit(config, "user1");
expect(result3.allowed).toBe(false);
// Advance time past the window
vi.advanceTimersByTime(61000);
// Should allow request after window reset
const result4 = rateLimiter.checkRateLimit(config, "user1");
expect(result4.allowed).toBe(true);
});
it("should isolate rate limits by identifier", () => {
const config = {
name: "test",
windowMs: 60000,
maxRequests: 1,
};
const result1 = rateLimiter.checkRateLimit(config, "user1");
const result2 = rateLimiter.checkRateLimit(config, "user2");
expect(result1.allowed).toBe(true);
expect(result2.allowed).toBe(true);
});
it("should isolate rate limits by key", () => {
const config = {
name: "test",
windowMs: 60000,
maxRequests: 1,
};
const result1 = rateLimiter.checkRateLimit(config, "user1:/api/v1");
const result2 = rateLimiter.checkRateLimit(config, "user1:/api/v2");
expect(result1.allowed).toBe(true);
expect(result2.allowed).toBe(true);
});
it("should isolate rate limits by config name", () => {
const config1 = {
name: "api",
windowMs: 60000,
maxRequests: 1,
};
const config2 = {
name: "auth",
windowMs: 60000,
maxRequests: 1,
};
const result1 = rateLimiter.checkRateLimit(config1, "user1");
const result2 = rateLimiter.checkRateLimit(config2, "user1");
expect(result1.allowed).toBe(true);
expect(result2.allowed).toBe(true);
});
it("should calculate correct resetInSeconds", () => {
const config = {
name: "test",
windowMs: 60000,
maxRequests: 1,
};
// First request allowed
rateLimiter.checkRateLimit(config, "user1");
// Advance time by 30 seconds
vi.advanceTimersByTime(30000);
// Second request blocked
const result = rateLimiter.checkRateLimit(config, "user1");
expect(result.allowed).toBe(false);
// Should have ~30 seconds remaining
assert(!result.allowed);
expect(result.resetInSeconds).toBeGreaterThan(29);
expect(result.resetInSeconds).toBeLessThanOrEqual(30);
});
});
describe("reset", () => {
it("should reset rate limit for specific identifier", () => {
const config = {
name: "test",
windowMs: 60000,
maxRequests: 1,
};
// Use up the limit
rateLimiter.checkRateLimit(config, "user1");
const result1 = rateLimiter.checkRateLimit(config, "user1");
expect(result1.allowed).toBe(false);
// Reset the limit
rateLimiter.reset(config, "user1");
// Should allow request again
const result2 = rateLimiter.checkRateLimit(config, "user1");
expect(result2.allowed).toBe(true);
});
it("should reset rate limit for specific key", () => {
const config = {
name: "test",
windowMs: 60000,
maxRequests: 1,
};
// Use up the limit for key1
rateLimiter.checkRateLimit(config, "user1:/path1");
const result1 = rateLimiter.checkRateLimit(config, "user1:/path1");
expect(result1.allowed).toBe(false);
// Reset only key1
rateLimiter.reset(config, "user1:/path1");
// key1 should be allowed
const result2 = rateLimiter.checkRateLimit(config, "user1:/path1");
expect(result2.allowed).toBe(true);
});
it("should not affect other identifiers", () => {
const config = {
name: "test",
windowMs: 60000,
maxRequests: 1,
};
// Use up limits for both users
rateLimiter.checkRateLimit(config, "user1");
rateLimiter.checkRateLimit(config, "user2");
// Reset only user1
rateLimiter.reset(config, "user1");
const result1 = rateLimiter.checkRateLimit(config, "user1");
const result2 = rateLimiter.checkRateLimit(config, "user2");
expect(result1.allowed).toBe(true);
expect(result2.allowed).toBe(false);
});
});
describe("clear", () => {
it("should clear all rate limits", () => {
const config = {
name: "test",
windowMs: 60000,
maxRequests: 1,
};
// Use up limits for multiple users
rateLimiter.checkRateLimit(config, "user1");
rateLimiter.checkRateLimit(config, "user2");
// Clear all limits
rateLimiter.clear();
// All should be allowed
const result1 = rateLimiter.checkRateLimit(config, "user1");
const result2 = rateLimiter.checkRateLimit(config, "user2");
expect(result1.allowed).toBe(true);
expect(result2.allowed).toBe(true);
});
});
describe("cleanup", () => {
it("should cleanup expired entries", () => {
const config = {
name: "test",
windowMs: 60000,
maxRequests: 1,
};
// Create an entry
rateLimiter.checkRateLimit(config, "user1");
// Advance time past window + cleanup interval
vi.advanceTimersByTime(61000 + 60000);
// Entry should be cleaned up and new request allowed
const result = rateLimiter.checkRateLimit(config, "user1");
expect(result.allowed).toBe(true);
});
});
});