Skip to content

Commit 529d73d

Browse files
authored
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
1 parent 2f141ff commit 529d73d

File tree

9 files changed

+253
-5
lines changed

9 files changed

+253
-5
lines changed

constants/requests.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const LOG_ACTION = {
1313
export const REQUEST_TYPE = {
1414
OOO: "OOO",
1515
EXTENSION: "EXTENSION",
16+
TASK: "TASK",
1617
ALL: "ALL",
1718
};
1819

constants/taskRequests.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const TASK_REQUEST_ERROR_MESSAGE = {
1313
INVALID_PREV: "Invalid 'prev' value",
1414
INVALID_NEXT: "Invalid 'next' value",
1515
};
16-
const TASK_REQUEST_TYPE = {
16+
export const TASK_REQUEST_TYPE = {
1717
ASSIGNMENT: "ASSIGNMENT",
1818
CREATION: "CREATION",
1919
};

constants/urls.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const GITHUB_URL = "https://github.com";
1+
export const GITHUB_URL = "https://github.com";
22

33
module.exports = {
44
GITHUB_URL,

middlewares/validators/requests.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { REQUEST_STATE, REQUEST_TYPE } from "../../constants/requests";
44
import { OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest";
55
import { createOooStatusRequestValidator } from "./oooRequests";
66
import { createExtensionRequestValidator } from "./extensionRequestsv2";
7+
import {createTaskRequestValidator} from "./taskRequests";
78
import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../types/extensionRequests";
89
import { CustomResponse } from "../../typeDefinitions/global";
910
import { UpdateRequest } from "../../types/requests";
11+
import { TaskRequestRequest, TaskRequestResponse } from "../../types/taskRequests";
1012

1113
export const createRequestsMiddleware = async (
12-
req: OooRequestCreateRequest|ExtensionRequestRequest,
14+
req: OooRequestCreateRequest|ExtensionRequestRequest | TaskRequestRequest,
1315
res: CustomResponse,
1416
next: NextFunction
1517
) => {
@@ -27,6 +29,9 @@ export const createRequestsMiddleware = async (
2729
case REQUEST_TYPE.EXTENSION:
2830
await createExtensionRequestValidator(req as ExtensionRequestRequest, res as ExtensionRequestResponse, next);
2931
break;
32+
case REQUEST_TYPE.TASK:
33+
await createTaskRequestValidator(req as TaskRequestRequest, res as TaskRequestResponse, next);
34+
break;
3035
default:
3136
res.boom.badRequest(`Invalid request type: ${type}`);
3237
}
@@ -82,7 +87,7 @@ export const getRequestsMiddleware = async (req: OooRequestCreateRequest, res: O
8287
id: joi.string().optional(),
8388
type: joi
8489
.string()
85-
.valid(REQUEST_TYPE.OOO, REQUEST_TYPE.EXTENSION, REQUEST_TYPE.ALL)
90+
.valid(REQUEST_TYPE.OOO, REQUEST_TYPE.EXTENSION, REQUEST_TYPE.TASK, REQUEST_TYPE.ALL)
8691
.optional(),
8792
requestedBy: joi.string().insensitive().optional(),
8893
state: joi
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import joi from "joi";
2+
import { TaskRequestResponse, TaskRequestRequest } from "../../types/taskRequests";
3+
import { NextFunction } from "express";
4+
import { REQUEST_TYPE, REQUEST_STATE } from "../../constants/requests";
5+
import { GITHUB_URL } from "../../constants/urls";
6+
7+
import config from "config";
8+
import { TASK_REQUEST_TYPE } from "../../constants/taskRequests";
9+
const githubOrg = config.get("githubApi.org");
10+
const githubBaseUrl = config.get("githubApi.baseUrl");
11+
const githubIssuerUrlPattern = new RegExp(`^${githubBaseUrl}/repos/${githubOrg}/.+/issues/\\d+$`);
12+
const githubIssueHtmlUrlPattern = new RegExp(`^${GITHUB_URL}/${githubOrg}/.+/issues/\\d+$`); // Example: https://github.com/Real-Dev-Squad/website-status/issues/1050
13+
14+
export const createTaskRequestValidator = async (
15+
req: TaskRequestRequest,
16+
res: TaskRequestResponse,
17+
next: NextFunction
18+
) => {
19+
const schema = joi
20+
.object()
21+
.strict()
22+
.keys({
23+
requestType: joi.string().valid(TASK_REQUEST_TYPE.CREATION, TASK_REQUEST_TYPE.ASSIGNMENT).required().messages({
24+
"string.empty": "requestType cannot be empty",
25+
"any.required": "requestType is required",
26+
}),
27+
externalIssueUrl: joi.string().required().regex(githubIssuerUrlPattern).required().messages({
28+
"string.empty": "externalIssueUrl cannot be empty",
29+
"any.required": "externalIssueUrl is required",
30+
}),
31+
externalIssueHtmlUrl: joi.string().required().regex(githubIssueHtmlUrlPattern).messages({
32+
"string.empty": "externalIssueHtmlUrl cannot be empty",
33+
"any.required": "externalIssueHtmlUrl is required",
34+
}),
35+
type: joi.string().valid(REQUEST_TYPE.TASK).required().messages({
36+
"string.empty": "type cannot be empty",
37+
"any.required": "type is required",
38+
}),
39+
state: joi.string().valid(REQUEST_STATE.PENDING).required().messages({
40+
"string.empty": "state cannot be empty",
41+
"any.required": "state is required",
42+
}),
43+
proposedStartDate: joi.number().required().messages({
44+
"number.base": "proposedStartDate must be a number",
45+
"any.required": "proposedStartDate is required",
46+
}),
47+
proposedDeadline: joi.number().required().greater(joi.ref("proposedStartDate")).
48+
messages({
49+
"number.base": "proposedDeadline must be a number",
50+
"any.required": "proposedDeadline is required",
51+
}),
52+
description: joi.string().optional().messages({
53+
"string.empty": "description cannot be empty",
54+
}),
55+
markdownEnabled: joi.boolean().optional().messages({
56+
"boolean.base": "markdownEnabled must be a boolean",
57+
}),
58+
taskId: joi.when('requestType', {
59+
is: TASK_REQUEST_TYPE.ASSIGNMENT,
60+
then: joi.string().required().messages({
61+
"string.empty": "taskId cannot be empty",
62+
"any.required": "taskId is required when requestType is ASSIGNMENT",
63+
}),
64+
otherwise: joi.forbidden()
65+
}),
66+
userId: joi.when('requestType', {
67+
is: TASK_REQUEST_TYPE.CREATION,
68+
then: joi.string().required().messages({
69+
"string.empty": "userId cannot be empty",
70+
"any.required": "userId is required when requestType is CREATION",
71+
}),
72+
otherwise: joi.forbidden()
73+
}),
74+
});
75+
await schema.validateAsync(req.body, { abortEarly: false });
76+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { REQUEST_STATE, REQUEST_TYPE } from "../../../constants/requests";
2+
import { TASK_REQUEST_TYPE } from "../../../constants/taskRequests";
3+
4+
export const validTaskCreqtionRequest = {
5+
externalIssueUrl: "https://api.github.com/repos/Real-Dev-Squad/website-my/issues/599",
6+
externalIssueHtmlUrl: "https://github.com/Real-Dev-Squad/website-my/issues/599",
7+
userId: "iODXB6ns8jaZB9p0XlBw",
8+
requestType: TASK_REQUEST_TYPE.CREATION,
9+
proposedStartDate: 1718845551203,
10+
proposedDeadline: 1719450351203,
11+
description: "Task Create Description",
12+
markdownEnabled: true,
13+
state: REQUEST_STATE.PENDING,
14+
type: REQUEST_TYPE.TASK,
15+
};
16+
17+
export const validTaskAssignmentRequest = {
18+
externalIssueUrl: "https://api.github.com/repos/Real-Dev-Squad/website-my/issues/599",
19+
externalIssueHtmlUrl: "https://github.com/Real-Dev-Squad/website-my/issues/599",
20+
taskId: "iODXB6ns8jaZB9p0XlBw",
21+
requestType: TASK_REQUEST_TYPE.ASSIGNMENT,
22+
proposedStartDate: 1718845551203,
23+
proposedDeadline: 1719450351203,
24+
description: "Task Create Description",
25+
markdownEnabled: true,
26+
state: REQUEST_STATE.PENDING,
27+
type: REQUEST_TYPE.TASK,
28+
};

test/integration/requests.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ describe("/requests OOO", function () {
383383
.end(function (err, res) {
384384
expect(res).to.have.status(400);
385385
expect(res.body.error).to.equal("Bad Request");
386-
expect(res.body.message).to.equal('"type" must be one of [OOO, EXTENSION, ALL]');
386+
expect(res.body.message).to.equal('"type" must be one of [OOO, EXTENSION, TASK, ALL]');
387387
done();
388388
});
389389
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import chai from "chai";
2+
import sinon from "sinon";
3+
const { expect } = chai;
4+
5+
import { createTaskRequestValidator } from "./../../../middlewares/validators/taskRequests";
6+
7+
import { validTaskCreqtionRequest, validTaskAssignmentRequest } from "../../fixtures/taskRequests/taskRequests";
8+
9+
describe("Task Request Validators", function () {
10+
let req: any;
11+
let res: any;
12+
let nextSpy;
13+
beforeEach(function () {
14+
res = {
15+
boom: {
16+
badRequest: sinon.spy(),
17+
},
18+
};
19+
nextSpy = sinon.spy();
20+
});
21+
describe("createTaskRequestValidator", function () {
22+
it("should validate for a valid create request", async function () {
23+
req = {
24+
body: validTaskCreqtionRequest,
25+
};
26+
res = {};
27+
28+
await createTaskRequestValidator(req as any, res as any, nextSpy);
29+
expect(nextSpy.calledOnce);
30+
});
31+
32+
it("should not validate for an invalid request on wrong type", async function () {
33+
req = {
34+
body: { type: "ACTIVE" },
35+
res: {},
36+
};
37+
try {
38+
await createTaskRequestValidator(req as any, res as any, nextSpy);
39+
} catch (error) {
40+
expect(error).to.be.an.instanceOf(Error);
41+
expect(error.details[0].message).to.equal("requestType is required");
42+
}
43+
});
44+
45+
it("should validate for varid task assignment request", async function () {
46+
req = {
47+
body: validTaskAssignmentRequest,
48+
};
49+
res = {};
50+
51+
await createTaskRequestValidator(req as any, res as any, nextSpy);
52+
expect(nextSpy.calledOnce);
53+
});
54+
55+
it("should not validate if taskID is missing in task assignment request", async function () {
56+
req = {
57+
body: {
58+
...validTaskAssignmentRequest,
59+
taskId: undefined,
60+
},
61+
};
62+
try {
63+
await createTaskRequestValidator(req as any, res as any, nextSpy);
64+
} catch (error) {
65+
expect(error).to.be.an.instanceOf(Error);
66+
expect(error.details[0].message).to.equal("taskId is required when requestType is ASSIGNMENT");
67+
}
68+
});
69+
});
70+
});

types/taskRequests.d.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { REQUEST_STATE } from "./../constants/requests";
2+
import { Request, Response } from "express";
3+
import { Boom } from "express-boom";
4+
import { REQUEST_STATE, REQUEST_TYPE } from "../constants/requests";
5+
import { TASK_REQUEST_STATUS, TASK_REQUEST_TYPE } from "../constants/taskRequests";
6+
7+
import { userData } from "./global";
8+
export type TaskCreationRequest = {
9+
id: string;
10+
type: REQUEST_TYPE.TASK;
11+
externalIssueUrl: string;
12+
externalIssueHtmlUrl: string;
13+
requestType: TASK_REQUEST_TYPE.CREATION | TASK_REQUEST_TYPE.ASSIGNMENT;
14+
userId?: string;
15+
taskId?: string;
16+
state: REQUEST_STATE;
17+
requestedBy?: string;
18+
proposedStartDate: number;
19+
proposedDeadline: number;
20+
description?: string;
21+
markdownEnabled?: boolean;
22+
createdAt?: Timestamp;
23+
updatedAt?: Timestamp;
24+
requesters?: string[];
25+
lastModifiedBy?: string;
26+
approvedTo?: string;
27+
};
28+
29+
export type TaskCreationRequestBody = {
30+
type: REQUEST_TYPE.TASK;
31+
state: REQUEST_STATE.PENDING;
32+
externalIssueUrl: string;
33+
externalIssueHtmlUrl: string;
34+
requestType: TASK_REQUEST_TYPE.CREATION;
35+
requestedBy?: string;
36+
proposedStartDate: number;
37+
proposedDeadline: number;
38+
description?: string;
39+
markdownEnabled?: boolean;
40+
};
41+
42+
export type TaskCreationRequestUpdateBody = {
43+
lastModifiedBy?: string;
44+
type?: REQUEST_TYPE.TASK;
45+
id?: string;
46+
state: REQUEST_STATE.APPROVED | REQUEST_STATE.REJECTED;
47+
approvedTo?: string;
48+
};
49+
50+
export type RequestQuery = {
51+
dev?: string;
52+
type?: string;
53+
requestedBy?: string;
54+
state?: REQUEST_STATE.APPROVED | REQUEST_STATE.PENDING | REQUEST_STATE.REJECTED;
55+
id?: string;
56+
prev?: string;
57+
next?: string;
58+
page?: number;
59+
size?: number;
60+
};
61+
62+
export type TaskRequestResponse = Response & { Boom: Boom };
63+
export type TaskRequestRequest = Request & {
64+
TaskCreationRequestBody: TaskCreationRequestBody;
65+
userData: userData;
66+
query: RequestQuery;
67+
Boom: Boom;
68+
};

0 commit comments

Comments
 (0)