diff --git a/controllers/oooRequests.ts b/controllers/oooRequests.ts index 36d2baeab..dcb657631 100644 --- a/controllers/oooRequests.ts +++ b/controllers/oooRequests.ts @@ -50,11 +50,9 @@ export const createOooRequestController = async ( if (!isUserPartOfDiscord) { return res.boom.forbidden(UNAUTHORIZED_TO_CREATE_OOO_REQUEST); } - try { const userStatus = await getUserStatus(userId); const validationResponse = await validateUserStatus(userId, userStatus); - if (validationResponse) { if (validationResponse.error === USER_STATUS_NOT_FOUND) { return res.boom.notFound(validationResponse.error); @@ -70,7 +68,7 @@ export const createOooRequestController = async ( status: REQUEST_STATE.PENDING, }); - if (latestOooRequest) { + if (latestOooRequest && latestOooRequest.status === REQUEST_STATE.PENDING ) { await addLog(logType.PENDING_REQUEST_FOUND, { userId, oooRequestId: latestOooRequest.id }, { message: REQUEST_ALREADY_PENDING } @@ -103,7 +101,7 @@ export const updateOooRequestController = async (req: UpdateRequest, res: Custom return res.boom.badRequest(requestResult.error); } const [logType, returnMessage] = - requestResult.state === REQUEST_STATE.APPROVED + requestResult.status === REQUEST_STATE.APPROVED ? [REQUEST_LOG_TYPE.REQUEST_APPROVED, REQUEST_APPROVED_SUCCESSFULLY] : [REQUEST_LOG_TYPE.REQUEST_REJECTED, REQUEST_REJECTED_SUCCESSFULLY]; @@ -118,19 +116,19 @@ export const updateOooRequestController = async (req: UpdateRequest, res: Custom body: requestResult, }; await addLog(requestLog.type, requestLog.meta, requestLog.body); - if (requestResult.state === REQUEST_STATE.APPROVED) { - const requestData = await getRequests({ id: requestId }); + if (requestResult.status === REQUEST_STATE.APPROVED) { + const requestData = await getRequests({ id: requestId, type: REQUEST_TYPE.OOO }); if (requestData) { - const { from, until, requestedBy, message } = requestData as any; + const { from, until, userId, reason } = requestData as any; const userFutureStatusData = { requestId, status: REQUEST_TYPE.OOO, state: statusState.UPCOMING, from, endsOn: until, - userId: requestedBy, - message, + userId, + message: reason, }; await createUserFutureStatus(userFutureStatusData); await addFutureStatus(userFutureStatusData); diff --git a/middlewares/validators/oooRequests.ts b/middlewares/validators/oooRequests.ts index ab73929f1..04a1ab4b9 100644 --- a/middlewares/validators/oooRequests.ts +++ b/middlewares/validators/oooRequests.ts @@ -34,6 +34,10 @@ export const createOooStatusRequestValidator = async ( "string.empty": "type cannot be empty", "any.required": "type is required", }), + status: joi.string().valid(REQUEST_STATE.PENDING).required().messages({ + "string.empty": "status cannot be empty", + "any.required": "status is required", + }), }); await schema.validateAsync(req.body, { abortEarly: false }); diff --git a/middlewares/validators/requests.ts b/middlewares/validators/requests.ts index 80ff0478b..cec3e56fc 100644 --- a/middlewares/validators/requests.ts +++ b/middlewares/validators/requests.ts @@ -50,23 +50,25 @@ export const updateRequestsMiddleware = async ( res: CustomResponse, next: NextFunction ) => { - const schema = joi - .object() - .strict() - .keys({ + const { type } = req.body; + const baseSchema = { reason: joi.string().optional() .messages({ "string.empty": "reason cannot be empty", }), - state: joi - .string() - .valid(REQUEST_STATE.APPROVED, REQUEST_STATE.REJECTED) - .required() - .messages({ - "any.only": "state must be APPROVED or REJECTED", - }), type: joi.string().valid(REQUEST_TYPE.OOO, REQUEST_TYPE.EXTENSION, REQUEST_TYPE.ONBOARDING).required(), - message: joi.string().optional() + }; + + const schema = joi.object().strict().keys({ + ...baseSchema, + status: type === REQUEST_TYPE.OOO ? joi.string().valid(REQUEST_STATE.PENDING, REQUEST_STATE.APPROVED, REQUEST_STATE.REJECTED).required() : joi.string().valid(REQUEST_STATE.APPROVED, REQUEST_STATE.REJECTED).optional(), + comment: type === REQUEST_TYPE.OOO + ? joi.string().required().messages({ + "any.required": "comment is required for OOO requests", + "string.empty": "comment cannot be empty for OOO requests" + }) + : joi.string().optional(), + state: type !== REQUEST_TYPE.OOO ? joi.string().valid(REQUEST_STATE.APPROVED, REQUEST_STATE.REJECTED).required() : joi.string().valid(REQUEST_STATE.APPROVED, REQUEST_STATE.REJECTED).optional(), }); try { diff --git a/models/requests.ts b/models/requests.ts index 064eebd8c..0dd8fcb57 100644 --- a/models/requests.ts +++ b/models/requests.ts @@ -1,6 +1,6 @@ import firestore from "../utils/firestore"; const requestModel = firestore.collection("requests"); -import { REQUEST_ALREADY_APPROVED, REQUEST_ALREADY_REJECTED, REQUEST_STATE } from "../constants/requests"; +import { REQUEST_ALREADY_APPROVED, REQUEST_ALREADY_REJECTED, REQUEST_STATE, REQUEST_TYPE } from "../constants/requests"; import { ERROR_WHILE_FETCHING_REQUEST, ERROR_WHILE_CREATING_REQUEST, @@ -8,6 +8,8 @@ import { REQUEST_DOES_NOT_EXIST, } from "../constants/requests"; import { getUserId } from "../utils/users"; +import { mapOOOResponseData } from "../utils/mapOOOResponseData"; +const logger = require("../utils/logger"); const SIZE = 5; export const createRequest = async (body: any) => { @@ -37,12 +39,12 @@ export const updateRequest = async (id: string, body: any, lastModifiedBy: strin error: REQUEST_DOES_NOT_EXIST, }; } - if (existingRequestDoc.data().state === REQUEST_STATE.APPROVED) { + if (existingRequestDoc.data().status === REQUEST_STATE.APPROVED) { return { error: REQUEST_ALREADY_APPROVED, }; } - if (existingRequestDoc.data().state === REQUEST_STATE.REJECTED) { + if (existingRequestDoc.data().status === REQUEST_STATE.REJECTED) { return { error: REQUEST_ALREADY_REJECTED, }; @@ -101,7 +103,8 @@ export const getRequests = async (query: any) => { requestQuery = requestQuery.where("type", "==", type); } if (state) { - requestQuery = requestQuery.where("state", "==", state); + const fieldName = (type === REQUEST_TYPE.OOO && !dev) ? "status" : "state"; + requestQuery = requestQuery.where(fieldName, "==", state); } requestQuery = requestQuery.orderBy("createdAt", "desc"); @@ -149,6 +152,10 @@ export const getRequests = async (query: any) => { return null; } + // because the Request document is changed and the dashboard site still expects the old data format when dev flag is off + if(type === REQUEST_TYPE.OOO && !dev){ + allRequests = mapOOOResponseData(allRequests); + } return { allRequests, prev: prevDoc.empty ? null : prevDoc.docs[0].id, diff --git a/test/fixtures/oooRequest/oooRequest.ts b/test/fixtures/oooRequest/oooRequest.ts index 30b72d2a0..a2c4fe49f 100644 --- a/test/fixtures/oooRequest/oooRequest.ts +++ b/test/fixtures/oooRequest/oooRequest.ts @@ -16,7 +16,8 @@ export const validOooStatusRequests = { type: "OOO", from: Date.now() + 1 * 24 * 60 * 60 * 1000, until: Date.now() + 5 * 24 * 60 * 60 * 1000, - reason: "Out of office for personal reasons." + reason: "Out of office for personal reasons.", + status: REQUEST_STATE.PENDING, }; export const createdOOORequest = { @@ -70,8 +71,8 @@ export const updateOooRejectedRequests = { }; export const validOooStatusUpdate ={ - state: REQUEST_STATE.APPROVED, - reason: "Welcome back! Enjoy the conference.", + status: REQUEST_STATE.APPROVED, + comment: "Welcome back! Enjoy the conference.", type:REQUEST_TYPE.OOO } diff --git a/test/integration/onboardingExtension.test.ts b/test/integration/onboardingExtension.test.ts index a862ad82e..ef1669896 100644 --- a/test/integration/onboardingExtension.test.ts +++ b/test/integration/onboardingExtension.test.ts @@ -440,7 +440,7 @@ describe("/requests Onboarding Extension", () => { if(err) return done(err); expect(res.statusCode).to.equal(400); expect(res.body.error).to.equal("Bad Request"); - expect(res.body.message).to.equal('"type" must be one of [OOO, EXTENSION, ONBOARDING]'); + expect(res.body.message).to.equal('Invalid request type'); done(); }) }) diff --git a/test/integration/requests.test.ts b/test/integration/requests.test.ts index 2e83acf18..e31ad0ac4 100644 --- a/test/integration/requests.test.ts +++ b/test/integration/requests.test.ts @@ -34,6 +34,8 @@ import { UNAUTHORIZED_TO_CREATE_OOO_REQUEST, USER_STATUS_NOT_FOUND, OOO_STATUS_ALREADY_EXIST, + REQUEST_FETCHED_SUCCESSFULLY, + ERROR_WHILE_FETCHING_REQUEST, } from "../../constants/requests"; import { updateTask } from "../../models/tasks"; import { validTaskAssignmentRequest, validTaskCreqtionRequest } from "../fixtures/taskRequests/taskRequests"; @@ -560,7 +562,6 @@ describe("/requests OOO", function () { done(); }); }); - it("should return 400 if request is already approved", function (done) { chai .request(app) @@ -691,7 +692,71 @@ describe("/requests OOO", function () { done(); }); }); + it("should return requests with page link when page query is passed", function (done) { + chai + .request(app) + .get("/requests?page=1") + .end(function (err, res) { + expect(res).to.have.status(200); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal(REQUEST_FETCHED_SUCCESSFULLY); + expect(res.body).to.have.property("page"); + expect(res.body.page).to.equal("/requests?page=2"); + done(); + }); + }); + + it("should return requests with next and prev links when cursor based pagination is used", function (done) { + chai + .request(app) + .get("/requests?next=someDocId") + .end(function (err, res) { + expect(res).to.have.status(200); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal(REQUEST_FETCHED_SUCCESSFULLY); + expect(res.body).to.have.property("next"); + expect(res.body).to.have.property("prev"); + done(); + }); + }); }); + + + + it("should return 204 if requests array is empty", function (done) { + chai + .request(app) + .get("/requests") + .end(function (err, res) { + expect(res).to.have.status(204); + done(); + }); + }); + + it("should return request when queried by id", function (done) { + chai + .request(app) + .get("/requests?id=someId") + .end(function (err, res) { + expect(res).to.have.status(200); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal(REQUEST_FETCHED_SUCCESSFULLY); + expect(res.body).to.have.property("data"); + done(); + }); + }); + + it("should return 500 if error occurs while fetching requests", function (done) { + chai + .request(app) + .get("/requests") + .end(function (err, res) { + expect(res).to.have.status(500); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal(ERROR_WHILE_FETCHING_REQUEST); + done(); + }); + }); }); describe("/requests Extension", function () { @@ -965,6 +1030,37 @@ describe("/requests Extension", function () { }); }); + it("should return 400(Bad Request) if request type is invalid", function (done) { + chai + .request(app) + .put(`/requests/${pendingExtensionRequestId}`) + .set("cookie", `${cookieName}=${superUserJwtToken}`) + .send({ + type: "INVALID_TYPE", + state: REQUEST_STATE.APPROVED + }) + .end(function (err, res) { + expect(res).to.have.status(400); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal("Invalid request type"); + done(); + }); + }); + + it("should return 400(Bad Request) if request state is invalid", function (done) { + chai + .request(app) + .put(`/requests/${pendingExtensionRequestId}`) + .set("cookie", `${cookieName}=${superUserJwtToken}`) + .send(invalidExtensionRequest) + .end(function (err, res) { + expect(res).to.have.status(400); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal("state must be APPROVED or REJECTED"); + done(); + }); + }); + it("should return 401 if user is not super user", function (done) { chai .request(app) @@ -1070,6 +1166,7 @@ describe("/requests Extension", function () { done(); }); }); + }); }); diff --git a/test/unit/middlewares/oooRequests.test.ts b/test/unit/middlewares/oooRequests.test.ts index 11272e860..86e3c3081 100644 --- a/test/unit/middlewares/oooRequests.test.ts +++ b/test/unit/middlewares/oooRequests.test.ts @@ -64,17 +64,20 @@ describe("OOO Status Request Validators", function () { from: null, until: null, reason: "", + status: "INVALID" }, }; try { await createOooStatusRequestValidator(req as any, res as any, nextSpy); } catch (error) { expect(error).to.be.an.instanceOf(Error); - expect(error.details.length).to.equal(4); + expect(error.details.length).to.equal(5); expect(error.details[0].message).to.equal(`"from" must be a number`); - expect(error.details[1].message).to.equal(`"until" must be a number`); + expect(error.details[1].message).to.equal(`"until" must be a number`); expect(error.details[2].message).to.equal('reason cannot be empty'); expect(error.details[3].message).to.equal('"type" must be [OOO]'); + expect(error.details[4].message).to.equal('"status" must be [PENDING]'); + } }); diff --git a/utils/mapOOOResponseData.ts b/utils/mapOOOResponseData.ts new file mode 100644 index 000000000..1e99dc66b --- /dev/null +++ b/utils/mapOOOResponseData.ts @@ -0,0 +1,22 @@ +import { OooStatusRequest } from "../types/oooRequest"; + +/** + * @description Map the OOO response data to the desired format + * @param data - The OOO response data in the new format + * @returns The mapped OOO response data in the old format + */ +export const mapOOOResponseData = (data: OooStatusRequest[]) => { + return data.map((item) => ({ + id: item.id, + from: item.from, + until: item.until, + type: item.type, + message: item.reason, + state: item.status, + reason: item.comment, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + requestedBy: item.userId, + lastModifiedBy: item.lastModifiedBy, + })); +}; \ No newline at end of file