Files
gridpilot.gg/apps/website/lib/rate-limit.test.ts
2026-01-01 22:46:59 +01:00

170 lines
5.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// Mock the config/env module
vi.mock('./config/env', () => ({
assertKvConfiguredInProduction: vi.fn(),
isKvConfigured: vi.fn(() => false),
isProductionEnvironment: vi.fn(() => false),
}));
describe('rate-limit', () => {
const originalEnv = process.env;
const mockNow = 1234567890000;
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
process.env = { ...originalEnv };
vi.spyOn(Date, 'now').mockReturnValue(mockNow);
});
afterEach(() => {
process.env = originalEnv;
vi.restoreAllMocks();
});
describe('checkRateLimit - Development Mode', () => {
it('should allow first request from a new identifier', async () => {
const { checkRateLimit } = await import('./rate-limit');
const result = await checkRateLimit('test-ip-1');
expect(result.allowed).toBe(true);
expect(result.remaining).toBe(4); // 5 total - 1 used
expect(result.resetAt).toBe(mockNow + 60 * 60 * 1000); // 1 hour
});
it('should increment count for subsequent requests within window', async () => {
const { checkRateLimit } = await import('./rate-limit');
await checkRateLimit('test-ip-2');
const result = await checkRateLimit('test-ip-2');
expect(result.allowed).toBe(true);
expect(result.remaining).toBe(3); // 5 total - 2 used
});
it('should block after 5 requests', async () => {
const { checkRateLimit } = await import('./rate-limit');
// Make 5 requests
for (let i = 0; i < 5; i++) {
await checkRateLimit('test-ip-3');
}
const result = await checkRateLimit('test-ip-3');
expect(result.allowed).toBe(false);
expect(result.remaining).toBe(0);
});
it('should reset after window expires', async () => {
const { checkRateLimit } = await import('./rate-limit');
// First request
await checkRateLimit('test-ip-4');
// Simulate time passing beyond window
const futureTime = mockNow + 60 * 60 * 1000 + 1;
vi.spyOn(Date, 'now').mockReturnValue(futureTime);
const result = await checkRateLimit('test-ip-4');
expect(result.allowed).toBe(true);
expect(result.remaining).toBe(4); // Reset to 5 - 1
});
it('should track different identifiers separately', async () => {
const { checkRateLimit } = await import('./rate-limit');
await checkRateLimit('ip-1');
await checkRateLimit('ip-1');
const result = await checkRateLimit('ip-2');
expect(result.allowed).toBe(true);
expect(result.remaining).toBe(4); // ip-2 is at 1, ip-1 is at 2
});
});
describe('getClientIp', () => {
it('should extract IP from x-forwarded-for header', async () => {
const { getClientIp } = await import('./rate-limit');
const mockRequest = {
headers: new Headers({
'x-forwarded-for': '192.168.1.1, 10.0.0.1',
}),
} as Request;
const ip = getClientIp(mockRequest);
expect(ip).toBe('192.168.1.1');
});
it('should extract IP from x-real-ip header', async () => {
const { getClientIp } = await import('./rate-limit');
const mockRequest = {
headers: new Headers({
'x-real-ip': '10.0.0.2',
}),
} as Request;
const ip = getClientIp(mockRequest);
expect(ip).toBe('10.0.0.2');
});
it('should extract IP from cf-connecting-ip header', async () => {
const { getClientIp } = await import('./rate-limit');
const mockRequest = {
headers: new Headers({
'cf-connecting-ip': '1.2.3.4',
}),
} as Request;
const ip = getClientIp(mockRequest);
expect(ip).toBe('1.2.3.4');
});
it('should prioritize x-forwarded-for over other headers', async () => {
const { getClientIp } = await import('./rate-limit');
const mockRequest = {
headers: new Headers({
'x-forwarded-for': '192.168.1.1',
'x-real-ip': '10.0.0.2',
'cf-connecting-ip': '1.2.3.4',
}),
} as Request;
const ip = getClientIp(mockRequest);
expect(ip).toBe('192.168.1.1');
});
it('should return "unknown" when no IP headers present', async () => {
const { getClientIp } = await import('./rate-limit');
const mockRequest = {
headers: new Headers({}),
} as Request;
const ip = getClientIp(mockRequest);
expect(ip).toBe('unknown');
});
it('should handle x-forwarded-for with single IP', async () => {
const { getClientIp } = await import('./rate-limit');
const mockRequest = {
headers: new Headers({
'x-forwarded-for': '203.0.113.1',
}),
} as Request;
const ip = getClientIp(mockRequest);
expect(ip).toBe('203.0.113.1');
});
it('should trim whitespace from IP', async () => {
const { getClientIp } = await import('./rate-limit');
const mockRequest = {
headers: new Headers({
'x-forwarded-for': ' 192.168.1.1 ',
}),
} as Request;
const ip = getClientIp(mockRequest);
expect(ip).toBe('192.168.1.1');
});
});
});