From 8dc8214f30d00960092b0ff0ff85d905e27c48c7 Mon Sep 17 00:00:00 2001 From: Louisa Best Date: Mon, 27 Apr 2026 15:03:22 +0930 Subject: [PATCH] fix(messages): allow guard-to-guard chat --- .../src/controllers/message.controller.js | 8 +- app-backend/src/routes/message.routes.js | 2 +- app-backend/tests/message.controller.test.js | 145 ++++++++++++++++++ 3 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 app-backend/tests/message.controller.test.js diff --git a/app-backend/src/controllers/message.controller.js b/app-backend/src/controllers/message.controller.js index 39edaf0dd..8f2ff38d5 100644 --- a/app-backend/src/controllers/message.controller.js +++ b/app-backend/src/controllers/message.controller.js @@ -37,16 +37,18 @@ const sendMessage = async (req, res, next) => { throw error; } - //Ensure communication is only between guards and employers + // Allow guard-to-guard and guard/employer messaging, but keep + // employer-to-employer and admin messaging blocked. const senderRole = req.user.role; const receiverRole = receiver.role; const validCommunication = + (senderRole === 'guard' && receiverRole === 'guard') || (senderRole === 'guard' && receiverRole === 'employer') || (senderRole === 'employer' && receiverRole === 'guard'); if (!validCommunication) { - const error = new Error('Messages can only be sent between guards and employers'); + const error = new Error('Messages can only be sent between guards, or between guards and employers'); error.status = 403; throw error; } @@ -276,4 +278,4 @@ export { getConversation, markMessageAsRead, getMessageStats -}; \ No newline at end of file +}; diff --git a/app-backend/src/routes/message.routes.js b/app-backend/src/routes/message.routes.js index be92629af..7faa71aba 100644 --- a/app-backend/src/routes/message.routes.js +++ b/app-backend/src/routes/message.routes.js @@ -17,7 +17,7 @@ router.use(auth); * @swagger * tags: * name: Messages - * description: Messaging endpoints between guards and employers + * description: Messaging endpoints between guards, and between guards and employers */ /** diff --git a/app-backend/tests/message.controller.test.js b/app-backend/tests/message.controller.test.js new file mode 100644 index 000000000..192d036dd --- /dev/null +++ b/app-backend/tests/message.controller.test.js @@ -0,0 +1,145 @@ +jest.mock('../src/models/User.js', () => ({ + __esModule: true, + default: { + findById: jest.fn(), + }, +})); + +jest.mock('../src/models/Message.js', () => ({ + __esModule: true, + default: jest.fn().mockImplementation(function Message(data) { + Object.assign(this, { + _id: 'message-123', + timestamp: new Date('2026-04-27T00:00:00.000Z'), + isRead: false, + ...data, + save: jest.fn().mockResolvedValue(undefined), + populate: jest.fn().mockResolvedValue(undefined), + }); + }), +})); + +jest.mock('express-validator', () => ({ + validationResult: jest.fn(), +})); + +import User from '../src/models/User.js'; +import Message from '../src/models/Message.js'; +import { validationResult } from 'express-validator'; +import { sendMessage } from '../src/controllers/message.controller.js'; + +const createReq = ({ senderId = 'guard-1', senderRole = 'guard', receiverId = 'guard-2' } = {}) => ({ + body: { + receiverId, + content: ' Hello from SecureShift ', + }, + user: { + id: senderId, + role: senderRole, + }, + audit: { + log: jest.fn().mockResolvedValue(undefined), + }, +}); + +const createRes = () => { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; +}; + +beforeEach(() => { + jest.clearAllMocks(); + validationResult.mockReturnValue({ + isEmpty: () => true, + array: () => [], + }); +}); + +test('sendMessage allows guard-to-guard messaging', async () => { + User.findById.mockResolvedValue({ + _id: 'guard-2', + role: 'guard', + email: 'guard2@example.com', + name: 'Guard Two', + }); + + const req = createReq(); + const res = createRes(); + const next = jest.fn(); + + await sendMessage(req, res, next); + + expect(Message).toHaveBeenCalledWith({ + sender: 'guard-1', + receiver: 'guard-2', + content: 'Hello from SecureShift', + }); + expect(Message.mock.instances[0].save).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: true, + data: expect.objectContaining({ + messageId: 'message-123', + content: 'Hello from SecureShift', + }), + }) + ); + expect(next).not.toHaveBeenCalled(); +}); + +test('sendMessage still blocks employer-to-employer messaging', async () => { + User.findById.mockResolvedValue({ + _id: 'employer-2', + role: 'employer', + email: 'employer2@example.com', + name: 'Employer Two', + }); + + const req = createReq({ + senderId: 'employer-1', + senderRole: 'employer', + receiverId: 'employer-2', + }); + const res = createRes(); + const next = jest.fn(); + + await sendMessage(req, res, next); + + expect(Message).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + status: 403, + message: 'Messages can only be sent between guards, or between guards and employers', + }) + ); +}); + +test('sendMessage still blocks admin messaging', async () => { + User.findById.mockResolvedValue({ + _id: 'guard-2', + role: 'guard', + email: 'guard2@example.com', + name: 'Guard Two', + }); + + const req = createReq({ + senderId: 'admin-1', + senderRole: 'admin', + receiverId: 'guard-2', + }); + const res = createRes(); + const next = jest.fn(); + + await sendMessage(req, res, next); + + expect(Message).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + status: 403, + message: 'Messages can only be sent between guards, or between guards and employers', + }) + ); +});