@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
304 lines (243 loc) • 9.06 kB
text/typescript
/**
* Rate Limiter Tests
* Tests for request throttling and rate limiting functionality
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MemoryRateLimitStore, RateLimiter, SlidingWindowRateLimiter } from './rate-limiter';
import { RateLimitOptions } from './types';
describe('RateLimiter', () => {
let rateLimiter: RateLimiter;
let store: MemoryRateLimitStore;
beforeEach(() => {
store = new MemoryRateLimitStore();
const options: RateLimitOptions = {
windowMs: 60000, // 1 minute
maxRequests: 5
};
rateLimiter = new RateLimiter(options, store);
});
afterEach(() => {
store.destroy();
});
describe('Basic Rate Limiting', () => {
it('should allow requests within limit', async () => {
const mockRequest = { ip: '127.0.0.1' };
for (let i = 0; i < 5; i++) {
const result = await rateLimiter.checkLimit(mockRequest);
expect(result.allowed).toBe(true);
expect(result.remaining).toBe(4 - i);
}
});
it('should block requests exceeding limit', async () => {
const mockRequest = { ip: '127.0.0.1' };
// Make 5 allowed requests
for (let i = 0; i < 5; i++) {
await rateLimiter.checkLimit(mockRequest);
}
// 6th request should be blocked
const result = await rateLimiter.checkLimit(mockRequest);
expect(result.allowed).toBe(false);
expect(result.remaining).toBe(0);
expect(result.retryAfter).toBeGreaterThan(0);
});
it('should track different IPs separately', async () => {
const request1 = { ip: '127.0.0.1' };
const request2 = { ip: '192.168.1.1' };
// Make 5 requests from first IP
for (let i = 0; i < 5; i++) {
const result = await rateLimiter.checkLimit(request1);
expect(result.allowed).toBe(true);
}
// First IP should be blocked
const blockedResult = await rateLimiter.checkLimit(request1);
expect(blockedResult.allowed).toBe(false);
// Second IP should still be allowed
const allowedResult = await rateLimiter.checkLimit(request2);
expect(allowedResult.allowed).toBe(true);
});
it('should reset limits after window expires', async () => {
const mockRequest = { ip: '127.0.0.1' };
// Use a short window for testing
const shortWindowLimiter = new RateLimiter({
windowMs: 100, // 100ms
maxRequests: 2
}, store);
// Make 2 requests (at limit)
await shortWindowLimiter.checkLimit(mockRequest);
await shortWindowLimiter.checkLimit(mockRequest);
// 3rd request should be blocked
const blockedResult = await shortWindowLimiter.checkLimit(mockRequest);
expect(blockedResult.allowed).toBe(false);
// Wait for window to expire
await new Promise(resolve => setTimeout(resolve, 150));
// Should be allowed again
const allowedResult = await shortWindowLimiter.checkLimit(mockRequest);
expect(allowedResult.allowed).toBe(true);
});
});
describe('Custom Key Generation', () => {
it('should use custom key generator', async () => {
const customKeyGenerator = (req: any) => `custom:${req.userId}`;
const customLimiter = new RateLimiter({
windowMs: 60000,
maxRequests: 3,
keyGenerator: customKeyGenerator
}, store);
const request1 = { userId: 'user1' };
const request2 = { userId: 'user2' };
// Make 3 requests for user1
for (let i = 0; i < 3; i++) {
const result = await customLimiter.checkLimit(request1);
expect(result.allowed).toBe(true);
}
// 4th request for user1 should be blocked
const blockedResult = await customLimiter.checkLimit(request1);
expect(blockedResult.allowed).toBe(false);
// user2 should still be allowed
const allowedResult = await customLimiter.checkLimit(request2);
expect(allowedResult.allowed).toBe(true);
});
});
describe('Callbacks and Options', () => {
it('should call onLimitReached callback', async () => {
const onLimitReached = vi.fn();
const callbackLimiter = new RateLimiter({
windowMs: 60000,
maxRequests: 1,
onLimitReached
}, store);
const mockRequest = { ip: '127.0.0.1' };
// First request should be allowed
await callbackLimiter.checkLimit(mockRequest);
// Second request should trigger callback
await callbackLimiter.checkLimit(mockRequest);
expect(onLimitReached).toHaveBeenCalledWith(mockRequest);
});
});
describe('Middleware Integration', () => {
it('should create Express-compatible middleware', async () => {
const middleware = rateLimiter.middleware();
const mockReq = { ip: '127.0.0.1' };
const mockRes = {
setHeader: vi.fn(),
status: vi.fn().mockReturnThis(),
json: vi.fn()
};
const mockNext = vi.fn();
// Should call next() for allowed requests
await middleware(mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
expect(mockRes.setHeader).toHaveBeenCalledWith('X-RateLimit-Limit', 5);
});
it('should return 429 for blocked requests', async () => {
const middleware = rateLimiter.middleware();
const mockReq = { ip: '127.0.0.1' };
const mockRes = {
setHeader: vi.fn(),
status: vi.fn().mockReturnThis(),
json: vi.fn()
};
const mockNext = vi.fn();
// Make requests to exceed limit
for (let i = 0; i < 6; i++) {
await middleware(mockReq, mockRes, mockNext);
}
// Last call should have returned 429
expect(mockRes.status).toHaveBeenCalledWith(429);
expect(mockRes.json).toHaveBeenCalledWith({
error: 'Too Many Requests',
message: 'Rate limit exceeded',
retryAfter: expect.any(Number)
});
});
});
describe('Reset Functionality', () => {
it('should reset limits for specific requests', async () => {
const mockRequest = { ip: '127.0.0.1' };
// Make requests to reach limit
for (let i = 0; i < 5; i++) {
await rateLimiter.checkLimit(mockRequest);
}
// Should be blocked
const blockedResult = await rateLimiter.checkLimit(mockRequest);
expect(blockedResult.allowed).toBe(false);
// Reset the limit
await rateLimiter.resetLimit(mockRequest);
// Should be allowed again
const allowedResult = await rateLimiter.checkLimit(mockRequest);
expect(allowedResult.allowed).toBe(true);
});
});
});
describe('MemoryRateLimitStore', () => {
let store: MemoryRateLimitStore;
beforeEach(() => {
store = new MemoryRateLimitStore();
});
afterEach(() => {
store.destroy();
});
describe('Basic Operations', () => {
it('should store and retrieve values', async () => {
await store.set('test-key', 5, 1000);
const value = await store.get('test-key');
expect(value).toBe(5);
});
it('should return null for non-existent keys', async () => {
const value = await store.get('non-existent');
expect(value).toBeNull();
});
it('should increment values', async () => {
const count1 = await store.increment('counter', 1000);
const count2 = await store.increment('counter', 1000);
expect(count1).toBe(1);
expect(count2).toBe(2);
});
it('should reset values', async () => {
await store.set('test-key', 10, 1000);
await store.reset('test-key');
const value = await store.get('test-key');
expect(value).toBeNull();
});
it('should expire values after TTL', async () => {
await store.set('expire-key', 1, 50); // 50ms TTL
// Should exist immediately
let value = await store.get('expire-key');
expect(value).toBe(1);
// Wait for expiration
await new Promise(resolve => setTimeout(resolve, 100));
// Should be expired
value = await store.get('expire-key');
expect(value).toBeNull();
});
});
});
describe('SlidingWindowRateLimiter', () => {
let slidingLimiter: SlidingWindowRateLimiter;
let store: MemoryRateLimitStore;
beforeEach(() => {
store = new MemoryRateLimitStore();
slidingLimiter = new SlidingWindowRateLimiter({
windowMs: 60000,
maxRequests: 5
}, store);
});
afterEach(() => {
store.destroy();
});
it('should implement sliding window logic', async () => {
const mockRequest = { ip: '127.0.0.1' };
// Make requests within limit
for (let i = 0; i < 5; i++) {
const result = await slidingLimiter.checkLimit(mockRequest);
expect(result.allowed).toBe(true);
}
// Should be blocked after limit
const blockedResult = await slidingLimiter.checkLimit(mockRequest);
expect(blockedResult.allowed).toBe(false);
});
it('should create middleware', async () => {
const middleware = slidingLimiter.middleware();
expect(typeof middleware).toBe('function');
});
});