diff --git a/constants/requests.ts b/constants/requests.ts index e9d510012..6a7358669 100644 --- a/constants/requests.ts +++ b/constants/requests.ts @@ -39,7 +39,9 @@ export const REQUEST_ALREADY_REJECTED = "Request already rejected"; export const ERROR_WHILE_FETCHING_REQUEST = "Error while fetching request"; export const ERROR_WHILE_CREATING_REQUEST = "Error while creating request"; export const ERROR_WHILE_UPDATING_REQUEST = "Error while updating request"; +export const ERROR_WHILE_ACKNOWLEDGING_REQUEST = "Error while acknowledging request"; +export const REQUEST_ID_REQUIRED = "Request id is required"; export const REQUEST_DOES_NOT_EXIST = "Request does not exist"; export const REQUEST_ALREADY_PENDING = "Request already exists please wait for approval or rejection"; export const UNAUTHORIZED_TO_CREATE_OOO_REQUEST = "Unauthorized to create OOO request"; diff --git a/controllers/oooRequests.ts b/controllers/oooRequests.ts index 741c88cfa..18b31f71e 100644 --- a/controllers/oooRequests.ts +++ b/controllers/oooRequests.ts @@ -8,10 +8,12 @@ import { ERROR_WHILE_UPDATING_REQUEST, REQUEST_APPROVED_SUCCESSFULLY, REQUEST_REJECTED_SUCCESSFULLY, - UNAUTHORIZED_TO_CREATE_OOO_REQUEST, REQUEST_ALREADY_PENDING, USER_STATUS_NOT_FOUND, OOO_STATUS_ALREADY_EXIST, + UNAUTHORIZED_TO_CREATE_OOO_REQUEST, + ERROR_WHILE_ACKNOWLEDGING_REQUEST, + REQUEST_ID_REQUIRED, } from "../constants/requests"; import { statusState } from "../constants/userStatus"; import { logType } from "../constants/logs"; @@ -19,10 +21,11 @@ import { addLog } from "../models/logs"; import { getRequestByKeyValues, getRequests, updateRequest } from "../models/requests"; import { createUserFutureStatus } from "../models/userFutureStatus"; import { getUserStatus, addFutureStatus } from "../models/userStatus"; -import { createOooRequest, validateUserStatus } from "../services/oooRequest"; +import { createOooRequest, validateUserStatus, acknowledgeOooRequest as acknowledgeOooRequestService } from "../services/oooRequest"; import { CustomResponse } from "../typeDefinitions/global"; -import { OooRequestCreateRequest, OooRequestResponse, OooStatusRequest } from "../types/oooRequest"; +import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse, OooStatusRequest } from "../types/oooRequest"; import { UpdateRequest } from "../types/requests"; +import { NextFunction } from "express"; /** * Controller to handle the creation of OOO requests. @@ -78,7 +81,7 @@ export const createOooRequestController = async ( return res.boom.conflict(REQUEST_ALREADY_PENDING); } - await createOooRequest(requestBody, username, userId); + await createOooRequest(requestBody, userId); return res.status(201).json({ message: REQUEST_CREATED_SUCCESSFULLY, @@ -148,3 +151,35 @@ export const updateOooRequestController = async (req: UpdateRequest, res: Custom return res.boom.badImplementation(ERROR_WHILE_UPDATING_REQUEST); } }; +/** + * Acknowledges an Out-of-Office (OOO) request by updating its status to approved or rejected + * + * @param {AcknowledgeOooRequest} req - The request object containing acknowledgment details (status, comment) and request ID in params + * @param {OooRequestResponse} res - The response object for sending success/error responses + * @param {NextFunction} next - Express next function for error handling + * @returns {Promise} Resolves with success message or passes error to next middleware + */ +export const acknowledgeOooRequestController = async ( + req: AcknowledgeOooRequest, + res: OooRequestResponse, + next: NextFunction +) + : Promise => { + try { + + const requestBody = req.body; + const superUserId = req.userData.id; + const requestId = req.params.id; + + const response = await acknowledgeOooRequestService(requestId, requestBody, superUserId); + + return res.status(200).json({ + message: response.message, + }); + } + catch(error){ + logger.error(ERROR_WHILE_ACKNOWLEDGING_REQUEST, error); + next(error); + return; + } +}; diff --git a/controllers/requests.ts b/controllers/requests.ts index fd8974ea0..e3a4fb681 100644 --- a/controllers/requests.ts +++ b/controllers/requests.ts @@ -5,8 +5,8 @@ import { } from "../constants/requests"; import { getRequests } from "../models/requests"; import { getPaginatedLink } from "../utils/helper"; -import { createOooRequestController, updateOooRequestController } from "./oooRequests"; -import { OooRequestCreateRequest, OooRequestResponse } from "../types/oooRequest"; +import { acknowledgeOooRequestController, createOooRequestController, updateOooRequestController } from "./oooRequests"; +import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse } from "../types/oooRequest"; import { CustomResponse } from "../typeDefinitions/global"; import { ExtensionRequestRequest, ExtensionRequestResponse } from "../types/extensionRequests"; import { createTaskExtensionRequest, updateTaskExtensionRequest } from "./extensionRequestsv2"; @@ -17,7 +17,7 @@ import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse, UpdateOn import { createOnboardingExtensionRequestController, updateOnboardingExtensionRequestController, updateOnboardingExtensionRequestState } from "./onboardingExtension"; import { UpdateOnboardingExtensionRequest } from "../types/onboardingExtension"; -import { Request } from "express"; +import { Request, NextFunction } from "express"; export const createRequestController = async ( req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest, @@ -121,9 +121,12 @@ export const getRequestsController = async (req: any, res: any) => { * @param {CustomResponse} res - The response object. * @returns {Promise} Resolves or sends an error for invalid types. */ -export const updateRequestBeforeAcknowledgedController = async (req: Request, res: CustomResponse) => { +export const updateRequestBeforeAcknowledgedController = async (req: Request, res: CustomResponse, next: NextFunction) => { const type = req.body.type; switch(type){ + case REQUEST_TYPE.OOO: + await acknowledgeOooRequestController(req as AcknowledgeOooRequest, res as OooRequestResponse, next); + break; case REQUEST_TYPE.ONBOARDING: await updateOnboardingExtensionRequestController(req as UpdateOnboardingExtensionRequest, res as OnboardingExtensionResponse); break; diff --git a/middlewares/oooRoleCheckMiddleware.ts b/middlewares/oooRoleCheckMiddleware.ts new file mode 100644 index 000000000..254932735 --- /dev/null +++ b/middlewares/oooRoleCheckMiddleware.ts @@ -0,0 +1,30 @@ +import { NextFunction } from "express"; +import { CustomRequest, CustomResponse } from "../types/global"; +import { REQUEST_TYPE } from "../constants/requests"; +import { devFlagMiddleware } from "./devFlag"; +import authorizeRoles from "./authorizeRoles"; + +const { SUPERUSER } = require("../constants/roles"); + +export const oooRoleCheckMiddleware = ( + req: CustomRequest, + res: CustomResponse, + next: NextFunction +) => { + const requestType = req.body?.type; + + if (requestType === REQUEST_TYPE.OOO) { + // TODO: Remove this middleware once the OOO feature is tested and ready + return devFlagMiddleware(req, res, (err) => { + if (err) return next(err); + + try { + return authorizeRoles([SUPERUSER])(req, res, next); + } catch (authErr) { + return next(authErr); + } + }); + } + + return next(); +}; diff --git a/middlewares/validators/oooRequests.ts b/middlewares/validators/oooRequests.ts index ab73929f1..e2db46a82 100644 --- a/middlewares/validators/oooRequests.ts +++ b/middlewares/validators/oooRequests.ts @@ -1,7 +1,7 @@ import joi from "joi"; import { NextFunction } from "express"; -import { REQUEST_STATE, REQUEST_TYPE } from "../../constants/requests"; -import { OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest"; +import { REQUEST_STATE, REQUEST_TYPE, ERROR_WHILE_ACKNOWLEDGING_REQUEST } from "../../constants/requests"; +import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest"; export const createOooStatusRequestValidator = async ( req: OooRequestCreateRequest, @@ -38,3 +38,65 @@ export const createOooStatusRequestValidator = async ( await schema.validateAsync(req.body, { abortEarly: false }); }; + +const acknowledgeOooRequestSchema = joi + .object() + .strict() + .keys({ + comment: joi.string().optional() + .messages({ + "string.empty": "comment cannot be empty", + }), + status: joi + .string() + .valid(REQUEST_STATE.APPROVED, REQUEST_STATE.REJECTED) + .required() + .messages({ + "any.only": "status must be APPROVED or REJECTED", + }), + type: joi.string().equal(REQUEST_TYPE.OOO).required().messages({ + "any.required": "type is required", + "any.only": "type must be OOO" + }) + }); + +const paramsSchema = joi + .object() + .strict() + .keys({ + id: joi.string().trim().required().messages({ + "any.required": "Request ID is required", + "string.empty": "Request ID cannot be empty" + }) + }); + +/** + * Middleware to validate the acknowledge Out-Of-Office (OOO) request payload. + * + * @param {AcknowledgeOooRequest} req - The request object containing the body to be validated. + * @param {OooRequestResponse} res - The response object used to send error responses if validation fails. + * @param {NextFunction} next - The next middleware function to call if validation succeeds. + * @returns {Promise} Resolves or sends errors. + */ +export const acknowledgeOooRequestValidator = async ( + req: AcknowledgeOooRequest, + res: OooRequestResponse, + next: NextFunction +): Promise => { + try { + await acknowledgeOooRequestSchema.validateAsync(req.body, { abortEarly: false }); + await paramsSchema.validateAsync(req.params, { abortEarly: false }); + return next(); + } catch (error: unknown) { + + if (error instanceof joi.ValidationError) { + const errorMessages = error.details.map((detail) => detail.message); + logger.error(`${ERROR_WHILE_ACKNOWLEDGING_REQUEST}: ${errorMessages}`); + return res.boom.badRequest(errorMessages); + } + + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`${ERROR_WHILE_ACKNOWLEDGING_REQUEST}: ${errorMessage}`); + return res.boom.badRequest([ERROR_WHILE_ACKNOWLEDGING_REQUEST]); + } +}; diff --git a/middlewares/validators/requests.ts b/middlewares/validators/requests.ts index 80ff0478b..f7307f1f2 100644 --- a/middlewares/validators/requests.ts +++ b/middlewares/validators/requests.ts @@ -1,8 +1,8 @@ import joi from "joi"; import { NextFunction } from "express"; import { REQUEST_STATE, REQUEST_TYPE } from "../../constants/requests"; -import { OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest"; -import { createOooStatusRequestValidator } from "./oooRequests"; +import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest"; +import { acknowledgeOooRequestValidator, createOooStatusRequestValidator } from "./oooRequests"; import { createExtensionRequestValidator } from "./extensionRequestsv2"; import {createTaskRequestValidator} from "./taskRequests"; import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../types/extensionRequests"; @@ -88,7 +88,11 @@ export const getRequestsMiddleware = async (req: OooRequestCreateRequest, res: O .valid(REQUEST_TYPE.OOO, REQUEST_TYPE.EXTENSION, REQUEST_TYPE.TASK, REQUEST_TYPE.ALL, REQUEST_TYPE.ONBOARDING) .optional(), requestedBy: joi.string().insensitive().optional(), - state: joi + state: joi + .string() + .valid(REQUEST_STATE.APPROVED, REQUEST_STATE.PENDING, REQUEST_STATE.REJECTED) + .optional(), + status: joi .string() .valid(REQUEST_STATE.APPROVED, REQUEST_STATE.PENDING, REQUEST_STATE.REJECTED) .optional(), @@ -125,13 +129,13 @@ export const getRequestsMiddleware = async (req: OooRequestCreateRequest, res: O /** * Validates update requests based on their type. * - * @param {UpdateOnboardingExtensionRequest} req - Request object. + * @param {UpdateOnboardingExtensionRequest | AcknowledgeOooRequest} req - Request object. * @param {CustomResponse} res - Response object. * @param {NextFunction} next - Next middleware if valid. * @returns {Promise} Resolves or sends errors. */ export const updateRequestValidator = async ( - req: UpdateOnboardingExtensionRequest, + req: UpdateOnboardingExtensionRequest | AcknowledgeOooRequest, res: CustomResponse, next: NextFunction ): Promise => { @@ -142,6 +146,9 @@ export const updateRequestValidator = async ( req, res as OnboardingExtensionResponse, next); break; + case REQUEST_TYPE.OOO: + await acknowledgeOooRequestValidator(req as AcknowledgeOooRequest, res as OooRequestResponse, next); + break; default: return res.boom.badRequest("Invalid type"); } diff --git a/models/requests.ts b/models/requests.ts index 775e0ee6f..aefa0012b 100644 --- a/models/requests.ts +++ b/models/requests.ts @@ -1,7 +1,6 @@ import firestore from "../utils/firestore"; -import type { OooStatusRequest } from "../types/oooRequest"; const requestModel = firestore.collection("requests"); -import { REQUEST_ALREADY_APPROVED, REQUEST_ALREADY_REJECTED, REQUEST_STATE, REQUEST_TYPE } from "../constants/requests"; +import { REQUEST_ALREADY_APPROVED, REQUEST_ALREADY_REJECTED, REQUEST_STATE } from "../constants/requests"; import { ERROR_WHILE_FETCHING_REQUEST, ERROR_WHILE_CREATING_REQUEST, diff --git a/routes/requests.ts b/routes/requests.ts index 098e00a82..b688ef512 100644 --- a/routes/requests.ts +++ b/routes/requests.ts @@ -3,6 +3,7 @@ const router = express.Router(); const authorizeRoles = require("../middlewares/authorizeRoles"); const { SUPERUSER } = require("../constants/roles"); import authenticate from "../middlewares/authenticate"; +import { oooRoleCheckMiddleware } from "../middlewares/oooRoleCheckMiddleware"; import { createRequestsMiddleware, updateRequestsMiddleware, @@ -22,6 +23,6 @@ import { verifyDiscordBot } from "../middlewares/authorizeBot"; router.get("/", getRequestsMiddleware, getRequestsController); router.post("/", skipAuthenticateForOnboardingExtensionRequest(authenticate, verifyDiscordBot), createRequestsMiddleware, createRequestController); router.put("/:id",authenticate, authorizeRoles([SUPERUSER]), updateRequestsMiddleware, updateRequestController); -router.patch("/:id", authenticate, updateRequestValidator, updateRequestBeforeAcknowledgedController); +router.patch("/:id", authenticate, oooRoleCheckMiddleware, updateRequestValidator, updateRequestBeforeAcknowledgedController); module.exports = router; diff --git a/services/oooRequest.ts b/services/oooRequest.ts index aa32b4f42..0e8aee622 100644 --- a/services/oooRequest.ts +++ b/services/oooRequest.ts @@ -4,13 +4,25 @@ import { OOO_STATUS_ALREADY_EXIST, REQUEST_LOG_TYPE, REQUEST_STATE, + REQUEST_TYPE, USER_STATUS_NOT_FOUND, + INVALID_REQUEST_TYPE, + REQUEST_ALREADY_APPROVED, + REQUEST_ALREADY_REJECTED, + REQUEST_APPROVED_SUCCESSFULLY, + REQUEST_REJECTED_SUCCESSFULLY, + ERROR_WHILE_ACKNOWLEDGING_REQUEST, + ERROR_WHILE_CREATING_REQUEST, } from "../constants/requests"; -import { userState } from "../constants/userStatus"; -import { createRequest } from "../models/requests"; -import { OooStatusRequest, OooStatusRequestBody } from "../types/oooRequest"; +import { statusState, userState } from "../constants/userStatus"; +import { createRequest, getRequests, updateRequest } from "../models/requests"; +import { AcknowledgeOooRequestBody, OooStatusRequest, oldOooStatusRequest, OooStatusRequestBody } from "../types/oooRequest"; import { UserStatus } from "../types/userStatus"; import { addLog } from "./logService"; +import { BadRequest, Conflict, NotFound } from "http-errors"; +import { addFutureStatus } from "../models/userStatus"; +import { createUserFutureStatus } from "../models/userFutureStatus"; +import { newOOOSchema} from "../utils/requests"; /** * Validates the user status. @@ -51,22 +63,20 @@ export const validateUserStatus = async ( * Create an OOO request for a user. * * @param {OooStatusRequestBody} body - The request body containing OOO details. - * @param {string} username - The username of the person creating the request. - * @param {string} userId - The unique identifier of the user. + * @param {string} requestedBy - The userId of the person creating the request. * @returns {Promise} The created OOO request. * @throws {Error} Throws an error if an issue occurs during validation. */ export const createOooRequest = async ( body: OooStatusRequestBody, - username: string, - userId: string + requestedBy: string, ) => { try { const request: OooStatusRequest = await createRequest({ from: body.from, until: body.until, type: body.type, - requestedBy: userId, + requestedBy: requestedBy, reason: body.reason, comment: null, status: REQUEST_STATE.PENDING, @@ -78,7 +88,7 @@ export const createOooRequest = async ( meta: { requestId: request.id, action: LOG_ACTION.CREATE, - userId, + userId: requestedBy, createdAt: Date.now(), }, body: request, @@ -88,7 +98,115 @@ export const createOooRequest = async ( return request; } catch (error) { - logger.error("Error while creating OOO request", error); + logger.error(ERROR_WHILE_CREATING_REQUEST, error); throw error; } } + +/** + * Validate the out-of-office acknowledgement request. + * + * @param {string} requestType - The type of the request OOO. + * @param {string} requestStatus - The status of the request (PENDING, APPROVED, REJECTED). + * @throws {Error} Throws an error if an issue occurs during validation. + */ + +export const validateOooAcknowledgeRequest = ( + requestType: string, + requestStatus: string, + ) => { + if (requestType !== REQUEST_TYPE.OOO) { + logger.error(`Invalid request type: ${requestType}`); + throw new BadRequest(INVALID_REQUEST_TYPE); + } + + if (requestStatus === REQUEST_STATE.APPROVED) { + logger.error(`Request already approved`); + throw new BadRequest(REQUEST_ALREADY_APPROVED); + } + + if (requestStatus === REQUEST_STATE.REJECTED) { + logger.error(`Request already rejected`); + throw new BadRequest(REQUEST_ALREADY_REJECTED); + } + }; + + +/** + * Acknowledges the OOO request. + * + * @param {string} requestId - The id of the request to be acknowledged. + * @param {AcknowledgeOooRequestBody} body - The Acknowledgement request body. + * @param {string} superUserId - The id of the super user. + * @returns {Promise} The acknowledged OOO request. + * @throws {Error} Throws an error if an issue occurs during acknowledgement. + */ + +export const acknowledgeOooRequest = async ( + requestId: string, + body: AcknowledgeOooRequestBody, + superUserId: string, + ) => { + try { + const requestData = await getRequests({ id: requestId }) as OooStatusRequest | oldOooStatusRequest; + if (!requestData) { + throw new NotFound("Request not found"); + } + + const { type, from, until, requestedBy } = requestData; + const status = 'status' in requestData ? requestData.status : (requestData as oldOooStatusRequest).state; + await validateOooAcknowledgeRequest(type, status); + + + const requestResult = await updateRequest(requestId, body, superUserId, REQUEST_TYPE.OOO); + if (requestResult.error) { + throw new BadRequest(requestResult.error); + } + + const [acknowledgeLogType, returnMessage] = + requestResult.status === REQUEST_STATE.APPROVED + ? [REQUEST_LOG_TYPE.REQUEST_APPROVED, REQUEST_APPROVED_SUCCESSFULLY] + : [REQUEST_LOG_TYPE.REQUEST_REJECTED, REQUEST_REJECTED_SUCCESSFULLY]; + + await addLog( + acknowledgeLogType, + { + requestId, + action: LOG_ACTION.UPDATE, + userId: superUserId, + }, + requestResult, + ); + + if (requestResult.status === REQUEST_STATE.APPROVED) { + await addFutureStatus({ + requestId, + state: REQUEST_TYPE.OOO, + from, + endsOn: until, + userId: requestedBy, + message: body.comment ?? "", + }); + + await createUserFutureStatus({ + requestId, + status: userState.OOO, + state: statusState.UPCOMING, + from, + endsOn: until, + userId: requestedBy, + message: body.comment ?? "", + createdAt: Date.now(), + }); + } + + return { + message: returnMessage, + request: requestResult, + }; + } catch (error) { + logger.error(ERROR_WHILE_ACKNOWLEDGING_REQUEST, { error }); + throw error; + } + }; + \ No newline at end of file diff --git a/test/fixtures/oooRequest/oooRequest.ts b/test/fixtures/oooRequest/oooRequest.ts index 10dce241d..36ba49cd8 100644 --- a/test/fixtures/oooRequest/oooRequest.ts +++ b/test/fixtures/oooRequest/oooRequest.ts @@ -167,7 +167,7 @@ export const createOooRequests3 = { status: REQUEST_STATE.PENDING }; -export const acknowledgeOooRequest = { +export const testAcknowledgeOooRequest = { type: REQUEST_TYPE.OOO, status: REQUEST_STATE.APPROVED, comment: "OOO request approved as it's emergency." diff --git a/test/integration/requests.test.ts b/test/integration/requests.test.ts index 94b0d371c..d5560138b 100644 --- a/test/integration/requests.test.ts +++ b/test/integration/requests.test.ts @@ -15,7 +15,7 @@ import { validOooStatusRequests, validOooStatusUpdate, createOooRequests2, - acknowledgeOooRequest, + testAcknowledgeOooRequest, createOooRequests3, } from "../fixtures/oooRequest/oooRequest"; import { createRequest, updateRequest } from "../../models/requests"; @@ -351,7 +351,7 @@ describe("/requests OOO", function () { chai .request(app) .patch(`/requests/${testOooRequest.id}?dev=true`) - .send(acknowledgeOooRequest) + .send(testAcknowledgeOooRequest) .end(function (err, res) { expect(res).to.have.status(401); expect(res.body.error).to.equal("Unauthorized"); @@ -365,7 +365,7 @@ describe("/requests OOO", function () { .request(app) .patch(`/requests/${testOooRequest.id}?dev=false`) .set("cookie", `${cookieName}=${superUserToken}`) - .send(acknowledgeOooRequest) + .send(testAcknowledgeOooRequest) .end(function (err, res) { if (err) { return done(err); @@ -381,7 +381,7 @@ describe("/requests OOO", function () { .request(app) .patch(`/requests/11111111111111?dev=true`) .set("cookie", `${cookieName}=${superUserToken}`) - .send(acknowledgeOooRequest) + .send(testAcknowledgeOooRequest) .end(function (err, res) { if (err) { return done(err); @@ -397,13 +397,13 @@ describe("/requests OOO", function () { .request(app) .patch(`/requests/${testOooRequest.id}?dev=true`) .set("cookie", `${cookieName}=${authToken}`) - .send(acknowledgeOooRequest) + .send(testAcknowledgeOooRequest) .end(function (err, res) { if (err) { return done(err); } expect(res.statusCode).to.equal(403); - // expect(res.body.message).to.equal(UNAUTHORIZED_TO_ACKNOWLEDGE_OOO_REQUEST); + expect(res.body.message).to.equal(UNAUTHORIZED_TO_CREATE_OOO_REQUEST); done(); }); }); @@ -413,7 +413,7 @@ describe("/requests OOO", function () { .request(app) .patch(`/requests/${approvedOooRequest.id}?dev=true`) .set("cookie", `${cookieName}=${superUserToken}`) - .send(acknowledgeOooRequest) + .send(testAcknowledgeOooRequest) .end(function (err, res) { if (err) { return done(err); @@ -429,7 +429,7 @@ describe("/requests OOO", function () { .request(app) .patch(`/requests/${rejectedOooRequest.id}?dev=true`) .set("cookie", `${cookieName}=${superUserToken}`) - .send(acknowledgeOooRequest) + .send(testAcknowledgeOooRequest) .end(function (err, res) { if (err) { return done(err); @@ -445,7 +445,7 @@ describe("/requests OOO", function () { .request(app) .patch(`/requests/${onboardingRequest.id}?dev=true`) .set("cookie", `${cookieName}=${superUserToken}`) - .send(acknowledgeOooRequest) + .send(testAcknowledgeOooRequest) .end(function (err, res) { if (err) { return done(err); @@ -461,7 +461,7 @@ describe("/requests OOO", function () { .request(app) .patch(`/requests/${testOooRequest.id}?dev=true`) .set("cookie", `${cookieName}=${superUserToken}`) - .send(acknowledgeOooRequest) + .send(testAcknowledgeOooRequest) .end(function (err, res) { if (err) { return done(err); @@ -477,7 +477,7 @@ describe("/requests OOO", function () { .request(app) .patch(`/requests/${testOooRequest.id}?dev=true`) .set("cookie", `${cookieName}=${superUserToken}`) - .send({...acknowledgeOooRequest, status: REQUEST_STATE.REJECTED}) + .send({...testAcknowledgeOooRequest, status: REQUEST_STATE.REJECTED}) .end(function (err, res) { if (err) { return done(err); @@ -494,7 +494,7 @@ describe("/requests OOO", function () { .request(app) .patch(`/requests/${testOooRequest.id}?dev=true`) .set("cookie", `${cookieName}=${superUserToken}`) - .send(acknowledgeOooRequest) + .send(testAcknowledgeOooRequest) .end(function (err, res) { if (err) return done(err); expect(res.statusCode).to.equal(500); diff --git a/test/unit/middlewares/oooRequests.test.ts b/test/unit/middlewares/oooRequests.test.ts index 11272e860..f8b4c103c 100644 --- a/test/unit/middlewares/oooRequests.test.ts +++ b/test/unit/middlewares/oooRequests.test.ts @@ -4,15 +4,17 @@ const { expect } = chai; import { createOooStatusRequestValidator, - // acknowledgeOOORequestsValidator, + acknowledgeOooRequestValidator, } from "./../../../middlewares/validators/oooRequests"; -import { acknowledgeOooRequest, validOooStatusRequests, validOooStatusUpdate } from "../../fixtures/oooRequest/oooRequest"; +import { testAcknowledgeOooRequest, validOooStatusRequests, validOooStatusUpdate } from "../../fixtures/oooRequest/oooRequest"; import _ from "lodash"; +import { AcknowledgeOooRequest, OooRequestResponse } from "../../../types/oooRequest"; describe("OOO Status Request Validators", function () { let req: any; let res: any; let nextSpy; + beforeEach(function () { res = { boom: { @@ -91,40 +93,46 @@ describe("OOO Status Request Validators", function () { }); }); - describe.skip("acknowledgeOOORequestsValidator", function () { + describe("acknowledgeOOORequestsValidator", function () { it("should not validate for an invalid request for invalid request type", async function () { req = { - body: { ...acknowledgeOooRequest, type: "XYZ"} + body: { ...testAcknowledgeOooRequest, type: "XYZ"}, + params: { id: "test-id" } }; - // await acknowledgeOOORequestsValidator(req, res, nextSpy); + await acknowledgeOooRequestValidator(req as AcknowledgeOooRequest, res as OooRequestResponse, nextSpy); + expect(res.boom.badRequest.calledOnce).to.be.true; expect(nextSpy.notCalled).to.be.true; }); it("should not validate for an invalid request if status is incorrect", async function () { req = { - body: { ...acknowledgeOooRequest, status: "PENDING"} + body: { ...testAcknowledgeOooRequest, status: "PENDING"}, + params: { id: "test-id" } }; - // await acknowledgeOOORequestsValidator(req, res, nextSpy); + await acknowledgeOooRequestValidator(req as AcknowledgeOooRequest, res as OooRequestResponse, nextSpy); + expect(res.boom.badRequest.calledOnce).to.be.true; expect(nextSpy.notCalled).to.be.true; }); it("should validate for a valid acknowledge OOO request if comment not provided by superusers", async function() { req = { - body: _.omit(acknowledgeOooRequest, "comment") + body: _.omit(testAcknowledgeOooRequest, "comment"), + params: { id: "test-id" } }; res = {}; - // await acknowledgeOOORequestsValidator(req, res, nextSpy); + await acknowledgeOooRequestValidator(req as AcknowledgeOooRequest, res as OooRequestResponse, nextSpy); expect(nextSpy.calledOnce).to.be.true; }); it("should validate for a valid acknowledge OOO request", async function() { req = { - body: acknowledgeOooRequest + body: testAcknowledgeOooRequest, + params: { id: "test-id" } }; res = {}; - // await acknowledgeOOORequestsValidator(req, res, nextSpy); + await acknowledgeOooRequestValidator(req as AcknowledgeOooRequest, res as OooRequestResponse, nextSpy); expect(nextSpy.calledOnce).to.be.true; }); }); diff --git a/test/unit/services/oooRequest.test.ts b/test/unit/services/oooRequest.test.ts index ae72a29ee..f66d67fca 100644 --- a/test/unit/services/oooRequest.test.ts +++ b/test/unit/services/oooRequest.test.ts @@ -1,245 +1,213 @@ import sinon from "sinon"; import cleanDb from "../../utils/cleanDb"; import { - INVALID_REQUEST_TYPE, - REQUEST_ALREADY_APPROVED, - REQUEST_ALREADY_REJECTED, - REQUEST_APPROVED_SUCCESSFULLY, - REQUEST_DOES_NOT_EXIST, - REQUEST_REJECTED_SUCCESSFULLY, - REQUEST_STATE, - REQUEST_TYPE, - OOO_STATUS_ALREADY_EXIST, - USER_STATUS_NOT_FOUND, + INVALID_REQUEST_TYPE, + REQUEST_ALREADY_APPROVED, + REQUEST_ALREADY_REJECTED, + REQUEST_APPROVED_SUCCESSFULLY, + REQUEST_DOES_NOT_EXIST, + REQUEST_REJECTED_SUCCESSFULLY, + REQUEST_STATE, + REQUEST_TYPE, + OOO_STATUS_ALREADY_EXIST, + USER_STATUS_NOT_FOUND, } from "../../../constants/requests"; -import { - createOooRequest, - validateUserStatus, - // acknowledgeOOORequest, - // validateOOOAcknowledgeRequest +import { + createOooRequest, + validateUserStatus, + acknowledgeOooRequest, + validateOooAcknowledgeRequest, } from "../../../services/oooRequest"; import { expect } from "chai"; -import { testUserStatus, validOooStatusRequests, validUserCurrentStatus, createdOOORequest } from "../../fixtures/oooRequest/oooRequest"; +import { + testUserStatus, + validOooStatusRequests, + validUserCurrentStatus, + createdOOORequest, +} from "../../fixtures/oooRequest/oooRequest"; import { updateUserStatus } from "../../../models/userStatus"; import { userState } from "../../../constants/userStatus"; import addUser from "../../utils/addUser"; import userDataFixture from "../../fixtures/user/user"; import * as logService from "../../../services/logService"; -import { acknowledgeOooRequest, createOooRequests3 } from "../../fixtures/oooRequest/oooRequest"; +import { createOooRequests3, testAcknowledgeOooRequest } from "../../fixtures/oooRequest/oooRequest"; import { createRequest } from "../../../models/requests"; -describe("Test OOO Request Service", function() { - - let testUserName: string; - let testUserId: string; - const errorMessage = "Unexpected error occured"; - - beforeEach(async function() { - const users = userDataFixture(); - testUserId = await addUser(users[8]); - testUserName = users[8].username; - }); - - afterEach(async function() { - sinon.restore(); - await cleanDb(); - }); - - describe("validateUserStatus", function() { - - it("should return USER_STATUS_NOT_FOUND if user status not found", async function() { - const validationResponse = await validateUserStatus( - testUserId, - { ...testUserStatus, userStatusExists: false } - ); - expect(validationResponse).to.be.not.undefined; - expect(validationResponse.error).to.equal(USER_STATUS_NOT_FOUND); - }); - - it("should return OOO_STATUS_ALREADY_EXIST if user status is already OOO", async function() { - const validationResponse = await validateUserStatus( - testUserId, - { - ...testUserStatus, - data: { - ...testUserStatus.data, - currentStatus: { - ...testUserStatus.data.currentStatus, - state: userState.OOO - } - } - } - ); - expect(validationResponse).to.be.not.undefined; - expect(validationResponse.error).to.equal(OOO_STATUS_ALREADY_EXIST); - }); - - it("should return undefined when all validation checks passes", async function() { - const response = await validateUserStatus(testUserId, testUserStatus); - expect(response).to.not.exist; - }); - }); - - describe("createOooRequest", function() { - - beforeEach(async function() { - await updateUserStatus(testUserId, testUserStatus.data); - }); - - afterEach(async function () { - sinon.restore(); - }); - - it("should create OOO request", async function() { - const response = await createOooRequest(validOooStatusRequests, testUserName, testUserId); - expect(response).to.deep.include({ - ...createdOOORequest, - id: response.id, - requestedBy: testUserId - }); - }); - - it("should throw error", async function () { - sinon.stub(logService, "addLog").throws(new Error(errorMessage)); - - try { - await createOooRequest(validOooStatusRequests, testUserName, testUserId); - } catch (error) { - expect(error.message).to.equal(errorMessage); - } - }); - }); - - describe.skip("validateOOOAcknowledgeRequest", function() { - - let testOooRequest; - - beforeEach(async function () { - testOooRequest = await createRequest({ - ...createOooRequests3, - userId: testUserId, - comment: null, - lastModifiedBy: null, - }); - }); - - it("should return INVALID_REQUEST_TYPE if request type is not OOO", async function() { - // const validationResponse = await validateOOOAcknowledgeRequest( - // testOooRequest.id, - // REQUEST_TYPE.ONBOARDING, - // testOooRequest.status - // ); - // expect(validationResponse.error).to.be.not.undefined; - // expect(validationResponse.error).to.equal(INVALID_REQUEST_TYPE); - }); - - it("should return REQUEST_ALREADY_APPROVED if request is already approved", async function() { - // const validationResponse = await validateOOOAcknowledgeRequest( - // testOooRequest.id, - // testOooRequest.type, - // REQUEST_STATE.APPROVED - // ); - // expect(validationResponse.error).to.be.not.undefined; - // expect(validationResponse.error).to.equal(REQUEST_ALREADY_APPROVED); - }); - - it("should return REQUEST_ALREADY_REJECTED if request is already rejected", async function() { - // const validationResponse = await validateOOOAcknowledgeRequest( - // testOooRequest.id, - // testOooRequest.type, - // REQUEST_STATE.REJECTED - // ); - // expect(validationResponse.error).to.be.not.undefined; - // expect(validationResponse.error).to.equal(REQUEST_ALREADY_REJECTED); - }); - - it("should return undefined when all validation checks passes", async function() { - // const response = await validateOOOAcknowledgeRequest( - // testOooRequest.id, - // testOooRequest.type, - // testOooRequest.status - // ); - // expect(response).to.not.exist; - }); - }); - - describe.skip("acknowledgeOOORequest", function() { - - let testSuperUserId; - let testOooRequest; - - beforeEach(async function () { - const users = userDataFixture(); - const superUserId = await addUser(users[4]); - testSuperUserId = superUserId; - - testOooRequest = await createRequest({ - ...createOooRequests3, - userId: testUserId, - comment: null, - lastModifiedBy: null, - }); - }); - - it("should return REQUEST_DOES_NOT_EXIST if invalid request id is passed", async function () { - // const invalidOOORequestId = "11111111111111111111"; - // const response = await acknowledgeOOORequest( - // invalidOOORequestId, - // acknowledgeOooRequest, - // testSuperUserId - // ); - // expect(response.error).to.equal(REQUEST_DOES_NOT_EXIST); - }); - - it("should approve OOO request", async function() { - // const response = await acknowledgeOOORequest( - // testOooRequest.id, - // acknowledgeOooRequest, - // testSuperUserId - // ); - // expect(response).to.deep.include({ - // message: REQUEST_APPROVED_SUCCESSFULLY, - // data: { - // ...acknowledgeOooRequest, - // id: testOooRequest.id, - // lastModifiedBy: testSuperUserId, - // updatedAt: response.data.updatedAt - // } - // }); - }); - - it("should reject OOO request", async function() { - // const response = await acknowledgeOOORequest( - // testOooRequest.id, - // { ...acknowledgeOooRequest, status: REQUEST_STATE.REJECTED }, - // testSuperUserId - // ); - // expect(response).to.deep.include({ - // message: REQUEST_REJECTED_SUCCESSFULLY, - // data: { - // ...acknowledgeOooRequest, - // id: testOooRequest.id, - // status: REQUEST_STATE.REJECTED, - // lastModifiedBy: testSuperUserId, - // updatedAt: response.data.updatedAt - // } - // }); - }); - - it("should throw error", async function() { - // sinon.stub(logService, "addLog").throws(new Error(errorMessage)); - // const createSpy = sinon.spy(require("../../../services/oooRequest"), "acknowledgeOOORequest"); - - // try { - // await acknowledgeOOORequest( - // testOooRequest.id, - // acknowledgeOooRequest, - // testSuperUserId - // ); - // } catch (error) { - // expect(error.message).to.equal(errorMessage); - // expect(createSpy.calledOnce).to.be.true; - // } - }); - }); -}); \ No newline at end of file +describe("Test OOO Request Service", function () { + let testUserName: string; + let testUserId: string; + const errorMessage = "Unexpected error occured"; + + + + beforeEach(async function () { + const users = userDataFixture(); + testUserId = await addUser(users[8]); + testUserName = users[8].username; + }); + + afterEach(async function () { + sinon.restore(); + await cleanDb(); + }); + + describe("validateUserStatus", function () { + it("should return USER_STATUS_NOT_FOUND if user status not found", async function () { + const validationResponse = await validateUserStatus(testUserId, { ...testUserStatus, userStatusExists: false }); + expect(validationResponse).to.be.not.undefined; + expect(validationResponse.error).to.equal(USER_STATUS_NOT_FOUND); + }); + + it("should return OOO_STATUS_ALREADY_EXIST if user status is already OOO", async function () { + const validationResponse = await validateUserStatus(testUserId, { + ...testUserStatus, + data: { + ...testUserStatus.data, + currentStatus: { + ...testUserStatus.data.currentStatus, + state: userState.OOO, + }, + }, + }); + expect(validationResponse).to.be.not.undefined; + expect(validationResponse.error).to.equal(OOO_STATUS_ALREADY_EXIST); + }); + + it("should return undefined when all validation checks passes", async function () { + const response = await validateUserStatus(testUserId, testUserStatus); + expect(response).to.not.exist; + }); + }); + + describe("createOooRequest", function () { + beforeEach(async function () { + await updateUserStatus(testUserId, testUserStatus.data); + }); + + afterEach(async function () { + sinon.restore(); + }); + + it("should create OOO request", async function () { + const response = await createOooRequest(validOooStatusRequests, testUserId); + expect(response).to.deep.include({ + ...createdOOORequest, + id: response.id, + requestedBy: testUserId, + }); + }); + + it("should throw error", async function () { + sinon.stub(logService, "addLog").throws(new Error(errorMessage)); + + try { + await createOooRequest(validOooStatusRequests, testUserId); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.message).to.equal(errorMessage); + } + }); + }); + + describe("validateOOOAcknowledgeRequest", function () { + let testOooRequest; + + beforeEach(async function () { + testOooRequest = await createRequest({ + ...createOooRequests3, + userId: testUserId, + comment: null, + lastModifiedBy: null, + }); + }); + + it("should return INVALID_REQUEST_TYPE if request type is not OOO", async function () { + try { + await validateOooAcknowledgeRequest(REQUEST_TYPE.ONBOARDING, testOooRequest.status); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.message).to.equal(INVALID_REQUEST_TYPE); + } + }); + + it("should return REQUEST_ALREADY_APPROVED if request is already approved", async function () { + try { + await validateOooAcknowledgeRequest(REQUEST_TYPE.OOO, REQUEST_STATE.APPROVED); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.message).to.equal(REQUEST_ALREADY_APPROVED); + } + }); + + it("should return REQUEST_ALREADY_REJECTED if request is already rejected", async function () { + try { + await validateOooAcknowledgeRequest(REQUEST_TYPE.OOO, REQUEST_STATE.REJECTED); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.message).to.equal(REQUEST_ALREADY_REJECTED); + } + }); + + it("should return undefined when all validation checks passes", async function () { + const response = await validateOooAcknowledgeRequest(REQUEST_TYPE.OOO, REQUEST_STATE.PENDING); + expect(response).to.not.exist; + }); + }); + + describe("acknowledgeOOORequest", function () { + let testSuperUserId; + let testOooRequest; + + beforeEach(async function () { + const users = userDataFixture(); + const superUserId = await addUser(users[4]); + testSuperUserId = superUserId; + + testOooRequest = await createRequest({ + ...createOooRequests3, + userId: testUserId, + comment: null, + lastModifiedBy: null, + }); + }); + + it("should return 'Request not found' if invalid request id is passed", async function () { + const invalidOOORequestId = "11111111111111111111"; + try { + await acknowledgeOooRequest(invalidOOORequestId, testAcknowledgeOooRequest, testSuperUserId); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.message).to.equal("Request not found"); + } + }); + + it("should approve OOO request", async function () { + const response = await acknowledgeOooRequest(testOooRequest.id, testAcknowledgeOooRequest, testSuperUserId); + expect(response).to.include({ + message: REQUEST_APPROVED_SUCCESSFULLY, + }); + }); + + it("should reject OOO request", async function () { + const response = await acknowledgeOooRequest( + testOooRequest.id, + { ...testAcknowledgeOooRequest, status: REQUEST_STATE.REJECTED }, + testSuperUserId + ); + expect(response).to.include({ + message: REQUEST_REJECTED_SUCCESSFULLY, + }); + }); + + it("should propagate error when logging fails", async function () { + sinon.stub(logService, "addLog").throws(new Error(errorMessage)); + + try { + await acknowledgeOooRequest(testOooRequest.id, testAcknowledgeOooRequest, testSuperUserId); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.message).to.equal(errorMessage); + } + }); + }); +}); diff --git a/types/oooRequest.d.ts b/types/oooRequest.d.ts index fb28c5b41..80cfff0e5 100644 --- a/types/oooRequest.d.ts +++ b/types/oooRequest.d.ts @@ -55,3 +55,20 @@ export type OooRequestCreateRequest = Request & { }; export type OooRequestUpdateRequest = Request & { oooRequestUpdateBody , userData: userData , query: RequestQuery , params: RequestParams }; + +export type AcknowledgeOooRequestQuery = RequestQuery & { + dev?: string +}; + +export type AcknowledgeOooRequestBody = { + type: REQUEST_TYPE.OOO; + comment?: string; + status: REQUEST_STATE.APPROVED | REQUEST_STATE.REJECTED; +} + +export type AcknowledgeOooRequest = Request & { + body: AcknowledgeOooRequestBody; + userData: userData; + query: AcknowledgeOooRequestQuery; + params: RequestParams; +} \ No newline at end of file diff --git a/utils/requests.ts b/utils/requests.ts index f51e8e2e2..23f80b4c8 100644 --- a/utils/requests.ts +++ b/utils/requests.ts @@ -69,27 +69,20 @@ export const newOOOSchema = (request: oldOooStatusRequest) => ({ * @param {boolean} dev - Development flag to determine transformation logic */ export const transformRequestResponse = (allRequests: (OooStatusRequest | oldOooStatusRequest)[], dev: boolean = false): (OooStatusRequest | oldOooStatusRequest)[] => { - const transformedRequests: (OooStatusRequest | oldOooStatusRequest)[] = []; - - for (const request of allRequests) { - if (request.type === REQUEST_TYPE.OOO) { - if (dev) { - if ('status' in request) { - transformedRequests.push(oldOOOSchema(request as OooStatusRequest)); - } else { - transformedRequests.push(request); - } - } else { - if ('state' in request) { - transformedRequests.push(newOOOSchema(request as oldOooStatusRequest)); - } else { - transformedRequests.push(request); - } - } - } else { - transformedRequests.push(request); + return allRequests.map(request => { + + if (request.type !== REQUEST_TYPE.OOO) { + return request; } - } - - return transformedRequests; + + if (dev && 'status' in request) { + return oldOOOSchema(request as OooStatusRequest); + } + + if (!dev && 'state' in request) { + return newOOOSchema(request as oldOooStatusRequest); + } + + return request; + }); }; \ No newline at end of file