Skip to content

Commit

Permalink
Add Middleware for Migrating Task Creation Requests to /request API E…
Browse files Browse the repository at this point in the history
…ndpoint (#2053)

* feat: added middleware for TCR with test

* test: fix failing tests

* refactor: removed console log from middleware
  • Loading branch information
sahsisunny authored Jul 5, 2024
1 parent 2f141ff commit 529d73d
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 5 deletions.
1 change: 1 addition & 0 deletions constants/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const LOG_ACTION = {
export const REQUEST_TYPE = {
OOO: "OOO",
EXTENSION: "EXTENSION",
TASK: "TASK",
ALL: "ALL",
};

Expand Down
2 changes: 1 addition & 1 deletion constants/taskRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};
Expand Down
2 changes: 1 addition & 1 deletion constants/urls.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const GITHUB_URL = "https://github.com";
export const GITHUB_URL = "https://github.com";

module.exports = {
GITHUB_URL,
Expand Down
9 changes: 7 additions & 2 deletions middlewares/validators/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
) => {
Expand All @@ -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}`);
}
Expand Down Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions middlewares/validators/taskRequests.ts
Original file line number Diff line number Diff line change
@@ -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 });
};
28 changes: 28 additions & 0 deletions test/fixtures/taskRequests/taskRequests.ts
Original file line number Diff line number Diff line change
@@ -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,
};
2 changes: 1 addition & 1 deletion test/integration/requests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Expand Down
70 changes: 70 additions & 0 deletions test/unit/middlewares/taskRequests.test.ts
Original file line number Diff line number Diff line change
@@ -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");
}
});
});
});
68 changes: 68 additions & 0 deletions types/taskRequests.d.ts
Original file line number Diff line number Diff line change
@@ -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;
};

0 comments on commit 529d73d

Please sign in to comment.