Files
rentall-app/backend/tests/unit/routes/messages.test.js
2025-09-19 19:46:41 -04:00

657 lines
17 KiB
JavaScript

const request = require('supertest');
const express = require('express');
const messagesRouter = require('../../../routes/messages');
// Mock all dependencies
jest.mock('../../../models', () => ({
Message: {
findAll: jest.fn(),
findOne: jest.fn(),
findByPk: jest.fn(),
create: jest.fn(),
count: jest.fn(),
},
User: {
findByPk: jest.fn(),
},
}));
jest.mock('../../../middleware/auth', () => ({
authenticateToken: jest.fn((req, res, next) => {
req.user = { id: 1 };
next();
}),
}));
jest.mock('sequelize', () => ({
Op: {
or: Symbol('or'),
},
}));
const { Message, User } = require('../../../models');
// Create express app with the router
const app = express();
app.use(express.json());
app.use('/messages', messagesRouter);
// Mock models
const mockMessageFindAll = Message.findAll;
const mockMessageFindOne = Message.findOne;
const mockMessageFindByPk = Message.findByPk;
const mockMessageCreate = Message.create;
const mockMessageCount = Message.count;
const mockUserFindByPk = User.findByPk;
describe('Messages Routes', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('GET /', () => {
it('should get inbox messages for authenticated user', async () => {
const mockMessages = [
{
id: 1,
senderId: 2,
receiverId: 1,
subject: 'Test Message',
content: 'Hello there!',
isRead: false,
createdAt: '2024-01-15T10:00:00.000Z',
sender: {
id: 2,
firstName: 'Jane',
lastName: 'Smith',
profileImage: 'jane.jpg'
}
},
{
id: 2,
senderId: 3,
receiverId: 1,
subject: 'Another Message',
content: 'Hi!',
isRead: true,
createdAt: '2024-01-14T10:00:00.000Z',
sender: {
id: 3,
firstName: 'Bob',
lastName: 'Johnson',
profileImage: null
}
}
];
mockMessageFindAll.mockResolvedValue(mockMessages);
const response = await request(app)
.get('/messages');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockMessages);
expect(mockMessageFindAll).toHaveBeenCalledWith({
where: { receiverId: 1 },
include: [
{
model: User,
as: 'sender',
attributes: ['id', 'firstName', 'lastName', 'profileImage']
}
],
order: [['createdAt', 'DESC']]
});
});
it('should handle database errors', async () => {
mockMessageFindAll.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.get('/messages');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Database error' });
});
});
describe('GET /sent', () => {
it('should get sent messages for authenticated user', async () => {
const mockSentMessages = [
{
id: 3,
senderId: 1,
receiverId: 2,
subject: 'My Message',
content: 'Hello Jane!',
isRead: false,
createdAt: '2024-01-15T12:00:00.000Z',
receiver: {
id: 2,
firstName: 'Jane',
lastName: 'Smith',
profileImage: 'jane.jpg'
}
}
];
mockMessageFindAll.mockResolvedValue(mockSentMessages);
const response = await request(app)
.get('/messages/sent');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockSentMessages);
expect(mockMessageFindAll).toHaveBeenCalledWith({
where: { senderId: 1 },
include: [
{
model: User,
as: 'receiver',
attributes: ['id', 'firstName', 'lastName', 'profileImage']
}
],
order: [['createdAt', 'DESC']]
});
});
it('should handle database errors', async () => {
mockMessageFindAll.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.get('/messages/sent');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Database error' });
});
});
describe('GET /:id', () => {
const mockMessage = {
id: 1,
senderId: 2,
receiverId: 1,
subject: 'Test Message',
content: 'Hello there!',
isRead: false,
createdAt: '2024-01-15T10:00:00.000Z',
sender: {
id: 2,
firstName: 'Jane',
lastName: 'Smith',
profileImage: 'jane.jpg'
},
receiver: {
id: 1,
firstName: 'John',
lastName: 'Doe',
profileImage: 'john.jpg'
},
replies: [
{
id: 4,
senderId: 1,
content: 'Reply message',
sender: {
id: 1,
firstName: 'John',
lastName: 'Doe',
profileImage: 'john.jpg'
}
}
],
update: jest.fn()
};
beforeEach(() => {
mockMessageFindOne.mockResolvedValue(mockMessage);
});
it('should get message with replies for receiver', async () => {
mockMessage.update.mockResolvedValue();
const response = await request(app)
.get('/messages/1');
expect(response.status).toBe(200);
expect(response.body).toEqual({
id: 1,
senderId: 2,
receiverId: 1,
subject: 'Test Message',
content: 'Hello there!',
isRead: false,
createdAt: '2024-01-15T10:00:00.000Z',
sender: {
id: 2,
firstName: 'Jane',
lastName: 'Smith',
profileImage: 'jane.jpg'
},
receiver: {
id: 1,
firstName: 'John',
lastName: 'Doe',
profileImage: 'john.jpg'
},
replies: [
{
id: 4,
senderId: 1,
content: 'Reply message',
sender: {
id: 1,
firstName: 'John',
lastName: 'Doe',
profileImage: 'john.jpg'
}
}
]
});
expect(mockMessage.update).toHaveBeenCalledWith({ isRead: true });
});
it('should get message without marking as read for sender', async () => {
const senderMessage = { ...mockMessage, senderId: 1, receiverId: 2 };
mockMessageFindOne.mockResolvedValue(senderMessage);
const response = await request(app)
.get('/messages/1');
expect(response.status).toBe(200);
expect(response.body).toEqual({
id: 1,
senderId: 1,
receiverId: 2,
subject: 'Test Message',
content: 'Hello there!',
isRead: false,
createdAt: '2024-01-15T10:00:00.000Z',
sender: {
id: 2,
firstName: 'Jane',
lastName: 'Smith',
profileImage: 'jane.jpg'
},
receiver: {
id: 1,
firstName: 'John',
lastName: 'Doe',
profileImage: 'john.jpg'
},
replies: [
{
id: 4,
senderId: 1,
content: 'Reply message',
sender: {
id: 1,
firstName: 'John',
lastName: 'Doe',
profileImage: 'john.jpg'
}
}
]
});
expect(mockMessage.update).not.toHaveBeenCalled();
});
it('should not mark already read message as read', async () => {
const readMessage = { ...mockMessage, isRead: true };
mockMessageFindOne.mockResolvedValue(readMessage);
const response = await request(app)
.get('/messages/1');
expect(response.status).toBe(200);
expect(mockMessage.update).not.toHaveBeenCalled();
});
it('should return 404 for non-existent message', async () => {
mockMessageFindOne.mockResolvedValue(null);
const response = await request(app)
.get('/messages/999');
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'Message not found' });
});
it('should handle database errors', async () => {
mockMessageFindOne.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.get('/messages/1');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Database error' });
});
});
describe('POST /', () => {
const mockReceiver = {
id: 2,
firstName: 'Jane',
lastName: 'Smith',
email: 'jane@example.com'
};
const mockCreatedMessage = {
id: 5,
senderId: 1,
receiverId: 2,
subject: 'New Message',
content: 'Hello Jane!',
parentMessageId: null
};
const mockMessageWithSender = {
...mockCreatedMessage,
sender: {
id: 1,
firstName: 'John',
lastName: 'Doe',
profileImage: 'john.jpg'
}
};
beforeEach(() => {
mockUserFindByPk.mockResolvedValue(mockReceiver);
mockMessageCreate.mockResolvedValue(mockCreatedMessage);
mockMessageFindByPk.mockResolvedValue(mockMessageWithSender);
});
it('should create a new message', async () => {
const messageData = {
receiverId: 2,
subject: 'New Message',
content: 'Hello Jane!',
parentMessageId: null
};
const response = await request(app)
.post('/messages')
.send(messageData);
expect(response.status).toBe(201);
expect(response.body).toEqual(mockMessageWithSender);
expect(mockMessageCreate).toHaveBeenCalledWith({
senderId: 1,
receiverId: 2,
subject: 'New Message',
content: 'Hello Jane!',
parentMessageId: null
});
});
it('should create a reply message with parentMessageId', async () => {
const replyData = {
receiverId: 2,
subject: 'Re: Original Message',
content: 'This is a reply',
parentMessageId: 1
};
const response = await request(app)
.post('/messages')
.send(replyData);
expect(response.status).toBe(201);
expect(mockMessageCreate).toHaveBeenCalledWith({
senderId: 1,
receiverId: 2,
subject: 'Re: Original Message',
content: 'This is a reply',
parentMessageId: 1
});
});
it('should return 404 for non-existent receiver', async () => {
mockUserFindByPk.mockResolvedValue(null);
const response = await request(app)
.post('/messages')
.send({
receiverId: 999,
subject: 'Test',
content: 'Test message'
});
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'Receiver not found' });
});
it('should prevent sending messages to self', async () => {
const response = await request(app)
.post('/messages')
.send({
receiverId: 1, // Same as sender ID
subject: 'Self Message',
content: 'Hello self!'
});
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Cannot send messages to yourself' });
});
it('should handle database errors during creation', async () => {
mockMessageCreate.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.post('/messages')
.send({
receiverId: 2,
subject: 'Test',
content: 'Test message'
});
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Database error' });
});
});
describe('PUT /:id/read', () => {
const mockMessage = {
id: 1,
senderId: 2,
receiverId: 1,
isRead: false,
update: jest.fn()
};
beforeEach(() => {
mockMessageFindOne.mockResolvedValue(mockMessage);
});
it('should mark message as read', async () => {
const updatedMessage = { ...mockMessage, isRead: true };
mockMessage.update.mockResolvedValue(updatedMessage);
const response = await request(app)
.put('/messages/1/read');
expect(response.status).toBe(200);
expect(response.body).toEqual({
id: 1,
senderId: 2,
receiverId: 1,
isRead: false
});
expect(mockMessage.update).toHaveBeenCalledWith({ isRead: true });
expect(mockMessageFindOne).toHaveBeenCalledWith({
where: {
id: '1',
receiverId: 1
}
});
});
it('should return 404 for non-existent message', async () => {
mockMessageFindOne.mockResolvedValue(null);
const response = await request(app)
.put('/messages/999/read');
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'Message not found' });
});
it('should return 404 when user is not the receiver', async () => {
// Message exists but user is not the receiver (query will return null)
mockMessageFindOne.mockResolvedValue(null);
const response = await request(app)
.put('/messages/1/read');
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'Message not found' });
});
it('should handle database errors', async () => {
mockMessageFindOne.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.put('/messages/1/read');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Database error' });
});
});
describe('GET /unread/count', () => {
it('should get unread message count for authenticated user', async () => {
mockMessageCount.mockResolvedValue(5);
const response = await request(app)
.get('/messages/unread/count');
expect(response.status).toBe(200);
expect(response.body).toEqual({ count: 5 });
expect(mockMessageCount).toHaveBeenCalledWith({
where: {
receiverId: 1,
isRead: false
}
});
});
it('should return count of 0 when no unread messages', async () => {
mockMessageCount.mockResolvedValue(0);
const response = await request(app)
.get('/messages/unread/count');
expect(response.status).toBe(200);
expect(response.body).toEqual({ count: 0 });
});
it('should handle database errors', async () => {
mockMessageCount.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.get('/messages/unread/count');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Database error' });
});
});
describe('Message authorization', () => {
it('should only find messages where user is sender or receiver', async () => {
const { Op } = require('sequelize');
await request(app)
.get('/messages/1');
expect(mockMessageFindOne).toHaveBeenCalledWith({
where: {
id: '1',
[Op.or]: [
{ senderId: 1 },
{ receiverId: 1 }
]
},
include: expect.any(Array)
});
});
});
describe('Edge cases', () => {
it('should handle empty inbox', async () => {
mockMessageFindAll.mockResolvedValue([]);
const response = await request(app)
.get('/messages');
expect(response.status).toBe(200);
expect(response.body).toEqual([]);
});
it('should handle empty sent messages', async () => {
mockMessageFindAll.mockResolvedValue([]);
const response = await request(app)
.get('/messages/sent');
expect(response.status).toBe(200);
expect(response.body).toEqual([]);
});
it('should handle message with no replies', async () => {
const messageWithoutReplies = {
id: 1,
senderId: 2,
receiverId: 1,
subject: 'Test Message',
content: 'Hello there!',
isRead: false,
replies: [],
update: jest.fn()
};
mockMessageFindOne.mockResolvedValue(messageWithoutReplies);
const response = await request(app)
.get('/messages/1');
expect(response.status).toBe(200);
expect(response.body.replies).toEqual([]);
});
it('should handle missing optional fields in message creation', async () => {
const mockReceiver = { id: 2, firstName: 'Jane', lastName: 'Smith' };
const mockCreatedMessage = {
id: 6,
senderId: 1,
receiverId: 2,
subject: undefined,
content: 'Just content',
parentMessageId: undefined
};
const mockMessageWithSender = {
...mockCreatedMessage,
sender: { id: 1, firstName: 'John', lastName: 'Doe' }
};
mockUserFindByPk.mockResolvedValue(mockReceiver);
mockMessageCreate.mockResolvedValue(mockCreatedMessage);
mockMessageFindByPk.mockResolvedValue(mockMessageWithSender);
const response = await request(app)
.post('/messages')
.send({
receiverId: 2,
content: 'Just content'
// subject and parentMessageId omitted
});
expect(response.status).toBe(201);
expect(mockMessageCreate).toHaveBeenCalledWith({
senderId: 1,
receiverId: 2,
subject: undefined,
content: 'Just content',
parentMessageId: undefined
});
});
});
});