170 lines
5.1 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
}); |