From a02baf157352f65ed4e01b27fadc39929a6f3a51 Mon Sep 17 00:00:00 2001 From: anuj chhikara Date: Sun, 18 Jan 2026 21:12:36 +0530 Subject: [PATCH 1/4] feat: add application feedback submission functionality --- constants/application.ts | 10 ++- controllers/applications.ts | 41 ++++++++++-- middlewares/validators/application.ts | 41 ++++++++---- models/applications.ts | 62 +++++++++++++++++-- routes/applications.ts | 4 +- test/integration/application.test.ts | 18 +++--- .../middlewares/application-validator.test.ts | 2 +- 7 files changed, 140 insertions(+), 38 deletions(-) diff --git a/constants/application.ts b/constants/application.ts index 8ff925b6d..9cd9b6398 100644 --- a/constants/application.ts +++ b/constants/application.ts @@ -18,6 +18,7 @@ const API_RESPONSE_MESSAGES = { APPLICATION_CREATED_SUCCESS: "Application created successfully", APPLICATION_RETURN_SUCCESS: "Applications returned successfully", NUDGE_SUCCESS: "Nudge sent successfully", + FEEDBACK_SUBMITTED_SUCCESS: "Application feedback submitted successfully", }; const APPLICATION_ERROR_MESSAGES = { @@ -26,7 +27,11 @@ const APPLICATION_ERROR_MESSAGES = { NUDGE_ONLY_PENDING_ALLOWED: "Nudge unavailable. Only pending applications can be nudged.", }; -const NUDGE_APPLICATION_STATUS = { +const APPLICATION_LOG_MESSAGES = { + ERROR_SUBMITTING_FEEDBACK: "Error while submitting the application feedback", +}; + +const APPLICATION_STATUS = { notFound: "notFound", unauthorized: "unauthorized", notPending: "notPending", @@ -45,6 +50,7 @@ module.exports = { APPLICATION_ROLES, API_RESPONSE_MESSAGES, APPLICATION_ERROR_MESSAGES, + APPLICATION_LOG_MESSAGES, APPLICATION_REVIEW_CYCLE_START_DATE, - NUDGE_APPLICATION_STATUS, + APPLICATION_STATUS }; diff --git a/controllers/applications.ts b/controllers/applications.ts index 12beb699d..169ba9db4 100644 --- a/controllers/applications.ts +++ b/controllers/applications.ts @@ -3,7 +3,7 @@ const { logType } = require("../constants/logs"); import { CustomRequest, CustomResponse } from "../types/global"; const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); const ApplicationModel = require("../models/applications"); -const { API_RESPONSE_MESSAGES, APPLICATION_ERROR_MESSAGES, NUDGE_APPLICATION_STATUS } = require("../constants/application"); +const { API_RESPONSE_MESSAGES, APPLICATION_ERROR_MESSAGES, APPLICATION_LOG_MESSAGES, APPLICATION_STATUS } = require("../constants/application"); const { createApplicationService } = require("../services/applicationService"); const { Conflict } = require("http-errors"); const logger = require("../utils/logger"); @@ -131,6 +131,34 @@ const updateApplication = async (req: CustomRequest, res: CustomResponse) => { } }; +const submitApplicationFeedback = async (req: CustomRequest, res: CustomResponse) => { + try { + const { applicationId } = req.params; + const { status, feedback } = req.body; + + const addApplicationFeedbackResult = await ApplicationModel.addApplicationFeedback({ + applicationId, + status, + feedback, + reviewerName: req.userData.username, + }); + + switch (addApplicationFeedbackResult.status) { + case APPLICATION_STATUS.notFound: + return res.boom.notFound("Application not found"); + case APPLICATION_STATUS.success: + return res.json({ + message: API_RESPONSE_MESSAGES.FEEDBACK_SUBMITTED_SUCCESS, + }); + default: + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } + } catch (err) { + logger.error(`${APPLICATION_LOG_MESSAGES.ERROR_SUBMITTING_FEEDBACK}: ${err}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + const getApplicationById = async (req: CustomRequest, res: CustomResponse) => { try { const { applicationId } = req.params; @@ -160,15 +188,15 @@ const nudgeApplication = async (req: CustomRequest, res: CustomResponse) => { }); switch (result.status) { - case NUDGE_APPLICATION_STATUS.notFound: + case APPLICATION_STATUS.notFound: return res.boom.notFound("Application not found"); - case NUDGE_APPLICATION_STATUS.unauthorized: + case APPLICATION_STATUS.unauthorized: return res.boom.unauthorized("You are not authorized to nudge this application"); - case NUDGE_APPLICATION_STATUS.notPending: + case APPLICATION_STATUS.notPending: return res.boom.badRequest(APPLICATION_ERROR_MESSAGES.NUDGE_ONLY_PENDING_ALLOWED); - case NUDGE_APPLICATION_STATUS.tooSoon: + case APPLICATION_STATUS.tooSoon: return res.boom.tooManyRequests(APPLICATION_ERROR_MESSAGES.NUDGE_TOO_SOON); - case NUDGE_APPLICATION_STATUS.success: + case APPLICATION_STATUS.success: return res.json({ message: API_RESPONSE_MESSAGES.NUDGE_SUCCESS, nudgeCount: result.nudgeCount, @@ -189,4 +217,5 @@ module.exports = { updateApplication, getApplicationById, nudgeApplication, + submitApplicationFeedback, }; diff --git a/middlewares/validators/application.ts b/middlewares/validators/application.ts index 746adfdf5..bd8c79d97 100644 --- a/middlewares/validators/application.ts +++ b/middlewares/validators/application.ts @@ -71,21 +71,38 @@ const validateApplicationData = async (req: CustomRequest, res: CustomResponse, const validateApplicationUpdateData = async (req: CustomRequest, res: CustomResponse, next: NextFunction) => { const schema = joi - .object() - .strict() - .keys({ - status: joi + .object({ + status: joi + .string() + .valid( + APPLICATION_STATUS_TYPES.ACCEPTED, + APPLICATION_STATUS_TYPES.REJECTED, + APPLICATION_STATUS_TYPES.CHANGES_REQUESTED + ) + .required() + .messages({ + "any.required": "Status is required", + "any.only": + "Status must be one of: accepted, rejected, or changes_requested", + }), + + feedback: joi.when("status", { + is: APPLICATION_STATUS_TYPES.CHANGES_REQUESTED, + then: joi .string() .min(1) - .optional() - .custom((value, helper) => { - if (!Object.values(APPLICATION_STATUS_TYPES).includes(value)) { - return helper.message("Status is not valid"); - } - return value; + .required() + .messages({ + "any.required": + "Feedback is required when status is changes_requested", + "string.min": + "Feedback cannot be empty when status is changes_requested", }), - feedback: joi.string().min(1).optional(), - }); + otherwise: joi.string().optional().allow(""), + }), + }) + .strict(); + try { await schema.validateAsync(req.body); diff --git a/models/applications.ts b/models/applications.ts index 402d9d9c3..636178460 100644 --- a/models/applications.ts +++ b/models/applications.ts @@ -2,7 +2,7 @@ import { application } from "../types/application"; const firestore = require("../utils/firestore"); const logger = require("../utils/logger"); const ApplicationsModel = firestore.collection("applicants"); -const { APPLICATION_STATUS_TYPES, NUDGE_APPLICATION_STATUS } = require("../constants/application"); +const { APPLICATION_STATUS_TYPES, APPLICATION_STATUS } = require("../constants/application"); const { convertDaysToMilliseconds } = require("../utils/time"); const getAllApplications = async (limit: number, lastDocId?: string) => { @@ -146,17 +146,17 @@ const nudgeApplication = async ({ applicationId, userId }: { applicationId: stri const applicationDoc = await transaction.get(applicationRef); if (!applicationDoc.exists) { - return { status: NUDGE_APPLICATION_STATUS.notFound }; + return { status: APPLICATION_STATUS.notFound }; } const application = applicationDoc.data(); if (application.userId !== userId) { - return { status: NUDGE_APPLICATION_STATUS.unauthorized }; + return { status: APPLICATION_STATUS.unauthorized }; } if (application.status !== APPLICATION_STATUS_TYPES.PENDING) { - return { status: NUDGE_APPLICATION_STATUS.notPending }; + return { status: APPLICATION_STATUS.notPending }; } const lastNudgeAt = application.lastNudgeAt; @@ -165,7 +165,7 @@ const nudgeApplication = async ({ applicationId, userId }: { applicationId: stri const timeDifference = currentTime - lastNudgeTimestamp; if (timeDifference <= twentyFourHoursInMilliseconds) { - return { status: NUDGE_APPLICATION_STATUS.tooSoon }; + return { status: APPLICATION_STATUS.tooSoon }; } } @@ -179,7 +179,7 @@ const nudgeApplication = async ({ applicationId, userId }: { applicationId: stri }); return { - status: NUDGE_APPLICATION_STATUS.success, + status: APPLICATION_STATUS.success, nudgeCount: updatedNudgeCount, lastNudgeAt: newLastNudgeAt, }; @@ -188,6 +188,55 @@ const nudgeApplication = async ({ applicationId, userId }: { applicationId: stri return result; }; +const addApplicationFeedback = async ({ + applicationId, + status, + feedback, + reviewerName, +}: { + applicationId: string; + status: string; + feedback?: string; + reviewerName: string; +}) => { + const addApplicationFeedbackResult = await firestore.runTransaction(async (transaction) => { + const applicationRef = ApplicationsModel.doc(applicationId); + const applicationDoc = await transaction.get(applicationRef); + + if (!applicationDoc.exists) { + return { status: APPLICATION_STATUS.notFound }; + } + + const application = applicationDoc.data(); + const existingFeedback = application.feedback || []; + + const feedbackItem: { + status: string; + feedback?: string; + reviewerName: string; + createdAt: string; + } = { + status, + reviewerName, + createdAt: new Date().toISOString(), + }; + + if (feedback && feedback.trim()) { + feedbackItem.feedback = feedback.trim(); + } + + const updatedFeedback = [...existingFeedback, feedbackItem]; + + transaction.update(applicationRef, { + feedback: updatedFeedback, + status, + }); + + return { status: APPLICATION_STATUS.success }; + }); + return addApplicationFeedbackResult; + }; + module.exports = { getAllApplications, getUserApplications, @@ -196,4 +245,5 @@ module.exports = { getApplicationsBasedOnStatus, getApplicationById, nudgeApplication, + addApplicationFeedback, }; diff --git a/routes/applications.ts b/routes/applications.ts index bb4e80915..2b309d883 100644 --- a/routes/applications.ts +++ b/routes/applications.ts @@ -18,11 +18,11 @@ router.get( router.get("/:applicationId", authenticate, authorizeRoles([SUPERUSER]), applications.getApplicationById); router.post("/", authenticate, applicationValidator.validateApplicationData, applications.addApplication); router.patch( - "/:applicationId", + "/:applicationId/feedback", authenticate, authorizeRoles([SUPERUSER]), applicationValidator.validateApplicationUpdateData, - applications.updateApplication + applications.submitApplicationFeedback ); router.patch("/:applicationId/nudge", authenticate, applications.nudgeApplication); diff --git a/test/integration/application.test.ts b/test/integration/application.test.ts index 0b9dcbdd0..e38ae2a07 100644 --- a/test/integration/application.test.ts +++ b/test/integration/application.test.ts @@ -356,11 +356,11 @@ describe("Application", function () { }); }); - describe("PATCH /application/:applicationId", function () { - it("should return 200 if the user is super user and application is updated", function (done) { + describe("PATCH /applications/:applicationId/feedback", function () { + it("should return 200 if the user is super user and application feedback is submitted", function (done) { chai .request(app) - .patch(`/applications/${applicationId1}`) + .patch(`/applications/${applicationId1}/feedback`) .set("cookie", `${cookieName}=${superUserJwt}`) .send({ status: "accepted", @@ -371,7 +371,7 @@ describe("Application", function () { } expect(res).to.have.status(200); - expect(res.body.message).to.be.equal("Application updated successfully!"); + expect(res.body.message).to.be.equal("Application feedback submitted successfully"); return done(); }); }); @@ -379,7 +379,7 @@ describe("Application", function () { it("should return 401 if the user is not super user", function (done) { chai .request(app) - .patch(`/applications/${applicationId1}`) + .patch(`/applications/${applicationId1}/feedback`) .set("cookie", `${cookieName}=${jwt}`) .send({ status: "accepted", @@ -398,7 +398,7 @@ describe("Application", function () { it("should return 400 if anything other than status and feedback is passed in the body", function (done) { chai .request(app) - .patch(`/applications/${applicationId1}`) + .patch(`/applications/${applicationId1}/feedback`) .set("cookie", `${cookieName}=${superUserJwt}`) .send({ status: "accepted", @@ -416,10 +416,10 @@ describe("Application", function () { }); }); - it("should return 400 if any status other than accepted, reject or pending is passed", function (done) { + it("should return 400 if any status other than accepted, rejected or changes_requested is passed", function (done) { chai .request(app) - .patch(`/applications/${applicationId1}`) + .patch(`/applications/${applicationId1}/feedback`) .set("cookie", `${cookieName}=${superUserJwt}`) .send({ status: "something", @@ -431,7 +431,7 @@ describe("Application", function () { expect(res).to.have.status(400); expect(res.body.error).to.be.equal("Bad Request"); - expect(res.body.message).to.be.equal("Status is not valid"); + expect(res.body.message).to.be.equal("Status must be one of: accepted, rejected, or changes_requested"); return done(); }); }); diff --git a/test/unit/middlewares/application-validator.test.ts b/test/unit/middlewares/application-validator.test.ts index 943e70b39..fc08ad2c4 100644 --- a/test/unit/middlewares/application-validator.test.ts +++ b/test/unit/middlewares/application-validator.test.ts @@ -92,7 +92,7 @@ describe("application validator test", function () { it("should call next function if only status and feedback is passed, and status has any of the allowed values", async function () { const req = { body: { - status: "pending", + status: "accepted", feedback: "some feedback", }, }; From 79721d92398844adc3786a1c7f9236aa89dd8397 Mon Sep 17 00:00:00 2001 From: anuj chhikara Date: Mon, 19 Jan 2026 23:29:52 +0530 Subject: [PATCH 2/4] feat: enhance application feedback functionality with comprehensive tests --- test/integration/application.test.ts | 178 +++++++++++++++++ test/unit/controllers/applications.test.ts | 146 +++++++++++++- .../middlewares/application-validator.test.ts | 150 ++++++++++++++ test/unit/models/application.test.ts | 188 ++++++++++++++++++ 4 files changed, 654 insertions(+), 8 deletions(-) diff --git a/test/integration/application.test.ts b/test/integration/application.test.ts index e38ae2a07..703805f2e 100644 --- a/test/integration/application.test.ts +++ b/test/integration/application.test.ts @@ -435,6 +435,184 @@ describe("Application", function () { return done(); }); }); + + it("should return 200 when submitting feedback with status rejected", function (done) { + chai + .request(app) + .patch(`/applications/${applicationId2}/feedback`) + .set("cookie", `${cookieName}=${superUserJwt}`) + .send({ + status: "rejected", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Application feedback submitted successfully"); + return done(); + }); + }); + + it("should return 200 when submitting feedback with status changes_requested and feedback text", function (done) { + chai + .request(app) + .patch(`/applications/${applicationId3}/feedback`) + .set("cookie", `${cookieName}=${superUserJwt}`) + .send({ + status: "changes_requested", + feedback: "Please update your skills section with more details", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Application feedback submitted successfully"); + return done(); + }); + }); + + it("should return 400 when status is changes_requested without feedback", function (done) { + chai + .request(app) + .patch(`/applications/${applicationId1}/feedback`) + .set("cookie", `${cookieName}=${superUserJwt}`) + .send({ + status: "changes_requested", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + expect(res.body.error).to.be.equal("Bad Request"); + expect(res.body.message).to.include("Feedback is required when status is changes_requested"); + return done(); + }); + }); + + it("should return 200 when submitting feedback with status accepted and optional feedback text", function (done) { + chai + .request(app) + .patch(`/applications/${applicationId4}/feedback`) + .set("cookie", `${cookieName}=${superUserJwt}`) + .send({ + status: "accepted", + feedback: "Great application!", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Application feedback submitted successfully"); + return done(); + }); + }); + + it("should return 200 when submitting feedback with status rejected and optional feedback text", function (done) { + chai + .request(app) + .patch(`/applications/${applicationId5}/feedback`) + .set("cookie", `${cookieName}=${superUserJwt}`) + .send({ + status: "rejected", + feedback: "Not a good fit for this role", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Application feedback submitted successfully"); + return done(); + }); + }); + + it("should return 200 when submitting feedback with status accepted and empty feedback string", function (done) { + chai + .request(app) + .patch(`/applications/${applicationId2}/feedback`) + .set("cookie", `${cookieName}=${superUserJwt}`) + .send({ + status: "accepted", + feedback: "", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Application feedback submitted successfully"); + return done(); + }); + }); + + it("should return 404 when application does not exist", function (done) { + chai + .request(app) + .patch(`/applications/non-existent-application-id/feedback`) + .set("cookie", `${cookieName}=${superUserJwt}`) + .send({ + status: "accepted", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(404); + expect(res.body.error).to.be.equal("Not Found"); + expect(res.body.message).to.be.equal("Application not found"); + return done(); + }); + }); + + it("should return 401 when user is not authenticated", function (done) { + chai + .request(app) + .patch(`/applications/${applicationId1}/feedback`) + .send({ + status: "accepted", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(401); + expect(res.body.error).to.be.equal("Unauthorized"); + expect(res.body.message).to.be.equal("Unauthenticated User"); + return done(); + }); + }); + + it("should return 401 if user is not a super user", function (done) { + chai + .request(app) + .patch(`/applications/${applicationId1}/feedback`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + status: "accepted", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(401); + expect(res.body.error).to.be.equal("Unauthorized"); + expect(res.body.message).to.be.equal("You are not authorized for this action."); + return done(); + }); + }); }); describe("GET /application/:applicationId", function () { diff --git a/test/unit/controllers/applications.test.ts b/test/unit/controllers/applications.test.ts index 2e3036270..39aff8302 100644 --- a/test/unit/controllers/applications.test.ts +++ b/test/unit/controllers/applications.test.ts @@ -23,6 +23,7 @@ describe("nudgeApplication", () => { let boomBadRequest: sinon.SinonSpy; let boomTooManyRequests: sinon.SinonSpy; let boomBadImplementation: sinon.SinonSpy; + let nudgeApplicationStub: sinon.SinonStub; const mockApplicationId = "test-application-id-123"; const mockUserId = "test-user-id-456"; @@ -55,6 +56,8 @@ describe("nudgeApplication", () => { badImplementation: boomBadImplementation, }, }; + + nudgeApplicationStub = sinon.stub(ApplicationModel, "nudgeApplication"); }); afterEach(() => { @@ -69,7 +72,7 @@ describe("nudgeApplication", () => { lastNudgeAt: new Date().toISOString(), }; - const nudgeApplicationStub = sinon.stub(ApplicationModel, "nudgeApplication").resolves(mockResult); + nudgeApplicationStub.resolves(mockResult); await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse); @@ -92,7 +95,7 @@ describe("nudgeApplication", () => { lastNudgeAt: new Date().toISOString(), }; - const nudgeApplicationStub = sinon.stub(ApplicationModel, "nudgeApplication").resolves(mockResult); + nudgeApplicationStub.resolves(mockResult); await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse); @@ -115,7 +118,7 @@ describe("nudgeApplication", () => { lastNudgeAt: new Date().toISOString(), }; - sinon.stub(ApplicationModel, "nudgeApplication").resolves(mockResult); + nudgeApplicationStub.resolves(mockResult); await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse); @@ -130,7 +133,7 @@ describe("nudgeApplication", () => { status: "notFound", }; - sinon.stub(ApplicationModel, "nudgeApplication").resolves(mockResult); + nudgeApplicationStub.resolves(mockResult); await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse); @@ -144,7 +147,7 @@ describe("nudgeApplication", () => { status: "unauthorized", }; - sinon.stub(ApplicationModel, "nudgeApplication").resolves(mockResult); + nudgeApplicationStub.resolves(mockResult); await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse); @@ -158,7 +161,7 @@ describe("nudgeApplication", () => { status: "tooSoon", }; - sinon.stub(ApplicationModel, "nudgeApplication").resolves(mockResult); + nudgeApplicationStub.resolves(mockResult); await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse); @@ -172,7 +175,7 @@ describe("nudgeApplication", () => { status: "tooSoon", }; - sinon.stub(ApplicationModel, "nudgeApplication").resolves(mockResult); + nudgeApplicationStub.resolves(mockResult); await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse); @@ -186,7 +189,7 @@ describe("nudgeApplication", () => { status: "notPending", }; - sinon.stub(ApplicationModel, "nudgeApplication").resolves(mockResult); + nudgeApplicationStub.resolves(mockResult); await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse); @@ -196,3 +199,130 @@ describe("nudgeApplication", () => { }); }); }); + +describe("submitApplicationFeedback", () => { + let req: Partial; + let res: Partial & { + json: sinon.SinonSpy; + boom: { + notFound: sinon.SinonSpy; + badImplementation: sinon.SinonSpy; + }; + }; + let jsonSpy: sinon.SinonSpy; + let boomNotFound: sinon.SinonSpy; + let boomBadImplementation: sinon.SinonSpy; + let addApplicationFeedbackStub: sinon.SinonStub; + + const mockApplicationId = "test-application-id-123"; + const mockUsername = "superuser"; + const mockFeedback = "Great application!"; + const mockStatus = "accepted"; + + beforeEach(() => { + jsonSpy = sinon.spy(); + boomNotFound = sinon.spy(); + boomBadImplementation = sinon.spy(); + + req = { + params: { + applicationId: mockApplicationId, + }, + body: { + status: mockStatus, + feedback: mockFeedback, + }, + userData: { + id: "superuser-id", + username: mockUsername, + }, + }; + + res = { + json: jsonSpy, + boom: { + notFound: boomNotFound, + badImplementation: boomBadImplementation, + }, + }; + + addApplicationFeedbackStub = sinon.stub(ApplicationModel, "addApplicationFeedback"); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe("Success cases", () => { + it("should successfully submit application feedback", async () => { + const mockResult = { + status: "success", + }; + + addApplicationFeedbackStub.resolves(mockResult); + + await applicationsController.submitApplicationFeedback(req as CustomRequest, res as CustomResponse); + + expect(addApplicationFeedbackStub.calledOnce).to.be.true; + expect(addApplicationFeedbackStub.firstCall.args[0]).to.deep.equal({ + applicationId: mockApplicationId, + status: mockStatus, + feedback: mockFeedback, + reviewerName: mockUsername, + }); + + expect(jsonSpy.calledOnce).to.be.true; + expect(jsonSpy.firstCall.args[0].message).to.equal(API_RESPONSE_MESSAGES.FEEDBACK_SUBMITTED_SUCCESS); + }); + + it("should successfully submit application feedback without optional feedback text", async () => { + req.body = { + status: mockStatus, + }; + + const mockResult = { + status: "success", + }; + + addApplicationFeedbackStub.resolves(mockResult); + + await applicationsController.submitApplicationFeedback(req as CustomRequest, res as CustomResponse); + + expect(addApplicationFeedbackStub.calledOnce).to.be.true; + expect(addApplicationFeedbackStub.firstCall.args[0]).to.deep.equal({ + applicationId: mockApplicationId, + status: mockStatus, + feedback: undefined, + reviewerName: mockUsername, + }); + + expect(jsonSpy.calledOnce).to.be.true; + expect(jsonSpy.firstCall.args[0].message).to.equal(API_RESPONSE_MESSAGES.FEEDBACK_SUBMITTED_SUCCESS); + }); + }); + + describe("Error cases", () => { + it("should return application not found error", async () => { + const mockResult = { + status: "notFound", + }; + + addApplicationFeedbackStub.resolves(mockResult); + + await applicationsController.submitApplicationFeedback(req as CustomRequest, res as CustomResponse); + + expect(boomNotFound.calledOnce).to.be.true; + expect(boomNotFound.firstCall.args[0]).to.equal("Application not found"); + expect(jsonSpy.notCalled).to.be.true; + }); + + it("should return internal server error when an unexpected error occurs", async () => { + addApplicationFeedbackStub.rejects(new Error("Database error")); + + await applicationsController.submitApplicationFeedback(req as CustomRequest, res as CustomResponse); + + expect(boomBadImplementation.calledOnce).to.be.true; + expect(jsonSpy.notCalled).to.be.true; + }); + }); +}); diff --git a/test/unit/middlewares/application-validator.test.ts b/test/unit/middlewares/application-validator.test.ts index fc08ad2c4..8be524e7f 100644 --- a/test/unit/middlewares/application-validator.test.ts +++ b/test/unit/middlewares/application-validator.test.ts @@ -139,6 +139,156 @@ describe("application validator test", function () { await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(0); }); + + it("should call next function when status is accepted with optional feedback", async function () { + const req = { + body: { + status: "accepted", + feedback: "Great work!", + }, + }; + const res = { + boom: { + badRequest: () => {}, + }, + }; + const nextSpy = Sinon.spy(); + await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); + expect(nextSpy.callCount).to.equal(1); + }); + + it("should call next function when status is rejected with optional feedback", async function () { + const req = { + body: { + status: "rejected", + feedback: "Not a good fit", + }, + }; + const res = { + boom: { + badRequest: () => {}, + }, + }; + const nextSpy = Sinon.spy(); + await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); + expect(nextSpy.callCount).to.equal(1); + }); + + it("should call next function when status is changes_requested with feedback", async function () { + const req = { + body: { + status: "changes_requested", + feedback: "Please update your skills section", + }, + }; + const res = { + boom: { + badRequest: () => {}, + }, + }; + const nextSpy = Sinon.spy(); + await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); + expect(nextSpy.callCount).to.equal(1); + }); + + it("should not call next function when status is changes_requested without feedback", async function () { + const req = { + body: { + status: "changes_requested", + }, + }; + const res = { + boom: { + badRequest: () => {}, + }, + }; + const nextSpy = Sinon.spy(); + await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); + expect(nextSpy.callCount).to.equal(0); + }); + + it("should not call next function when status is changes_requested with empty feedback string", async function () { + const req = { + body: { + status: "changes_requested", + feedback: "", + }, + }; + const res = { + boom: { + badRequest: () => {}, + }, + }; + const nextSpy = Sinon.spy(); + await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); + expect(nextSpy.callCount).to.equal(0); + }); + + it("should call next function when status is accepted with empty feedback string", async function () { + const req = { + body: { + status: "accepted", + feedback: "", + }, + }; + const res = { + boom: { + badRequest: () => {}, + }, + }; + const nextSpy = Sinon.spy(); + await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); + expect(nextSpy.callCount).to.equal(1); + }); + + it("should call next function when status is rejected with empty feedback string", async function () { + const req = { + body: { + status: "rejected", + feedback: "", + }, + }; + const res = { + boom: { + badRequest: () => {}, + }, + }; + const nextSpy = Sinon.spy(); + await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); + expect(nextSpy.callCount).to.equal(1); + }); + + it("should not call next function when status is missing", async function () { + const req = { + body: { + feedback: "Some feedback", + }, + }; + const res = { + boom: { + badRequest: () => {}, + }, + }; + const nextSpy = Sinon.spy(); + await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); + expect(nextSpy.callCount).to.equal(0); + }); + + it("should not call next function when status is null", async function () { + const req = { + body: { + status: null, + }, + }; + const res = { + boom: { + badRequest: () => {}, + }, + }; + const nextSpy = Sinon.spy(); + await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); + expect(nextSpy.callCount).to.equal(0); + }); }); describe("validateApplicationQueryParam", function () { diff --git a/test/unit/models/application.test.ts b/test/unit/models/application.test.ts index ea5f2d81d..07e1080aa 100644 --- a/test/unit/models/application.test.ts +++ b/test/unit/models/application.test.ts @@ -118,4 +118,192 @@ describe("applications", function () { expect(application.status).to.be.equal("accepted"); }); }); + + describe("addApplicationFeedback", function () { + let testApplicationId: string; + const reviewerName = "test-reviewer"; + + beforeEach(async function () { + const testApplication = { ...applicationsData[0], userId: "test-user-feedback" }; + testApplicationId = await ApplicationModel.addApplication(testApplication); + }); + + it("should successfully add feedback with status accepted", async function () { + const result = await ApplicationModel.addApplicationFeedback({ + applicationId: testApplicationId, + status: "accepted", + reviewerName, + }); + + expect(result.status).to.be.equal("success"); + + const application = await ApplicationModel.getApplicationById(testApplicationId); + expect(application.status).to.be.equal("accepted"); + expect(application.feedback).to.be.a("array"); + expect(application.feedback.length).to.be.equal(1); + expect(application.feedback[0].status).to.be.equal("accepted"); + expect(application.feedback[0].reviewerName).to.be.equal(reviewerName); + expect(application.feedback[0].createdAt).to.exist; + expect(application.feedback[0]).to.not.have.property("feedback"); + }); + + it("should successfully add feedback with status rejected", async function () { + const result = await ApplicationModel.addApplicationFeedback({ + applicationId: testApplicationId, + status: "rejected", + reviewerName, + }); + + expect(result.status).to.be.equal("success"); + + const application = await ApplicationModel.getApplicationById(testApplicationId); + expect(application.status).to.be.equal("rejected"); + expect(application.feedback.length).to.be.equal(1); + expect(application.feedback[0].status).to.be.equal("rejected"); + }); + + it("should successfully add feedback with status changes_requested", async function () { + const feedbackText = "Please update your skills section"; + const result = await ApplicationModel.addApplicationFeedback({ + applicationId: testApplicationId, + status: "changes_requested", + feedback: feedbackText, + reviewerName, + }); + + expect(result.status).to.be.equal("success"); + + const application = await ApplicationModel.getApplicationById(testApplicationId); + expect(application.status).to.be.equal("changes_requested"); + expect(application.feedback.length).to.be.equal(1); + expect(application.feedback[0].status).to.be.equal("changes_requested"); + expect(application.feedback[0].feedback).to.be.equal(feedbackText); + }); + + it("should successfully add feedback with feedback text for accepted status", async function () { + const feedbackText = "Great application!"; + const result = await ApplicationModel.addApplicationFeedback({ + applicationId: testApplicationId, + status: "accepted", + feedback: feedbackText, + reviewerName, + }); + + expect(result.status).to.be.equal("success"); + + const application = await ApplicationModel.getApplicationById(testApplicationId); + expect(application.feedback[0].feedback).to.be.equal(feedbackText); + }); + + it("should trim whitespace from feedback text", async function () { + const feedbackText = " Please update your skills "; + const result = await ApplicationModel.addApplicationFeedback({ + applicationId: testApplicationId, + status: "changes_requested", + feedback: feedbackText, + reviewerName, + }); + + expect(result.status).to.be.equal("success"); + + const application = await ApplicationModel.getApplicationById(testApplicationId); + expect(application.feedback[0].feedback).to.be.equal("Please update your skills"); + }); + + it("should append feedback to existing feedback array", async function () { + await ApplicationModel.addApplicationFeedback({ + applicationId: testApplicationId, + status: "changes_requested", + feedback: "First feedback", + reviewerName: "reviewer1", + }); + + const result = await ApplicationModel.addApplicationFeedback({ + applicationId: testApplicationId, + status: "accepted", + feedback: "Second feedback", + reviewerName: "reviewer2", + }); + + expect(result.status).to.be.equal("success"); + + const application = await ApplicationModel.getApplicationById(testApplicationId); + expect(application.feedback.length).to.be.equal(2); + expect(application.feedback[0].status).to.be.equal("changes_requested"); + expect(application.feedback[0].reviewerName).to.be.equal("reviewer1"); + expect(application.feedback[1].status).to.be.equal("accepted"); + expect(application.feedback[1].reviewerName).to.be.equal("reviewer2"); + expect(application.status).to.be.equal("accepted"); + }); + + it("should handle application with no existing feedback array", async function () { + const result = await ApplicationModel.addApplicationFeedback({ + applicationId: testApplicationId, + status: "accepted", + reviewerName, + }); + + expect(result.status).to.be.equal("success"); + + const application = await ApplicationModel.getApplicationById(testApplicationId); + expect(application.feedback).to.be.a("array"); + expect(application.feedback.length).to.be.equal(1); + }); + + it("should not include feedback field when feedback is empty string", async function () { + const result = await ApplicationModel.addApplicationFeedback({ + applicationId: testApplicationId, + status: "accepted", + feedback: "", + reviewerName, + }); + + expect(result.status).to.be.equal("success"); + + const application = await ApplicationModel.getApplicationById(testApplicationId); + expect(application.feedback[0]).to.not.have.property("feedback"); + }); + + it("should not include feedback field when feedback is only whitespace", async function () { + const result = await ApplicationModel.addApplicationFeedback({ + applicationId: testApplicationId, + status: "accepted", + feedback: " ", + reviewerName, + }); + + expect(result.status).to.be.equal("success"); + + const application = await ApplicationModel.getApplicationById(testApplicationId); + expect(application.feedback[0]).to.not.have.property("feedback"); + }); + + it("should return notFound status when application does not exist", async function () { + const result = await ApplicationModel.addApplicationFeedback({ + applicationId: "non-existent-id", + status: "accepted", + reviewerName, + }); + + expect(result.status).to.be.equal("notFound"); + }); + + it("should include createdAt timestamp in feedback item", async function () { + const beforeTime = new Date().toISOString(); + const result = await ApplicationModel.addApplicationFeedback({ + applicationId: testApplicationId, + status: "accepted", + reviewerName, + }); + const afterTime = new Date().toISOString(); + + expect(result.status).to.be.equal("success"); + + const application = await ApplicationModel.getApplicationById(testApplicationId); + expect(application.feedback[0].createdAt).to.exist; + expect(application.feedback[0].createdAt).to.be.a("string"); + expect(application.feedback[0].createdAt >= beforeTime).to.be.true; + expect(application.feedback[0].createdAt <= afterTime).to.be.true; + }); + }); }); From 9ef5a175d7640fe31c1fb7af44fa2511494b0d5b Mon Sep 17 00:00:00 2001 From: anuj chhikara Date: Mon, 19 Jan 2026 23:51:11 +0530 Subject: [PATCH 3/4] refactor: update application validator tests for improved clarity and consistency --- .../middlewares/application-validator.test.ts | 215 ++++++------------ 1 file changed, 67 insertions(+), 148 deletions(-) diff --git a/test/unit/middlewares/application-validator.test.ts b/test/unit/middlewares/application-validator.test.ts index 8be524e7f..633d8231f 100644 --- a/test/unit/middlewares/application-validator.test.ts +++ b/test/unit/middlewares/application-validator.test.ts @@ -1,5 +1,6 @@ import chai from "chai"; -const Sinon = require("sinon"); +import sinon from "sinon"; +const Sinon = sinon; const { expect } = chai; const applicationValidator = require("../../../middlewares/validators/application"); const applicationsData = require("../../fixtures/applications/applications")(); @@ -89,241 +90,159 @@ describe("application validator test", function () { }); describe("validateApplicationUpdateData", function () { - it("should call next function if only status and feedback is passed, and status has any of the allowed values", async function () { - const req = { - body: { - status: "accepted", - feedback: "some feedback", - }, + let req: any; + let res: any; + let nextSpy: sinon.SinonSpy; + + beforeEach(function () { + req = { + body: {}, }; - const res = { + res = { boom: { badRequest: () => {}, }, }; - const nextSpy = Sinon.spy(); + nextSpy = Sinon.spy(); + }); + + it("should call next function if only status and feedback is passed, and status has any of the allowed values", async function () { + req.body = { + status: "accepted", + feedback: "some feedback", + }; await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(1); }); it("should not call next function if any value other than status and feedback is passed", async function () { - const req = { - body: { - batman: true, - }, - }; - const res = { - boom: { - badRequest: () => {}, - }, + req.body = { + batman: true, }; - - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(0); }); it("should not call the next function if any value which is not allowed is sent in status", async function () { - const req = { - body: { - status: "something", - }, + req.body = { + status: "something", }; - const res = { - boom: { - badRequest: () => {}, - }, - }; - - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(0); }); it("should call next function when status is accepted with optional feedback", async function () { - const req = { - body: { - status: "accepted", - feedback: "Great work!", - }, + req.body = { + status: "accepted", + feedback: "Great work!", }; - const res = { - boom: { - badRequest: () => {}, - }, - }; - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(1); }); it("should call next function when status is rejected with optional feedback", async function () { - const req = { - body: { - status: "rejected", - feedback: "Not a good fit", - }, + req.body = { + status: "rejected", + feedback: "Not a good fit", }; - const res = { - boom: { - badRequest: () => {}, - }, - }; - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(1); }); it("should call next function when status is changes_requested with feedback", async function () { - const req = { - body: { - status: "changes_requested", - feedback: "Please update your skills section", - }, + req.body = { + status: "changes_requested", + feedback: "Please update your skills section", }; - const res = { - boom: { - badRequest: () => {}, - }, - }; - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(1); }); it("should not call next function when status is changes_requested without feedback", async function () { - const req = { - body: { - status: "changes_requested", - }, - }; - const res = { - boom: { - badRequest: () => {}, - }, + req.body = { + status: "changes_requested", }; - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(0); }); it("should not call next function when status is changes_requested with empty feedback string", async function () { - const req = { - body: { - status: "changes_requested", - feedback: "", - }, - }; - const res = { - boom: { - badRequest: () => {}, - }, + req.body = { + status: "changes_requested", + feedback: "", }; - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(0); }); it("should call next function when status is accepted with empty feedback string", async function () { - const req = { - body: { - status: "accepted", - feedback: "", - }, - }; - const res = { - boom: { - badRequest: () => {}, - }, + req.body = { + status: "accepted", + feedback: "", }; - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(1); }); it("should call next function when status is rejected with empty feedback string", async function () { - const req = { - body: { - status: "rejected", - feedback: "", - }, + req.body = { + status: "rejected", + feedback: "", }; - const res = { - boom: { - badRequest: () => {}, - }, - }; - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(1); }); it("should not call next function when status is missing", async function () { - const req = { - body: { - feedback: "Some feedback", - }, + req.body = { + feedback: "Some feedback", }; - const res = { - boom: { - badRequest: () => {}, - }, - }; - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(0); }); it("should not call next function when status is null", async function () { - const req = { - body: { - status: null, - }, + req.body = { + status: null, }; - const res = { - boom: { - badRequest: () => {}, - }, - }; - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationUpdateData(req, res, nextSpy); expect(nextSpy.callCount).to.equal(0); }); }); describe("validateApplicationQueryParam", function () { - it("should call the next function if allowed query params are passed", async function () { - const req = { - query: { - userId: "kfjadskfj", - status: "accepted", - size: "4", - next: "kfsdfksdfjksd", - dev: "true", - }, + let req: any; + let res: any; + let nextSpy: sinon.SinonSpy; + + beforeEach(function () { + req = { + query: {}, }; - const res = { + res = { boom: { badRequest: () => {}, }, }; - const nextSpy = Sinon.spy(); + nextSpy = Sinon.spy(); + }); + + it("should call the next function if allowed query params are passed", async function () { + req.query = { + userId: "kfjadskfj", + status: "accepted", + size: "4", + next: "kfsdfksdfjksd", + dev: "true", + }; await applicationValidator.validateApplicationQueryParam(req, res, nextSpy); expect(nextSpy.callCount).to.equal(1); }); it("should not call next function if any value that is not allowed is passed in query params", async function () { - const req = { - query: { - hello: "true", - }, + req.query = { + hello: "true", }; - const res = { - boom: { - badRequest: () => {}, - }, - }; - const nextSpy = Sinon.spy(); await applicationValidator.validateApplicationQueryParam(req, res, nextSpy); expect(nextSpy.callCount).to.equal(0); }); From ed2d28a2a3e127eadce736026c04b7808d277d72 Mon Sep 17 00:00:00 2001 From: anuj chhikara Date: Wed, 21 Jan 2026 00:58:16 +0530 Subject: [PATCH 4/4] test: remove unauthorized user test case for application feedback --- test/integration/application.test.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/test/integration/application.test.ts b/test/integration/application.test.ts index 703805f2e..3d1212a3b 100644 --- a/test/integration/application.test.ts +++ b/test/integration/application.test.ts @@ -376,25 +376,6 @@ describe("Application", function () { }); }); - it("should return 401 if the user is not super user", function (done) { - chai - .request(app) - .patch(`/applications/${applicationId1}/feedback`) - .set("cookie", `${cookieName}=${jwt}`) - .send({ - status: "accepted", - }) - .end((err, res) => { - if (err) { - return done(err); - } - - expect(res).to.have.status(401); - expect(res.body.message).to.be.equal("You are not authorized for this action."); - return done(); - }); - }); - it("should return 400 if anything other than status and feedback is passed in the body", function (done) { chai .request(app)