import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { checkLeaveRoomRate, checkChatMessageRate, CHAT_MESSAGE_RATE_LIMIT, CHAT_MESSAGE_RATE_WINDOW_MS, chatMessageCounts, LEAVE_ROOM_RATE_LIMIT, LEAVE_ROOM_RATE_WINDOW_MS, rateLimitDenied, leaveRoomCounts, clearRateLimitMaps } from './rate-limiter.js'; describe('LEAVE_ROOM Rate Limiter', () => { const testSocketId = 'test-socket-123'; beforeEach(() => { clearRateLimitMaps(); rateLimitDenied.leaveRoom = 0; }); afterEach(() => { clearRateLimitMaps(); }); it('should allow LEAVE_ROOM within limit', () => { // Test within the rate limit for (let i = 0; i < LEAVE_ROOM_RATE_LIMIT; i++) { const result = checkLeaveRoomRate(testSocketId); expect(result).toBe(true); } expect(rateLimitDenied.leaveRoom).toBe(0); }); it('should block LEAVE_ROOM when exceeding limit', () => { // Fill up to the limit for (let i = 0; i < LEAVE_ROOM_RATE_LIMIT; i++) { checkLeaveRoomRate(testSocketId); } // Next request should be blocked const result = checkLeaveRoomRate(testSocketId); expect(result).toBe(false); expect(rateLimitDenied.leaveRoom).toBe(1); }); it('should reset count after window expires', () => { // Fill up to the limit for (let i = 0; i < LEAVE_ROOM_RATE_LIMIT; i++) { checkLeaveRoomRate(testSocketId); } // Verify we're at the limit let result = checkLeaveRoomRate(testSocketId); expect(result).toBe(false); // Fast-forward time beyond the rate limit window const entry = leaveRoomCounts.get(testSocketId); entry.resetTime = Date.now() - LEAVE_ROOM_RATE_WINDOW_MS - 1000; leaveRoomCounts.set(testSocketId, entry); // Next request should be allowed again result = checkLeaveRoomRate(testSocketId); expect(result).toBe(true); }); it('should handle multiple sockets independently', () => { const socketId2 = 'test-socket-456'; // Fill up first socket for (let i = 0; i < LEAVE_ROOM_RATE_LIMIT; i++) { checkLeaveRoomRate(testSocketId); } // Second socket should still be allowed const result = checkLeaveRoomRate(socketId2); expect(result).toBe(true); // First socket should be blocked const result2 = checkLeaveRoomRate(testSocketId); expect(result2).toBe(false); }); it('should increment rateLimitDenied counter on block', () => { for (let i = 0; i < LEAVE_ROOM_RATE_LIMIT; i++) { checkLeaveRoomRate(testSocketId); } checkLeaveRoomRate(testSocketId); expect(rateLimitDenied.leaveRoom).toBe(1); checkLeaveRoomRate(testSocketId); expect(rateLimitDenied.leaveRoom).toBe(2); }); it('should be cleared by the shared reset helper', () => { checkLeaveRoomRate(testSocketId); expect(leaveRoomCounts.size).toBe(1); clearRateLimitMaps(); expect(leaveRoomCounts.size).toBe(0); }); }); describe('CHAT_MESSAGE Rate Limiter', () => { const socketId = 'chat-socket'; beforeEach(() => { clearRateLimitMaps(); rateLimitDenied.chatMessages = 0; }); afterEach(() => clearRateLimitMaps()); it('allows ten messages per ten-second window and blocks the next', () => { for (let i = 0; i < CHAT_MESSAGE_RATE_LIMIT; i++) { expect(checkChatMessageRate(socketId)).toBe(true); } expect(checkChatMessageRate(socketId)).toBe(false); expect(rateLimitDenied.chatMessages).toBe(1); }); it('resets after the window expires', () => { for (let i = 0; i <= CHAT_MESSAGE_RATE_LIMIT; i++) checkChatMessageRate(socketId); const entry = chatMessageCounts.get(socketId); entry.resetTime = Date.now() - CHAT_MESSAGE_RATE_WINDOW_MS - 1; expect(checkChatMessageRate(socketId)).toBe(true); }); }); describe('Rate Limit Constants', () => { it('should have correct rate limit values', () => { expect(LEAVE_ROOM_RATE_LIMIT).toBe(10); expect(LEAVE_ROOM_RATE_WINDOW_MS).toBe(60000); // 1 minute }); });