unit tests
This commit is contained in:
@@ -1,156 +1,395 @@
|
||||
const { Server } = require('socket.io');
|
||||
const Client = require('socket.io-client');
|
||||
const http = require('http');
|
||||
const { initializeMessageSocket, emitNewMessage, emitMessageRead } = require('../../../sockets/messageSocket');
|
||||
// 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 io, serverSocket, clientSocket;
|
||||
let httpServer;
|
||||
let messageSocket;
|
||||
let mockIo;
|
||||
let mockSocket;
|
||||
let connectionHandler;
|
||||
|
||||
beforeAll((done) => {
|
||||
// Create HTTP server
|
||||
httpServer = http.createServer();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllTimers();
|
||||
|
||||
// Create Socket.io server
|
||||
io = new Server(httpServer);
|
||||
// Reset the module to clear the typingStatus Map
|
||||
jest.resetModules();
|
||||
messageSocket = require('../../../sockets/messageSocket');
|
||||
|
||||
httpServer.listen(() => {
|
||||
const port = httpServer.address().port;
|
||||
|
||||
// Initialize message socket handlers
|
||||
initializeMessageSocket(io);
|
||||
|
||||
// Create client socket
|
||||
clientSocket = new Client(`http://localhost:${port}`);
|
||||
|
||||
// Mock authentication by setting userId
|
||||
io.use((socket, next) => {
|
||||
socket.userId = 'test-user-123';
|
||||
socket.user = {
|
||||
id: 'test-user-123',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User'
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
// Wait for connection
|
||||
io.on('connection', (socket) => {
|
||||
serverSocket = socket;
|
||||
});
|
||||
|
||||
clientSocket.on('connect', done);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
io.close();
|
||||
clientSocket.close();
|
||||
httpServer.close();
|
||||
});
|
||||
|
||||
test('should connect successfully', () => {
|
||||
expect(clientSocket.connected).toBe(true);
|
||||
});
|
||||
|
||||
test('should join conversation room', (done) => {
|
||||
const otherUserId = 'other-user-456';
|
||||
|
||||
clientSocket.on('conversation_joined', (data) => {
|
||||
expect(data.otherUserId).toBe(otherUserId);
|
||||
expect(data.conversationRoom).toContain('conv_');
|
||||
done();
|
||||
});
|
||||
|
||||
clientSocket.emit('join_conversation', { otherUserId });
|
||||
});
|
||||
|
||||
test('should emit typing start event', (done) => {
|
||||
const receiverId = 'receiver-789';
|
||||
|
||||
serverSocket.on('typing_start', (data) => {
|
||||
expect(data.receiverId).toBe(receiverId);
|
||||
done();
|
||||
});
|
||||
|
||||
clientSocket.emit('typing_start', { receiverId });
|
||||
});
|
||||
|
||||
test('should emit typing stop event', (done) => {
|
||||
const receiverId = 'receiver-789';
|
||||
|
||||
serverSocket.on('typing_stop', (data) => {
|
||||
expect(data.receiverId).toBe(receiverId);
|
||||
done();
|
||||
});
|
||||
|
||||
clientSocket.emit('typing_stop', { receiverId });
|
||||
});
|
||||
|
||||
test('should emit new message to receiver', (done) => {
|
||||
const receiverId = 'receiver-123';
|
||||
const messageData = {
|
||||
id: 'message-456',
|
||||
senderId: 'sender-789',
|
||||
receiverId: receiverId,
|
||||
subject: 'Test Subject',
|
||||
content: 'Test message content',
|
||||
createdAt: new Date().toISOString()
|
||||
// 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 a second client to receive the message
|
||||
const port = httpServer.address().port;
|
||||
const receiverClient = new Client(`http://localhost:${port}`);
|
||||
|
||||
receiverClient.on('connect', () => {
|
||||
receiverClient.on('new_message', (message) => {
|
||||
expect(message.id).toBe(messageData.id);
|
||||
expect(message.content).toBe(messageData.content);
|
||||
receiverClient.close();
|
||||
done();
|
||||
});
|
||||
|
||||
// Emit the message
|
||||
emitNewMessage(io, receiverId, messageData);
|
||||
});
|
||||
});
|
||||
|
||||
test('should emit message read status to sender', (done) => {
|
||||
const senderId = 'sender-123';
|
||||
const readData = {
|
||||
messageId: 'message-789',
|
||||
readAt: new Date().toISOString(),
|
||||
readBy: 'reader-456'
|
||||
// Create mock io
|
||||
mockIo = {
|
||||
on: jest.fn((event, handler) => {
|
||||
if (event === 'connection') {
|
||||
connectionHandler = handler;
|
||||
}
|
||||
}),
|
||||
to: jest.fn().mockReturnThis(),
|
||||
emit: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Create a sender client to receive the read receipt
|
||||
const port = httpServer.address().port;
|
||||
const senderClient = new Client(`http://localhost:${port}`);
|
||||
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');
|
||||
|
||||
senderClient.on('connect', () => {
|
||||
senderClient.on('message_read', (data) => {
|
||||
expect(data.messageId).toBe(readData.messageId);
|
||||
expect(data.readBy).toBe(readData.readBy);
|
||||
senderClient.close();
|
||||
done();
|
||||
});
|
||||
expect(room1).toBe(room2);
|
||||
expect(room1).toMatch(/^conv_/);
|
||||
});
|
||||
|
||||
// Emit the read status
|
||||
emitMessageRead(io, senderId, readData);
|
||||
it('should sort user IDs alphabetically', () => {
|
||||
const room = messageSocket.getConversationRoom('zebra', 'alpha');
|
||||
expect(room).toBe('conv_alpha_zebra');
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle disconnection gracefully', (done) => {
|
||||
const testClient = new Client(`http://localhost:${httpServer.address().port}`);
|
||||
describe('getUserRoom', () => {
|
||||
it('should generate user room name', () => {
|
||||
const room = messageSocket.getUserRoom('user-123');
|
||||
expect(room).toBe('user_user-123');
|
||||
});
|
||||
});
|
||||
|
||||
testClient.on('connect', () => {
|
||||
testClient.on('disconnect', (reason) => {
|
||||
expect(reason).toBeTruthy();
|
||||
done();
|
||||
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);
|
||||
});
|
||||
|
||||
testClient.disconnect();
|
||||
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',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user