// Mock logger before requiring modules jest.mock('../../../utils/logger', () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), })); // Mock timers to prevent the cleanup interval from keeping Jest running jest.useFakeTimers(); describe('Message Socket', () => { let messageSocket; let mockIo; let mockSocket; let connectionHandler; beforeEach(() => { jest.clearAllMocks(); jest.clearAllTimers(); // Reset the module to clear the typingStatus Map jest.resetModules(); messageSocket = require('../../../sockets/messageSocket'); // Create mock socket mockSocket = { id: 'socket-123', userId: 'user-1', user: { id: 'user-1', email: 'user1@example.com', firstName: 'John', }, join: jest.fn(), leave: jest.fn(), emit: jest.fn(), on: jest.fn(), }; // Create mock io mockIo = { on: jest.fn((event, handler) => { if (event === 'connection') { connectionHandler = handler; } }), to: jest.fn().mockReturnThis(), emit: jest.fn(), }; }); describe('getConversationRoom', () => { it('should generate consistent room name regardless of user order', () => { const room1 = messageSocket.getConversationRoom('user-a', 'user-b'); const room2 = messageSocket.getConversationRoom('user-b', 'user-a'); expect(room1).toBe(room2); expect(room1).toMatch(/^conv_/); }); it('should sort user IDs alphabetically', () => { const room = messageSocket.getConversationRoom('zebra', 'alpha'); expect(room).toBe('conv_alpha_zebra'); }); }); describe('getUserRoom', () => { it('should generate user room name', () => { const room = messageSocket.getUserRoom('user-123'); expect(room).toBe('user_user-123'); }); }); describe('initializeMessageSocket', () => { it('should register connection handler', () => { messageSocket.initializeMessageSocket(mockIo); expect(mockIo.on).toHaveBeenCalledWith('connection', expect.any(Function)); }); describe('connection handler', () => { beforeEach(() => { messageSocket.initializeMessageSocket(mockIo); // Trigger the connection handler connectionHandler(mockSocket); }); it('should join user personal room on connection', () => { expect(mockSocket.join).toHaveBeenCalledWith('user_user-1'); }); it('should register event handlers', () => { expect(mockSocket.on).toHaveBeenCalledWith('join_conversation', expect.any(Function)); expect(mockSocket.on).toHaveBeenCalledWith('leave_conversation', expect.any(Function)); expect(mockSocket.on).toHaveBeenCalledWith('typing_start', expect.any(Function)); expect(mockSocket.on).toHaveBeenCalledWith('typing_stop', expect.any(Function)); expect(mockSocket.on).toHaveBeenCalledWith('mark_message_read', expect.any(Function)); expect(mockSocket.on).toHaveBeenCalledWith('disconnect', expect.any(Function)); expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function)); }); }); describe('join_conversation event', () => { let joinConversationHandler; beforeEach(() => { messageSocket.initializeMessageSocket(mockIo); connectionHandler(mockSocket); // Get the join_conversation handler joinConversationHandler = mockSocket.on.mock.calls.find( (call) => call[0] === 'join_conversation' )[1]; }); it('should join conversation room and emit confirmation', () => { joinConversationHandler({ otherUserId: 'user-2' }); expect(mockSocket.join).toHaveBeenCalledWith('conv_user-1_user-2'); expect(mockSocket.emit).toHaveBeenCalledWith('conversation_joined', { conversationRoom: 'conv_user-1_user-2', otherUserId: 'user-2', }); }); it('should not join if otherUserId is missing', () => { mockSocket.join.mockClear(); mockSocket.emit.mockClear(); joinConversationHandler({}); expect(mockSocket.join).not.toHaveBeenCalled(); expect(mockSocket.emit).not.toHaveBeenCalled(); }); }); describe('leave_conversation event', () => { let leaveConversationHandler; beforeEach(() => { messageSocket.initializeMessageSocket(mockIo); connectionHandler(mockSocket); leaveConversationHandler = mockSocket.on.mock.calls.find( (call) => call[0] === 'leave_conversation' )[1]; }); it('should leave conversation room', () => { leaveConversationHandler({ otherUserId: 'user-2' }); expect(mockSocket.leave).toHaveBeenCalledWith('conv_user-1_user-2'); }); it('should not leave if otherUserId is missing', () => { leaveConversationHandler({}); expect(mockSocket.leave).not.toHaveBeenCalled(); }); }); describe('typing_start event', () => { let typingStartHandler; beforeEach(() => { messageSocket.initializeMessageSocket(mockIo); connectionHandler(mockSocket); typingStartHandler = mockSocket.on.mock.calls.find( (call) => call[0] === 'typing_start' )[1]; }); it('should emit typing indicator to receiver', () => { typingStartHandler({ receiverId: 'user-2' }); expect(mockIo.to).toHaveBeenCalledWith('user_user-2'); expect(mockIo.emit).toHaveBeenCalledWith('user_typing', { userId: 'user-1', firstName: 'John', isTyping: true, }); }); it('should throttle rapid typing events', () => { typingStartHandler({ receiverId: 'user-2' }); mockIo.emit.mockClear(); // Should be throttled typingStartHandler({ receiverId: 'user-2' }); expect(mockIo.emit).not.toHaveBeenCalled(); // Advance time past throttle jest.advanceTimersByTime(1001); typingStartHandler({ receiverId: 'user-2' }); expect(mockIo.emit).toHaveBeenCalled(); }); it('should not emit if receiverId is missing', () => { typingStartHandler({}); expect(mockIo.to).not.toHaveBeenCalled(); }); }); describe('typing_stop event', () => { let typingStopHandler; beforeEach(() => { messageSocket.initializeMessageSocket(mockIo); connectionHandler(mockSocket); typingStopHandler = mockSocket.on.mock.calls.find( (call) => call[0] === 'typing_stop' )[1]; }); it('should emit typing stop to receiver', () => { typingStopHandler({ receiverId: 'user-2' }); expect(mockIo.to).toHaveBeenCalledWith('user_user-2'); expect(mockIo.emit).toHaveBeenCalledWith('user_typing', { userId: 'user-1', firstName: 'John', isTyping: false, }); }); it('should not emit if receiverId is missing', () => { typingStopHandler({}); expect(mockIo.to).not.toHaveBeenCalled(); }); }); describe('mark_message_read event', () => { let markMessageReadHandler; beforeEach(() => { messageSocket.initializeMessageSocket(mockIo); connectionHandler(mockSocket); markMessageReadHandler = mockSocket.on.mock.calls.find( (call) => call[0] === 'mark_message_read' )[1]; }); it('should emit message_read to sender room', () => { const data = { messageId: 'msg-123', senderId: 'user-2' }; markMessageReadHandler(data); expect(mockIo.to).toHaveBeenCalledWith('user_user-2'); expect(mockIo.emit).toHaveBeenCalledWith('message_read', { messageId: 'msg-123', readAt: expect.any(String), readBy: 'user-1', }); }); it('should not emit if messageId is missing', () => { markMessageReadHandler({ senderId: 'user-2' }); expect(mockIo.to).not.toHaveBeenCalled(); }); it('should not emit if senderId is missing', () => { markMessageReadHandler({ messageId: 'msg-123' }); expect(mockIo.to).not.toHaveBeenCalled(); }); }); describe('disconnect event', () => { let disconnectHandler; beforeEach(() => { messageSocket.initializeMessageSocket(mockIo); connectionHandler(mockSocket); disconnectHandler = mockSocket.on.mock.calls.find( (call) => call[0] === 'disconnect' )[1]; }); it('should handle disconnect', () => { const logger = require('../../../utils/logger'); disconnectHandler('client disconnect'); expect(logger.info).toHaveBeenCalledWith( 'User disconnected from messaging', expect.objectContaining({ socketId: 'socket-123', userId: 'user-1', reason: 'client disconnect', }) ); }); }); describe('error event', () => { let errorHandler; beforeEach(() => { messageSocket.initializeMessageSocket(mockIo); connectionHandler(mockSocket); errorHandler = mockSocket.on.mock.calls.find( (call) => call[0] === 'error' )[1]; }); it('should log socket errors', () => { const logger = require('../../../utils/logger'); const error = new Error('Socket error'); errorHandler(error); expect(logger.error).toHaveBeenCalledWith( 'Socket error', expect.objectContaining({ socketId: 'socket-123', userId: 'user-1', error: 'Socket error', }) ); }); }); }); describe('emitNewMessage', () => { it('should emit new_message to receiver room', () => { const messageData = { id: 'msg-456', senderId: 'user-1', content: 'Hello!', }; messageSocket.emitNewMessage(mockIo, 'user-2', messageData); expect(mockIo.to).toHaveBeenCalledWith('user_user-2'); expect(mockIo.emit).toHaveBeenCalledWith('new_message', messageData); }); it('should handle errors gracefully', () => { const logger = require('../../../utils/logger'); mockIo.to.mockImplementation(() => { throw new Error('Emit failed'); }); const messageData = { id: 'msg-456', senderId: 'user-1' }; messageSocket.emitNewMessage(mockIo, 'user-2', messageData); expect(logger.error).toHaveBeenCalledWith( 'Error emitting new message', expect.objectContaining({ error: 'Emit failed', }) ); }); }); describe('emitMessageRead', () => { it('should emit message_read to sender room', () => { const readData = { messageId: 'msg-789', readAt: '2024-01-01T00:00:00Z', readBy: 'user-2', }; messageSocket.emitMessageRead(mockIo, 'user-1', readData); expect(mockIo.to).toHaveBeenCalledWith('user_user-1'); expect(mockIo.emit).toHaveBeenCalledWith('message_read', readData); }); it('should handle errors gracefully', () => { const logger = require('../../../utils/logger'); mockIo.to.mockImplementation(() => { throw new Error('Emit failed'); }); const readData = { messageId: 'msg-789' }; messageSocket.emitMessageRead(mockIo, 'user-1', readData); expect(logger.error).toHaveBeenCalledWith( 'Error emitting message read status', expect.objectContaining({ error: 'Emit failed', }) ); }); }); });