396 lines
11 KiB
JavaScript
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',
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|