From 2959abdf677616657ab6287bf4824cdfb3125b04 Mon Sep 17 00:00:00 2001 From: Aditya Gambhir <67105262+Aditya-gam@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:36:14 -0800 Subject: [PATCH 1/3] fix(bmdashboard): validate getLessonsLearnt query and log errors Validate projectId (ObjectId when not ALL), startDate and endDate before querying; return 400 for invalid values. Replace console.error with logger.logException for structured error logging. Made-with: Cursor --- .../bmdashboard/bmNewLessonController.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/controllers/bmdashboard/bmNewLessonController.js b/src/controllers/bmdashboard/bmNewLessonController.js index f0682143f..4c52bd345 100644 --- a/src/controllers/bmdashboard/bmNewLessonController.js +++ b/src/controllers/bmdashboard/bmNewLessonController.js @@ -1,5 +1,6 @@ /* eslint-disable no-unused-vars */ const mongoose = require('mongoose'); +const logger = require('../../startup/logger'); const bmNewLessonController = function (BuildingNewLesson) { const buildingProject = require('../../models/bmdashboard/buildingProject'); @@ -219,6 +220,18 @@ const bmNewLessonController = function (BuildingNewLesson) { try { const { projectId, startDate, endDate } = req.query; + if (projectId && projectId !== 'ALL' && !mongoose.Types.ObjectId.isValid(projectId)) { + return res.status(400).json({ error: 'Invalid projectId' }); + } + + if (startDate && Number.isNaN(Date.parse(startDate))) { + return res.status(400).json({ error: 'Invalid startDate' }); + } + + if (endDate && Number.isNaN(Date.parse(endDate))) { + return res.status(400).json({ error: 'Invalid endDate' }); + } + const filter = {}; if (projectId && projectId !== 'ALL') { filter.relatedProject = new mongoose.Types.ObjectId(projectId); @@ -339,7 +352,7 @@ const bmNewLessonController = function (BuildingNewLesson) { res.status(200).json({ data: result }); } catch (err) { - console.error('Error fetching lessons learnt:', err); + logger.logException(err, 'getLessonsLearnt', { query: req.query }); res.status(500).json({ error: 'Internal Server Error' }); } }; From b0b8ff7ad97f60b01e7c470c91f143c451e43ce8 Mon Sep 17 00:00:00 2001 From: Aditya Gambhir <67105262+Aditya-gam@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:31:55 -0800 Subject: [PATCH 2/3] refactor(bmdashboard): extract getLessonsLearnt helpers and use logger Extract buildProjectIdFilter and calculateChangePercentage; add end-of-day constants for month boundaries. Simplify getLessonsLearnt aggregation and map building. Replace console.error with logger.logException in all handler catch blocks. Made-with: Cursor --- .../bmdashboard/bmNewLessonController.js | 134 ++++++++---------- 1 file changed, 56 insertions(+), 78 deletions(-) diff --git a/src/controllers/bmdashboard/bmNewLessonController.js b/src/controllers/bmdashboard/bmNewLessonController.js index 4c52bd345..bb304481a 100644 --- a/src/controllers/bmdashboard/bmNewLessonController.js +++ b/src/controllers/bmdashboard/bmNewLessonController.js @@ -2,6 +2,28 @@ const mongoose = require('mongoose'); const logger = require('../../startup/logger'); +/** Builds a MongoDB filter fragment for projectId. Returns {} for 'ALL' or absent value. */ +const buildProjectIdFilter = (projectId) => { + if (!projectId || projectId === 'ALL') return {}; + return { relatedProject: new mongoose.Types.ObjectId(projectId) }; +}; + +/** Returns month-over-month change as a signed percentage string (e.g. '+50.0%', '-25.0%'). */ +const calculateChangePercentage = (thisMonth, lastMonth) => { + if (lastMonth === 0 && thisMonth > 0) return '+100%'; + if (lastMonth > 0) { + const change = ((thisMonth - lastMonth) / lastMonth) * 100; + return `${change >= 0 ? '+' : ''}${change.toFixed(1)}%`; + } + return '0%'; +}; + +// Named constants for end-of-day time components used in month boundary Date construction +const END_HOUR = 23; +const END_MINUTE = 59; +const END_SECOND = 59; + +// eslint-disable-next-line max-lines-per-function const bmNewLessonController = function (BuildingNewLesson) { const buildingProject = require('../../models/bmdashboard/buildingProject'); const Like = require('../../models/bmdashboard/buldingLessonLike'); @@ -36,7 +58,7 @@ const bmNewLessonController = function (BuildingNewLesson) { res.json(lesson); } catch (error) { - console.error(`Error fetching lesson with ID ${lessonId}:`, error); + logger.logException(error, 'bmGetSingleLesson', { lessonId }); res.status(500).json({ error: 'Internal Server Error' }); } }; @@ -76,7 +98,7 @@ const bmNewLessonController = function (BuildingNewLesson) { res.json(updatedLesson); } catch (error) { - console.error(`Error updating lesson with ID ${req.params.lessonId}:`, error); + logger.logException(error, 'bmEditSingleLesson', { lessonId }); res.status(500).json({ error: 'Internal Server Error' }); } }; @@ -108,7 +130,7 @@ const bmNewLessonController = function (BuildingNewLesson) { res.json({ message: 'Lesson deleted successfully', deletedLesson }); } catch (error) { - console.error(`Error removing lesson with ID ${lessonId}:`, error); + logger.logException(error, 'bmDeleteSingleLesson', { lessonId }); res.status(500).json({ error: 'Internal Server Error' }); } }; @@ -137,7 +159,7 @@ const bmNewLessonController = function (BuildingNewLesson) { const tags = await BuildingNewLesson.getAllTags(); return res.status(201).json(tags); } catch (error) { - console.error('Tag creation error:', error); + logger.logException(error, 'addNewTag', { tag: req.body.tag }); return res.status(500).json({ error: 'Error adding new tag', details: error.message, @@ -165,7 +187,7 @@ const bmNewLessonController = function (BuildingNewLesson) { const remainingTags = await BuildingNewLesson.getAllTags(); return res.status(200).json(remainingTags); } catch (error) { - console.error('Delete tag error:', error); + logger.logException(error, 'deleteTag', { tag: req.params.tag }); return res.status(500).json({ error: 'Error deleting tag', details: error.message, @@ -202,7 +224,7 @@ const bmNewLessonController = function (BuildingNewLesson) { return res.status(200).json({ status: 'success', message: 'Lesson liked successfully' }); } catch (error) { - console.error('Error liking/unliking lesson:', error); + logger.logException(error, 'likeLesson', { lessonId, userId }); return res.status(500).json({ status: 'error', message: 'Error liking/unliking lesson' }); } }; @@ -223,34 +245,23 @@ const bmNewLessonController = function (BuildingNewLesson) { if (projectId && projectId !== 'ALL' && !mongoose.Types.ObjectId.isValid(projectId)) { return res.status(400).json({ error: 'Invalid projectId' }); } - if (startDate && Number.isNaN(Date.parse(startDate))) { return res.status(400).json({ error: 'Invalid startDate' }); } - if (endDate && Number.isNaN(Date.parse(endDate))) { return res.status(400).json({ error: 'Invalid endDate' }); } - const filter = {}; - if (projectId && projectId !== 'ALL') { - filter.relatedProject = new mongoose.Types.ObjectId(projectId); - } + const filter = { ...buildProjectIdFilter(projectId) }; if (startDate || endDate) { filter.date = {}; if (startDate) filter.date.$gte = new Date(startDate); if (endDate) filter.date.$lte = new Date(endDate); } - // Current Period const lessonsInRange = await BuildingNewLesson.aggregate([ { $match: filter }, - { - $group: { - _id: '$relatedProject', - lessonsCount: { $sum: 1 }, - }, - }, + { $group: { _id: '$relatedProject', lessonsCount: { $sum: 1 } } }, { $lookup: { from: 'buildingProjects', @@ -269,84 +280,51 @@ const bmNewLessonController = function (BuildingNewLesson) { }, ]); - // This Month - let now = new Date(); - if (endDate) { - now = new Date(endDate); - } - const thisMonthStart = new Date(now.getFullYear(), now.getMonth(), 1); - const thisMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); - - const thisMonthFilter = { - ...(projectId && - projectId !== 'ALL' && { - relatedProject: new mongoose.Types.ObjectId(projectId), - }), - date: { $gte: thisMonthStart, $lte: thisMonthEnd }, - }; + const now = endDate ? new Date(endDate) : new Date(); + const year = now.getFullYear(); + const month = now.getMonth(); + const projectFilter = buildProjectIdFilter(projectId); const thisMonthLessons = await BuildingNewLesson.aggregate([ - { $match: thisMonthFilter }, { - $group: { - _id: '$relatedProject', - thisMonthCount: { $sum: 1 }, + $match: { + ...projectFilter, + date: { + $gte: new Date(year, month, 1), + $lte: new Date(year, month + 1, 0, END_HOUR, END_MINUTE, END_SECOND), + }, }, }, + { $group: { _id: '$relatedProject', thisMonthCount: { $sum: 1 } } }, ]); - // Last Month - const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1); - const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59); - - const lastMonthFilter = { - ...(projectId && - projectId !== 'ALL' && { - relatedProject: new mongoose.Types.ObjectId(projectId), - }), - date: { $gte: lastMonthStart, $lte: lastMonthEnd }, - }; - const lastMonthLessons = await BuildingNewLesson.aggregate([ - { $match: lastMonthFilter }, { - $group: { - _id: '$relatedProject', - lastMonthCount: { $sum: 1 }, + $match: { + ...projectFilter, + date: { + $gte: new Date(year, month - 1, 1), + $lte: new Date(year, month, 0, END_HOUR, END_MINUTE, END_SECOND), + }, }, }, + { $group: { _id: '$relatedProject', lastMonthCount: { $sum: 1 } } }, ]); - // Mapping this month and last month counts - const thisMonthMap = {}; - thisMonthLessons.forEach((entry) => { - thisMonthMap[entry._id.toString()] = entry.thisMonthCount; - }); - - const lastMonthMap = {}; - lastMonthLessons.forEach((entry) => { - lastMonthMap[entry._id.toString()] = entry.lastMonthCount; - }); - // console.log(lastMonthMap, thisMonthMap) + const thisMonthMap = Object.fromEntries( + thisMonthLessons.map((e) => [e._id.toString(), e.thisMonthCount]), + ); + const lastMonthMap = Object.fromEntries( + lastMonthLessons.map((e) => [e._id.toString(), e.lastMonthCount]), + ); - // Build final result const result = lessonsInRange.map((entry) => { - const projectIdStr = entry.projectId.toString(); - const thisMonth = thisMonthMap[projectIdStr] || 0; - const lastMonth = lastMonthMap[projectIdStr] || 0; - let changePercentage = '0%'; - if (lastMonth === 0 && thisMonth > 0) { - changePercentage = '+100%'; - } else if (lastMonth > 0) { - const change = ((thisMonth - lastMonth) / lastMonth) * 100; - changePercentage = `${change >= 0 ? '+' : ''}${change.toFixed(1)}%`; - } - + const id = entry.projectId.toString(); return { project: entry.project, projectId: entry.projectId, lessonsCount: entry.lessonsCount, - changePercentage, + changePercentage: calculateChangePercentage(thisMonthMap[id] || 0, lastMonthMap[id] || 0), }; }); From 760ecfd73f7aaf0544a473e112d63516f516e984 Mon Sep 17 00:00:00 2001 From: Aditya Gambhir <67105262+Aditya-gam@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:32:42 -0800 Subject: [PATCH 3/3] test(bmdashboard): add getLessonsLearnt unit tests Cover validation (invalid projectId, startDate, endDate), filter construction (projectId ALL/valid, date range), changePercentage cases (+100%, 0%, +/-50%, +0.0%, multi-project), and 500 + logger on aggregate error. Made-with: Cursor --- .../__tests__/bmNewLessonController.test.js | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) diff --git a/src/controllers/bmdashboard/__tests__/bmNewLessonController.test.js b/src/controllers/bmdashboard/__tests__/bmNewLessonController.test.js index 617ca927d..0ce794a59 100644 --- a/src/controllers/bmdashboard/__tests__/bmNewLessonController.test.js +++ b/src/controllers/bmdashboard/__tests__/bmNewLessonController.test.js @@ -1,4 +1,5 @@ const bmNewLessonController = require('../bmNewLessonController'); +const logger = require('../../../startup/logger'); // Mock dependencies const mockBuildingNewLesson = { @@ -11,6 +12,7 @@ const mockBuildingNewLesson = { updateMany: jest.fn(), deleteMany: jest.fn(), getAllTags: jest.fn(), + aggregate: jest.fn(), }; const mockBuildingProject = { @@ -47,6 +49,7 @@ describe('bmNewLessonController', () => { mockReq = { body: {}, params: {}, + query: {}, }; mockRes = { @@ -398,6 +401,296 @@ describe('bmNewLessonController', () => { }); }); + describe('getLessonsLearnt', () => { + const VALID_PROJECT_ID = '507f1f77bcf86cd799439011'; + const mockProjectObjId = { toString: () => VALID_PROJECT_ID }; + + const defaultLessonsInRange = [ + { project: 'Project A', projectId: mockProjectObjId, lessonsCount: 5 }, + ]; + const defaultThisMonth = [{ _id: mockProjectObjId, thisMonthCount: 3 }]; + const defaultLastMonth = [{ _id: mockProjectObjId, lastMonthCount: 2 }]; + + beforeEach(() => { + mockBuildingNewLesson.aggregate + .mockResolvedValueOnce(defaultLessonsInRange) + .mockResolvedValueOnce(defaultThisMonth) + .mockResolvedValueOnce(defaultLastMonth); + }); + + // --- Validation: Issue 1 (invalid projectId) --- + it('should return 400 for an invalid projectId', async () => { + mockReq.query = { projectId: 'not-a-valid-objectid' }; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith({ error: 'Invalid projectId' }); + expect(mockBuildingNewLesson.aggregate).not.toHaveBeenCalled(); + }); + + it('should not reject projectId=ALL and proceed normally', async () => { + mockReq.query = { projectId: 'ALL' }; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockBuildingNewLesson.aggregate).toHaveBeenCalledTimes(3); + }); + + it('should not apply a relatedProject filter when projectId=ALL', async () => { + mockReq.query = { projectId: 'ALL' }; + + await controller.getLessonsLearnt(mockReq, mockRes); + + const firstMatchStage = mockBuildingNewLesson.aggregate.mock.calls[0][0][0].$match; + expect(firstMatchStage.relatedProject).toBeUndefined(); + }); + + // --- Validation: Issue 3 (invalid dates) --- + it('should return 400 for an invalid startDate', async () => { + mockReq.query = { startDate: 'not-a-date' }; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith({ error: 'Invalid startDate' }); + expect(mockBuildingNewLesson.aggregate).not.toHaveBeenCalled(); + }); + + it('should return 400 for an invalid endDate', async () => { + mockReq.query = { endDate: 'not-a-date' }; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith({ error: 'Invalid endDate' }); + expect(mockBuildingNewLesson.aggregate).not.toHaveBeenCalled(); + }); + + // --- Happy path: no params --- + it('should return 200 with lessons grouped by project when no params given', async () => { + mockReq.query = {}; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockBuildingNewLesson.aggregate).toHaveBeenCalledTimes(3); + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.json).toHaveBeenCalledWith({ + data: [ + { + project: 'Project A', + projectId: mockProjectObjId, + lessonsCount: 5, + changePercentage: '+50.0%', + }, + ], + }); + }); + + it('should return empty data array when no lessons exist', async () => { + mockBuildingNewLesson.aggregate.mockReset(); + mockBuildingNewLesson.aggregate + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([]); + mockReq.query = {}; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.json).toHaveBeenCalledWith({ data: [] }); + }); + + // --- Filter construction: valid projectId --- + it('should apply relatedProject filter when a valid projectId is given', async () => { + mockReq.query = { projectId: VALID_PROJECT_ID }; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + const firstMatchStage = mockBuildingNewLesson.aggregate.mock.calls[0][0][0].$match; + expect(firstMatchStage.relatedProject).toBeDefined(); + }); + + // --- Filter construction: date range --- + it('should apply $gte and $lte date filters when startDate and endDate are given', async () => { + mockReq.query = { startDate: '2024-01-01', endDate: '2024-12-31' }; + + await controller.getLessonsLearnt(mockReq, mockRes); + + const firstMatchStage = mockBuildingNewLesson.aggregate.mock.calls[0][0][0].$match; + expect(firstMatchStage.date.$gte).toEqual(new Date('2024-01-01')); + expect(firstMatchStage.date.$lte).toEqual(new Date('2024-12-31')); + }); + + it('should apply only $gte when only startDate is given', async () => { + mockReq.query = { startDate: '2024-01-01' }; + + await controller.getLessonsLearnt(mockReq, mockRes); + + const firstMatchStage = mockBuildingNewLesson.aggregate.mock.calls[0][0][0].$match; + expect(firstMatchStage.date.$gte).toEqual(new Date('2024-01-01')); + expect(firstMatchStage.date.$lte).toBeUndefined(); + }); + + it('should apply only $lte when only endDate is given', async () => { + mockReq.query = { endDate: '2024-12-31' }; + + await controller.getLessonsLearnt(mockReq, mockRes); + + const firstMatchStage = mockBuildingNewLesson.aggregate.mock.calls[0][0][0].$match; + expect(firstMatchStage.date.$lte).toEqual(new Date('2024-12-31')); + expect(firstMatchStage.date.$gte).toBeUndefined(); + }); + + it('should not apply a date filter when neither startDate nor endDate is given', async () => { + mockReq.query = {}; + + await controller.getLessonsLearnt(mockReq, mockRes); + + const firstMatchStage = mockBuildingNewLesson.aggregate.mock.calls[0][0][0].$match; + expect(firstMatchStage.date).toBeUndefined(); + }); + + // --- changePercentage calculation --- + it('should return +100% when lastMonth is 0 and thisMonth is positive', async () => { + mockBuildingNewLesson.aggregate.mockReset(); + mockBuildingNewLesson.aggregate + .mockResolvedValueOnce([ + { project: 'Project A', projectId: mockProjectObjId, lessonsCount: 3 }, + ]) + .mockResolvedValueOnce([{ _id: mockProjectObjId, thisMonthCount: 3 }]) + .mockResolvedValueOnce([]); + mockReq.query = {}; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalledWith({ + data: [expect.objectContaining({ changePercentage: '+100%' })], + }); + }); + + it('should return 0% when both lastMonth and thisMonth are 0', async () => { + mockBuildingNewLesson.aggregate.mockReset(); + mockBuildingNewLesson.aggregate + .mockResolvedValueOnce([ + { project: 'Project A', projectId: mockProjectObjId, lessonsCount: 5 }, + ]) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([]); + mockReq.query = {}; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalledWith({ + data: [expect.objectContaining({ changePercentage: '0%' })], + }); + }); + + it('should return a positive percentage when thisMonth exceeds lastMonth', async () => { + mockBuildingNewLesson.aggregate.mockReset(); + mockBuildingNewLesson.aggregate + .mockResolvedValueOnce([ + { project: 'Project A', projectId: mockProjectObjId, lessonsCount: 6 }, + ]) + .mockResolvedValueOnce([{ _id: mockProjectObjId, thisMonthCount: 6 }]) + .mockResolvedValueOnce([{ _id: mockProjectObjId, lastMonthCount: 4 }]); + mockReq.query = {}; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalledWith({ + data: [expect.objectContaining({ changePercentage: '+50.0%' })], + }); + }); + + it('should return a negative percentage when thisMonth is less than lastMonth', async () => { + mockBuildingNewLesson.aggregate.mockReset(); + mockBuildingNewLesson.aggregate + .mockResolvedValueOnce([ + { project: 'Project A', projectId: mockProjectObjId, lessonsCount: 2 }, + ]) + .mockResolvedValueOnce([{ _id: mockProjectObjId, thisMonthCount: 2 }]) + .mockResolvedValueOnce([{ _id: mockProjectObjId, lastMonthCount: 4 }]); + mockReq.query = {}; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalledWith({ + data: [expect.objectContaining({ changePercentage: '-50.0%' })], + }); + }); + + it('should return +0.0% when thisMonth equals lastMonth (no change)', async () => { + mockBuildingNewLesson.aggregate.mockReset(); + mockBuildingNewLesson.aggregate + .mockResolvedValueOnce([ + { project: 'Project A', projectId: mockProjectObjId, lessonsCount: 4 }, + ]) + .mockResolvedValueOnce([{ _id: mockProjectObjId, thisMonthCount: 4 }]) + .mockResolvedValueOnce([{ _id: mockProjectObjId, lastMonthCount: 4 }]); + mockReq.query = {}; + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalledWith({ + data: [expect.objectContaining({ changePercentage: '+0.0%' })], + }); + }); + + it('should correctly compute changePercentage independently for multiple projects', async () => { + const projectObjId2 = { toString: () => '507f1f77bcf86cd799439012' }; + mockBuildingNewLesson.aggregate.mockReset(); + mockBuildingNewLesson.aggregate + .mockResolvedValueOnce([ + { project: 'Project A', projectId: mockProjectObjId, lessonsCount: 6 }, + { project: 'Project B', projectId: projectObjId2, lessonsCount: 2 }, + ]) + .mockResolvedValueOnce([ + { _id: mockProjectObjId, thisMonthCount: 6 }, + { _id: projectObjId2, thisMonthCount: 1 }, + ]) + .mockResolvedValueOnce([ + { _id: mockProjectObjId, lastMonthCount: 4 }, + { _id: projectObjId2, lastMonthCount: 2 }, + ]); + mockReq.query = {}; + + await controller.getLessonsLearnt(mockReq, mockRes); + + const responseData = mockRes.json.mock.calls[0][0].data; + expect(responseData).toHaveLength(2); + expect(responseData[0]).toMatchObject({ project: 'Project A', changePercentage: '+50.0%' }); + expect(responseData[1]).toMatchObject({ project: 'Project B', changePercentage: '-50.0%' }); + }); + + // --- Error path: Issue 4 (logger) --- + it('should return 500 and call logger.logException when aggregate throws', async () => { + const dbError = new Error('Database error'); + mockBuildingNewLesson.aggregate.mockReset(); + mockBuildingNewLesson.aggregate.mockRejectedValue(dbError); + mockReq.query = { projectId: VALID_PROJECT_ID }; + + // The controller captures `logger` at module-load time (top-level require), so + // jest.mock factory cannot intercept it. jest.spyOn mutates the shared cached + // module object that the controller already holds a reference to. + const logExceptionSpy = jest + .spyOn(logger, 'logException') + .mockReturnValue('mock-tracking-id'); + + await controller.getLessonsLearnt(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.json).toHaveBeenCalledWith({ error: 'Internal Server Error' }); + expect(logExceptionSpy).toHaveBeenCalledWith(dbError, 'getLessonsLearnt', { + query: { projectId: VALID_PROJECT_ID }, + }); + + logExceptionSpy.mockRestore(); + }); + }); + describe('getLessonTags', () => { it('should return unique sorted tags', async () => { const mockLessons = [