diff --git a/constants/requests.ts b/constants/requests.ts index e948c588f..29f871a02 100644 --- a/constants/requests.ts +++ b/constants/requests.ts @@ -12,6 +12,7 @@ export const LOG_ACTION = { export const REQUEST_TYPE = { OOO: "OOO", + EXTENSION: "EXTENSION", ALL: "ALL", }; diff --git a/controllers/extensionRequestsv2.ts b/controllers/extensionRequestsv2.ts new file mode 100644 index 000000000..90bca33ae --- /dev/null +++ b/controllers/extensionRequestsv2.ts @@ -0,0 +1,93 @@ +import { getRequestByKeyValues } from "../models/requests"; +import { LOG_ACTION, REQUEST_LOG_TYPE, REQUEST_STATE, REQUEST_TYPE } from "../constants/requests"; +import { addLog } from "../models/logs"; +import { createRequest } from "../models/requests"; +import { fetchTask } from "../models/tasks"; +import { CustomResponse } from "../typeDefinitions/global"; +import { ExtensionRequest, ExtensionRequestCreateBody, ExtensionRequestRequest } from "../types/extensionRequests"; +import { getUsernameElseUndefined } from "../utils/users"; + +export const createTaskExtensionRequest = async (req: ExtensionRequestRequest, res: CustomResponse) => { + try { + const { userData } = req; + const { id: requestedBy, roles } = userData || {}; + const { body } = req; + + if (!requestedBy) { + return res.boom.unauthorized(); + } + + const { taskId } = body; + let extensionBody: ExtensionRequestCreateBody = { ...body, requestedBy }; + let assignee: string | undefined = undefined; + + const { taskData: task } = await fetchTask(taskId); + if (!task) { + return res.boom.badRequest("Task Not Found"); + } + + const { assigneeId, endsOn } = task; + if (!assigneeId) { + return res.boom.badRequest("Assignee is not present for this task"); + } + + assignee = await getUsernameElseUndefined(assigneeId); + if (!assignee) { + return res.boom.badRequest("Assignee is not present for this task"); + } else { + extensionBody = { ...extensionBody, assignee }; + } + + if (requestedBy !== assigneeId && !roles?.super_user) { + return res.boom.forbidden("Only assigned user and super user can create an extension request for this task."); + } + + if (endsOn >= body.newEndsOn) { + return res.boom.badRequest("New ETA must be greater than Old ETA"); + } + + if (body.oldEndsOn != endsOn) { + return res.boom.badRequest("Old ETA does not match the task's ETA"); + } + + const latestExtensionRequest: ExtensionRequest | undefined = await getRequestByKeyValues({ + taskId, + state: REQUEST_STATE.PENDING, + type: REQUEST_TYPE.EXTENSION, + }); + + if (latestExtensionRequest && latestExtensionRequest.state === REQUEST_STATE.PENDING) { + return res.boom.badRequest("An extension request for this task already exists."); + } + + let requestNumber: number = latestExtensionRequest?.requestedBy === requestedBy && latestExtensionRequest.requestNumber ? latestExtensionRequest.requestNumber + 1 : 1; + extensionBody = { ...extensionBody, requestNumber }; + + const extensionRequest = await createRequest(extensionBody); + if ("error" in extensionRequest) { + return res.boom.badRequest(extensionRequest.error); + } + + const extensionLog = { + type: REQUEST_LOG_TYPE.REQUEST_CREATED, + meta: { + taskId, + requestId: extensionRequest.id, + action: LOG_ACTION.CREATE, + createdBy: requestedBy, + createdAt: Date.now(), + }, + body: extensionBody, + }; + + await addLog(extensionLog.type, extensionLog.meta, extensionLog.body); + + return res.status(201).json({ + message: "Extension Request created successfully!", + extensionRequest: { ...extensionBody, id: extensionRequest.id }, + }); + } catch (err) { + logger.error(`Error while creating new extension request: ${err}`); + return res.boom.badImplementation('Internal Server Error'); + } +}; diff --git a/controllers/oooRequests.ts b/controllers/oooRequests.ts new file mode 100644 index 000000000..825b04e69 --- /dev/null +++ b/controllers/oooRequests.ts @@ -0,0 +1,55 @@ +import { + REQUEST_LOG_TYPE, + LOG_ACTION, + REQUEST_CREATED_SUCCESSFULLY, + ERROR_WHILE_CREATING_REQUEST, + REQUEST_ALREADY_PENDING, + REQUEST_STATE, + REQUEST_TYPE, +} from "../constants/requests"; +import { addLog } from "../models/logs"; +import { createRequest, getRequestByKeyValues } from "../models/requests"; +import { CustomResponse } from "../typeDefinitions/global"; +import { OooRequestCreateRequest, OooStatusRequest } from "../types/oooRequest"; + +export const createOooRequestController = async (req: OooRequestCreateRequest, res: CustomResponse) => { + const requestBody = req.body; + const userId = req?.userData?.id; + + if (!userId) { + return res.boom.unauthorized(); + } + + try { + const latestOooRequest:OooStatusRequest = await getRequestByKeyValues({ requestedBy: userId, type: REQUEST_TYPE.OOO , state: REQUEST_STATE.PENDING }); + + if (latestOooRequest && latestOooRequest.state === REQUEST_STATE.PENDING) { + return res.boom.badRequest(REQUEST_ALREADY_PENDING); + } + + const requestResult = await createRequest({ requestedBy: userId, ...requestBody }); + + const requestLog = { + type: REQUEST_LOG_TYPE.REQUEST_CREATED, + meta: { + requestId: requestResult.id, + action: LOG_ACTION.CREATE, + createdBy: userId, + createdAt: Date.now(), + }, + body: requestResult, + }; + await addLog(requestLog.type, requestLog.meta, requestLog.body); + + return res.status(201).json({ + message: REQUEST_CREATED_SUCCESSFULLY, + data: { + id: requestResult.id, + ...requestResult, + }, + }); + } catch (err) { + logger.error(ERROR_WHILE_CREATING_REQUEST, err); + return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST); + } +}; diff --git a/controllers/progresses.js b/controllers/progresses.js index 41b51a9fb..cdf1f31bc 100644 --- a/controllers/progresses.js +++ b/controllers/progresses.js @@ -49,8 +49,8 @@ const createProgress = async (req, res) => { body: { type, completed, planned, blockers, taskId }, } = req; try { - const data = await createProgressDocument({ ...req.body, userId: req.userData.id }); - await sendTaskUpdate(completed, blockers, planned, req.userData.username, taskId); + const { data, taskTitle } = await createProgressDocument({ ...req.body, userId: req.userData.id }); + await sendTaskUpdate(completed, blockers, planned, req.userData.username, taskId, taskTitle); return res.status(201).json({ data, message: `${type.charAt(0).toUpperCase() + type.slice(1)} ${PROGRESS_DOCUMENT_CREATED_SUCCEEDED}`, diff --git a/controllers/requests.ts b/controllers/requests.ts index a5d0708c4..be66c19bd 100644 --- a/controllers/requests.ts +++ b/controllers/requests.ts @@ -1,71 +1,38 @@ import { ERROR_WHILE_FETCHING_REQUEST, - ERROR_WHILE_CREATING_REQUEST, ERROR_WHILE_UPDATING_REQUEST, REQUEST_REJECTED_SUCCESSFULLY, REQUEST_APPROVED_SUCCESSFULLY, REQUEST_FETCHED_SUCCESSFULLY, - REQUEST_CREATED_SUCCESSFULLY, REQUEST_STATE, LOG_ACTION, REQUEST_LOG_TYPE, REQUEST_TYPE, } from "../constants/requests"; import { statusState } from "../constants/userStatus"; -import {addFutureStatus} from "../models/userStatus"; +import { addFutureStatus } from "../models/userStatus"; import { createUserFutureStatus } from "../models/userFutureStatus"; -import { createRequest, getRequests, updateRequest } from "../models/requests"; +import { getRequests, updateRequest } from "../models/requests"; import { addLog } from "../models/logs"; import { getPaginatedLink } from "../utils/helper"; +import { createOooRequestController } from "./oooRequests"; +import { OooRequestCreateRequest, OooRequestResponse } from "../types/oooRequest"; +import { CustomResponse } from "../typeDefinitions/global"; +import { ExtensionRequestRequest, ExtensionRequestResponse } from "../types/extensionRequests"; +import { createTaskExtensionRequest } from "./extensionRequestsv2"; -export const createRequestController = async (req: any, res: any) => { - const requestBody = req.body; - const userId = req?.userData?.id; - if (!userId) { - return res.boom.unauthorized(); - } - - try { - const requestResult = await createRequest({ requestedBy: userId, ...requestBody }); - if ("error" in requestResult) { - const requestLog = { - type: REQUEST_LOG_TYPE.REQUEST_BLOCKED, - meta: { - action: LOG_ACTION.ERRORS, - createdBy: userId, - createdAt: Date.now(), - }, - body: { - error: requestResult.error, - ...requestBody, - }, - }; - await addLog(requestLog.type, requestLog.meta, requestLog.body); +export const createRequestController = async ( + req: OooRequestCreateRequest | ExtensionRequestRequest, + res: CustomResponse) => { - return res.boom.badRequest(requestResult.error); - } else { - const requestLog = { - type: REQUEST_LOG_TYPE.REQUEST_CREATED, - meta: { - requestId: requestResult.id, - action: LOG_ACTION.CREATE, - createdBy: userId, - createdAt: Date.now(), - }, - body: requestResult, - }; - await addLog(requestLog.type, requestLog.meta, requestLog.body); - return res.status(201).json({ - message: REQUEST_CREATED_SUCCESSFULLY, - data: { - id: requestResult.id, - ...requestResult, - }, - }); - } - } catch (err) { - logger.error(ERROR_WHILE_CREATING_REQUEST, err); - return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST); + const type = req.body.type; + switch (type) { + case REQUEST_TYPE.OOO: + return await createOooRequestController(req as OooRequestCreateRequest, res as OooRequestResponse); + case REQUEST_TYPE.EXTENSION: + return await createTaskExtensionRequest(req as ExtensionRequestRequest, res as ExtensionRequestResponse); + default: + return res.boom.badRequest("Invalid request type"); } }; @@ -142,7 +109,7 @@ export const getRequestsController = async (req: any, res: any) => { return res.status(204).send(); } - if(page) { + if (page) { const pageLink = `/requests?page=${page}&dev=${query.dev}`; return res.status(200).json({ message: REQUEST_FETCHED_SUCCESSFULLY, diff --git a/middlewares/validators/extensionRequestsv2.ts b/middlewares/validators/extensionRequestsv2.ts new file mode 100644 index 000000000..40dee4eac --- /dev/null +++ b/middlewares/validators/extensionRequestsv2.ts @@ -0,0 +1,46 @@ +import joi from "joi"; +import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../types/extensionRequests"; +import { NextFunction } from "express"; +import { REQUEST_TYPE,REQUEST_STATE } from "../../constants/requests"; + +export const createExtensionRequestValidator = async ( + req: ExtensionRequestRequest, + res: ExtensionRequestResponse, + next: NextFunction +) => { + + const schema = joi + .object() + .strict() + .keys({ + taskId: joi.string().required().messages({ + "string.empty": "taskId cannot be empty", + "any.required": "taskId is required", + }), + title: joi.string().required().messages({ + "string.empty": "title cannot be empty", + "any.required": "title is required", + }), + oldEndsOn: joi.number().required().messages({ + "number.base": "oldEndsOn must be a number", + "any.required": "oldEndsOn is required", + }), + newEndsOn: joi.number().required().min(joi.ref("oldEndsOn")).messages({ + "number.base": "newEndsOn must be a number", + "any.required": "newEndsOn is required", + }), + message: joi.string().required().messages({ + "string.empty": "message cannot be empty", + }), + state: joi.string().valid(REQUEST_STATE.PENDING).required().messages({ + "string.empty": "state cannot be empty", + "any.required": "state is required", + }), + type: joi.string().valid(REQUEST_TYPE.EXTENSION).required().messages({ + "string.empty": "type cannot be empty", + "any.required": "type is required", + }), + }); + + await schema.validateAsync(req.body, { abortEarly: false }); +}; diff --git a/middlewares/validators/requests.ts b/middlewares/validators/requests.ts index 866ce9550..c434930e4 100644 --- a/middlewares/validators/requests.ts +++ b/middlewares/validators/requests.ts @@ -3,10 +3,13 @@ import { NextFunction } from "express"; import { REQUEST_STATE, REQUEST_TYPE } from "../../constants/requests"; import { OooRequestCreateRequest, OooRequestResponse, OooRequestUpdateRequest } from "../../types/oooRequest"; import { createOooStatusRequestValidator, updateOooStatusRequestValidator } from "./oooRequests"; +import { createExtensionRequestValidator } from "./extensionRequestsv2"; +import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../types/extensionRequests"; +import { CustomResponse } from "../../typeDefinitions/global"; export const createRequestsMiddleware = async ( - req: OooRequestCreateRequest, - res: OooRequestResponse, + req: OooRequestCreateRequest|ExtensionRequestRequest, + res: CustomResponse, next: NextFunction ) => { const type = req.body.type; @@ -20,13 +23,16 @@ export const createRequestsMiddleware = async ( case REQUEST_TYPE.OOO: await createOooStatusRequestValidator(req as OooRequestCreateRequest, res as OooRequestResponse, next); break; + case REQUEST_TYPE.EXTENSION: + await createExtensionRequestValidator(req as ExtensionRequestRequest, res as ExtensionRequestResponse, next); + break; default: res.boom.badRequest(`Invalid request type: ${type}`); } next(); } catch (error) { - const errorMessages = error.details.map((detail) => detail.message); + const errorMessages = error.details.map((detail:any) => detail.message); logger.error(`Error while validating request payload : ${errorMessages}`); res.boom.badRequest(errorMessages); } @@ -67,7 +73,7 @@ export const getRequestsMiddleware = async (req: OooRequestCreateRequest, res: O id: joi.string().optional(), type: joi .string() - .valid(REQUEST_TYPE.OOO, REQUEST_TYPE.ALL) + .valid(REQUEST_TYPE.OOO, REQUEST_TYPE.EXTENSION, REQUEST_TYPE.ALL) .optional(), requestedBy: joi.string().insensitive().optional(), state: joi diff --git a/models/progresses.js b/models/progresses.js index ffc56b6c4..8e8a622d9 100644 --- a/models/progresses.js +++ b/models/progresses.js @@ -25,8 +25,9 @@ const createProgressDocument = async (progressData) => { const { type, taskId } = progressData; const createdAtTimestamp = new Date().getTime(); const progressDateTimestamp = getProgressDateTimestamp(); + let taskTitle; if (taskId) { - await assertTaskExists(taskId); + taskTitle = await assertTaskExists(taskId); } const query = buildQueryForPostingProgress(progressData); const existingDocumentSnapshot = await query.where("date", "==", progressDateTimestamp).get(); @@ -35,7 +36,8 @@ const createProgressDocument = async (progressData) => { } const progressDocumentData = { ...progressData, createdAt: createdAtTimestamp, date: progressDateTimestamp }; const { id } = await progressesCollection.add(progressDocumentData); - return { id, ...progressDocumentData }; + const data = { id, ...progressDocumentData }; + return { data, taskTitle }; }; /** diff --git a/models/requests.ts b/models/requests.ts index c34f360d0..5eabb0b40 100644 --- a/models/requests.ts +++ b/models/requests.ts @@ -7,7 +7,6 @@ import { ERROR_WHILE_CREATING_REQUEST, ERROR_WHILE_UPDATING_REQUEST, REQUEST_DOES_NOT_EXIST, - REQUEST_ALREADY_PENDING, } from "../constants/requests"; import * as admin from "firebase-admin"; import { getUserId } from "../utils/users"; @@ -15,17 +14,6 @@ const SIZE = 5; export const createRequest = async (body: any) => { try { - const existingRequest = await requestModel - .where("requestedBy", "==", body.requestedBy) - .where("state", "==", REQUEST_STATE.PENDING) - .where("type", "==", body.type) - .get(); - - if (!existingRequest.empty) { - return { - error: REQUEST_ALREADY_PENDING, - }; - } const requestBody: any = { createdAt: Date.now(), updatedAt: Date.now(), @@ -163,3 +151,33 @@ export const getRequests = async (query: any) => { throw error; } }; + +interface KeyValues { + [key: string]: string; +} + +export const getRequestByKeyValues = async (keyValues: KeyValues) => { + try { + let requestQuery: any = requestModel; + Object.entries(keyValues).forEach(([key, value]) => { + requestQuery = requestQuery.where(key, "==", value); + }); + + const requestQueryDoc = await requestQuery.get(); + if (requestQueryDoc.empty) { + return null; + } + + let requests: any; + requestQueryDoc.forEach((doc: any) => { + requests = { + id: doc.id, + ...doc.data(), + }; + }); + return requests; + } catch (error) { + logger.error(ERROR_WHILE_FETCHING_REQUEST, error); + throw error; + } +}; diff --git a/test/fixtures/extension-requests/extensionRequests.ts b/test/fixtures/extension-requests/extensionRequests.ts new file mode 100644 index 000000000..f1d38fa63 --- /dev/null +++ b/test/fixtures/extension-requests/extensionRequests.ts @@ -0,0 +1,11 @@ +import { REQUEST_STATE, REQUEST_TYPE } from "../../../constants/requests"; + +export const extensionCreateObject = { + taskId: "4XlEQ64H8puuLTrwIi93", + title: "Extension Request", + oldEndsOn: 1708674980000, + newEndsOn: 1709674980000, + message: "request message", + type: REQUEST_TYPE.EXTENSION, + state: REQUEST_STATE.PENDING, +}; diff --git a/test/integration/requests.test.ts b/test/integration/requests.test.ts index da425dedc..650f3998b 100644 --- a/test/integration/requests.test.ts +++ b/test/integration/requests.test.ts @@ -25,6 +25,7 @@ import { REQUEST_DOES_NOT_EXIST, REQUEST_ALREADY_PENDING, } from "../../constants/requests"; +import { updateTask } from "../../models/tasks"; const userData = userDataFixture(); chai.use(chaiHttp); @@ -38,7 +39,7 @@ let oooRequestData: any; let oooRequestData2: any; let testUserId: string; -describe("/requests", function () { +describe("/requests OOO", function () { beforeEach(async function () { const userIdPromises = [addUser(userData[16]), addUser(userData[4])]; const [userId, superUserId] = await Promise.all(userIdPromises); @@ -380,7 +381,222 @@ describe("/requests", function () { .end(function (err, res) { expect(res).to.have.status(400); expect(res.body.error).to.equal("Bad Request"); - expect(res.body.message).to.equal('"type" must be one of [OOO, ALL]'); + expect(res.body.message).to.equal('"type" must be one of [OOO, EXTENSION, ALL]'); + done(); + }); + }); + }); +}); + + +describe("/requests Extension", function () { + let taskId1: string; + let taskId2: string; + let userId1: string; + let userId2: string; + let superUserId: string; + let userJwtToken1: string; + let userJwtToken2: string; + let superUserJwtToken: string; + + let extensionRequest = { + type: "EXTENSION", + title: "change ETA", + oldEndsOn: 1694736000, + newEndsOn: 1709674980000, + message: "Due to some reasons", + state: "PENDING" + }; + + const taskData = [ + { + title: "Test task 1", + type: "feature", + endsOn: 1694736000, + startedOn: 1694736000, + status: "ACTIVE", + percentCompleted: 10, + participants: [], + isNoteworthy: true, + }, + { + title: "Test task", + type: "feature", + endsOn: 1234, + startedOn: 4567, + status: "AVAILABLE", + percentCompleted: 10, + participants: [], + isNoteworthy: true, + }, + ]; + + beforeEach(async function () { + userId1 = await addUser(userData[16]); + userId2 = await addUser(userData[17]); + superUserId = await addUser(userData[4]); + + userJwtToken1 = authService.generateAuthToken({ userId: userId1 }); + userJwtToken2 = authService.generateAuthToken({ userId: userId2 }); + superUserJwtToken = authService.generateAuthToken({ userId: superUserId }); + + taskId1 = (await updateTask({ ...taskData[0], assigneeId: userId1 })).taskId; + taskId2 = (await updateTask({ ...taskData[1] })).taskId; + + }); + + afterEach(async function () { + await cleanDb(); + }); + + describe("POST /requests", function () { + it("should return 401(Unauthorized) if user is not logged in", function (done) { + chai + .request(app) + .post("/requests?dev=true") + .send(extensionRequest) + .end(function (err, res) { + expect(res).to.have.status(401); + done(); + }); + }); + it("should create a new extension request", function (done) { + const extensionRequestObj = { + taskId: taskId1, + ...extensionRequest + }; + chai + .request(app) + .post("/requests?dev=true") + .set("cookie", `${cookieName}=${userJwtToken1}`) + .send(extensionRequestObj) + .end(function (err, res) { + expect(res).to.have.status(201); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal('Extension Request created successfully!'); + done(); + }); + }); + + it("should create a new extension request by super user", function (done) { + const extensionRequestObj = { + taskId: taskId1, + ...extensionRequest + }; + chai + .request(app) + .post("/requests?dev=true") + .set("cookie", `${cookieName}=${superUserJwtToken}`) + .send(extensionRequestObj) + .end(function (err, res) { + expect(res).to.have.status(201); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal('Extension Request created successfully!'); + done(); + }); + }); + + it("should not create a new extension request by another user", function (done) { + const extensionRequestObj = { + taskId: taskId1, + ...extensionRequest + }; + chai + .request(app) + .post("/requests?dev=true") + .set("cookie", `${cookieName}=${userJwtToken2}`) + .send(extensionRequestObj) + .end(function (err, res) { + expect(res).to.have.status(403); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal('Only assigned user and super user can create an extension request for this task.'); + done(); + }); + }); + + it("should not create a new extension request if task is not exist", function (done) { + const extensionRequestObj = { + taskId: "randomId", + ...extensionRequest + }; + chai + .request(app) + .post("/requests?dev=true") + .set("cookie", `${cookieName}=${userJwtToken1}`) + .send(extensionRequestObj) + .end(function (err, res) { + expect(res).to.have.status(400); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal('Task Not Found'); + done(); + }); + }); + + it("should not create a new extension request if assignee is not present", function (done) { + const extensionRequestObj = { + taskId: taskId2, + ...extensionRequest + }; + chai + .request(app) + .post("/requests?dev=true") + .set("cookie", `${cookieName}=${userJwtToken1}`) + .send(extensionRequestObj) + .end(function (err, res) { + expect(res).to.have.status(400); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal('Assignee is not present for this task'); + done(); + }); + }); + + it("should not create a new extension request if old ETA does not match the task's ETA", function (done) { + const extensionRequestObj = { + taskId: taskId1, + ...extensionRequest, + oldEndsOn: 1234 + }; + chai + .request(app) + .post("/requests?dev=true") + .set("cookie", `${cookieName}=${userJwtToken1}`) + .send(extensionRequestObj) + .end(function (err, res) { + expect(res).to.have.status(400); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal('Old ETA does not match the task\'s ETA'); + done(); + }); + }); + + // Should Return Bad Request for Existing Pending Extension Request + it("should not create a new extension request if an extension request for this task already exists", function (done) { + const extensionRequestObj = { + taskId: taskId1, + ...extensionRequest + }; + chai + .request(app) + .post("/requests?dev=true") + .set("cookie", `${cookieName}=${userJwtToken1}`) + .send(extensionRequestObj) + .end(async function (err, res) { + expect(res).to.have.status(201); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal('Extension Request created successfully!'); + + const extensionRequestObj2 = { + taskId: taskId1, + ...extensionRequest + }; + const response = await chai + .request(app) + .post("/requests?dev=true") + .set("cookie", `${cookieName}=${userJwtToken1}`) + .send(extensionRequestObj2); + expect(response).to.have.status(400); + expect(response.body).to.have.property("message"); + expect(response.body.message).to.equal('An extension request for this task already exists.'); done(); }); }); diff --git a/test/unit/middlewares/extensionRequests.test.ts b/test/unit/middlewares/extensionRequests.test.ts new file mode 100644 index 000000000..4631542d6 --- /dev/null +++ b/test/unit/middlewares/extensionRequests.test.ts @@ -0,0 +1,64 @@ +import chai from "chai"; +import sinon from "sinon"; +const { expect } = chai; + +import { createExtensionRequestValidator } from "../../../middlewares/validators/extensionRequestsv2"; +import { extensionCreateObject } from "../../fixtures/extension-requests/extensionRequests"; +import { REQUEST_STATE } from "../../../constants/requests"; +import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../../types/extensionRequests"; + +describe("Extension Request Validators", function () { + describe("createExtensionRequestValidator", function () { + it("should validate for a valid create extension request", async function () { + const req = { + body: extensionCreateObject, + }; + const res = {}; + const nextSpy = sinon.spy(); + const response = await createExtensionRequestValidator(req as ExtensionRequestRequest, res as ExtensionRequestResponse, nextSpy); + expect(nextSpy.calledOnce); + }); + + it("should not validate for an invalid extension request on wrong type", async function () { + const req = { + body: { ...extensionCreateObject, type: "ACTIVE" }, + }; + const res = {}; + const nextSpy = sinon.spy(); + try { + await createExtensionRequestValidator(req as ExtensionRequestRequest, res as ExtensionRequestResponse, nextSpy); + } catch (error) { + expect(error).to.be.an.instanceOf(Error); + expect(error.details[0].message).to.equal(`"type" must be [EXTENSION]`); + } + }); + + it("should not validate for an invalid extension request on wrong status", async function () { + const req = { + body: { ...extensionCreateObject, state: REQUEST_STATE.APPROVED }, + }; + const res = {}; + const nextSpy = sinon.spy(); + try { + await createExtensionRequestValidator(req as ExtensionRequestRequest, res as ExtensionRequestResponse, nextSpy); + } catch (error) { + expect(error).to.be.an.instanceOf(Error); + expect(error.details[0].message).to.equal(`"state" must be [PENDING]`); + } + }); + + it("should not validate for an invalid extension request on missing taskId", async function () { + const req = { + body: { ...extensionCreateObject, taskId: "" }, + }; + const res = {}; + const nextSpy = sinon.spy(); + try { + await createExtensionRequestValidator(req as ExtensionRequestRequest, res as ExtensionRequestResponse, nextSpy); + } catch (error) { + expect(error).to.be.an.instanceOf(Error); + expect(error.details[0].message).to.equal(`taskId cannot be empty`); + } + }); + }); +}); diff --git a/test/unit/models/requests.test.ts b/test/unit/models/requests.test.ts index eab4ed9f0..6c1cb1df6 100644 --- a/test/unit/models/requests.test.ts +++ b/test/unit/models/requests.test.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import cleanDb from "../../utils/cleanDb"; -import { createRequest, getRequests, updateRequest } from "../../../models/requests"; +import { createRequest, getRequests, updateRequest, getRequestByKeyValues } from "../../../models/requests"; import { createOooRequests, createOooRequests2, @@ -8,7 +8,7 @@ import { updateOooApprovedRequests, updateOooRejectedRequests, } from "./../../fixtures/oooRequest/oooRequest"; -import { REQUEST_ALREADY_PENDING, REQUEST_STATE, REQUEST_TYPE } from "../../../constants/requests"; +import { REQUEST_STATE, REQUEST_TYPE } from "../../../constants/requests"; import userDataFixture from "./../../fixtures/user/user"; import addUser from "../../utils/addUser"; const userData = userDataFixture(); @@ -42,13 +42,6 @@ describe("models/oooRequests", () => { expect(error.message).to.equal("User already has an OOO request"); } }); - - it("should throw an error if the user already has an OOO request", async () => { - await createRequest(createOooStatusRequests); - const oooRequest = await createRequest(createOooStatusRequests); - expect(oooRequest).to.not.be.null; - expect(oooRequest.error).to.equal(REQUEST_ALREADY_PENDING); - }); }); describe("updateRequest", () => { @@ -171,4 +164,18 @@ describe("models/oooRequests", () => { expect(oooRequestData.allRequests).to.have.lengthOf(1); }); }); + + describe("getRequestByKeyValue", () => { + it("Should return the request with the specified key value", async () => { + const oooRequestObj = { ...createOooRequests2, requestedBy: testUserId }; + const oooRequest = await createRequest(oooRequestObj); + const oooRequestData: any = await getRequestByKeyValues({ requestedBy: testUserId, type: REQUEST_TYPE.OOO }); + expect(oooRequestData.requestedBy).to.be.equal(oooRequest.requestedBy); + }); + + it("Should return null if the request with the specified key value does not exist", async () => { + const oooRequestData = await getRequestByKeyValues({ requestedBy: "randomId", type: REQUEST_TYPE.OOO }); + expect(oooRequestData).to.be.equal(null); + }); + }); }); diff --git a/test/unit/utils/sendTaskUpdate.test.js b/test/unit/utils/sendTaskUpdate.test.js index a44f06ac0..c8f9fcc6a 100644 --- a/test/unit/utils/sendTaskUpdate.test.js +++ b/test/unit/utils/sendTaskUpdate.test.js @@ -22,7 +22,8 @@ describe("sendTaskUpdate function", function () { "No blockers", "Plan for the next phase", "userName", - "taskId" + "taskId", + "Task title" ); expect(result).to.equal(undefined); }); @@ -31,7 +32,14 @@ describe("sendTaskUpdate function", function () { const error = new Error("Error"); fetchMock.rejects(error); try { - await sendTaskUpdate("Task completed", "No blockers", "Plan for the next phase", "userName", "taskId"); + await sendTaskUpdate( + "Task completed", + "No blockers", + "Plan for the next phase", + "userName", + "taskId", + "task title" + ); } catch (err) { expect(err).to.be.equal(error); } diff --git a/typeDefinitions/global.d.ts b/typeDefinitions/global.d.ts index a4a301dea..8167573f0 100644 --- a/typeDefinitions/global.d.ts +++ b/typeDefinitions/global.d.ts @@ -9,6 +9,7 @@ export type UserData = { archived: boolean; in_discord: boolean; member: boolean; + super_user: boolean; }; profileStatus: string; created_at: number; diff --git a/types/extensionRequests.d.ts b/types/extensionRequests.d.ts new file mode 100644 index 000000000..fdbab7553 --- /dev/null +++ b/types/extensionRequests.d.ts @@ -0,0 +1,62 @@ +import { Request, Response } from "express"; +import { Boom } from "express-boom"; +import { REQUEST_STATE, REQUEST_TYPE } from "../constants/requests"; +import { userData } from "./global"; + +export type ExtensionRequest = { + id: string; + type: REQUEST_TYPE.EXTENSION; + taskId: string; + title: string; + oldEndsOn: number; + newEndsOn: number; + message?: string; + requestedBy?: string; + state?: REQUEST_STATE; + lastModifiedBy?: string; + reason?: string; + createdAt?: Timestamp; + updatedAt?: Timestamp; + requestNumber?: number; +}; + +export type ExtensionRequestCreateBody = { + type: REQUEST_TYPE.EXTENSION; + taskId: string; + title: string; + message?: string; + requestedBy?: string; + oldEndsOn: number; + newEndsOn: number; + state: REQUEST_STATE.PENDING; + requestNumber?: number; + assignee?: string; +}; + +export type ExtensionRequestUpdateBody = { + lastModifiedBy?: string; + type?: REQUEST_TYPE.EXTENSION; + id?: string; + reason?: string; + state: REQUEST_STATE.APPROVED | REQUEST_STATE.REJECTED; +}; + +export type RequestQuery = { + dev?: string; + type?: string; + requestedBy?: string; + state?: REQUEST_STATE.APPROVED | REQUEST_STATE.PENDING | REQUEST_STATE.REJECTED; + id?: string; + prev?: string; + next?: string; + page?: number; + size?: number; +}; + +export type ExtensionRequestResponse = Response & { Boom: Boom }; +export type ExtensionRequestRequest = Request & { + ExtensionRequestCreateBody: ExtensionRequestCreateBody; + userData: userData; + query: RequestQuery; + Boom: Boom; +}; diff --git a/types/global.d.ts b/types/global.d.ts index 6333142f8..6c6afa057 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -12,6 +12,7 @@ export type userData = { maven?: boolean; designer?: boolean; product_manager?: boolean; + super_user?: boolean; }; profileStatus: string; created_at: number; diff --git a/utils/progresses.js b/utils/progresses.js index 62a6b67b9..6459a4beb 100644 --- a/utils/progresses.js +++ b/utils/progresses.js @@ -69,6 +69,7 @@ const assertTaskExists = async (taskId) => { if (!taskData) { throw new NotFound(`Task with id ${taskId} does not exist.`); } + return taskData.title; }; /** diff --git a/utils/sendTaskUpdate.js b/utils/sendTaskUpdate.js index 933df8ca4..24b78a78f 100644 --- a/utils/sendTaskUpdate.js +++ b/utils/sendTaskUpdate.js @@ -1,7 +1,7 @@ import { generateCloudFlareHeaders } from "../utils/discord-actions.js"; const DISCORD_BASE_URL = config.get("services.discordBot.baseUrl"); -export const sendTaskUpdate = async (completed, blockers, planned, userName, taskId) => { +export const sendTaskUpdate = async (completed, blockers, planned, userName, taskId, taskTitle) => { try { const headers = generateCloudFlareHeaders(); const body = { @@ -11,6 +11,7 @@ export const sendTaskUpdate = async (completed, blockers, planned, userName, tas planned, userName, taskId, + taskTitle, }, }; await fetch(`${DISCORD_BASE_URL}/task/update`, {