UNPKG

@ordojs/security

Version:

Security package for OrdoJS with XSS, CSRF, and injection protection

304 lines (243 loc) 9.06 kB
/** * 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'); }); });