// Mock express-rate-limit const mockRateLimitInstance = jest.fn(); jest.mock('express-rate-limit', () => { const rateLimitFn = jest.fn((config) => { // Store the config for inspection in tests rateLimitFn.lastConfig = config; return mockRateLimitInstance; }); rateLimitFn.defaultKeyGenerator = jest.fn().mockReturnValue('127.0.0.1'); return rateLimitFn; }); const rateLimit = require('express-rate-limit'); const { placesAutocomplete, placeDetails, geocoding, loginLimiter, registerLimiter, passwordResetLimiter, generalLimiter, burstProtection, createMapsRateLimiter, createUserBasedRateLimiter } = require('../../../middleware/rateLimiter'); describe('Rate Limiter Middleware', () => { let req, res, next; beforeEach(() => { req = { ip: '127.0.0.1', user: null }; res = { status: jest.fn().mockReturnThis(), json: jest.fn(), set: jest.fn() }; next = jest.fn(); jest.clearAllMocks(); }); describe('createMapsRateLimiter', () => { it('should create rate limiter with correct configuration', () => { const windowMs = 60000; const max = 30; const message = 'Test message'; createMapsRateLimiter(windowMs, max, message); expect(rateLimit).toHaveBeenCalledWith({ windowMs, max, message: { error: message, retryAfter: Math.ceil(windowMs / 1000) }, standardHeaders: true, legacyHeaders: false, keyGenerator: expect.any(Function) }); }); describe('keyGenerator', () => { it('should use user ID when user is authenticated', () => { const windowMs = 60000; const max = 30; const message = 'Test message'; createMapsRateLimiter(windowMs, max, message); const config = rateLimit.lastConfig; const reqWithUser = { user: { id: 123 } }; const key = config.keyGenerator(reqWithUser); expect(key).toBe('user:123'); }); it('should use default IP generator when user is not authenticated', () => { const windowMs = 60000; const max = 30; const message = 'Test message'; createMapsRateLimiter(windowMs, max, message); const config = rateLimit.lastConfig; const reqWithoutUser = { user: null }; config.keyGenerator(reqWithoutUser); expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(reqWithoutUser); }); it('should use default IP generator when user has no ID', () => { const windowMs = 60000; const max = 30; const message = 'Test message'; createMapsRateLimiter(windowMs, max, message); const config = rateLimit.lastConfig; const reqWithUserNoId = { user: {} }; config.keyGenerator(reqWithUserNoId); expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(reqWithUserNoId); }); }); it('should calculate retryAfter correctly', () => { const windowMs = 90000; // 90 seconds const max = 10; const message = 'Test message'; createMapsRateLimiter(windowMs, max, message); expect(rateLimit).toHaveBeenCalledWith(expect.objectContaining({ message: { error: message, retryAfter: 90 // Math.ceil(90000 / 1000) } })); }); }); describe('Pre-configured rate limiters', () => { describe('placesAutocomplete', () => { it('should be a function (rate limiter middleware)', () => { expect(typeof placesAutocomplete).toBe('function'); }); }); describe('placeDetails', () => { it('should be a function (rate limiter middleware)', () => { expect(typeof placeDetails).toBe('function'); }); }); describe('geocoding', () => { it('should be a function (rate limiter middleware)', () => { expect(typeof geocoding).toBe('function'); }); }); describe('loginLimiter', () => { it('should be a function (rate limiter middleware)', () => { expect(typeof loginLimiter).toBe('function'); }); }); describe('registerLimiter', () => { it('should be a function (rate limiter middleware)', () => { expect(typeof registerLimiter).toBe('function'); }); }); describe('passwordResetLimiter', () => { it('should be a function (rate limiter middleware)', () => { expect(typeof passwordResetLimiter).toBe('function'); }); }); describe('generalLimiter', () => { it('should be a function (rate limiter middleware)', () => { expect(typeof generalLimiter).toBe('function'); }); }); }); describe('createUserBasedRateLimiter', () => { let userBasedLimiter; const windowMs = 10000; // 10 seconds const max = 5; const message = 'Too many requests'; beforeEach(() => { userBasedLimiter = createUserBasedRateLimiter(windowMs, max, message); }); describe('Key generation', () => { it('should use user ID when user is authenticated', () => { req.user = { id: 123 }; userBasedLimiter(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should use IP when user is not authenticated', () => { req.user = null; rateLimit.defaultKeyGenerator.mockReturnValue('192.168.1.1'); userBasedLimiter(req, res, next); expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(req); expect(next).toHaveBeenCalled(); }); }); describe('Rate limiting logic', () => { it('should allow requests within limit', () => { req.user = { id: 123 }; // Make requests within limit for (let i = 0; i < max; i++) { jest.clearAllMocks(); userBasedLimiter(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); } }); it('should block requests when limit exceeded', () => { req.user = { id: 123 }; // Exhaust the limit for (let i = 0; i < max; i++) { userBasedLimiter(req, res, next); } // Next request should be blocked jest.clearAllMocks(); userBasedLimiter(req, res, next); expect(res.status).toHaveBeenCalledWith(429); expect(res.json).toHaveBeenCalledWith({ error: message, retryAfter: expect.any(Number) }); expect(next).not.toHaveBeenCalled(); }); it('should set correct rate limit headers', () => { req.user = { id: 123 }; userBasedLimiter(req, res, next); expect(res.set).toHaveBeenCalledWith({ 'RateLimit-Limit': max, 'RateLimit-Remaining': max - 1, 'RateLimit-Reset': expect.any(String) }); }); it('should update remaining count correctly', () => { req.user = { id: 123 }; // First request userBasedLimiter(req, res, next); expect(res.set).toHaveBeenCalledWith(expect.objectContaining({ 'RateLimit-Remaining': 4 })); // Second request jest.clearAllMocks(); userBasedLimiter(req, res, next); expect(res.set).toHaveBeenCalledWith(expect.objectContaining({ 'RateLimit-Remaining': 3 })); }); it('should not go below 0 for remaining count', () => { req.user = { id: 123 }; // Exhaust the limit for (let i = 0; i < max; i++) { userBasedLimiter(req, res, next); } // Check that remaining doesn't go negative const lastCall = res.set.mock.calls[res.set.mock.calls.length - 1][0]; expect(lastCall['RateLimit-Remaining']).toBe(0); }); }); describe('Window management', () => { it('should reset count after window expires', () => { req.user = { id: 123 }; const originalDateNow = Date.now; // Mock time to start of window let currentTime = 1000000000; Date.now = jest.fn(() => currentTime); // Exhaust the limit for (let i = 0; i < max; i++) { userBasedLimiter(req, res, next); } // Verify limit is reached jest.clearAllMocks(); userBasedLimiter(req, res, next); expect(res.status).toHaveBeenCalledWith(429); // Move time forward past the window currentTime += windowMs + 1000; jest.clearAllMocks(); // Should allow requests again userBasedLimiter(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); // Restore original Date.now Date.now = originalDateNow; }); it('should clean up old entries from store', () => { const originalDateNow = Date.now; let currentTime = 1000000000; Date.now = jest.fn(() => currentTime); // Create entries for different users req.user = { id: 1 }; userBasedLimiter(req, res, next); req.user = { id: 2 }; userBasedLimiter(req, res, next); // Move time forward to expire first entries currentTime += windowMs + 1000; req.user = { id: 3 }; userBasedLimiter(req, res, next); // The cleanup should have occurred when processing user 3's request // We can't directly test the internal store, but we can verify the behavior expect(next).toHaveBeenCalled(); // Restore original Date.now Date.now = originalDateNow; }); }); describe('Different users/IPs', () => { it('should maintain separate counts for different users', () => { // User 1 makes max requests req.user = { id: 1 }; for (let i = 0; i < max; i++) { userBasedLimiter(req, res, next); } // User 1 should be blocked jest.clearAllMocks(); userBasedLimiter(req, res, next); expect(res.status).toHaveBeenCalledWith(429); // User 2 should still be allowed jest.clearAllMocks(); req.user = { id: 2 }; userBasedLimiter(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should maintain separate counts for different IPs', () => { req.user = null; // IP 1 makes max requests rateLimit.defaultKeyGenerator.mockReturnValue('192.168.1.1'); for (let i = 0; i < max; i++) { userBasedLimiter(req, res, next); } // IP 1 should be blocked jest.clearAllMocks(); userBasedLimiter(req, res, next); expect(res.status).toHaveBeenCalledWith(429); // IP 2 should still be allowed jest.clearAllMocks(); rateLimit.defaultKeyGenerator.mockReturnValue('192.168.1.2'); userBasedLimiter(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); }); describe('Edge cases', () => { it('should handle undefined user gracefully', () => { req.user = undefined; rateLimit.defaultKeyGenerator.mockReturnValue('127.0.0.1'); userBasedLimiter(req, res, next); expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(req); expect(next).toHaveBeenCalled(); }); it('should handle user object without id', () => { req.user = { email: 'test@test.com' }; rateLimit.defaultKeyGenerator.mockReturnValue('127.0.0.1'); userBasedLimiter(req, res, next); expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(req); expect(next).toHaveBeenCalled(); }); it('should set correct reset time in ISO format', () => { req.user = { id: 123 }; userBasedLimiter(req, res, next); const setCall = res.set.mock.calls[0][0]; const resetTime = setCall['RateLimit-Reset']; // Should be a valid ISO string expect(() => new Date(resetTime)).not.toThrow(); expect(resetTime).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); }); it('should calculate retry after correctly when limit exceeded', () => { req.user = { id: 123 }; const originalDateNow = Date.now; const currentTime = 1000000000; Date.now = jest.fn(() => currentTime); // Exhaust the limit for (let i = 0; i < max; i++) { userBasedLimiter(req, res, next); } jest.clearAllMocks(); userBasedLimiter(req, res, next); const jsonCall = res.json.mock.calls[0][0]; expect(jsonCall.retryAfter).toBe(Math.ceil(windowMs / 1000)); // Restore original Date.now Date.now = originalDateNow; }); }); }); describe('burstProtection', () => { it('should be a function', () => { expect(typeof burstProtection).toBe('function'); }); it('should allow requests within burst limit', () => { req.user = { id: 123 }; // Should allow up to 5 requests in 10 seconds for (let i = 0; i < 5; i++) { jest.clearAllMocks(); burstProtection(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); } }); it('should block requests when burst limit exceeded', () => { req.user = { id: 123 }; // Exhaust burst limit for (let i = 0; i < 5; i++) { burstProtection(req, res, next); } // Next request should be blocked jest.clearAllMocks(); burstProtection(req, res, next); expect(res.status).toHaveBeenCalledWith(429); expect(res.json).toHaveBeenCalledWith({ error: 'Too many requests in a short period. Please slow down.', retryAfter: expect.any(Number) }); expect(next).not.toHaveBeenCalled(); }); }); describe('Module exports', () => { it('should export all required rate limiters', () => { const rateLimiterModule = require('../../../middleware/rateLimiter'); expect(rateLimiterModule).toHaveProperty('placesAutocomplete'); expect(rateLimiterModule).toHaveProperty('placeDetails'); expect(rateLimiterModule).toHaveProperty('geocoding'); expect(rateLimiterModule).toHaveProperty('loginLimiter'); expect(rateLimiterModule).toHaveProperty('registerLimiter'); expect(rateLimiterModule).toHaveProperty('passwordResetLimiter'); expect(rateLimiterModule).toHaveProperty('generalLimiter'); expect(rateLimiterModule).toHaveProperty('burstProtection'); expect(rateLimiterModule).toHaveProperty('createMapsRateLimiter'); expect(rateLimiterModule).toHaveProperty('createUserBasedRateLimiter'); }); it('should export functions for utility methods', () => { const rateLimiterModule = require('../../../middleware/rateLimiter'); expect(typeof rateLimiterModule.createMapsRateLimiter).toBe('function'); expect(typeof rateLimiterModule.createUserBasedRateLimiter).toBe('function'); expect(typeof rateLimiterModule.burstProtection).toBe('function'); }); }); });