From 529d73dd4b4efbd9c714b1f355d0b677df76de6b Mon Sep 17 00:00:00 2001 From: Sunny Sahsi Date: Fri, 5 Jul 2024 14:31:12 +0530 Subject: [PATCH 1/2] Add Middleware for Migrating Task Creation Requests to /request API Endpoint (#2053) * feat: added middleware for TCR with test * test: fix failing tests * refactor: removed console log from middleware --- constants/requests.ts | 1 + constants/taskRequests.ts | 2 +- constants/urls.ts | 2 +- middlewares/validators/requests.ts | 9 ++- middlewares/validators/taskRequests.ts | 76 ++++++++++++++++++++++ test/fixtures/taskRequests/taskRequests.ts | 28 ++++++++ test/integration/requests.test.ts | 2 +- test/unit/middlewares/taskRequests.test.ts | 70 ++++++++++++++++++++ types/taskRequests.d.ts | 68 +++++++++++++++++++ 9 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 middlewares/validators/taskRequests.ts create mode 100644 test/fixtures/taskRequests/taskRequests.ts create mode 100644 test/unit/middlewares/taskRequests.test.ts create mode 100644 types/taskRequests.d.ts diff --git a/constants/requests.ts b/constants/requests.ts index 29f871a02..b1d2836ed 100644 --- a/constants/requests.ts +++ b/constants/requests.ts @@ -13,6 +13,7 @@ export const LOG_ACTION = { export const REQUEST_TYPE = { OOO: "OOO", EXTENSION: "EXTENSION", + TASK: "TASK", ALL: "ALL", }; diff --git a/constants/taskRequests.ts b/constants/taskRequests.ts index 8faa893bc..4b1a222c3 100644 --- a/constants/taskRequests.ts +++ b/constants/taskRequests.ts @@ -13,7 +13,7 @@ const TASK_REQUEST_ERROR_MESSAGE = { INVALID_PREV: "Invalid 'prev' value", INVALID_NEXT: "Invalid 'next' value", }; -const TASK_REQUEST_TYPE = { +export const TASK_REQUEST_TYPE = { ASSIGNMENT: "ASSIGNMENT", CREATION: "CREATION", }; diff --git a/constants/urls.ts b/constants/urls.ts index 4cf520b16..fe31dc3a5 100644 --- a/constants/urls.ts +++ b/constants/urls.ts @@ -1,4 +1,4 @@ -const GITHUB_URL = "https://github.com"; +export const GITHUB_URL = "https://github.com"; module.exports = { GITHUB_URL, diff --git a/middlewares/validators/requests.ts b/middlewares/validators/requests.ts index 8b84b6dd2..295c81f3a 100644 --- a/middlewares/validators/requests.ts +++ b/middlewares/validators/requests.ts @@ -4,12 +4,14 @@ import { REQUEST_STATE, REQUEST_TYPE } from "../../constants/requests"; import { OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest"; import { createOooStatusRequestValidator } from "./oooRequests"; import { createExtensionRequestValidator } from "./extensionRequestsv2"; +import {createTaskRequestValidator} from "./taskRequests"; import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../types/extensionRequests"; import { CustomResponse } from "../../typeDefinitions/global"; import { UpdateRequest } from "../../types/requests"; +import { TaskRequestRequest, TaskRequestResponse } from "../../types/taskRequests"; export const createRequestsMiddleware = async ( - req: OooRequestCreateRequest|ExtensionRequestRequest, + req: OooRequestCreateRequest|ExtensionRequestRequest | TaskRequestRequest, res: CustomResponse, next: NextFunction ) => { @@ -27,6 +29,9 @@ export const createRequestsMiddleware = async ( case REQUEST_TYPE.EXTENSION: await createExtensionRequestValidator(req as ExtensionRequestRequest, res as ExtensionRequestResponse, next); break; + case REQUEST_TYPE.TASK: + await createTaskRequestValidator(req as TaskRequestRequest, res as TaskRequestResponse, next); + break; default: res.boom.badRequest(`Invalid request type: ${type}`); } @@ -82,7 +87,7 @@ export const getRequestsMiddleware = async (req: OooRequestCreateRequest, res: O id: joi.string().optional(), type: joi .string() - .valid(REQUEST_TYPE.OOO, REQUEST_TYPE.EXTENSION, REQUEST_TYPE.ALL) + .valid(REQUEST_TYPE.OOO, REQUEST_TYPE.EXTENSION, REQUEST_TYPE.TASK, REQUEST_TYPE.ALL) .optional(), requestedBy: joi.string().insensitive().optional(), state: joi diff --git a/middlewares/validators/taskRequests.ts b/middlewares/validators/taskRequests.ts new file mode 100644 index 000000000..383ee3e6b --- /dev/null +++ b/middlewares/validators/taskRequests.ts @@ -0,0 +1,76 @@ +import joi from "joi"; +import { TaskRequestResponse, TaskRequestRequest } from "../../types/taskRequests"; +import { NextFunction } from "express"; +import { REQUEST_TYPE, REQUEST_STATE } from "../../constants/requests"; +import { GITHUB_URL } from "../../constants/urls"; + +import config from "config"; +import { TASK_REQUEST_TYPE } from "../../constants/taskRequests"; +const githubOrg = config.get("githubApi.org"); +const githubBaseUrl = config.get("githubApi.baseUrl"); +const githubIssuerUrlPattern = new RegExp(`^${githubBaseUrl}/repos/${githubOrg}/.+/issues/\\d+$`); +const githubIssueHtmlUrlPattern = new RegExp(`^${GITHUB_URL}/${githubOrg}/.+/issues/\\d+$`); // Example: https://github.com/Real-Dev-Squad/website-status/issues/1050 + +export const createTaskRequestValidator = async ( + req: TaskRequestRequest, + res: TaskRequestResponse, + next: NextFunction +) => { + const schema = joi + .object() + .strict() + .keys({ + requestType: joi.string().valid(TASK_REQUEST_TYPE.CREATION, TASK_REQUEST_TYPE.ASSIGNMENT).required().messages({ + "string.empty": "requestType cannot be empty", + "any.required": "requestType is required", + }), + externalIssueUrl: joi.string().required().regex(githubIssuerUrlPattern).required().messages({ + "string.empty": "externalIssueUrl cannot be empty", + "any.required": "externalIssueUrl is required", + }), + externalIssueHtmlUrl: joi.string().required().regex(githubIssueHtmlUrlPattern).messages({ + "string.empty": "externalIssueHtmlUrl cannot be empty", + "any.required": "externalIssueHtmlUrl is required", + }), + type: joi.string().valid(REQUEST_TYPE.TASK).required().messages({ + "string.empty": "type cannot be empty", + "any.required": "type is required", + }), + state: joi.string().valid(REQUEST_STATE.PENDING).required().messages({ + "string.empty": "state cannot be empty", + "any.required": "state is required", + }), + proposedStartDate: joi.number().required().messages({ + "number.base": "proposedStartDate must be a number", + "any.required": "proposedStartDate is required", + }), + proposedDeadline: joi.number().required().greater(joi.ref("proposedStartDate")). + messages({ + "number.base": "proposedDeadline must be a number", + "any.required": "proposedDeadline is required", + }), + description: joi.string().optional().messages({ + "string.empty": "description cannot be empty", + }), + markdownEnabled: joi.boolean().optional().messages({ + "boolean.base": "markdownEnabled must be a boolean", + }), + taskId: joi.when('requestType', { + is: TASK_REQUEST_TYPE.ASSIGNMENT, + then: joi.string().required().messages({ + "string.empty": "taskId cannot be empty", + "any.required": "taskId is required when requestType is ASSIGNMENT", + }), + otherwise: joi.forbidden() + }), + userId: joi.when('requestType', { + is: TASK_REQUEST_TYPE.CREATION, + then: joi.string().required().messages({ + "string.empty": "userId cannot be empty", + "any.required": "userId is required when requestType is CREATION", + }), + otherwise: joi.forbidden() + }), + }); + await schema.validateAsync(req.body, { abortEarly: false }); +}; diff --git a/test/fixtures/taskRequests/taskRequests.ts b/test/fixtures/taskRequests/taskRequests.ts new file mode 100644 index 000000000..fcfe2e685 --- /dev/null +++ b/test/fixtures/taskRequests/taskRequests.ts @@ -0,0 +1,28 @@ +import { REQUEST_STATE, REQUEST_TYPE } from "../../../constants/requests"; +import { TASK_REQUEST_TYPE } from "../../../constants/taskRequests"; + +export const validTaskCreqtionRequest = { + externalIssueUrl: "https://api.github.com/repos/Real-Dev-Squad/website-my/issues/599", + externalIssueHtmlUrl: "https://github.com/Real-Dev-Squad/website-my/issues/599", + userId: "iODXB6ns8jaZB9p0XlBw", + requestType: TASK_REQUEST_TYPE.CREATION, + proposedStartDate: 1718845551203, + proposedDeadline: 1719450351203, + description: "Task Create Description", + markdownEnabled: true, + state: REQUEST_STATE.PENDING, + type: REQUEST_TYPE.TASK, +}; + +export const validTaskAssignmentRequest = { + externalIssueUrl: "https://api.github.com/repos/Real-Dev-Squad/website-my/issues/599", + externalIssueHtmlUrl: "https://github.com/Real-Dev-Squad/website-my/issues/599", + taskId: "iODXB6ns8jaZB9p0XlBw", + requestType: TASK_REQUEST_TYPE.ASSIGNMENT, + proposedStartDate: 1718845551203, + proposedDeadline: 1719450351203, + description: "Task Create Description", + markdownEnabled: true, + state: REQUEST_STATE.PENDING, + type: REQUEST_TYPE.TASK, +}; diff --git a/test/integration/requests.test.ts b/test/integration/requests.test.ts index 0add1ee6e..bde701100 100644 --- a/test/integration/requests.test.ts +++ b/test/integration/requests.test.ts @@ -383,7 +383,7 @@ describe("/requests OOO", 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, EXTENSION, ALL]'); + expect(res.body.message).to.equal('"type" must be one of [OOO, EXTENSION, TASK, ALL]'); done(); }); }); diff --git a/test/unit/middlewares/taskRequests.test.ts b/test/unit/middlewares/taskRequests.test.ts new file mode 100644 index 000000000..f410125e7 --- /dev/null +++ b/test/unit/middlewares/taskRequests.test.ts @@ -0,0 +1,70 @@ +import chai from "chai"; +import sinon from "sinon"; +const { expect } = chai; + +import { createTaskRequestValidator } from "./../../../middlewares/validators/taskRequests"; + +import { validTaskCreqtionRequest, validTaskAssignmentRequest } from "../../fixtures/taskRequests/taskRequests"; + +describe("Task Request Validators", function () { + let req: any; + let res: any; + let nextSpy; + beforeEach(function () { + res = { + boom: { + badRequest: sinon.spy(), + }, + }; + nextSpy = sinon.spy(); + }); + describe("createTaskRequestValidator", function () { + it("should validate for a valid create request", async function () { + req = { + body: validTaskCreqtionRequest, + }; + res = {}; + + await createTaskRequestValidator(req as any, res as any, nextSpy); + expect(nextSpy.calledOnce); + }); + + it("should not validate for an invalid request on wrong type", async function () { + req = { + body: { type: "ACTIVE" }, + res: {}, + }; + try { + await createTaskRequestValidator(req as any, res as any, nextSpy); + } catch (error) { + expect(error).to.be.an.instanceOf(Error); + expect(error.details[0].message).to.equal("requestType is required"); + } + }); + + it("should validate for varid task assignment request", async function () { + req = { + body: validTaskAssignmentRequest, + }; + res = {}; + + await createTaskRequestValidator(req as any, res as any, nextSpy); + expect(nextSpy.calledOnce); + }); + + it("should not validate if taskID is missing in task assignment request", async function () { + req = { + body: { + ...validTaskAssignmentRequest, + taskId: undefined, + }, + }; + try { + await createTaskRequestValidator(req as any, res as any, nextSpy); + } catch (error) { + expect(error).to.be.an.instanceOf(Error); + expect(error.details[0].message).to.equal("taskId is required when requestType is ASSIGNMENT"); + } + }); + }); +}); diff --git a/types/taskRequests.d.ts b/types/taskRequests.d.ts new file mode 100644 index 000000000..fb9fd018b --- /dev/null +++ b/types/taskRequests.d.ts @@ -0,0 +1,68 @@ +import { REQUEST_STATE } from "./../constants/requests"; +import { Request, Response } from "express"; +import { Boom } from "express-boom"; +import { REQUEST_STATE, REQUEST_TYPE } from "../constants/requests"; +import { TASK_REQUEST_STATUS, TASK_REQUEST_TYPE } from "../constants/taskRequests"; + +import { userData } from "./global"; +export type TaskCreationRequest = { + id: string; + type: REQUEST_TYPE.TASK; + externalIssueUrl: string; + externalIssueHtmlUrl: string; + requestType: TASK_REQUEST_TYPE.CREATION | TASK_REQUEST_TYPE.ASSIGNMENT; + userId?: string; + taskId?: string; + state: REQUEST_STATE; + requestedBy?: string; + proposedStartDate: number; + proposedDeadline: number; + description?: string; + markdownEnabled?: boolean; + createdAt?: Timestamp; + updatedAt?: Timestamp; + requesters?: string[]; + lastModifiedBy?: string; + approvedTo?: string; +}; + +export type TaskCreationRequestBody = { + type: REQUEST_TYPE.TASK; + state: REQUEST_STATE.PENDING; + externalIssueUrl: string; + externalIssueHtmlUrl: string; + requestType: TASK_REQUEST_TYPE.CREATION; + requestedBy?: string; + proposedStartDate: number; + proposedDeadline: number; + description?: string; + markdownEnabled?: boolean; +}; + +export type TaskCreationRequestUpdateBody = { + lastModifiedBy?: string; + type?: REQUEST_TYPE.TASK; + id?: string; + state: REQUEST_STATE.APPROVED | REQUEST_STATE.REJECTED; + approvedTo?: string; +}; + +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 TaskRequestResponse = Response & { Boom: Boom }; +export type TaskRequestRequest = Request & { + TaskCreationRequestBody: TaskCreationRequestBody; + userData: userData; + query: RequestQuery; + Boom: Boom; +}; From 0f012da1b089c5285446cf113ed18bc0b3ebbaf5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 09:02:57 +0000 Subject: [PATCH 2/2] chore(deps): update dependency nock to v13.5.4 --- package.json | 2 +- yarn.lock | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 7df8a48a8..9f647ad66 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "eslint-plugin-security": "^1.7.1", "firebase-tools": "13.4.0", "mocha": "10.3.0", - "nock": "13.2.9", + "nock": "13.5.4", "nodemon": "3.1.3", "nyc": "15.1.0", "pre-commit": "1.2.2", diff --git a/yarn.lock b/yarn.lock index 5781ddc3d..686005348 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5805,14 +5805,13 @@ nise@^6.0.0: just-extend "^6.2.0" path-to-regexp "^6.2.1" -nock@13.2.9: - version "13.2.9" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.2.9.tgz#4faf6c28175d36044da4cfa68e33e5a15086ad4c" - integrity sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA== +nock@13.5.4: + version "13.5.4" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.4.tgz#8918f0addc70a63736170fef7106a9721e0dc479" + integrity sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" - lodash "^4.17.21" propagate "^2.0.0" node-abi@^3.3.0: @@ -7307,7 +7306,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 +7380,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== @@ -8149,7 +8164,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 +8182,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"