unit tests

This commit is contained in:
jackiettran
2025-12-12 16:27:56 -05:00
parent 25bbf5d20b
commit 3f319bfdd0
24 changed files with 4282 additions and 1806 deletions

View File

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