diff --git a/constants/requests.ts b/constants/requests.ts index b1d2836ed..f0fdb9907 100644 --- a/constants/requests.ts +++ b/constants/requests.ts @@ -39,3 +39,17 @@ export const ERROR_WHILE_UPDATING_REQUEST = "Error while updating request"; 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 TASK_REQUEST_MESSAGES = { + NOT_AUTHORIZED_TO_CREATE_REQUEST: "Not authorized to create the request", + USER_NOT_FOUND: "User not found", + TASK_NOT_EXIST: "Task does not exist", + INVALID_EXTERNAL_ISSUE_URL: "External issue url is not valid", + ISSUE_NOT_EXIST: "Issue does not exist", + TASK_REQUEST_EXISTS: "Task request already exists", + TASK_EXISTS_FOR_GIVEN_ISSUE: "Task exists for the given issue.", + TASK_ALREADY_REQUESTED: "Task was already requested", + TASK_REQUEST_CREATED_SUCCESS: "Task request created successfully", + ERROR_CREATING_TASK_REQUEST: "Error while creating task request", + TASK_REQUEST_UPDATED_SUCCESS: "Task request updated successfully", +}; diff --git a/controllers/requests.ts b/controllers/requests.ts index 6e1d55d82..64a97bcee 100644 --- a/controllers/requests.ts +++ b/controllers/requests.ts @@ -11,9 +11,11 @@ import { CustomResponse } from "../typeDefinitions/global"; import { ExtensionRequestRequest, ExtensionRequestResponse } from "../types/extensionRequests"; import { createTaskExtensionRequest, updateTaskExtensionRequest } from "./extensionRequestsv2"; import { UpdateRequest } from "../types/requests"; +import { TaskRequestRequest } from "../types/taskRequests"; +import { createTaskRequestController } from "./taskRequestsv2"; export const createRequestController = async ( - req: OooRequestCreateRequest | ExtensionRequestRequest, + req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest, res: CustomResponse ) => { const type = req.body.type; @@ -22,6 +24,8 @@ export const createRequestController = async ( return await createOooRequestController(req as OooRequestCreateRequest, res as OooRequestResponse); case REQUEST_TYPE.EXTENSION: return await createTaskExtensionRequest(req as ExtensionRequestRequest, res as ExtensionRequestResponse); + case REQUEST_TYPE.TASK: + return await createTaskRequestController(req as TaskRequestRequest, res as CustomResponse); default: return res.boom.badRequest("Invalid request type"); } diff --git a/controllers/taskRequestsv2.ts b/controllers/taskRequestsv2.ts new file mode 100644 index 000000000..545634d35 --- /dev/null +++ b/controllers/taskRequestsv2.ts @@ -0,0 +1,174 @@ +import { REQUEST_STATE, TASK_REQUEST_MESSAGES } from "../constants/requests"; +import { TASK_REQUEST_TYPE } from "../constants/taskRequests"; +import { addLog } from "../models/logs"; +import { createRequest, getRequestByKeyValues } from "../models/requests"; +import { fetchTask } from "../models/tasks"; +import { fetchUser } from "../models/users"; +import { fetchIssuesById } from "../services/githubService"; +import { CustomResponse } from "../typeDefinitions/global"; +import { userData } from "../types/global"; +import { TaskRequestRequest } from "../types/taskRequests"; + +export const createTaskRequestController = async (req: TaskRequestRequest, res: CustomResponse) => { + const taskRequestData = req.body; + const requestedBy = req?.userData?.id; + + if (!requestedBy) { + return res.boom.unauthorized(); + } + + if (req.userData.id !== taskRequestData.userId && !req.userData.roles?.super_user) { + return res.boom.forbidden(TASK_REQUEST_MESSAGES.NOT_AUTHORIZED_TO_CREATE_REQUEST); + } + + const userPromise: any = await fetchUser({ userId: taskRequestData.userId }); + const userData: userData = userPromise.user; + if (!userData.id || !userData.username) { + return res.boom.notFound(TASK_REQUEST_MESSAGES.USER_NOT_FOUND); + } + try { + switch (taskRequestData.requestType) { + case TASK_REQUEST_TYPE.ASSIGNMENT: { + if (!req.userData.roles?.super_user) { + return res.boom.unauthorized(TASK_REQUEST_MESSAGES.NOT_AUTHORIZED_TO_CREATE_REQUEST); + } + const { taskData } = await fetchTask(taskRequestData.taskId); + if (!taskData) { + return res.boom.badRequest(TASK_REQUEST_MESSAGES.TASK_NOT_EXIST); + } + taskRequestData.taskTitle = taskData?.title; + break; + } + case TASK_REQUEST_TYPE.CREATION: { + let issueData: any; + try { + const url = new URL(taskRequestData.externalIssueUrl); + const issueUrlPaths = url.pathname.split("/"); + const repositoryName = issueUrlPaths[3]; + const issueNumber = issueUrlPaths[5]; + issueData = await fetchIssuesById(repositoryName, issueNumber); + } catch (error) { + return res.boom.badRequest(TASK_REQUEST_MESSAGES.INVALID_EXTERNAL_ISSUE_URL); + } + if (!issueData) { + return res.boom.badRequest(TASK_REQUEST_MESSAGES.ISSUE_NOT_EXIST); + } + taskRequestData.taskTitle = issueData?.title; + break; + } + } + const existingRequest = await getRequestByKeyValues({ + externalIssueUrl: taskRequestData.externalIssueUrl, + requestType: taskRequestData.requestType, + }); + + if ( + existingRequest && + existingRequest.state === REQUEST_STATE.PENDING && + existingRequest.requestors.includes(requestedBy) + ) { + return res.boom.badRequest(TASK_REQUEST_MESSAGES.TASK_REQUEST_EXISTS); + } else if ( + existingRequest && + existingRequest.state === REQUEST_STATE.PENDING && + !existingRequest.requestors.includes(requestedBy) + ) { + existingRequest.requestors.push(requestedBy); + existingRequest.users.push({ + userId: userData.id, + username: userData.username, + proposedStartDate: taskRequestData.proposedStartDate, + proposedDeadline: taskRequestData.proposedDeadline, + description: taskRequestData.description, + markdownEnabled: taskRequestData.markdownEnabled, + firstName: userData.first_name, + lastName: userData.last_name, + state: REQUEST_STATE.PENDING, + requestedAt: Date.now(), + }); + const updatedRequest = await createRequest(existingRequest); + const taskRequestLog = { + type: "taskRequests", + meta: { + taskRequestId: updatedRequest.id, + action: "update", + createdBy: req.userData.id, + createdAt: Date.now(), + lastModifiedBy: req.userData.id, + lastModifiedAt: Date.now(), + }, + body: updatedRequest, + }; + await addLog(taskRequestLog.type, taskRequestLog.meta, taskRequestLog.body); + const data = { + message: TASK_REQUEST_MESSAGES.TASK_REQUEST_UPDATED_SUCCESS, + data: { + id: updatedRequest.id, + ...updatedRequest, + }, + }; + return res.status(200).json(data); + } + + taskRequestData.requestedBy = requestedBy; + const createtaskRequestData = { + externalIssueUrl: taskRequestData.externalIssueUrl, + externalIssueHtmlUrl: taskRequestData.externalIssueHtmlUrl, + requestType: taskRequestData.requestType, + type: taskRequestData.type, + state: REQUEST_STATE.PENDING, + requestedBy: requestedBy, + taskTitle: taskRequestData.taskTitle, + users: [ + { + userId: userData.id, + username: userData.username, + proposedStartDate: taskRequestData.proposedStartDate, + proposedDeadline: taskRequestData.proposedDeadline, + description: taskRequestData.description, + markdownEnabled: taskRequestData.markdownEnabled, + firstName: userData.first_name, + lastName: userData.last_name, + state: REQUEST_STATE.PENDING, + requestedAt: Date.now(), + }, + ], + + requestors: [requestedBy], + }; + const newTaskRequest = await createRequest(createtaskRequestData); + + if (newTaskRequest.isCreationRequestApproved) { + return res.boom.badRequest(TASK_REQUEST_MESSAGES.TASK_EXISTS_FOR_GIVEN_ISSUE); + } + if (newTaskRequest.alreadyRequesting) { + return res.boom.badRequest(TASK_REQUEST_MESSAGES.TASK_ALREADY_REQUESTED); + } + + const taskRequestLog = { + type: "taskRequests", + meta: { + taskRequestId: newTaskRequest.id, + action: "create", + createdBy: req.userData.id, + createdAt: Date.now(), + lastModifiedBy: req.userData.id, + lastModifiedAt: Date.now(), + }, + body: newTaskRequest, + }; + await addLog(taskRequestLog.type, taskRequestLog.meta, taskRequestLog.body); + + const data = { + message: TASK_REQUEST_MESSAGES.TASK_REQUEST_CREATED_SUCCESS, + data: { + id: newTaskRequest.id, + ...newTaskRequest, + }, + }; + return res.status(201).json(data); + } catch (err) { + logger.error(`${TASK_REQUEST_MESSAGES.ERROR_CREATING_TASK_REQUEST} : ${err}`); + return res.boom.serverUnavailable(TASK_REQUEST_MESSAGES.ERROR_CREATING_TASK_REQUEST); + } +}; diff --git a/package.json b/package.json index 7df8a48a8..8a0e238d7 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "sinon": "18.0.0", "ts-node": "10.9.2", "ts-node-dev": "2.0.0", - "typescript": "5.3.3" + "typescript": "5.5.3" }, "engines": { "node": "20.x" diff --git a/test/integration/requests.test.ts b/test/integration/requests.test.ts index bde701100..bdc2f2867 100644 --- a/test/integration/requests.test.ts +++ b/test/integration/requests.test.ts @@ -28,6 +28,7 @@ import { REQUEST_ALREADY_REJECTED, } from "../../constants/requests"; import { updateTask } from "../../models/tasks"; +import { validTaskAssignmentRequest, validTaskCreqtionRequest } from "../fixtures/taskRequests/taskRequests"; const userData = userDataFixture(); chai.use(chaiHttp); @@ -759,3 +760,64 @@ describe("/requests Extension", function () { }); }); + + +describe("/requests Task", function () { + let userId1: string; + let userJwtToken1: string; + + beforeEach(async function () { + userId1 = await addUser(userData[16]); + userJwtToken1 = authService.generateAuthToken({ userId: userId1 }); + }); + + 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(validTaskCreqtionRequest) + .end(function (err, res) { + expect(res).to.have.status(401); + done(); + }); + }); + + it("should not create a new task request if issue does not exist", function (done) { + let taskRequestObj = validTaskCreqtionRequest + taskRequestObj.externalIssueUrl = "https://api.github.com/repos/Real-Dev-Squad/website-my/issues/1245"; + taskRequestObj.userId = userId1; + chai + .request(app) + .post("/requests?dev=true") + .set("cookie", `${cookieName}=${userJwtToken1}`) + .send(taskRequestObj) + .end(function (err, res) { + expect(res).to.have.status(400); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal("Issue does not exist"); + done(); + }); + }); + + it("should not create a new task request if task id is not present in the request body", function (done) { + let taskRequestObj = validTaskAssignmentRequest + delete taskRequestObj.taskId; + chai + .request(app) + .post("/requests?dev=true") + .set("cookie", `${cookieName}=${userJwtToken1}`) + .send(taskRequestObj) + .end(function (err, res) { + expect(res).to.have.status(400); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal('taskId is required when requestType is ASSIGNMENT'); + done(); + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 5781ddc3d..f370d4d13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7307,7 +7307,16 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7372,7 +7381,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7827,10 +7843,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +typescript@5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" + integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" @@ -8149,7 +8165,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8167,6 +8183,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"