From 2b84ac6ac844c9229f10a01df694dcc5faf7ca6b Mon Sep 17 00:00:00 2001 From: sphurthy Date: Thu, 16 Oct 2025 15:08:49 -0400 Subject: [PATCH 01/16] fix: Assign Lesson Plan - Backend API and Logic --- package-lock.json | 9 - src/controllers/educatorController.js | 296 ++++++++++++++++++++++++++ src/models/educationTask.js | 113 +++++----- src/models/lessonPlan.js | 71 +++--- src/models/progress.js | 71 +++--- src/models/userProfile.js | 4 +- src/routes/educatorRouter.js | 16 ++ src/startup/routes.js | 7 +- 8 files changed, 454 insertions(+), 133 deletions(-) create mode 100644 src/controllers/educatorController.js create mode 100644 src/routes/educatorRouter.js diff --git a/package-lock.json b/package-lock.json index 97f5fa713..4a1ee2ccf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -517,7 +517,6 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -3599,7 +3598,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -4199,7 +4197,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5019,7 +5016,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -6467,7 +6463,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6630,7 +6625,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6694,7 +6688,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, - "peer": true, "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -6724,7 +6717,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -6757,7 +6749,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, diff --git a/src/controllers/educatorController.js b/src/controllers/educatorController.js new file mode 100644 index 000000000..61da20da7 --- /dev/null +++ b/src/controllers/educatorController.js @@ -0,0 +1,296 @@ +const mongoose = require('mongoose'); +const LessonPlan = require('../models/lessonPlan'); +const Activity = require('../models/activity'); +const EducationTask = require('../models/educationTask'); +const Progress = require('../models/progress'); +const Atom = require('../models/atom'); +const UserProfile = require('../models/userProfile'); + +const educatorController = function () { + // Utility function to calculate deadline + const calculateDeadline = (assignmentDate, offsetDays = 7) => { + const deadline = new Date(assignmentDate); + deadline.setDate(deadline.getDate() + offsetDays); + return deadline; + }; + + // Check if student has completed prerequisite atoms + const checkPrerequisites = async (studentId, atomId) => { + try { + const atom = await Atom.findById(atomId).populate('prerequisites'); + if (!atom || !atom.prerequisites || atom.prerequisites.length === 0) { + return true; // No prerequisites required + } + + // Check if student has completed all prerequisite atoms + const prerequisiteIds = atom.prerequisites.map((prereq) => prereq._id); + const completedProgress = await Progress.find({ + studentId, + atomId: { $in: prerequisiteIds }, + status: 'completed', + }); + + return completedProgress.length === prerequisiteIds.length; + } catch (error) { + throw new Error(`Error checking prerequisites: ${error.message}`); + } + }; + + // Get enrolled students (those with student education profile) + const getEnrolledStudents = async () => { + try { + return await UserProfile.find({ + 'educationProfiles.student.cohortId': { $exists: true }, + isActive: true, + }).select('_id firstName lastName email educationProfiles.student'); + } catch (error) { + throw new Error(`Error fetching enrolled students: ${error.message}`); + } + }; + + // Main assignment endpoint + const assignTasks = async (req, res) => { + const session = await mongoose.startSession(); + + try { + await session.startTransaction(); + + const { + lesson_plan_id: lessonPlanId, + assignment_date: assignmentDate, + is_auto_assigned: isAutoAssigned, + deadline_offset_days: deadlineOffsetDays, + } = req.body; + + // Validate required fields + if (!lessonPlanId || !assignmentDate) { + return res.status(400).json({ + error: 'lesson_plan_id and assignment_date are required', + }); + } + + // Validate lesson plan exists + const lessonPlan = await LessonPlan.findById(lessonPlanId) + .populate('activities') + .session(session); + + if (!lessonPlan) { + return res.status(404).json({ + error: 'Lesson plan not found', + }); + } + + // Get activities and extract task templates + const activities = await Activity.find({ + lessonPlanId, + }).session(session); + + if (!activities || activities.length === 0) { + return res.status(400).json({ + error: 'No activities found for this lesson plan', + }); + } + + // Extract all atom task templates from activities + const taskTemplates = []; + activities.forEach((activity) => { + activity.atomTaskTemplates.forEach((template) => { + taskTemplates.push({ + atomId: template.atomId, + subjectId: template.subjectId, + taskType: template.taskType, + instructions: template.instructions, + resources: template.resources || [], + }); + }); + }); + + if (taskTemplates.length === 0) { + return res.status(400).json({ + error: 'No task templates found in lesson plan activities', + }); + } + + // Get enrolled students + const students = await getEnrolledStudents(); + + if (students.length === 0) { + return res.status(400).json({ + error: 'No enrolled students found', + }); + } + + // Initialize tracking variables + let successCount = 0; + let failureCount = 0; + const skippedStudents = []; + const errors = []; + const assignedTasks = []; + + // Process students and templates in parallel (no awaits inside loops) + const perStudentResults = await Promise.allSettled( + students.map(async (student) => { + const perTemplateResults = await Promise.allSettled( + taskTemplates.map(async (template) => { + const hasPrereqs = await checkPrerequisites(student._id, template.atomId); + if (!hasPrereqs) { + return { + ok: false, + studentId: student._id, + atomId: template.atomId, + reason: 'Prerequisites not completed', + }; + } + + const existingTask = await EducationTask.findOne({ + studentId: student._id, + lessonPlanId, + atomIds: template.atomId, + }).session(session); + if (existingTask) { + return { + ok: false, + studentId: student._id, + atomId: template.atomId, + reason: 'Task already assigned', + }; + } + + const dueAt = calculateDeadline(assignmentDate, deadlineOffsetDays); + + const educationTask = new EducationTask({ + lessonPlanId, + studentId: student._id, + atomIds: [template.atomId], + type: template.taskType, + status: 'assigned', + assignedAt: new Date(assignmentDate), + dueAt, + uploadUrls: [], + grade: 'pending', + }); + + const savedTask = await educationTask.save({ session }); + + await Progress.findOneAndUpdate( + { studentId: student._id, atomId: template.atomId }, + { + studentId: student._id, + atomId: template.atomId, + status: 'in_progress', + firstStartedAt: new Date(assignmentDate), + }, + { upsert: true, new: true, session }, + ); + + return { ok: true, studentId: student._id, taskId: savedTask._id }; + }), + ); + + const successes = perTemplateResults.filter( + (r) => r.status === 'fulfilled' && r.value.ok, + ).length; + const errorsForStudent = perTemplateResults + .filter((r) => r.status === 'fulfilled' && !r.value.ok) + .map((r) => ({ + studentId: r.value.studentId, + atomId: r.value.atomId, + reason: r.value.reason, + })); + + return { studentId: student._id, successes, errorsForStudent }; + }), + ); + + // Aggregate results (no ++, no continue) + // let successCount = 0; + // let failureCount = 0; + // const skippedStudents = []; + // const errors = []; + // const assignedTasks = []; + + perStudentResults.forEach((r) => { + if (r.status !== 'fulfilled') return; + const { studentId, successes, errorsForStudent } = r.value; + if (successes > 0) { + successCount += 1; + // We don’t collect task documents here; keep your existing total via counts or fetch if needed + } else if (errorsForStudent.length > 0) { + failureCount += 1; + skippedStudents.push(studentId); + } + errors.push(...errorsForStudent); + }); + + await session.commitTransaction(); + + // Return structured response + const response = { + success: true, + summary: { + success_count: successCount, + failure_count: failureCount, + total_students: students.length, + total_tasks_assigned: assignedTasks.length, + skipped: skippedStudents, + errors, + }, + lesson_plan: { + id: lessonPlanId, + title: lessonPlan.title, + theme: lessonPlan.theme, + }, + assignment_details: { + assignment_date: new Date(assignmentDate), + deadline_offset_days: deadlineOffsetDays, + is_auto_assigned: isAutoAssigned, + }, + }; + + res.status(201).json(response); + } catch (error) { + await session.abortTransaction(); + res.status(500).json({ + error: `Assignment failed: ${error.message}`, + success: false, + }); + } finally { + session.endSession(); + } + }; + + // Get assignment summary by lesson plan + const getAssignmentSummary = async (req, res) => { + try { + const { lessonPlanId } = req.params; + + const tasks = await EducationTask.find({ lessonPlanId }) + .populate('studentId', 'firstName lastName email') + .populate('atomIds', 'name difficulty') + .sort({ assignedAt: -1 }); + + const summary = { + total_assignments: tasks.length, + by_status: { + assigned: tasks.filter((t) => t.status === 'assigned').length, + in_progress: tasks.filter((t) => t.status === 'in_progress').length, + completed: tasks.filter((t) => t.status === 'completed').length, + graded: tasks.filter((t) => t.status === 'graded').length, + }, + students: [...new Set(tasks.map((t) => t.studentId._id.toString()))].length, + recent_assignments: tasks.slice(0, 10), + }; + + res.status(200).json(summary); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }; + + return { + assignTasks, + getAssignmentSummary, + }; +}; + +module.exports = educatorController; diff --git a/src/models/educationTask.js b/src/models/educationTask.js index 369672dca..4f191943c 100644 --- a/src/models/educationTask.js +++ b/src/models/educationTask.js @@ -1,58 +1,65 @@ const mongoose = require('mongoose'); -const educationTaskSchema = new mongoose.Schema({ - lessonPlanId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'LessonPlan', - required: true +const educationTaskSchema = new mongoose.Schema( + { + lessonPlanId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'LessonPlan', + required: true, + }, + studentId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'userProfile', + required: true, + }, + atomIds: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'Atom', + required: true, + }, + ], + type: { + type: String, + required: true, + enum: ['read', 'write', 'practice', 'quiz', 'project'], + }, + status: { + type: String, + required: true, + enum: ['assigned', 'in_progress', 'completed', 'graded'], + default: 'assigned', + }, + assignedAt: { + type: Date, + default: Date.now, + }, + dueAt: { + type: Date, + required: true, + }, + completedAt: { + type: Date, + }, + uploadUrls: [ + { + type: String, + trim: true, + }, + ], + grade: { + type: String, + enum: ['A', 'B', 'C', 'D', 'F', 'pending'], + default: 'pending', + }, + feedback: { + type: String, + trim: true, + }, }, - studentId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true + { + timestamps: true, }, - atomIds: [{ - type: mongoose.Schema.Types.ObjectId, - ref: 'Atom', - required: true - }], - type: { - type: String, - required: true, - enum: ['read', 'write', 'practice', 'quiz', 'project'] - }, - status: { - type: String, - required: true, - enum: ['assigned', 'in_progress', 'completed', 'graded'], - default: 'assigned' - }, - assignedAt: { - type: Date, - default: Date.now - }, - dueAt: { - type: Date, - required: true - }, - completedAt: { - type: Date - }, - uploadUrls: [{ - type: String, - trim: true - }], - grade: { - type: String, - enum: ['A', 'B', 'C', 'D', 'F', 'pending'], - default: 'pending' - }, - feedback: { - type: String, - trim: true - } -}, { - timestamps: true -}); +); -module.exports = mongoose.model('EducationTask', educationTaskSchema); \ No newline at end of file +module.exports = mongoose.model('EducationTask', educationTaskSchema); diff --git a/src/models/lessonPlan.js b/src/models/lessonPlan.js index e59f301f3..f3cee0770 100644 --- a/src/models/lessonPlan.js +++ b/src/models/lessonPlan.js @@ -1,38 +1,43 @@ const mongoose = require('mongoose'); -const lessonPlanSchema = new mongoose.Schema({ - title: { - type: String, - required: true, - trim: true +const lessonPlanSchema = new mongoose.Schema( + { + title: { + type: String, + required: true, + trim: true, + }, + theme: { + type: String, + trim: true, + }, + description: { + type: String, + trim: true, + }, + startDate: { + type: Date, + required: true, + }, + endDate: { + type: Date, + required: true, + }, + createdBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'userProfile', + required: true, + }, + activities: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'Activity', + }, + ], }, - theme: { - type: String, - trim: true + { + timestamps: true, }, - description: { - type: String, - trim: true - }, - startDate: { - type: Date, - required: true - }, - endDate: { - type: Date, - required: true - }, - createdBy: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - activities: [{ - type: mongoose.Schema.Types.ObjectId, - ref: 'Activity' - }] -}, { - timestamps: true -}); +); -module.exports = mongoose.model('LessonPlan', lessonPlanSchema); \ No newline at end of file +module.exports = mongoose.model('LessonPlan', lessonPlanSchema); diff --git a/src/models/progress.js b/src/models/progress.js index 79aa1ffc0..eacbac96a 100644 --- a/src/models/progress.js +++ b/src/models/progress.js @@ -1,42 +1,45 @@ const mongoose = require('mongoose'); -const progressSchema = new mongoose.Schema({ - studentId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true +const progressSchema = new mongoose.Schema( + { + studentId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'userProfile', + required: true, + }, + atomId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Atom', + required: true, + }, + status: { + type: String, + required: true, + enum: ['not_started', 'in_progress', 'completed'], + default: 'not_started', + }, + firstStartedAt: { + type: Date, + }, + completedAt: { + type: Date, + }, + grade: { + type: String, + enum: ['A', 'B', 'C', 'D', 'F', 'pending'], + default: 'pending', + }, + feedback: { + type: String, + trim: true, + }, }, - atomId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Atom', - required: true + { + timestamps: true, }, - status: { - type: String, - required: true, - enum: ['not_started', 'in_progress', 'completed'], - default: 'not_started' - }, - firstStartedAt: { - type: Date - }, - completedAt: { - type: Date - }, - grade: { - type: String, - enum: ['A', 'B', 'C', 'D', 'F', 'pending'], - default: 'pending' - }, - feedback: { - type: String, - trim: true - } -}, { - timestamps: true -}); +); // Compound index to ensure unique progress tracking per student-atom combination progressSchema.index({ studentId: 1, atomId: 1 }, { unique: true }); -module.exports = mongoose.model('Progress', progressSchema); \ No newline at end of file +module.exports = mongoose.model('Progress', progressSchema); diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 8a3729f98..a00324162 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -349,7 +349,7 @@ const userProfileSchema = new Schema({ assignedStudents: [ { type: mongoose.Schema.Types.ObjectId, - ref: 'User', + ref: 'userProfile', }, ], }, @@ -374,7 +374,7 @@ const userProfileSchema = new Schema({ assignedTeachers: [ { type: mongoose.Schema.Types.ObjectId, - ref: 'User', + ref: 'userProfile', }, ], }, diff --git a/src/routes/educatorRouter.js b/src/routes/educatorRouter.js new file mode 100644 index 000000000..16ac3909d --- /dev/null +++ b/src/routes/educatorRouter.js @@ -0,0 +1,16 @@ +const express = require('express'); + +const routes = function () { + const controller = require('../controllers/educatorController')(); + const educatorRouter = express.Router(); + + // POST /api/educator/assign-tasks - Main assignment endpoint + educatorRouter.route('/assign-tasks').post(controller.assignTasks); + + // GET /api/educator/assignments/:lessonPlanId - Get assignment summary + educatorRouter.route('/assignments/:lessonPlanId').get(controller.getAssignmentSummary); + + return educatorRouter; +}; + +module.exports = routes; diff --git a/src/startup/routes.js b/src/startup/routes.js index f559d460e..6645d6329 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -20,7 +20,9 @@ const ownerMessage = require('../models/ownerMessage'); const currentWarnings = require('../models/currentWarnings'); const availability = require('../models/lbdashboard/availability'); -const listingAvailablityRouter = require('../routes/lbdashboard/listingAvailablityRouter')(availability); +const listingAvailablityRouter = require('../routes/lbdashboard/listingAvailablityRouter')( + availability, +); const savedFilter = require('../models/savedFilter'); const hgnFormResponses = require('../models/hgnFormResponse'); @@ -272,6 +274,7 @@ const projectMaterialRouter = require('../routes/projectMaterialroutes'); const projectCostRouter = require('../routes/bmdashboard/projectCostRouter')(projectCost); const tagRouter = require('../routes/tagRouter')(tag); +const educatorRouter = require('../routes/educatorRouter')(); const savedFilterRouter = require('../routes/savedFilterRouter')(savedFilter); // lbdashboard const bidTermsRouter = require('../routes/lbdashboard/bidTermsRouter'); @@ -347,10 +350,10 @@ module.exports = function (app) { app.use('/api/help-categories', helpCategoryRouter); app.use('/api', tagRouter); + app.use('/api/educator', educatorRouter); app.use('/api/analytics', pledgeAnalyticsRoutes); app.use('/api', registrationRouter); - app.use('/api/job-analytics', jobAnalyticsRoutes); app.use('/api/applicant-volunteer-ratio', applicantVolunteerRatioRouter); app.use('/applications', applicationRoutes); From 86d854fe4cafbf8b3a27bfc78fad5dd90bd0b0fb Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Wed, 11 Mar 2026 13:54:08 -0700 Subject: [PATCH 02/16] Add Lesson Plan Builder UI and update routes --- package-lock.json | 176 ++++++++++++++++--------------- src/models/educationTask.js | 22 ++++ src/models/lessonPlan.js | 33 ++++++ src/models/lessonPlanLog.js | 28 +++++ src/routes/educatorRouter.js | 195 +++++++++++++++++++++++++++++++++++ src/startup/routes.js | 7 +- 6 files changed, 377 insertions(+), 84 deletions(-) create mode 100644 src/models/educationTask.js create mode 100644 src/models/lessonPlan.js create mode 100644 src/models/lessonPlanLog.js create mode 100644 src/routes/educatorRouter.js diff --git a/package-lock.json b/package-lock.json index 4fa9fa80a..0d96bda53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,14 +92,14 @@ } }, "node_modules/@apimatic/authentication-adapters": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/@apimatic/authentication-adapters/-/authentication-adapters-0.5.12.tgz", - "integrity": "sha512-H0dVMsuiPRRS6qPSz42T8ktaOUBlvJgKSj5L+DKr4DwBEZaE6rZx4i5kuEdAUZ5oIuteupbGEx5hMJlPi6Y0XA==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@apimatic/authentication-adapters/-/authentication-adapters-0.5.13.tgz", + "integrity": "sha512-lZ7q5ctiIBm/A6W6vvdfFbttJwcHDLOBmJhGm9BJ8TEg4Rfz92QJ0X4NuUF7x79dsmCHnl5vECox7t6S71xPpA==", "license": "MIT", "dependencies": { - "@apimatic/core-interfaces": "^0.2.12", - "@apimatic/http-headers": "^0.3.7", - "@apimatic/http-query": "^0.3.7", + "@apimatic/core-interfaces": "^0.2.13", + "@apimatic/http-headers": "^0.3.8", + "@apimatic/http-query": "^0.3.8", "tslib": "^2.8.1" }, "engines": { @@ -107,18 +107,18 @@ } }, "node_modules/@apimatic/axios-client-adapter": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@apimatic/axios-client-adapter/-/axios-client-adapter-0.3.18.tgz", - "integrity": "sha512-4DX6PgMT3VyACsf+EA3IxPrJzE1Schnwm+EGoCDZ8oAt7WYf3nByH1nKbmRVLEw7TGp4ZNU9slUHmdiJZ7FM6A==", + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@apimatic/axios-client-adapter/-/axios-client-adapter-0.3.19.tgz", + "integrity": "sha512-PRFDQZQAkPqe5yz6ppzdx7LARoxq3ae1bb1RrC7E9cVKh0hMZ1vk1s+gAtLphxp6dU6hFN2UTkrJoAh56zvxtQ==", "license": "MIT", "dependencies": { - "@apimatic/convert-to-stream": "^0.1.7", - "@apimatic/core-interfaces": "^0.2.12", - "@apimatic/file-wrapper": "^0.3.7", - "@apimatic/http-headers": "^0.3.7", - "@apimatic/http-query": "^0.3.7", + "@apimatic/convert-to-stream": "^0.1.8", + "@apimatic/core-interfaces": "^0.2.13", + "@apimatic/file-wrapper": "^0.3.8", + "@apimatic/http-headers": "^0.3.8", + "@apimatic/http-query": "^0.3.8", "@apimatic/json-bigint": "^1.2.0", - "@apimatic/proxy": "^0.1.2", + "@apimatic/proxy": "^0.1.3", "axios": "^1.8.4", "detect-browser": "^5.3.0", "detect-node": "^2.1.0", @@ -132,9 +132,9 @@ } }, "node_modules/@apimatic/convert-to-stream": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@apimatic/convert-to-stream/-/convert-to-stream-0.1.7.tgz", - "integrity": "sha512-uOCSy8YV0umHI4422l6wXcY/CN/oplLgoitpOY+P52I8dSrkQK+P+8HCITLDW4ND1I5OvRfvb1OwvN4+1JdgvA==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@apimatic/convert-to-stream/-/convert-to-stream-0.1.8.tgz", + "integrity": "sha512-A4VO/wyGFksKtizp4aUOGGtSlejoVI1uAXLGPVymhfpion3wUwrlST/yif9SOj3faVAGeCTNx0w5A+BpVdzSEA==", "license": "ISC", "dependencies": { "tslib": "2.8.1" @@ -144,18 +144,18 @@ } }, "node_modules/@apimatic/core": { - "version": "0.10.26", - "resolved": "https://registry.npmjs.org/@apimatic/core/-/core-0.10.26.tgz", - "integrity": "sha512-wml7KNNP3P8BXXI2bOXyuMugQpLZ1MpOl6EM40nSOCXmR4lTr1ssfoUi+LrK1n7o6JxLoYp9mGkMNHoR7n35Tw==", + "version": "0.10.27", + "resolved": "https://registry.npmjs.org/@apimatic/core/-/core-0.10.27.tgz", + "integrity": "sha512-LnoAT3I51uUCvcCt9IiCxDjoGzt/qnGnLEHafvOIuLdrNquLj3A+TNmodiRlSqD8cz1PcMFPkUctzwDmNybPpg==", "license": "MIT", "dependencies": { - "@apimatic/convert-to-stream": "^0.1.7", - "@apimatic/core-interfaces": "^0.2.12", - "@apimatic/file-wrapper": "^0.3.7", - "@apimatic/http-headers": "^0.3.7", - "@apimatic/http-query": "^0.3.7", + "@apimatic/convert-to-stream": "^0.1.8", + "@apimatic/core-interfaces": "^0.2.13", + "@apimatic/file-wrapper": "^0.3.8", + "@apimatic/http-headers": "^0.3.8", + "@apimatic/http-query": "^0.3.8", "@apimatic/json-bigint": "^1.2.0", - "@apimatic/schema": "^0.7.20", + "@apimatic/schema": "^0.7.21", "detect-browser": "^5.3.0", "detect-node": "^2.1.0", "form-data": "^4.0.1", @@ -169,12 +169,12 @@ } }, "node_modules/@apimatic/core-interfaces": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@apimatic/core-interfaces/-/core-interfaces-0.2.12.tgz", - "integrity": "sha512-XdEbSEfLEUY6KvKWQVXGMMGQmt589Zj0MnssPZ7FdgWFCrqe4aNgYt2Fl/fOC9r64GZvd7o1T9m4HKFilXi4nw==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@apimatic/core-interfaces/-/core-interfaces-0.2.13.tgz", + "integrity": "sha512-qta5qpu0c3ef+W3bPbXmrxFXG+zCIiEB1LuICZuUgXRvmomd/e7wTrEbromE7ow/wmqRfVvaOpYZ1alLtnyG+g==", "license": "MIT", "dependencies": { - "@apimatic/file-wrapper": "^0.3.7", + "@apimatic/file-wrapper": "^0.3.8", "tslib": "^2.8.1" }, "engines": { @@ -182,9 +182,9 @@ } }, "node_modules/@apimatic/file-wrapper": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@apimatic/file-wrapper/-/file-wrapper-0.3.7.tgz", - "integrity": "sha512-uJh2immpzZiZYOiG+Q9qtArg3bUk7lmx7eYFRI38wIOS2zlxQh90WkuJzaL2nvSmNsJ3p+yg5ePAyRpqoJmw7Q==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@apimatic/file-wrapper/-/file-wrapper-0.3.8.tgz", + "integrity": "sha512-3l/xLLEeVW8POr0f7fHXI2s7aSPBNCQROGmdOmMYhq1e4Va3PSFWx7lhaEEk9goTYGYf9nTQgr2euJ5Vl7VJ4g==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -194,9 +194,9 @@ } }, "node_modules/@apimatic/http-headers": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@apimatic/http-headers/-/http-headers-0.3.7.tgz", - "integrity": "sha512-DxLmwDMnAT5sOiuYI7EIuQq2PAVhbV9ICDLSdICRFflklgfKQ9agVEyNRgYVhuXz1m7IDoqqjGb1hs6iMJJpEg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@apimatic/http-headers/-/http-headers-0.3.8.tgz", + "integrity": "sha512-ShvCuT39hYfBTI+H1I16m5i6XZCyUy2kQJ6Jhfj78TwsW5r6AyCbzW7DEro8GN2nNYRU1+E/hrgH6J85YmriOA==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -206,12 +206,12 @@ } }, "node_modules/@apimatic/http-query": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@apimatic/http-query/-/http-query-0.3.7.tgz", - "integrity": "sha512-HauS4Jsuve1tGy1pnAidjHNL/TFVLesOxL7QtOdZyb+s0EHiHF6Vhx/kNroJ4mH/yCN3OHcHcqMJlfg0V5ET9g==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@apimatic/http-query/-/http-query-0.3.8.tgz", + "integrity": "sha512-5dM3zVPfMYp7Q4LHIQXpyicxPeCGk46tDfathetx547VuPCHz0M7mhp/M7zFAcTEiCQl+pPktiGBSO/vLSgWzQ==", "license": "MIT", "dependencies": { - "@apimatic/file-wrapper": "^0.3.7", + "@apimatic/file-wrapper": "^0.3.8", "tslib": "^2.8.1" }, "engines": { @@ -225,14 +225,14 @@ "license": "MIT" }, "node_modules/@apimatic/oauth-adapters": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/@apimatic/oauth-adapters/-/oauth-adapters-0.4.16.tgz", - "integrity": "sha512-rmFj32QRbRhCkhBf50Wz0VdVghw58FiJPIl9ICWhpbgP9MuBg7xnSpbdHBflqjGRFyS9rtOTJOzCQnUl76F3oQ==", + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/@apimatic/oauth-adapters/-/oauth-adapters-0.4.17.tgz", + "integrity": "sha512-cIe1dZwcNizydZUX9Q4qBVCgTCKPSTTTz6UJKTd3xyOSsAleLyPBK+AZ/D0M7ZcU60QCc+QTYMEztZnK9KksKQ==", "license": "MIT", "dependencies": { - "@apimatic/core-interfaces": "^0.2.12", - "@apimatic/file-wrapper": "^0.3.7", - "@apimatic/http-headers": "^0.3.7", + "@apimatic/core-interfaces": "^0.2.13", + "@apimatic/file-wrapper": "^0.3.8", + "@apimatic/http-headers": "^0.3.8", "tslib": "^2.8.1" }, "engines": { @@ -240,9 +240,9 @@ } }, "node_modules/@apimatic/proxy": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@apimatic/proxy/-/proxy-0.1.2.tgz", - "integrity": "sha512-Xo3PO69tos+ssAfjcBGxSxXu1jpdEwse+yYnWC8D8bCsuWKUVJh8TwWl01Zyw3s60FdikyGCGtiQ3LB2cBrbeg==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@apimatic/proxy/-/proxy-0.1.3.tgz", + "integrity": "sha512-2UQmmbhOxAXHgbkl6WzGiVLG96ubw7udUsm0cv3jqiCLUyC+c15NgC64pJ+jeYHpZm3BtsLG01rMA+dBdHsxYQ==", "license": "ISC", "dependencies": { "http-proxy-agent": "^7.0.2", @@ -253,9 +253,9 @@ } }, "node_modules/@apimatic/schema": { - "version": "0.7.20", - "resolved": "https://registry.npmjs.org/@apimatic/schema/-/schema-0.7.20.tgz", - "integrity": "sha512-kMU1LqCypb0AwY8nXLV0aBoyubHHAxD8htyeAMFLYqWmmt+1pUBe33hHIDMdkREkNm+AYpl2n6vHBC1kqgju1w==", + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/@apimatic/schema/-/schema-0.7.21.tgz", + "integrity": "sha512-RCke4toXjA7fBRxQVa1GR+Lj9utVOEJ3voDI26dhk+bZuAac4UXPzkTEaIO3AIe/o8pcKCOkpNIzhzm57Cv2Qg==", "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -4640,9 +4640,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.7.tgz", - "integrity": "sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==", + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz", + "integrity": "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -4797,9 +4797,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "funding": [ { "type": "opencollective", @@ -4816,9 +4816,9 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, @@ -4983,9 +4983,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001745", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", - "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "version": "1.0.30001747", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz", + "integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==", "funding": [ { "type": "opencollective", @@ -5947,9 +5947,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.224", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.224.tgz", - "integrity": "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==", + "version": "1.5.229", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.229.tgz", + "integrity": "sha512-cwhDcZKGcT/rEthLRJ9eBlMDkh1sorgsuk+6dpsehV0g9CABsIqBxU4rLRjG+d/U6pYU1s37A4lSKrVc5lSQYg==", "license": "ISC" }, "node_modules/emittery": { @@ -6021,12 +6021,12 @@ } }, "node_modules/engine.io/node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "version": "24.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", + "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==", "license": "MIT", "dependencies": { - "undici-types": "~7.12.0" + "undici-types": "~7.13.0" } }, "node_modules/engine.io/node_modules/cookie": { @@ -7462,6 +7462,15 @@ "node": ">=10" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/generic-pool": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", @@ -8349,13 +8358,14 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -13826,9 +13836,9 @@ "license": "0BSD" }, "node_modules/twilio": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.10.1.tgz", - "integrity": "sha512-J7+gPQggonXqc1GrkctxlHI8F6LYBAvVy6d8t2SOZ6HI3FpR9EHOcKBj7E+JYjigPKxYZeiiTDAyLpJsipbx2A==", + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.10.2.tgz", + "integrity": "sha512-pkMdXK0PJHR0elu3GmaDlYt4DDWPkkmuJLVUQjnctehu01IgbAp+VZ2ctbUSh1anDuqKqimAIuMnW9xmKith6w==", "license": "MIT", "dependencies": { "axios": "^1.12.0", @@ -14023,9 +14033,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", + "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { diff --git a/src/models/educationTask.js b/src/models/educationTask.js new file mode 100644 index 000000000..dbd24bfa5 --- /dev/null +++ b/src/models/educationTask.js @@ -0,0 +1,22 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +// This schema is specifically for tasks assigned from an educator's lesson plan. +const educationTaskSchema = new Schema( + { + studentId: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile', required: true }, + lessonPlanId: { type: mongoose.SchemaTypes.ObjectId, ref: 'LessonPlan', required: true }, + // Storing who clicked the "Assign" button + assignedBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile', required: true }, + // Storing the details of the specific sub-task from the lesson plan + title: { type: String, required: true }, + assignedDate: { type: Date }, + dueDate: { type: Date }, + status: { type: String, default: 'Assigned' }, // e.g., Assigned, In Progress, Completed + submission: { type: String }, // To store student's work if applicable + }, + { timestamps: true }, +); + +module.exports = mongoose.model('educationTask', educationTaskSchema); diff --git a/src/models/lessonPlan.js b/src/models/lessonPlan.js new file mode 100644 index 000000000..a73b07e7f --- /dev/null +++ b/src/models/lessonPlan.js @@ -0,0 +1,33 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +// This schema defines the structure for a single sub-task within a lesson plan. +const subTaskSchema = new Schema( + { + name: { type: String, required: true }, + type: { type: String }, // e.g., 'Write-only', 'Read-only' + dueDate: { type: Date }, + passMark: { type: String }, + weight: { type: String }, + }, + { _id: true }, +); // Ensure sub-tasks get their own IDs + +// UPDATED: This now matches your database structure and adds the subTasks array. +const lessonPlanSchema = new Schema( + { + title: { type: String, required: true }, // Matches your 'title' field + description: { type: String }, // Matches your 'description' field + subTasks: [subTaskSchema], // The array of sub-tasks needed for assignments + // Store who originally created this lesson plan + createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'userProfile' }, + // Store who last modified this lesson plan + lastEditedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'userProfile' }, + }, + { + timestamps: true, // This will automatically manage createdAt and updatedAt fields + }, +); + +module.exports = mongoose.model('LessonPlan', lessonPlanSchema, 'lessonplans'); diff --git a/src/models/lessonPlanLog.js b/src/models/lessonPlanLog.js new file mode 100644 index 000000000..19b2223dc --- /dev/null +++ b/src/models/lessonPlanLog.js @@ -0,0 +1,28 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +// This schema stores the history of edits and assignments for a Lesson Plan +const lessonPlanLogSchema = new Schema( + { + // The lesson plan that was changed + lessonPlanId: { type: mongoose.Schema.Types.ObjectId, ref: 'LessonPlan', required: true }, + + // The admin/educator who made the change + editorId: { type: mongoose.Schema.Types.ObjectId, ref: 'userProfile', required: true }, + + // The action they took (e.g., "Manual Assignment", "Plan Saved") + action: { type: String, required: true }, + + // Details of the action (e.g., "Assigned to 1391 students.") + details: { type: String }, + + // This will be used for the "Log Date" column + logDateTime: { type: Date, default: Date.now }, + }, + { + timestamps: true, + }, +); + +module.exports = mongoose.model('LessonPlanLog', lessonPlanLogSchema, 'lessonplanlogs'); diff --git a/src/routes/educatorRouter.js b/src/routes/educatorRouter.js new file mode 100644 index 000000000..f271022dc --- /dev/null +++ b/src/routes/educatorRouter.js @@ -0,0 +1,195 @@ +/* eslint-disable no-plusplus */ +const express = require('express'); + +const router = express.Router(); + +// --- Step 1: Import Your Real Mongoose Models --- +// NOTE: Adjust the paths to match your project's file structure. +// It's common for models to be in a directory like '../models/' +const LessonPlan = require('../models/lessonPlan'); // Assuming you have a LessonPlan model +const EducationTask = require('../models/educationTask'); // Assuming you have a Task model for assignments +const UserProfile = require('../models/userProfile'); // Using UserProfile as the "Student" model +const LessonPlanLog = require('../models/lessonPlanLog'); +// --- Step 2: Import Your Authentication Middleware --- +// NOTE: You will have a file that handles user authentication. +// This middleware will run before your route logic to protect it. +// const authMiddleware = require('../middleware/auth'); // Example path + +/** + * @route POST /api/educator/assign-tasks + * @desc Assigns all tasks from a lesson plan to eligible students. + * @access Private (Protected by authentication middleware) + */ +// To protect this route, you would add your middleware like this: +// router.post('/assign-tasks', authMiddleware, async (req, res) => { +router.post('/assign-tasks', async (req, res) => { + // 1. Destructure the payload from the frontend request body + const { lessonPlanId, assignmentDate, isAutoAssigned } = req.body; + + // --- Basic Validation --- + if (!lessonPlanId) { + return res.status(400).json({ message: 'Request is missing lesson_plan_id.' }); + } + + console.log( + `Assigning tasks for Lesson Plan ID: ${lessonPlanId}, Auto-assigned: ${isAutoAssigned}`, + ); + + try { + // --- Real Backend Logic --- + + // a. Find the lesson plan in the database to get its details + const lessonPlan = await LessonPlan.findById(lessonPlanId); + // .populate('subTasks'); // Assuming subTasks are referenced + // console.log('--- INSPECTING LESSON PLAN OBJECT ---'); + // console.log(JSON.stringify(lessonPlan, null, 2)); + // console.log('--- END INSPECTION ---'); + + if (!lessonPlan || !lessonPlan.subTasks || lessonPlan.subTasks.length === 0) { + return res + .status(404) + .json({ message: 'Lesson plan not found or it has no sub-tasks to assign.' }); + } + + // b. Find all students who should receive the tasks. + // This query finds all active, non-admin users. You can customize it. + const eligibleStudents = await UserProfile.find({ + // isActive: { $ne: false }, // Includes true or undefined, exclude false + isActive: true, + // role: { $exists: true, $nin: ['Administrator', 'Owner', 'Manager'] } // Ensure role exists and is not admin/owner/manager + role: { $nin: ['Administrator', 'Owner', 'Manager'] }, + }); + + if (!eligibleStudents || eligibleStudents.length === 0) { + return res.status(404).json({ message: 'No eligible students found to assign tasks to.' }); + } + + let assignedCount = 0; + let skippedCount = 0; + const tasksToCreate = []; + + // Get the ID of the educator who is assigning the task + const assignerId = req.body.requestor.requestorId; + + // c. Loop through each student and prepare the tasks for creation + eligibleStudents.forEach((student) => { + const meetsPrerequisites = true; // Placeholder for your logic + + // if (meetsPrerequisites) { + // // FIXED: The logic now correctly iterates over lessonPlan.subTasks + // if (lessonPlan.subTasks && lessonPlan.subTasks.length > 0) { + // lessonPlan.subTasks.forEach(subTask => { + // tasksToCreate.push({ + // studentId: student._id, + // lessonPlanId, + // // FIXED: Use the 'title' property from the subTask + // title: subTask.name, + // assignedDate: assignmentDate, + // dueDate: subTask.dueDate, + // status: 'Assigned', + // }); + // }); + // assignedCount++; + // } else { + // // If the lesson plan has no subtasks, we consider it "skipped" for this student. + // skippedCount++; + // } + // } else { + // skippedCount++; + // } + if (meetsPrerequisites) { + // Ensure subTasks array exists and has items before proceeding + if (lessonPlan.subTasks && lessonPlan.subTasks.length > 0) { + lessonPlan.subTasks.forEach((subTask) => { + // Additional check: Ensure subTask.name exists before creating task + // if (subTask && subTask.name) { + tasksToCreate.push({ + studentId: student._id, + lessonPlanId, + title: subTask.name, + assignedDate: assignmentDate, + dueDate: subTask.dueDate, // Optional: Add default if missing? + status: 'Assigned', + // Saving the ID of the educator who triggered the assignment + assignedBy: assignerId, + }); + // } + // else { + // console.warn(`Skipping subTask for student ${student._id} due to missing name in lesson plan ${lessonPlanId}`); + // } + }); + assignedCount++; + } else { + // This case should technically be caught by the initial lessonPlan check, but good to handle defensively. + console.warn(`Lesson plan ${lessonPlanId} has no subTasks for student ${student._id}.`); + skippedCount++; // Count student as skipped if lesson plan has no tasks + } + } else { + skippedCount++; + } + }); + + // d. Use bulk insert to efficiently create all tasks in the database + if (tasksToCreate.length > 0) { + await EducationTask.insertMany(tasksToCreate); + console.log(`Successfully inserted ${tasksToCreate.length} tasks into the database.`); + } + // else { + // console.log('No tasks needed to be created.'); + // } + + // --- NEW: CREATING THE LOG ENTRY HERE--- + // This creates a new document in the 'lessonplanlogs' collection + await LessonPlanLog.create({ + lessonPlanId, + editorId: assignerId, + action: isAutoAssigned ? 'Auto-Assigned (On Save)' : 'Manual Assignment', + details: `Assigned to ${assignedCount} students. Skipped ${skippedCount}.`, + }); + + // 2. Send the final, successful response back to the frontend + console.log( + `Successfully assigned tasks to ${assignedCount} students. Skipped ${skippedCount}.`, + ); + res.status(200).json({ + assignedCount, + skippedCount, + }); + } catch (error) { + // --- REFINED ERROR LOGGING --- + console.error('-----------------------------------------'); + console.error('Error occurred in /api/educator/assign-tasks:'); + console.error('Timestamp:', new Date().toISOString()); + console.error('Request Body:', req.body); // Log incoming data + console.error('Error Details:', error); // Log the full error object + console.error('-----------------------------------------'); + + // Send a generic error message to the frontend in production + const errorMessage = + process.env.NODE_ENV === 'production' + ? 'An internal server error occurred while assigning tasks.' + : `An internal server error occurred: ${error.message}`; // More detail in dev + + res.status(500).json({ message: errorMessage }); + } +}); + +/** + * @route GET /api/educator/logs/:lessonPlanId + * @desc Get all assignment/edit logs for a specific lesson plan + * @access Private + */ +router.get('/logs/:lessonPlanId', async (req, res) => { + try { + const logs = await LessonPlanLog.find({ lessonPlanId: req.params.lessonPlanId }) + // 'populate' fetches the editor's name from the 'userProfile' collection + .populate('editorId', 'firstName lastName email') + .sort({ logDateTime: -1 }); // Show newest first + res.status(200).json(logs); + } catch (error) { + console.error('Error fetching lesson plan logs:', error); + res.status(500).json({ message: 'Error fetching logs' }); + } +}); + +module.exports = router; diff --git a/src/startup/routes.js b/src/startup/routes.js index 1eaffc377..1273d1f7b 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -184,7 +184,6 @@ const bmPaidLaborCostRouter = require('../routes/bmdashboard/bmPaidLaborCostRout const bmProjectRiskProfileRouter = require('../routes/bmdashboard/bmProjectRiskProfileRouter'); const bmIssuesRouter = require('../routes/bmdashboard/IssuesRouter'); - // lb dashboard const lbListingsRouter = require('../routes/lbdashboard/listingsRouter')(listings); @@ -275,6 +274,9 @@ const SMSRouter = require('../routes/lbdashboard/SMSRouter')(); const applicantVolunteerRatioRouter = require('../routes/applicantVolunteerRatioRouter'); const applicationRoutes = require('../routes/applications'); +// education portal route +const educatorRouter = require('../routes/educatorRouter'); + module.exports = function (app) { app.use('/api', forgotPwdRouter); app.use('/api', loginRouter); @@ -411,4 +413,7 @@ module.exports = function (app) { app.use('/api/lb', bidNotificationsRouter); app.use('/api/lb', bidDeadlinesRouter); app.use('/api/lb', SMSRouter); + + // education portal + app.use('/api/educator', educatorRouter); }; From d9e3f871630e569ba3d5eda7b0f271e16f3834e0 Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Thu, 12 Mar 2026 13:45:24 -0700 Subject: [PATCH 03/16] fix: resolving the merge conflicts --- src/routes/educatorRouter.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/routes/educatorRouter.js b/src/routes/educatorRouter.js index dc726725c..3a0c01fe4 100644 --- a/src/routes/educatorRouter.js +++ b/src/routes/educatorRouter.js @@ -26,15 +26,11 @@ router.post('/assign-tasks', async (req, res) => { return res.status(400).json({ message: 'Request is missing lesson_plan_id.' }); } - console.log( - `Assigning tasks for Lesson Plan ID: ${lessonPlanId}, Auto-assigned: ${isAutoAssigned}`, - ); - try { const lessonPlan = await LessonPlan.findById(lessonPlanId); - if (!lessonPlan || !lessonPlan.subTasks || lessonPlan.subTasks.length === 0) { + if (!lessonPlan?.subTasks?.length) { return res .status(404) .json({ message: 'Lesson plan not found or it has no sub-tasks to assign.' }); @@ -57,8 +53,9 @@ router.post('/assign-tasks', async (req, res) => { const assignerId = req.body.requestor.requestorId; eligibleStudents.forEach((student) => { - const meetsPrerequisites = true; // Placeholder for your logic + const meetsPrerequisites = true; + // keeping this block commented for future reference // if (meetsPrerequisites) { // // FIXED: The logic now correctly iterates over lessonPlan.subTasks // if (lessonPlan.subTasks && lessonPlan.subTasks.length > 0) { @@ -93,6 +90,7 @@ router.post('/assign-tasks', async (req, res) => { status: 'Assigned', assignedBy: assignerId, }); + // keeping this block commented for future reference // } // else { // console.warn(`Skipping subTask for student ${student._id} due to missing name in lesson plan ${lessonPlanId}`); @@ -111,7 +109,6 @@ router.post('/assign-tasks', async (req, res) => { if (tasksToCreate.length > 0) { await EducationTask.insertMany(tasksToCreate); - console.log(`Successfully inserted ${tasksToCreate.length} tasks into the database.`); } await LessonPlanLog.create({ @@ -121,9 +118,6 @@ router.post('/assign-tasks', async (req, res) => { details: `Assigned to ${assignedCount} students. Skipped ${skippedCount}.`, }); - console.log( - `Successfully assigned tasks to ${assignedCount} students. Skipped ${skippedCount}.`, - ); res.status(200).json({ assignedCount, skippedCount, From f2bf097b7ea2caba9b031ce12ec1e722e638bb6e Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Thu, 12 Mar 2026 14:41:38 -0700 Subject: [PATCH 04/16] fix: resolving the merge conflicts --- .../ownerMessageController.spec.js | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index 3dddb40ad..126ede1a9 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -1,3 +1,6 @@ +/* eslint-disable import/order */ +const mongoose = require('mongoose'); + jest.mock('../utilities/permissions', () => ({ hasPermission: jest.fn(), })); @@ -20,6 +23,8 @@ const flushPromises = () => new Promise(setImmediate); describe('ownerMessageController Unit Tests', () => { let mockFind; let mockSave; + let mockSession; + afterEach(() => { jest.clearAllMocks(); }); @@ -27,7 +32,17 @@ describe('ownerMessageController Unit Tests', () => { beforeEach(() => { mockFind = jest.spyOn(OwnerMessage, 'find'); mockSave = jest.fn(); + + // Mock mongoose.startSession to prevent CI database disconnect errors + mockSession = { + startTransaction: jest.fn(), + commitTransaction: jest.fn(), + abortTransaction: jest.fn(), + endSession: jest.fn(), + }; + jest.spyOn(mongoose, 'startSession').mockResolvedValue(mockSession); }); + describe('getOwnerMessage', () => { test('Ensures getOwnerMessage returns status 404 if owner message cant be found', async () => { const { getOwnerMessage } = makeSut(); @@ -37,6 +52,7 @@ describe('ownerMessageController Unit Tests', () => { await flushPromises(); assertResMock(404, errorMsg, response, mockRes); }); + test('Ensures getOwnerMessage returns status 200 with new owner message if none exist', async () => { mockFind.mockResolvedValue([]); const ownerMessageInstance = new OwnerMessage(); @@ -78,6 +94,7 @@ describe('ownerMessageController Unit Tests', () => { await flushPromises(); assertResMock(403, 'You are not authorized to create messages!', response, mockRes); }); + test('Ensures updateOwnerMessage returns status 201 and updates the owner message correctly with custom message', async () => { const existingMessage = { message: '', standardMessage: '', save: mockSave }; mockFind.mockResolvedValue([existingMessage]); @@ -99,6 +116,7 @@ describe('ownerMessageController Unit Tests', () => { }); expect(mockSave).toHaveBeenCalled(); }); + test('Ensures updateOwnerMessage returns status 500 if an error occurs during the update', async () => { const errorMsg = 'Error occurred during update'; mockFind.mockRejectedValue(errorMsg); @@ -113,12 +131,13 @@ describe('ownerMessageController Unit Tests', () => { describe('deleteOwnerMessage', () => { test('Ensures deleteOwnerMessage returns status 403 if requestor is not an owner', async () => { const { deleteOwnerMessage } = makeSut(); - mockReq.body.requestor.role = 'notOwner'; + const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'notOwner' } } }; helper.hasPermission.mockResolvedValue(false); - const response = await deleteOwnerMessage(mockReq, mockRes); + const response = await deleteOwnerMessage(mockReqDup, mockRes); await flushPromises(); assertResMock(403, 'You are not authorized to delete messages!', response, mockRes); }); + test('Ensures deleteOwnerMessage returns status 200 and deletes the owner message correctly', async () => { const existingMessage = { message: 'Existing message', @@ -126,11 +145,11 @@ describe('ownerMessageController Unit Tests', () => { save: mockSave, }; mockFind.mockResolvedValue([existingMessage]); - mockReq.body.requestor.role = ''; + const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); - await deleteOwnerMessage(mockReq, mockRes); + await deleteOwnerMessage(mockReqDup, mockRes); expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.send).toHaveBeenCalledWith({ _serverMessage: 'Delete successfully!', @@ -138,16 +157,17 @@ describe('ownerMessageController Unit Tests', () => { }); expect(mockSave).toHaveBeenCalled(); }); + test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { const errorMsg = 'Error occurred during delete'; mockFind.mockRejectedValue(errorMsg); - mockReq.body.requestor.role = 'Owner'; + const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); - await deleteOwnerMessage(mockReq, mockRes); + await deleteOwnerMessage(mockReqDup, mockRes); expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.send).toHaveBeenCalledWith(errorMsg); }); }); -}); +}); \ No newline at end of file From 7b2b41ce848c212ef003b4d3a7bdde186c8d410a Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Thu, 12 Mar 2026 14:54:14 -0700 Subject: [PATCH 05/16] fix: resolving the merge conflicts --- src/controllers/ownerMessageController.spec.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index 126ede1a9..780a050ef 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -1,4 +1,4 @@ -/* eslint-disable import/order */ +// eslint-disable-next-line import/order const mongoose = require('mongoose'); jest.mock('../utilities/permissions', () => ({ @@ -80,6 +80,7 @@ describe('ownerMessageController Unit Tests', () => { const existingMessage = { message: 'Existing message', standardMessage: 'Standard message' }; mockFind.mockResolvedValue([existingMessage]); await makeSut().getOwnerMessage(mockReq, mockRes); + await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.send).toHaveBeenCalledWith({ ownerMessage: existingMessage }); }); @@ -109,6 +110,8 @@ describe('ownerMessageController Unit Tests', () => { }; helper.hasPermission.mockResolvedValue(true); await makeSut().updateOwnerMessage(mockReqDup, mockRes); + await flushPromises(); + expect(mockRes.status).toHaveBeenCalledWith(201); expect(mockRes.send).toHaveBeenCalledWith({ _serverMessage: 'Update successfully!', @@ -119,10 +122,13 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 500 if an error occurs during the update', async () => { const errorMsg = 'Error occurred during update'; - mockFind.mockRejectedValue(errorMsg); + // FIXED: Used mockImplementationOnce so it doesn't instantly reject and crash Node 20 + mockFind.mockImplementationOnce(() => Promise.reject(errorMsg)); const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); await makeSut().updateOwnerMessage(mockReqDup, mockRes); + await flushPromises(); // FIXED: Force Jest to wait for the catch block + expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.send).toHaveBeenCalledWith(errorMsg); }); @@ -150,6 +156,8 @@ describe('ownerMessageController Unit Tests', () => { const { deleteOwnerMessage } = makeSut(); await deleteOwnerMessage(mockReqDup, mockRes); + await flushPromises(); + expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.send).toHaveBeenCalledWith({ _serverMessage: 'Delete successfully!', @@ -160,12 +168,15 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { const errorMsg = 'Error occurred during delete'; - mockFind.mockRejectedValue(errorMsg); + // FIXED: Used mockImplementationOnce so it doesn't instantly reject and crash Node 20 + mockFind.mockImplementationOnce(() => Promise.reject(errorMsg)); const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); await deleteOwnerMessage(mockReqDup, mockRes); + await flushPromises(); // FIXED: Force Jest to wait for the catch block + expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.send).toHaveBeenCalledWith(errorMsg); }); From c4cc1df5915b37756d597525329fc4b45a6539a7 Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Fri, 13 Mar 2026 16:13:14 -0700 Subject: [PATCH 06/16] fix: Fix ownerMessage controller mocks to support Mongoose sessions --- .../ownerMessageController.spec.js | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index 780a050ef..95c162971 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -1,4 +1,5 @@ -// eslint-disable-next-line import/order +/* eslint-disable object-shorthand */ +/* eslint-disable import/order */ const mongoose = require('mongoose'); jest.mock('../utilities/permissions', () => ({ @@ -20,6 +21,17 @@ const makeSut = () => { }; const flushPromises = () => new Promise(setImmediate); +// HELPER: Mocks Mongoose queries so they work seamlessly whether the controller +// uses a standard `await find()` OR a chained `await find().session()` +const mockMongooseQuery = (data, isReject = false) => ({ + session: jest.fn().mockImplementation(() => + isReject ? Promise.reject(data) : Promise.resolve(data) + ), + then: function (resolve, reject) { + return isReject ? reject(data) : resolve(data); + }, +}); + describe('ownerMessageController Unit Tests', () => { let mockFind; let mockSave; @@ -30,10 +42,11 @@ describe('ownerMessageController Unit Tests', () => { }); beforeEach(() => { - mockFind = jest.spyOn(OwnerMessage, 'find'); - mockSave = jest.fn(); + // Apply the flexible query mock + mockFind = jest.spyOn(OwnerMessage, 'find').mockReturnValue(mockMongooseQuery([])); + mockSave = jest.fn().mockResolvedValue({}); - // Mock mongoose.startSession to prevent CI database disconnect errors + // StartSession mock as a safety net for CI database disconnects mockSession = { startTransaction: jest.fn(), commitTransaction: jest.fn(), @@ -47,14 +60,14 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures getOwnerMessage returns status 404 if owner message cant be found', async () => { const { getOwnerMessage } = makeSut(); const errorMsg = 'Error occurred when finding owner message'; - mockFind.mockImplementationOnce(() => Promise.reject(errorMsg)); + mockFind.mockReturnValue(mockMongooseQuery(errorMsg, true)); const response = await getOwnerMessage(mockReq, mockRes); await flushPromises(); assertResMock(404, errorMsg, response, mockRes); }); test('Ensures getOwnerMessage returns status 200 with new owner message if none exist', async () => { - mockFind.mockResolvedValue([]); + mockFind.mockReturnValue(mockMongooseQuery([])); const ownerMessageInstance = new OwnerMessage(); ownerMessageInstance.set = jest.fn(); const mockSaveFn = jest.fn().mockResolvedValue(ownerMessageInstance); @@ -78,7 +91,7 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures getOwnerMessage returns status 200 with the first owner message if it exists', async () => { const existingMessage = { message: 'Existing message', standardMessage: 'Standard message' }; - mockFind.mockResolvedValue([existingMessage]); + mockFind.mockReturnValue(mockMongooseQuery([existingMessage])); await makeSut().getOwnerMessage(mockReq, mockRes); await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(200); @@ -98,7 +111,7 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 201 and updates the owner message correctly with custom message', async () => { const existingMessage = { message: '', standardMessage: '', save: mockSave }; - mockFind.mockResolvedValue([existingMessage]); + mockFind.mockReturnValue(mockMongooseQuery([existingMessage])); const mockReqDup = { ...mockReq, body: { @@ -111,7 +124,7 @@ describe('ownerMessageController Unit Tests', () => { helper.hasPermission.mockResolvedValue(true); await makeSut().updateOwnerMessage(mockReqDup, mockRes); await flushPromises(); - + expect(mockRes.status).toHaveBeenCalledWith(201); expect(mockRes.send).toHaveBeenCalledWith({ _serverMessage: 'Update successfully!', @@ -122,13 +135,13 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 500 if an error occurs during the update', async () => { const errorMsg = 'Error occurred during update'; - // FIXED: Used mockImplementationOnce so it doesn't instantly reject and crash Node 20 - mockFind.mockImplementationOnce(() => Promise.reject(errorMsg)); + mockFind.mockReturnValue(mockMongooseQuery(errorMsg, true)); const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); + await makeSut().updateOwnerMessage(mockReqDup, mockRes); - await flushPromises(); // FIXED: Force Jest to wait for the catch block - + await flushPromises(); + expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.send).toHaveBeenCalledWith(errorMsg); }); @@ -150,14 +163,14 @@ describe('ownerMessageController Unit Tests', () => { standardMessage: 'Standard message', save: mockSave, }; - mockFind.mockResolvedValue([existingMessage]); + mockFind.mockReturnValue(mockMongooseQuery([existingMessage])); const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); await deleteOwnerMessage(mockReqDup, mockRes); await flushPromises(); - + expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.send).toHaveBeenCalledWith({ _serverMessage: 'Delete successfully!', @@ -168,15 +181,14 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { const errorMsg = 'Error occurred during delete'; - // FIXED: Used mockImplementationOnce so it doesn't instantly reject and crash Node 20 - mockFind.mockImplementationOnce(() => Promise.reject(errorMsg)); + mockFind.mockReturnValue(mockMongooseQuery(errorMsg, true)); const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); await deleteOwnerMessage(mockReqDup, mockRes); - await flushPromises(); // FIXED: Force Jest to wait for the catch block - + await flushPromises(); + expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.send).toHaveBeenCalledWith(errorMsg); }); From 0403058df02eb5359f213153a13835333d37cdb9 Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Fri, 13 Mar 2026 16:25:30 -0700 Subject: [PATCH 07/16] fix: Fix ownerMessage controller mocks to support Mongoose sessions --- src/controllers/ownerMessageController.spec.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index 07e9eff18..766ba0862 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -1,6 +1,4 @@ /* eslint-disable import/order */ -/* eslint-disable object-shorthand */ -/* eslint-disable no-unused-vars */ const mongoose = require('mongoose'); jest.mock('../utilities/permissions', () => ({ @@ -22,17 +20,6 @@ const makeSut = () => { }; const flushPromises = () => new Promise(setImmediate); -// HELPER: Mocks Mongoose queries so they work seamlessly whether the controller -// uses a standard `await find()` OR a chained `await find().session()` -const mockMongooseQuery = (data, isReject = false) => ({ - session: jest.fn().mockImplementation(() => - isReject ? Promise.reject(data) : Promise.resolve(data) - ), - then: function (resolve, reject) { - return isReject ? reject(data) : resolve(data); - }, -}); - describe('ownerMessageController Unit Tests', () => { let mockFind; let mockSave; From 9efcfec659daa307f9fdd00282737a09cf94a8a9 Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Fri, 13 Mar 2026 16:40:38 -0700 Subject: [PATCH 08/16] fix: Fix ownerMessage controller mocks to support Mongoose sessions --- .../ownerMessageController.spec.js | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index 766ba0862..2e9db5fad 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -1,3 +1,4 @@ +/* eslint-disable arrow-body-style */ /* eslint-disable import/order */ const mongoose = require('mongoose'); @@ -20,6 +21,23 @@ const makeSut = () => { }; const flushPromises = () => new Promise(setImmediate); +// HELPER: Mocks Mongoose queries so they work seamlessly whether the controller +// uses a standard `await find()` OR a chained `await find().session()` +const mockMongooseQuery = (data, isReject = false) => { + return { + session: jest.fn().mockImplementation(() => + isReject ? Promise.reject(data) : Promise.resolve(data) + ), + then (resolve, reject) { + return isReject ? reject(data) : resolve(data); + }, + catch (reject) { + if (isReject) return reject(data); + return this; + } + }; +}; + describe('ownerMessageController Unit Tests', () => { let mockFind; let mockSave; @@ -30,13 +48,11 @@ describe('ownerMessageController Unit Tests', () => { }); beforeEach(() => { - // Merged: Uses the team's session mock structure - mockFind = jest.spyOn(OwnerMessage, 'find').mockReturnValue({ - session: jest.fn().mockResolvedValue([]), - }); + // Apply the flexible query mock + mockFind = jest.spyOn(OwnerMessage, 'find').mockReturnValue(mockMongooseQuery([])); mockSave = jest.fn().mockResolvedValue({}); - // Merged: Keeps your fix for the GitHub Actions database crash + // StartSession mock as a safety net for CI database disconnects mockSession = { startTransaction: jest.fn(), commitTransaction: jest.fn(), @@ -50,14 +66,14 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures getOwnerMessage returns status 404 if owner message cant be found', async () => { const { getOwnerMessage } = makeSut(); const errorMsg = 'Error occurred when finding owner message'; - mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); + mockFind.mockReturnValue(mockMongooseQuery(errorMsg, true)); const response = await getOwnerMessage(mockReq, mockRes); await flushPromises(); assertResMock(404, errorMsg, response, mockRes); }); test('Ensures getOwnerMessage returns status 200 with new owner message if none exist', async () => { - mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([]) }); + mockFind.mockReturnValue(mockMongooseQuery([])); const ownerMessageInstance = new OwnerMessage(); ownerMessageInstance.set = jest.fn(); const mockSaveFn = jest.fn().mockResolvedValue(ownerMessageInstance); @@ -81,7 +97,7 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures getOwnerMessage returns status 200 with the first owner message if it exists', async () => { const existingMessage = { message: 'Existing message', standardMessage: 'Standard message' }; - mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); + mockFind.mockReturnValue(mockMongooseQuery([existingMessage])); await makeSut().getOwnerMessage(mockReq, mockRes); await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(200); @@ -101,7 +117,7 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 201 and updates the owner message correctly with custom message', async () => { const existingMessage = { message: '', standardMessage: '', save: mockSave }; - mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); + mockFind.mockReturnValue(mockMongooseQuery([existingMessage])); const mockReqDup = { ...mockReq, body: { @@ -125,7 +141,7 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 500 if an error occurs during the update', async () => { const errorMsg = 'Error occurred during update'; - mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); + mockFind.mockReturnValue(mockMongooseQuery(errorMsg, true)); const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); @@ -153,7 +169,7 @@ describe('ownerMessageController Unit Tests', () => { standardMessage: 'Standard message', save: mockSave, }; - mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); + mockFind.mockReturnValue(mockMongooseQuery([existingMessage])); const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); @@ -171,7 +187,7 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { const errorMsg = 'Error occurred during delete'; - mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); + mockFind.mockReturnValue(mockMongooseQuery(errorMsg, true)); const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); From 88d3a56b69bff0dda1cc49b812a70974938408c5 Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Fri, 13 Mar 2026 17:01:43 -0700 Subject: [PATCH 09/16] fix: Fix ownerMessage controller mocks to support Mongoose sessions --- .../ownerMessageController.spec.js | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index 2e9db5fad..f9f46ed20 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -1,4 +1,3 @@ -/* eslint-disable arrow-body-style */ /* eslint-disable import/order */ const mongoose = require('mongoose'); @@ -21,21 +20,22 @@ const makeSut = () => { }; const flushPromises = () => new Promise(setImmediate); -// HELPER: Mocks Mongoose queries so they work seamlessly whether the controller -// uses a standard `await find()` OR a chained `await find().session()` +// HELPER: Fully chainable Mongoose mock. Safely handles .session(), .exec(), and basic awaits. const mockMongooseQuery = (data, isReject = false) => { - return { - session: jest.fn().mockImplementation(() => + const queryObj = { + session: jest.fn().mockReturnThis(), + exec: jest.fn().mockImplementation(() => isReject ? Promise.reject(data) : Promise.resolve(data) ), then (resolve, reject) { return isReject ? reject(data) : resolve(data); }, - catch (reject) { - if (isReject) return reject(data); + catch (rejectFn) { + if (isReject) return rejectFn(data); return this; } }; + return queryObj; }; describe('ownerMessageController Unit Tests', () => { @@ -48,16 +48,15 @@ describe('ownerMessageController Unit Tests', () => { }); beforeEach(() => { - // Apply the flexible query mock mockFind = jest.spyOn(OwnerMessage, 'find').mockReturnValue(mockMongooseQuery([])); mockSave = jest.fn().mockResolvedValue({}); - // StartSession mock as a safety net for CI database disconnects + // Bulletproof session mock: Every transaction method safely resolves as a promise mockSession = { - startTransaction: jest.fn(), - commitTransaction: jest.fn(), - abortTransaction: jest.fn(), - endSession: jest.fn(), + startTransaction: jest.fn().mockResolvedValue(), + commitTransaction: jest.fn().mockResolvedValue(), + abortTransaction: jest.fn().mockResolvedValue(), + endSession: jest.fn().mockResolvedValue(), }; jest.spyOn(mongoose, 'startSession').mockResolvedValue(mockSession); }); @@ -156,9 +155,10 @@ describe('ownerMessageController Unit Tests', () => { describe('deleteOwnerMessage', () => { test('Ensures deleteOwnerMessage returns status 403 if requestor is not an owner', async () => { const { deleteOwnerMessage } = makeSut(); - const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'notOwner' } } }; + // Reverted to original direct mutation style to prevent stripping express methods + mockReq.body.requestor = { role: 'notOwner' }; helper.hasPermission.mockResolvedValue(false); - const response = await deleteOwnerMessage(mockReqDup, mockRes); + const response = await deleteOwnerMessage(mockReq, mockRes); await flushPromises(); assertResMock(403, 'You are not authorized to delete messages!', response, mockRes); }); @@ -170,11 +170,11 @@ describe('ownerMessageController Unit Tests', () => { save: mockSave, }; mockFind.mockReturnValue(mockMongooseQuery([existingMessage])); - const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; + mockReq.body.requestor = { role: 'Owner' }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); - await deleteOwnerMessage(mockReqDup, mockRes); + await deleteOwnerMessage(mockReq, mockRes); await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(200); @@ -188,11 +188,11 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { const errorMsg = 'Error occurred during delete'; mockFind.mockReturnValue(mockMongooseQuery(errorMsg, true)); - const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; + mockReq.body.requestor = { role: 'Owner' }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); - await deleteOwnerMessage(mockReqDup, mockRes); + await deleteOwnerMessage(mockReq, mockRes); await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(500); From 69a4d74a58b0f144938613aa0352e7172c8f1712 Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Fri, 13 Mar 2026 17:09:21 -0700 Subject: [PATCH 10/16] fix: Fix ownerMessage controller mocks to support Mongoose sessions --- .../ownerMessageController.spec.js | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index f9f46ed20..d95390dab 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -1,3 +1,6 @@ +/* eslint-disable no-return-await */ +/* eslint-disable object-shorthand */ +/* eslint-disable arrow-body-style */ /* eslint-disable import/order */ const mongoose = require('mongoose'); @@ -20,14 +23,13 @@ const makeSut = () => { }; const flushPromises = () => new Promise(setImmediate); -// HELPER: Fully chainable Mongoose mock. Safely handles .session(), .exec(), and basic awaits. const mockMongooseQuery = (data, isReject = false) => { - const queryObj = { + return { session: jest.fn().mockReturnThis(), exec: jest.fn().mockImplementation(() => isReject ? Promise.reject(data) : Promise.resolve(data) ), - then (resolve, reject) { + then: function (resolve, reject) { return isReject ? reject(data) : resolve(data); }, catch (rejectFn) { @@ -35,7 +37,6 @@ const mockMongooseQuery = (data, isReject = false) => { return this; } }; - return queryObj; }; describe('ownerMessageController Unit Tests', () => { @@ -51,12 +52,13 @@ describe('ownerMessageController Unit Tests', () => { mockFind = jest.spyOn(OwnerMessage, 'find').mockReturnValue(mockMongooseQuery([])); mockSave = jest.fn().mockResolvedValue({}); - // Bulletproof session mock: Every transaction method safely resolves as a promise + // Added withTransaction to support Mongoose write operations mockSession = { startTransaction: jest.fn().mockResolvedValue(), commitTransaction: jest.fn().mockResolvedValue(), abortTransaction: jest.fn().mockResolvedValue(), endSession: jest.fn().mockResolvedValue(), + withTransaction: jest.fn().mockImplementation(async (cb) => await cb()), }; jest.spyOn(mongoose, 'startSession').mockResolvedValue(mockSession); }); @@ -108,8 +110,8 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 403 if requestor is not an owner', async () => { const { updateOwnerMessage } = makeSut(); helper.hasPermission.mockResolvedValue(false); - const req = { body: { requestor: { role: 'User' } } }; - const response = await updateOwnerMessage(req, mockRes); + const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'User' } } }; + const response = await updateOwnerMessage(mockReqDup, mockRes); await flushPromises(); assertResMock(403, 'You are not authorized to create messages!', response, mockRes); }); @@ -155,10 +157,9 @@ describe('ownerMessageController Unit Tests', () => { describe('deleteOwnerMessage', () => { test('Ensures deleteOwnerMessage returns status 403 if requestor is not an owner', async () => { const { deleteOwnerMessage } = makeSut(); - // Reverted to original direct mutation style to prevent stripping express methods - mockReq.body.requestor = { role: 'notOwner' }; + const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'notOwner' } } }; helper.hasPermission.mockResolvedValue(false); - const response = await deleteOwnerMessage(mockReq, mockRes); + const response = await deleteOwnerMessage(mockReqDup, mockRes); await flushPromises(); assertResMock(403, 'You are not authorized to delete messages!', response, mockRes); }); @@ -170,11 +171,11 @@ describe('ownerMessageController Unit Tests', () => { save: mockSave, }; mockFind.mockReturnValue(mockMongooseQuery([existingMessage])); - mockReq.body.requestor = { role: 'Owner' }; + const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); - await deleteOwnerMessage(mockReq, mockRes); + await deleteOwnerMessage(mockReqDup, mockRes); await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(200); @@ -188,11 +189,11 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { const errorMsg = 'Error occurred during delete'; mockFind.mockReturnValue(mockMongooseQuery(errorMsg, true)); - mockReq.body.requestor = { role: 'Owner' }; + const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); - await deleteOwnerMessage(mockReq, mockRes); + await deleteOwnerMessage(mockReqDup, mockRes); await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(500); From 1e318cbbfeef59ed5a532042763b4392f521f128 Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Fri, 13 Mar 2026 17:16:02 -0700 Subject: [PATCH 11/16] fix: Fix ownerMessage controller mocks to support Mongoose sessions --- .../ownerMessageController.spec.js | 79 ++++++++----------- 1 file changed, 32 insertions(+), 47 deletions(-) diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index d95390dab..5e8169a87 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -1,6 +1,4 @@ /* eslint-disable no-return-await */ -/* eslint-disable object-shorthand */ -/* eslint-disable arrow-body-style */ /* eslint-disable import/order */ const mongoose = require('mongoose'); @@ -23,22 +21,6 @@ const makeSut = () => { }; const flushPromises = () => new Promise(setImmediate); -const mockMongooseQuery = (data, isReject = false) => { - return { - session: jest.fn().mockReturnThis(), - exec: jest.fn().mockImplementation(() => - isReject ? Promise.reject(data) : Promise.resolve(data) - ), - then: function (resolve, reject) { - return isReject ? reject(data) : resolve(data); - }, - catch (rejectFn) { - if (isReject) return rejectFn(data); - return this; - } - }; -}; - describe('ownerMessageController Unit Tests', () => { let mockFind; let mockSave; @@ -49,32 +31,37 @@ describe('ownerMessageController Unit Tests', () => { }); beforeEach(() => { - mockFind = jest.spyOn(OwnerMessage, 'find').mockReturnValue(mockMongooseQuery([])); + mockFind = jest.spyOn(OwnerMessage, 'find').mockReturnValue({ + session: jest.fn().mockResolvedValue([]), + }); mockSave = jest.fn().mockResolvedValue({}); - // Added withTransaction to support Mongoose write operations + // Bulletproof session mock containing withTransaction callback support mockSession = { startTransaction: jest.fn().mockResolvedValue(), commitTransaction: jest.fn().mockResolvedValue(), abortTransaction: jest.fn().mockResolvedValue(), endSession: jest.fn().mockResolvedValue(), - withTransaction: jest.fn().mockImplementation(async (cb) => await cb()), + withTransaction: jest.fn().mockImplementation(async (cb) => await cb(mockSession)), }; jest.spyOn(mongoose, 'startSession').mockResolvedValue(mockSession); + + // Reset mockReq body before every test to prevent bleeding + mockReq.body = {}; }); describe('getOwnerMessage', () => { test('Ensures getOwnerMessage returns status 404 if owner message cant be found', async () => { const { getOwnerMessage } = makeSut(); const errorMsg = 'Error occurred when finding owner message'; - mockFind.mockReturnValue(mockMongooseQuery(errorMsg, true)); + mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); const response = await getOwnerMessage(mockReq, mockRes); await flushPromises(); assertResMock(404, errorMsg, response, mockRes); }); test('Ensures getOwnerMessage returns status 200 with new owner message if none exist', async () => { - mockFind.mockReturnValue(mockMongooseQuery([])); + mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([]) }); const ownerMessageInstance = new OwnerMessage(); ownerMessageInstance.set = jest.fn(); const mockSaveFn = jest.fn().mockResolvedValue(ownerMessageInstance); @@ -98,7 +85,7 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures getOwnerMessage returns status 200 with the first owner message if it exists', async () => { const existingMessage = { message: 'Existing message', standardMessage: 'Standard message' }; - mockFind.mockReturnValue(mockMongooseQuery([existingMessage])); + mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); await makeSut().getOwnerMessage(mockReq, mockRes); await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(200); @@ -110,26 +97,24 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 403 if requestor is not an owner', async () => { const { updateOwnerMessage } = makeSut(); helper.hasPermission.mockResolvedValue(false); - const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'User' } } }; - const response = await updateOwnerMessage(mockReqDup, mockRes); + mockReq.body = { requestor: { role: 'User' } }; + const response = await updateOwnerMessage(mockReq, mockRes); await flushPromises(); assertResMock(403, 'You are not authorized to create messages!', response, mockRes); }); test('Ensures updateOwnerMessage returns status 201 and updates the owner message correctly with custom message', async () => { const existingMessage = { message: '', standardMessage: '', save: mockSave }; - mockFind.mockReturnValue(mockMongooseQuery([existingMessage])); - const mockReqDup = { - ...mockReq, - body: { - ...mockReq.body, - isStandard: false, - newMessage: 'New custom message', - requestor: { role: 'Owner' }, - }, + mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); + + mockReq.body = { + isStandard: false, + newMessage: 'New custom message', + requestor: { role: 'Owner' }, }; helper.hasPermission.mockResolvedValue(true); - await makeSut().updateOwnerMessage(mockReqDup, mockRes); + + await makeSut().updateOwnerMessage(mockReq, mockRes); await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(201); @@ -142,11 +127,11 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 500 if an error occurs during the update', async () => { const errorMsg = 'Error occurred during update'; - mockFind.mockReturnValue(mockMongooseQuery(errorMsg, true)); - const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; + mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); + mockReq.body = { requestor: { role: 'Owner' } }; helper.hasPermission.mockResolvedValue(true); - await makeSut().updateOwnerMessage(mockReqDup, mockRes); + await makeSut().updateOwnerMessage(mockReq, mockRes); await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(500); @@ -157,9 +142,9 @@ describe('ownerMessageController Unit Tests', () => { describe('deleteOwnerMessage', () => { test('Ensures deleteOwnerMessage returns status 403 if requestor is not an owner', async () => { const { deleteOwnerMessage } = makeSut(); - const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'notOwner' } } }; + mockReq.body = { requestor: { role: 'notOwner' } }; helper.hasPermission.mockResolvedValue(false); - const response = await deleteOwnerMessage(mockReqDup, mockRes); + const response = await deleteOwnerMessage(mockReq, mockRes); await flushPromises(); assertResMock(403, 'You are not authorized to delete messages!', response, mockRes); }); @@ -170,12 +155,12 @@ describe('ownerMessageController Unit Tests', () => { standardMessage: 'Standard message', save: mockSave, }; - mockFind.mockReturnValue(mockMongooseQuery([existingMessage])); - const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; + mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); + mockReq.body = { requestor: { role: 'Owner' } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); - await deleteOwnerMessage(mockReqDup, mockRes); + await deleteOwnerMessage(mockReq, mockRes); await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(200); @@ -188,12 +173,12 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { const errorMsg = 'Error occurred during delete'; - mockFind.mockReturnValue(mockMongooseQuery(errorMsg, true)); - const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; + mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); + mockReq.body = { requestor: { role: 'Owner' } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); - await deleteOwnerMessage(mockReqDup, mockRes); + await deleteOwnerMessage(mockReq, mockRes); await flushPromises(); expect(mockRes.status).toHaveBeenCalledWith(500); From c8e990676b9a36c1e831b0c7282e4feca9cc0ea4 Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Fri, 13 Mar 2026 17:39:50 -0700 Subject: [PATCH 12/16] fix: Fix ownerMessage controller mocks to support Mongoose sessions --- src/controllers/ownerMessageController.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index 5e8169a87..8f075b79c 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -1,4 +1,3 @@ -/* eslint-disable no-return-await */ /* eslint-disable import/order */ const mongoose = require('mongoose'); @@ -42,6 +41,7 @@ describe('ownerMessageController Unit Tests', () => { commitTransaction: jest.fn().mockResolvedValue(), abortTransaction: jest.fn().mockResolvedValue(), endSession: jest.fn().mockResolvedValue(), + // eslint-disable-next-line no-return-await withTransaction: jest.fn().mockImplementation(async (cb) => await cb(mockSession)), }; jest.spyOn(mongoose, 'startSession').mockResolvedValue(mockSession); From e65598e44cae6f28e4cd64e1632c85f9472a68ad Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Sun, 15 Mar 2026 13:46:34 -0700 Subject: [PATCH 13/16] fix: Fix ownerMessage controller mocks to support Mongoose sessions --- .../ownerMessageController.spec.js | 63 +++++++++++++------ 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index 8f075b79c..c6b8eeecd 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -4,7 +4,18 @@ const mongoose = require('mongoose'); jest.mock('../utilities/permissions', () => ({ hasPermission: jest.fn(), })); + +jest.mock('../models/userProfile', () => ({ + findById: jest.fn(), +})); + +jest.mock('../models/ownerMessageLog', () => ({ + create: jest.fn(), +})); + const helper = require('../utilities/permissions'); +const UserProfile = require('../models/userProfile'); +const OwnerMessageLog = require('../models/ownerMessageLog'); const OwnerMessage = require('../models/ownerMessage'); const { mockReq, mockRes, assertResMock } = require('../test'); const ownerMessageController = require('./ownerMessageController'); @@ -18,6 +29,7 @@ const makeSut = () => { deleteOwnerMessage, }; }; + const flushPromises = () => new Promise(setImmediate); describe('ownerMessageController Unit Tests', () => { @@ -30,22 +42,30 @@ describe('ownerMessageController Unit Tests', () => { }); beforeEach(() => { - mockFind = jest.spyOn(OwnerMessage, 'find').mockReturnValue({ - session: jest.fn().mockResolvedValue([]), - }); mockSave = jest.fn().mockResolvedValue({}); - // Bulletproof session mock containing withTransaction callback support + // Mock session with manual transaction flow (matching controller) mockSession = { startTransaction: jest.fn().mockResolvedValue(), commitTransaction: jest.fn().mockResolvedValue(), abortTransaction: jest.fn().mockResolvedValue(), endSession: jest.fn().mockResolvedValue(), - // eslint-disable-next-line no-return-await - withTransaction: jest.fn().mockImplementation(async (cb) => await cb(mockSession)), }; jest.spyOn(mongoose, 'startSession').mockResolvedValue(mockSession); + // Mock UserProfile.findById to return a fake requestor + UserProfile.findById.mockResolvedValue({ + email: 'test@test.com', + firstName: 'John', + lastName: 'Doe', + }); + + // Mock OwnerMessageLog.create to do nothing + OwnerMessageLog.create.mockResolvedValue({}); + + // Default find mock — no session chaining needed (controller calls find({}) directly) + mockFind = jest.spyOn(OwnerMessage, 'find').mockResolvedValue([]); + // Reset mockReq body before every test to prevent bleeding mockReq.body = {}; }); @@ -54,19 +74,18 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures getOwnerMessage returns status 404 if owner message cant be found', async () => { const { getOwnerMessage } = makeSut(); const errorMsg = 'Error occurred when finding owner message'; - mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); + mockFind.mockRejectedValue(errorMsg); const response = await getOwnerMessage(mockReq, mockRes); await flushPromises(); assertResMock(404, errorMsg, response, mockRes); }); test('Ensures getOwnerMessage returns status 200 with new owner message if none exist', async () => { - mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([]) }); + mockFind.mockResolvedValue([]); const ownerMessageInstance = new OwnerMessage(); - ownerMessageInstance.set = jest.fn(); const mockSaveFn = jest.fn().mockResolvedValue(ownerMessageInstance); - jest.spyOn(OwnerMessage.prototype, 'save').mockImplementation(mockSaveFn); + await makeSut().getOwnerMessage(mockReq, mockRes); await flushPromises(); @@ -85,9 +104,11 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures getOwnerMessage returns status 200 with the first owner message if it exists', async () => { const existingMessage = { message: 'Existing message', standardMessage: 'Standard message' }; - mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); + mockFind.mockResolvedValue([existingMessage]); + await makeSut().getOwnerMessage(mockReq, mockRes); await flushPromises(); + expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.send).toHaveBeenCalledWith({ ownerMessage: existingMessage }); }); @@ -105,15 +126,15 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 201 and updates the owner message correctly with custom message', async () => { const existingMessage = { message: '', standardMessage: '', save: mockSave }; - mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); - + mockFind.mockResolvedValue([existingMessage]); + mockReq.body = { isStandard: false, newMessage: 'New custom message', - requestor: { role: 'Owner' }, + requestor: { role: 'Owner', requestorId: 'requestorId123' }, }; helper.hasPermission.mockResolvedValue(true); - + await makeSut().updateOwnerMessage(mockReq, mockRes); await flushPromises(); @@ -123,11 +144,12 @@ describe('ownerMessageController Unit Tests', () => { ownerMessage: { standardMessage: '', message: 'New custom message' }, }); expect(mockSave).toHaveBeenCalled(); + expect(mockSession.commitTransaction).toHaveBeenCalled(); }); test('Ensures updateOwnerMessage returns status 500 if an error occurs during the update', async () => { const errorMsg = 'Error occurred during update'; - mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); + mockFind.mockRejectedValue(errorMsg); mockReq.body = { requestor: { role: 'Owner' } }; helper.hasPermission.mockResolvedValue(true); @@ -136,6 +158,7 @@ describe('ownerMessageController Unit Tests', () => { expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.send).toHaveBeenCalledWith(errorMsg); + expect(mockSession.abortTransaction).toHaveBeenCalled(); }); }); @@ -155,8 +178,8 @@ describe('ownerMessageController Unit Tests', () => { standardMessage: 'Standard message', save: mockSave, }; - mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); - mockReq.body = { requestor: { role: 'Owner' } }; + mockFind.mockResolvedValue([existingMessage]); + mockReq.body = { requestor: { role: 'Owner', requestorId: 'requestorId123' } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); @@ -169,11 +192,12 @@ describe('ownerMessageController Unit Tests', () => { ownerMessage: existingMessage, }); expect(mockSave).toHaveBeenCalled(); + expect(mockSession.commitTransaction).toHaveBeenCalled(); }); test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { const errorMsg = 'Error occurred during delete'; - mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); + mockFind.mockRejectedValue(errorMsg); mockReq.body = { requestor: { role: 'Owner' } }; helper.hasPermission.mockResolvedValue(true); @@ -183,6 +207,7 @@ describe('ownerMessageController Unit Tests', () => { expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.send).toHaveBeenCalledWith(errorMsg); + expect(mockSession.abortTransaction).toHaveBeenCalled(); }); }); }); \ No newline at end of file From 86881a69ca352e3bdf4a8a29d518ec9828b9c8ad Mon Sep 17 00:00:00 2001 From: Mohammed Taariq Amin Mansurie Date: Sun, 15 Mar 2026 13:55:12 -0700 Subject: [PATCH 14/16] fix: Fix ownerMessage controller mocks to support Mongoose sessions --- .../ownerMessageController.spec.js | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index c6b8eeecd..3eaac0c94 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -23,11 +23,7 @@ const ownerMessageController = require('./ownerMessageController'); const makeSut = () => { const { getOwnerMessage, updateOwnerMessage, deleteOwnerMessage } = ownerMessageController(OwnerMessage); - return { - getOwnerMessage, - updateOwnerMessage, - deleteOwnerMessage, - }; + return { getOwnerMessage, updateOwnerMessage, deleteOwnerMessage }; }; const flushPromises = () => new Promise(setImmediate); @@ -44,7 +40,6 @@ describe('ownerMessageController Unit Tests', () => { beforeEach(() => { mockSave = jest.fn().mockResolvedValue({}); - // Mock session with manual transaction flow (matching controller) mockSession = { startTransaction: jest.fn().mockResolvedValue(), commitTransaction: jest.fn().mockResolvedValue(), @@ -53,20 +48,19 @@ describe('ownerMessageController Unit Tests', () => { }; jest.spyOn(mongoose, 'startSession').mockResolvedValue(mockSession); - // Mock UserProfile.findById to return a fake requestor UserProfile.findById.mockResolvedValue({ email: 'test@test.com', firstName: 'John', lastName: 'Doe', }); - // Mock OwnerMessageLog.create to do nothing OwnerMessageLog.create.mockResolvedValue({}); - // Default find mock — no session chaining needed (controller calls find({}) directly) - mockFind = jest.spyOn(OwnerMessage, 'find').mockResolvedValue([]); + // Default mock supports .session() chaining — needed by update and delete + mockFind = jest.spyOn(OwnerMessage, 'find').mockReturnValue({ + session: jest.fn().mockResolvedValue([]), + }); - // Reset mockReq body before every test to prevent bleeding mockReq.body = {}; }); @@ -74,6 +68,7 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures getOwnerMessage returns status 404 if owner message cant be found', async () => { const { getOwnerMessage } = makeSut(); const errorMsg = 'Error occurred when finding owner message'; + // getOwnerMessage uses find({}) with NO .session() — override with direct rejection mockFind.mockRejectedValue(errorMsg); const response = await getOwnerMessage(mockReq, mockRes); await flushPromises(); @@ -81,6 +76,7 @@ describe('ownerMessageController Unit Tests', () => { }); test('Ensures getOwnerMessage returns status 200 with new owner message if none exist', async () => { + // getOwnerMessage uses find({}) with NO .session() — override with direct resolution mockFind.mockResolvedValue([]); const ownerMessageInstance = new OwnerMessage(); const mockSaveFn = jest.fn().mockResolvedValue(ownerMessageInstance); @@ -104,6 +100,7 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures getOwnerMessage returns status 200 with the first owner message if it exists', async () => { const existingMessage = { message: 'Existing message', standardMessage: 'Standard message' }; + // getOwnerMessage uses find({}) with NO .session() — override with direct resolution mockFind.mockResolvedValue([existingMessage]); await makeSut().getOwnerMessage(mockReq, mockRes); @@ -126,7 +123,8 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 201 and updates the owner message correctly with custom message', async () => { const existingMessage = { message: '', standardMessage: '', save: mockSave }; - mockFind.mockResolvedValue([existingMessage]); + // updateOwnerMessage uses find({}).session() — use .session() chaining mock + mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); mockReq.body = { isStandard: false, @@ -149,7 +147,8 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 500 if an error occurs during the update', async () => { const errorMsg = 'Error occurred during update'; - mockFind.mockRejectedValue(errorMsg); + // updateOwnerMessage uses find({}).session() — reject inside .session() + mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); mockReq.body = { requestor: { role: 'Owner' } }; helper.hasPermission.mockResolvedValue(true); @@ -178,7 +177,8 @@ describe('ownerMessageController Unit Tests', () => { standardMessage: 'Standard message', save: mockSave, }; - mockFind.mockResolvedValue([existingMessage]); + // deleteOwnerMessage uses find({}).session() — use .session() chaining mock + mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); mockReq.body = { requestor: { role: 'Owner', requestorId: 'requestorId123' } }; helper.hasPermission.mockResolvedValue(true); @@ -197,7 +197,8 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { const errorMsg = 'Error occurred during delete'; - mockFind.mockRejectedValue(errorMsg); + // deleteOwnerMessage uses find({}).session() — reject inside .session() + mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); mockReq.body = { requestor: { role: 'Owner' } }; helper.hasPermission.mockResolvedValue(true); From 9f02f75fa33b3a6e24c89b1a1e37f6f3152ec813 Mon Sep 17 00:00:00 2001 From: sphurthy Date: Fri, 17 Apr 2026 17:41:51 -0400 Subject: [PATCH 15/16] fix: resolved conflicts --- package-lock.json | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index b63c9c9b5..6cd53232c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,7 @@ "@types/node": "^8.10.61", "@types/supertest": "^6.0.2", "babel-jest": "^29.7.0", + "baseline-browser-mapping": "^2.10.19", "eslint": "^8.47.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", diff --git a/package.json b/package.json index a4e5a61a9..3926847e2 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@types/node": "^8.10.61", "@types/supertest": "^6.0.2", "babel-jest": "^29.7.0", + "baseline-browser-mapping": "^2.10.19", "eslint": "^8.47.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", From 4277caa7fd08bb169a4198fcbabda95e80842980 Mon Sep 17 00:00:00 2001 From: sphurthy Date: Fri, 17 Apr 2026 23:35:19 -0400 Subject: [PATCH 16/16] fix: package-lock.json to sync with package.json --- package-lock.json | 123 ++++++++++++++++------------------------------ 1 file changed, 42 insertions(+), 81 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6cd53232c..48b9a33d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1172,6 +1172,7 @@ "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -1233,6 +1234,7 @@ "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -1421,6 +1423,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -4523,6 +4526,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -5665,6 +5669,7 @@ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "form-data": "^4.0.4" @@ -5853,6 +5858,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5884,6 +5890,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7074,6 +7081,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -8106,6 +8114,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -8308,7 +8317,8 @@ "version": "0.0.1595872", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1595872.tgz", "integrity": "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dezalgo": { "version": "1.0.4", @@ -8966,6 +8976,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9169,6 +9180,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9226,6 +9238,7 @@ "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -9256,6 +9269,7 @@ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -9289,6 +9303,7 @@ "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -14467,6 +14482,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/log-update/node_modules/slice-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", @@ -14484,6 +14506,24 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-update/node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -18042,12 +18082,6 @@ "memory-pager": "^1.0.2" } }, - "node_modules/sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "license": "BSD-3-Clause" - }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -19612,80 +19646,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -19735,6 +19695,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" },