552 lines
14 KiB
JavaScript
552 lines
14 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'),
|
|
},
|
|
}));
|
|
|
|
jest.mock('../../../utils/logger', () => ({
|
|
info: jest.fn(),
|
|
error: jest.fn(),
|
|
warn: jest.fn(),
|
|
withRequestId: jest.fn(() => ({
|
|
info: jest.fn(),
|
|
error: jest.fn(),
|
|
warn: jest.fn(),
|
|
})),
|
|
}));
|
|
|
|
jest.mock('../../../sockets/messageSocket', () => ({
|
|
emitNewMessage: jest.fn(),
|
|
emitMessageRead: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('../../../services/email', () => ({
|
|
messaging: {
|
|
sendNewMessageNotification: jest.fn().mockResolvedValue(),
|
|
},
|
|
}));
|
|
|
|
const { Message, User } = require('../../../models');
|
|
|
|
// Create express app with the router
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use('/messages', messagesRouter);
|
|
|
|
// Add error handler middleware
|
|
app.use((err, req, res, next) => {
|
|
res.status(500).json({ error: err.message });
|
|
});
|
|
|
|
// 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,
|
|
content: 'Hello there!',
|
|
isRead: false,
|
|
createdAt: '2024-01-15T10:00:00.000Z',
|
|
sender: {
|
|
id: 2,
|
|
firstName: 'Jane',
|
|
lastName: 'Smith',
|
|
imageFilename: 'jane.jpg'
|
|
}
|
|
},
|
|
{
|
|
id: 2,
|
|
senderId: 3,
|
|
receiverId: 1,
|
|
content: 'Hi!',
|
|
isRead: true,
|
|
createdAt: '2024-01-14T10:00:00.000Z',
|
|
sender: {
|
|
id: 3,
|
|
firstName: 'Bob',
|
|
lastName: 'Johnson',
|
|
imageFilename: 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', 'imageFilename']
|
|
}
|
|
],
|
|
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,
|
|
content: 'Hello Jane!',
|
|
isRead: false,
|
|
createdAt: '2024-01-15T12:00:00.000Z',
|
|
receiver: {
|
|
id: 2,
|
|
firstName: 'Jane',
|
|
lastName: 'Smith',
|
|
imageFilename: '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', 'imageFilename']
|
|
}
|
|
],
|
|
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,
|
|
content: 'Hello there!',
|
|
isRead: false,
|
|
createdAt: '2024-01-15T10:00:00.000Z',
|
|
sender: {
|
|
id: 2,
|
|
firstName: 'Jane',
|
|
lastName: 'Smith',
|
|
imageFilename: 'jane.jpg'
|
|
},
|
|
receiver: {
|
|
id: 1,
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
imageFilename: 'john.jpg'
|
|
},
|
|
update: jest.fn()
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockMessageFindOne.mockResolvedValue(mockMessage);
|
|
});
|
|
|
|
it('should get message for receiver and mark as read', 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,
|
|
content: 'Hello there!',
|
|
isRead: false,
|
|
createdAt: '2024-01-15T10:00:00.000Z',
|
|
sender: {
|
|
id: 2,
|
|
firstName: 'Jane',
|
|
lastName: 'Smith',
|
|
imageFilename: 'jane.jpg'
|
|
},
|
|
receiver: {
|
|
id: 1,
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
imageFilename: '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,
|
|
content: 'Hello there!',
|
|
isRead: false,
|
|
createdAt: '2024-01-15T10:00:00.000Z',
|
|
sender: {
|
|
id: 2,
|
|
firstName: 'Jane',
|
|
lastName: 'Smith',
|
|
imageFilename: 'jane.jpg'
|
|
},
|
|
receiver: {
|
|
id: 1,
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
imageFilename: '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,
|
|
content: 'Hello Jane!'
|
|
};
|
|
|
|
const mockMessageWithSender = {
|
|
...mockCreatedMessage,
|
|
sender: {
|
|
id: 1,
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
imageFilename: 'john.jpg'
|
|
}
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockUserFindByPk.mockResolvedValue(mockReceiver);
|
|
mockMessageCreate.mockResolvedValue(mockCreatedMessage);
|
|
mockMessageFindByPk.mockResolvedValue(mockMessageWithSender);
|
|
});
|
|
|
|
it('should create a new message', async () => {
|
|
const messageData = {
|
|
receiverId: 2,
|
|
content: 'Hello Jane!'
|
|
};
|
|
|
|
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,
|
|
content: 'Hello Jane!',
|
|
imageFilename: null
|
|
});
|
|
});
|
|
|
|
it('should return 404 for non-existent receiver', async () => {
|
|
mockUserFindByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(app)
|
|
.post('/messages')
|
|
.send({
|
|
receiverId: 999,
|
|
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
|
|
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,
|
|
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([]);
|
|
});
|
|
});
|
|
}); |