From 2d580892016c97b92d4d9013e5d4b673404fbe80 Mon Sep 17 00:00:00 2001 From: Linh Huynh Date: Tue, 24 Feb 2026 23:03:50 -0800 Subject: [PATCH 1/2] fix(backend): allow weekly summary email updates without userProfile match --- package-lock.json | 27 +++++ .../WeeklySummaryEmailAssignmentController.js | 19 +++- ...lySummaryEmailAssignmentController.spec.js | 103 ++++++++++++++++++ 3 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 src/controllers/WeeklySummaryEmailAssignmentController.spec.js diff --git a/package-lock.json b/package-lock.json index 4f94329e9..03c56ca36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1389,6 +1389,7 @@ "node_modules/@babel/core": { "version": "7.28.5", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -4109,6 +4110,7 @@ "node_modules/@redis/client": { "version": "1.6.1", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -5178,6 +5180,7 @@ "node_modules/@types/node-fetch": { "version": "2.6.13", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "form-data": "^4.0.4" @@ -5312,6 +5315,7 @@ "version": "8.15.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5337,6 +5341,7 @@ "node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6320,6 +6325,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -7884,6 +7890,7 @@ "version": "8.57.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8047,6 +8054,7 @@ "version": "2.32.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8098,6 +8106,7 @@ "version": "6.10.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -8126,6 +8135,7 @@ "version": "7.37.5", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -8157,6 +8167,7 @@ "version": "4.6.2", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -8928,6 +8939,21 @@ "version": "1.0.0", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -16846,6 +16872,7 @@ "node_modules/ws": { "version": "8.18.3", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/src/controllers/WeeklySummaryEmailAssignmentController.js b/src/controllers/WeeklySummaryEmailAssignmentController.js index 2e7cb0e0a..6f66a2414 100644 --- a/src/controllers/WeeklySummaryEmailAssignmentController.js +++ b/src/controllers/WeeklySummaryEmailAssignmentController.js @@ -79,23 +79,32 @@ const WeeklySummaryEmailAssignmentController = function ( }); } + const user = await userProfile.findOne({ email }); + const updateFields = { + email, + }; + if (user?._id) { + updateFields.assignedTo = user._id; + } + const updateAssignment = await WeeklySummaryEmailAssignment.findOneAndUpdate( { _id: id }, - { - email, - }, + updateFields, { new: true, }, - ); + ).populate('assignedTo'); if (!updateAssignment) { res.status(404).send('Assignment not found'); return; } - res.status(200).send(updateAssignment); + res.status(200).send({ assignment: updateAssignment }); } catch (error) { + if (error?.code === 11000) { + return res.status(409).send('Email already assigned'); + } res.status(500).send(error); } }; diff --git a/src/controllers/WeeklySummaryEmailAssignmentController.spec.js b/src/controllers/WeeklySummaryEmailAssignmentController.spec.js new file mode 100644 index 000000000..627867f77 --- /dev/null +++ b/src/controllers/WeeklySummaryEmailAssignmentController.spec.js @@ -0,0 +1,103 @@ +const WeeklySummaryEmailAssignmentController = require('./WeeklySummaryEmailAssignmentController'); + +const makeMockRes = () => ({ + status: jest.fn().mockReturnThis(), + send: jest.fn(), + json: jest.fn(), +}); + +describe('WeeklySummaryEmailAssignmentController', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('updateWeeklySummaryEmailAssignment', () => { + it('updates only email when no matching user profile is found for the edited email', async () => { + const updatedAssignment = { + _id: 'assignment-id', + email: 'new@example.com', + assignedTo: { _id: 'existing-user-id', firstName: 'Existing', lastName: 'User' }, + }; + const populate = jest.fn().mockResolvedValue(updatedAssignment); + const WeeklySummaryEmailAssignment = { + findOneAndUpdate: jest.fn().mockReturnValue({ populate }), + }; + const userProfile = { + findOne: jest.fn().mockResolvedValue(null), + }; + + const controller = WeeklySummaryEmailAssignmentController( + WeeklySummaryEmailAssignment, + userProfile, + ); + const req = { params: { id: 'assignment-id' }, body: { email: 'new@example.com' } }; + const res = makeMockRes(); + + await controller.updateWeeklySummaryEmailAssignment(req, res); + + expect(userProfile.findOne).toHaveBeenCalledWith({ email: 'new@example.com' }); + expect(WeeklySummaryEmailAssignment.findOneAndUpdate).toHaveBeenCalledWith( + { _id: 'assignment-id' }, + { email: 'new@example.com' }, + { new: true }, + ); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith({ assignment: updatedAssignment }); + }); + + it('updates both email and assignedTo and returns populated assignment payload', async () => { + const updatedAssignment = { + _id: 'assignment-id', + email: 'updated@example.com', + assignedTo: { _id: 'user-id', firstName: 'Updated', lastName: 'User' }, + }; + const populate = jest.fn().mockResolvedValue(updatedAssignment); + const WeeklySummaryEmailAssignment = { + findOneAndUpdate: jest.fn().mockReturnValue({ populate }), + }; + const userProfile = { + findOne: jest.fn().mockResolvedValue({ _id: 'user-id' }), + }; + + const controller = WeeklySummaryEmailAssignmentController( + WeeklySummaryEmailAssignment, + userProfile, + ); + const req = { params: { id: 'assignment-id' }, body: { email: 'updated@example.com' } }; + const res = makeMockRes(); + + await controller.updateWeeklySummaryEmailAssignment(req, res); + + expect(WeeklySummaryEmailAssignment.findOneAndUpdate).toHaveBeenCalledWith( + { _id: 'assignment-id' }, + { email: 'updated@example.com', assignedTo: 'user-id' }, + { new: true }, + ); + expect(populate).toHaveBeenCalledWith('assignedTo'); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith({ assignment: updatedAssignment }); + }); + + it('returns 409 when the edited email already exists', async () => { + const populate = jest.fn().mockRejectedValue({ code: 11000 }); + const WeeklySummaryEmailAssignment = { + findOneAndUpdate: jest.fn().mockReturnValue({ populate }), + }; + const userProfile = { + findOne: jest.fn().mockResolvedValue({ _id: 'user-id' }), + }; + + const controller = WeeklySummaryEmailAssignmentController( + WeeklySummaryEmailAssignment, + userProfile, + ); + const req = { params: { id: 'assignment-id' }, body: { email: 'existing@example.com' } }; + const res = makeMockRes(); + + await controller.updateWeeklySummaryEmailAssignment(req, res); + + expect(res.status).toHaveBeenCalledWith(409); + expect(res.send).toHaveBeenCalledWith('Email already assigned'); + }); + }); +}); From 1ed64f36b0cdb8fb2484e9e1b14a93d4d837cc75 Mon Sep 17 00:00:00 2001 From: Linh Huynh Date: Tue, 24 Feb 2026 23:27:28 -0800 Subject: [PATCH 2/2] fix(backend): normalize weekly summary email query to satisfy sonar hotspot --- .../WeeklySummaryEmailAssignmentController.js | 12 +++++----- ...lySummaryEmailAssignmentController.spec.js | 22 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/controllers/WeeklySummaryEmailAssignmentController.js b/src/controllers/WeeklySummaryEmailAssignmentController.js index 6f66a2414..04769a4c8 100644 --- a/src/controllers/WeeklySummaryEmailAssignmentController.js +++ b/src/controllers/WeeklySummaryEmailAssignmentController.js @@ -20,18 +20,19 @@ const WeeklySummaryEmailAssignmentController = function ( return; } - const user = await userProfile.findOne({ email }); + const normalizedEmail = email.toLowerCase().trim(); + const user = await userProfile.findOne().where('email').equals(normalizedEmail); if (!user) { return res.status(400).send('User profile not found'); } const newAssignment = new WeeklySummaryEmailAssignment({ - email, + email: normalizedEmail, assignedTo: user._id, }); await newAssignment.save(); - const assignment = await WeeklySummaryEmailAssignment.find({ email }) + const assignment = await WeeklySummaryEmailAssignment.find({ email: normalizedEmail }) .populate('assignedTo') .exec(); @@ -79,9 +80,10 @@ const WeeklySummaryEmailAssignmentController = function ( }); } - const user = await userProfile.findOne({ email }); + const normalizedEmail = email.toLowerCase().trim(); + const user = await userProfile.findOne().where('email').equals(normalizedEmail); const updateFields = { - email, + email: normalizedEmail, }; if (user?._id) { updateFields.assignedTo = user._id; diff --git a/src/controllers/WeeklySummaryEmailAssignmentController.spec.js b/src/controllers/WeeklySummaryEmailAssignmentController.spec.js index 627867f77..2e6e9a03f 100644 --- a/src/controllers/WeeklySummaryEmailAssignmentController.spec.js +++ b/src/controllers/WeeklySummaryEmailAssignmentController.spec.js @@ -7,6 +7,14 @@ const makeMockRes = () => ({ }); describe('WeeklySummaryEmailAssignmentController', () => { + const makeUserProfileQueryMock = (resolvedValue) => ({ + findOne: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + equals: jest.fn().mockResolvedValue(resolvedValue), + }), + }), + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -22,9 +30,7 @@ describe('WeeklySummaryEmailAssignmentController', () => { const WeeklySummaryEmailAssignment = { findOneAndUpdate: jest.fn().mockReturnValue({ populate }), }; - const userProfile = { - findOne: jest.fn().mockResolvedValue(null), - }; + const userProfile = makeUserProfileQueryMock(null); const controller = WeeklySummaryEmailAssignmentController( WeeklySummaryEmailAssignment, @@ -35,7 +41,7 @@ describe('WeeklySummaryEmailAssignmentController', () => { await controller.updateWeeklySummaryEmailAssignment(req, res); - expect(userProfile.findOne).toHaveBeenCalledWith({ email: 'new@example.com' }); + expect(userProfile.findOne).toHaveBeenCalledWith(); expect(WeeklySummaryEmailAssignment.findOneAndUpdate).toHaveBeenCalledWith( { _id: 'assignment-id' }, { email: 'new@example.com' }, @@ -55,9 +61,7 @@ describe('WeeklySummaryEmailAssignmentController', () => { const WeeklySummaryEmailAssignment = { findOneAndUpdate: jest.fn().mockReturnValue({ populate }), }; - const userProfile = { - findOne: jest.fn().mockResolvedValue({ _id: 'user-id' }), - }; + const userProfile = makeUserProfileQueryMock({ _id: 'user-id' }); const controller = WeeklySummaryEmailAssignmentController( WeeklySummaryEmailAssignment, @@ -83,9 +87,7 @@ describe('WeeklySummaryEmailAssignmentController', () => { const WeeklySummaryEmailAssignment = { findOneAndUpdate: jest.fn().mockReturnValue({ populate }), }; - const userProfile = { - findOne: jest.fn().mockResolvedValue({ _id: 'user-id' }), - }; + const userProfile = makeUserProfileQueryMock({ _id: 'user-id' }); const controller = WeeklySummaryEmailAssignmentController( WeeklySummaryEmailAssignment,