Files
rentall-app/backend/tests/unit/sockets/messageSocket.test.js
jackiettran 3f319bfdd0 unit tests
2025-12-12 16:27:56 -05:00

396 lines
11 KiB
JavaScript

// 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',
})
);
});
});
});