From 93131d849a00bb3ba77104644bdac3207010d74d Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 11:58:15 +0530 Subject: [PATCH 01/21] added types for create-onboarding-extension-request feature --- types/onboardingExtension.d.ts | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 types/onboardingExtension.d.ts diff --git a/types/onboardingExtension.d.ts b/types/onboardingExtension.d.ts new file mode 100644 index 000000000..b3e6db0f8 --- /dev/null +++ b/types/onboardingExtension.d.ts @@ -0,0 +1,42 @@ +import { Request, Response } from "express"; +import { Boom } from "express-boom"; +import { REQUEST_STATE, REQUEST_TYPE } from "../constants/requests"; +import { userData } from "./global"; +import { RequestQuery } from "./requests"; + +export type OnboardingExtension = { + id: string; + type: REQUEST_TYPE.ONBOARDING; + oldEndsOn: number; + newEndsOn: number; + message?: string; + reason: string; + requestedBy: string; + state: REQUEST_STATE; + lastModifiedBy?: string; + createdAt: Timestamp; + updatedAt: Timestamp; + requestNumber: number; + userId: string; +} + +export type CreateOnboardingExtensionBody = { + type: string; + numberOfDays: number; + requestedBy: string; + username: string; + reason: string; +} + +export type OnboardingExtensionRequestQuery = RequestQuery & { + dev?: string +} + +export type OnboardingExtensionResponse = Response & { + Boom: Boom +} +export type OnboardingExtensionCreateRequest = Request & { + CreateOnboardingExtension: CreateOnboardingExtensionBody; + query: OnboardingExtensionRequestQuery; + Boom: Boom; +} \ No newline at end of file From 2de015e98f538e117de38d77f1b149a247e07b97 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 11:59:07 +0530 Subject: [PATCH 02/21] added tests for create-onboarding-extension-request feature --- test/integration/requests.test.ts | 187 ++++++++++++++++++ ...nboardingExtensionRequestValidator.test.ts | 71 +++++++ ...AuthenticateForOnboardingExtension.test.ts | 49 +++++ 3 files changed, 307 insertions(+) create mode 100644 test/unit/middlewares/onboardingExtensionRequestValidator.test.ts create mode 100644 test/unit/middlewares/skipAuthenticateForOnboardingExtension.test.ts diff --git a/test/integration/requests.test.ts b/test/integration/requests.test.ts index 18348df93..16c601bda 100644 --- a/test/integration/requests.test.ts +++ b/test/integration/requests.test.ts @@ -29,6 +29,15 @@ import { } from "../../constants/requests"; import { updateTask } from "../../models/tasks"; import { validTaskAssignmentRequest, validTaskCreqtionRequest } from "../fixtures/taskRequests/taskRequests"; +import { CreateOnboardingExtensionBody } from "../../types/onboardingExtension"; +const { BAD_TOKEN, CLOUDFLARE_WORKER } = require("../../constants/bot"); +const { generateToken } = require("../../test/utils/generateBotToken"); +import sinon from "sinon"; +import { createUserStatusWithState } from "../../utils/userStatus"; +import { userState } from "../../constants/userStatus"; +const firestore = require("../../utils/firestore"); +const userStatusModel = firestore.collection("usersStatus"); +import * as requestsQuery from "../../models/requests" const userData = userDataFixture(); chai.use(chaiHttp); @@ -793,3 +802,181 @@ describe("/requests Task", function () { }); }); }); + +describe("/requests Onboarding Extension", () => { + describe("POST /requests", () => { + let testUserId: string; + const testUserDiscordId = "654321"; + const testUserName = userData[6].username; + + beforeEach(async () => { + testUserId = await addUser({...userData[6], discordId: testUserDiscordId, discordJoinedAt: "2023-04-06T01:47:34.488000+00:00"}); + }) + + afterEach(async ()=>{ + sinon.restore(); + await cleanDb(); + }) + + const postEndpoint = "/requests"; + const botToken = generateToken({name: CLOUDFLARE_WORKER}) + const body: CreateOnboardingExtensionBody = { + type: REQUEST_TYPE.ONBOARDING, + numberOfDays: 5, + reason: "This is the reason", + requestedBy: "11111", + username: "user-name-2" + } + it("should return Feature not implemented when dev is not true", (done) => { + chai.request(app) + .post(`${postEndpoint}`) + .send(body) + .end((err, res)=>{ + if (err) return done(err); + expect(res.statusCode).to.equal(501); + expect(res.body.message).to.equal("Feature not implemented"); + done(); + }) + }) + + it("should return Invalid Request when authorization header is missing", (done) => { + chai + .request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", "") + .send(body) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("Invalid Request"); + done(); + }) + }) + + it("should return Unauthorized Bot for invalid token", (done) => { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${BAD_TOKEN}`) + .send(body) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(401); + expect(res.body.message).to.equal("Unauthorized Bot"); + done(); + }) + }) + + it("should return 400 response for invalid request body", (done) => { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({...body, numberOfDays:"1"}) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("numberOfDays must be a number"); + expect(res.body.error).to.equal("Bad Request"); + done(); + }) + }) + + it("should return 500 response when fails to create extension request", (done) => { + sinon.stub(requestsQuery, "createRequest") + .throws("Error while creating extension request"); + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({ + ...body, + username: testUserName, + requestedBy:testUserDiscordId + }) + .end((err, res)=>{ + if (err) return done(err); + expect(res.statusCode).to.equal(500); + expect(res.body.message).to.equal("An internal server error occurred"); + done(); + }) + }) + + it("should return 404 response when user does not exist", (done) => { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send(body) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(404); + expect(res.body.error).to.equal("Not Found"); + expect(res.body.message).to.equal("User not found"); + done(); + }) + }) + + it("should return 401 response when user is not a super user or status is not onboarding", (done)=> { + createUserStatusWithState(testUserId, userStatusModel, userState.ACTIVE); + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({ + ...body, + requestedBy:testUserDiscordId + }) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(401); + expect(res.body.error).to.equal("Unauthorized"); + done(); + }) + }) + + it("should return 400 response when a user already has a pending request", (done)=> { + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); + const extension = { + state: REQUEST_STATE.PENDING, + type: REQUEST_TYPE.ONBOARDING, + userId: testUserId, + } + + createRequest(extension); + + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({ + ...body, + username: testUserName, + requestedBy:testUserDiscordId + }) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.error).to.equal("Bad Request"); + expect(res.body.message).to.equal(REQUEST_ALREADY_PENDING); + done(); + }) + }) + + it("should return 201 for successful response", (done)=> { + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({ + ...body, + username: testUserName, + requestedBy:testUserDiscordId + }) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(201); + expect(res.body.message).to.equal(REQUEST_CREATED_SUCCESSFULLY); + expect(res.body.data.requestNumber).to.equal(1); + expect(res.body.data.reason).to.equal(body.reason); + expect(res.body.data.state).to.equal(REQUEST_STATE.PENDING) + done(); + }) + }) + }) +}); \ No newline at end of file diff --git a/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts b/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts new file mode 100644 index 000000000..390528006 --- /dev/null +++ b/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts @@ -0,0 +1,71 @@ +import { REQUEST_TYPE } from "../../../constants/requests"; +import { createOnboardingExtensionRequestValidator } from "../../../middlewares/validators/onboardingExtensionRequest"; +import sinon from "sinon"; +import { CreateOnboardingExtensionBody } from "../../../types/onboardingExtension"; +import { expect } from "chai"; + +describe("Onboarding Extension Request Validators", () => { + let req: any; + let res: any; + let nextSpy: sinon.SinonSpy; + beforeEach(function () { + res = { + boom: { + badRequest: sinon.spy(), + }, + }; + nextSpy = sinon.spy(); + }); + + describe("createOnboardingExtensionRequestValidator", () => { + const requestBody:CreateOnboardingExtensionBody = { + numberOfDays: 1, + reason: "This is reason", + username: "user-name-2", + requestedBy: "1111", + type: REQUEST_TYPE.ONBOARDING + } + it("should validate for a valid create request", async () => { + req = { + body: requestBody + }; + res = {}; + + await createOnboardingExtensionRequestValidator(req as any, res as any, nextSpy); + expect(nextSpy.calledOnce, "next should be called once"); + }); + + it("should not validate for an invalid request on wrong type", async () => { + req = { + body: { ...requestBody, type: REQUEST_TYPE.EXTENSION }, + }; + try { + await createOnboardingExtensionRequestValidator(req as any, res as any, nextSpy); + } catch (error) { + expect(error.details[0].message).to.equal(`"type" must be [ONBOARDING]`); + } + }); + + it("should not validate for an invalid request on wrong numberOfDays", async () => { + req = { + body: { ...requestBody, numberOfDays: "2" }, + }; + try { + await createOnboardingExtensionRequestValidator(req as any, res as any, nextSpy); + } catch (error) { + expect(error.details[0].message).to.equal(`numberOfDays must be a number`); + } + }); + + it("should not validate for an invalid request on wrong username", async () => { + req = { + body: { ...requestBody, username: undefined }, + }; + try { + await createOnboardingExtensionRequestValidator(req as any, res as any, nextSpy); + } catch (error) { + expect(error.details[0].message).to.equal(`"username" is required`); + } + }); + }); +}); \ No newline at end of file diff --git a/test/unit/middlewares/skipAuthenticateForOnboardingExtension.test.ts b/test/unit/middlewares/skipAuthenticateForOnboardingExtension.test.ts new file mode 100644 index 000000000..4c9492fe2 --- /dev/null +++ b/test/unit/middlewares/skipAuthenticateForOnboardingExtension.test.ts @@ -0,0 +1,49 @@ +import sinon from "sinon"; +import { skipAuthenticateForOnboardingExtensionRequest } from "../../../middlewares/skipAuthenticateForOnboardingExtension"; +import { REQUEST_TYPE } from "../../../constants/requests"; +import { assert } from "chai"; + +describe("skipAuthenticateForOnboardingExtensionRequest Middleware", () => { + let req, res, next, authenticate: sinon.SinonSpy, verifyDiscordBot: sinon.SinonSpy; + + beforeEach(() => { + authenticate = sinon.spy(); + verifyDiscordBot = sinon.spy(); + req = { + body:{}, + query:{}, + }, + res = {} + }); + + it("should call authenticate when type is not onboarding", () => { + req.body.type = REQUEST_TYPE.TASK + const middleware = skipAuthenticateForOnboardingExtensionRequest(authenticate, verifyDiscordBot); + middleware(req, res, next); + + assert.isTrue(authenticate.calledOnce, "authenticate should be called once"); + assert.isTrue(verifyDiscordBot.notCalled, "verifyDiscordBot should not be called"); + }); + + it("should not call verifyDicordBot and authenticate when dev is not true and type is onboarding", async () => { + req.query.dev = "false"; + req.body.type = REQUEST_TYPE.ONBOARDING; + + const middleware = skipAuthenticateForOnboardingExtensionRequest(authenticate, verifyDiscordBot); + middleware(req, res, next); + + assert.isTrue(verifyDiscordBot.notCalled, "verifyDiscordBot should not be called"); + assert.isTrue(authenticate.notCalled, "authenticate should not be called"); + }); + + it("should call verifyDiscordBot when dev is true and type is onboarding", () => { + req.query.dev = "true"; + req.body.type = REQUEST_TYPE.ONBOARDING; + + const middleware = skipAuthenticateForOnboardingExtensionRequest(authenticate, verifyDiscordBot); + middleware(req, res, next); + + assert.isTrue(verifyDiscordBot.calledOnce, "verifyDiscordBot should be called once"); + assert.isTrue(authenticate.notCalled, "authenticate should not be called"); + }); +}); \ No newline at end of file From 74cd7a0e77e7453343f224abda00dbdbe8f8a674 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 12:00:45 +0530 Subject: [PATCH 03/21] fix: added request type to support onboarding extension request --- constants/requests.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/constants/requests.ts b/constants/requests.ts index f0fdb9907..810b02610 100644 --- a/constants/requests.ts +++ b/constants/requests.ts @@ -15,6 +15,7 @@ export const REQUEST_TYPE = { EXTENSION: "EXTENSION", TASK: "TASK", ALL: "ALL", + ONBOARDING: "ONBOARDING", }; export const REQUEST_LOG_TYPE = { From 2176e1c37ecd166b16389486435580fbc7992cc6 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 12:02:12 +0530 Subject: [PATCH 04/21] feat: added middlwares for create-onboarding-extension-request feature --- .../skipAuthenticateForOnboardingExtension.ts | 20 +++++++++++ .../validators/onboardingExtensionRequest.ts | 36 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 middlewares/skipAuthenticateForOnboardingExtension.ts create mode 100644 middlewares/validators/onboardingExtensionRequest.ts diff --git a/middlewares/skipAuthenticateForOnboardingExtension.ts b/middlewares/skipAuthenticateForOnboardingExtension.ts new file mode 100644 index 000000000..c67686fd8 --- /dev/null +++ b/middlewares/skipAuthenticateForOnboardingExtension.ts @@ -0,0 +1,20 @@ +import { NextFunction, Request, Response } from "express" +import { REQUEST_TYPE } from "../constants/requests"; + +export const skipAuthenticateForOnboardingExtensionRequest = (authenticate, verifyDiscordBot) => { + return async (req: Request, res: Response, next: NextFunction) => { + const type = req.body.type; + const dev = req.query.dev; + + if(type === REQUEST_TYPE.ONBOARDING){ + if (dev != "true"){ + return res.status(501).json({ + message: "Feature not implemented" + }) + } + return await verifyDiscordBot(req, res, next); + } + + return await authenticate(req, res, next) + } +} \ No newline at end of file diff --git a/middlewares/validators/onboardingExtensionRequest.ts b/middlewares/validators/onboardingExtensionRequest.ts new file mode 100644 index 000000000..9ffb45089 --- /dev/null +++ b/middlewares/validators/onboardingExtensionRequest.ts @@ -0,0 +1,36 @@ +import joi from "joi"; +import { NextFunction } from "express"; +import { REQUEST_TYPE } from "../../constants/requests"; +import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../../types/onboardingExtension"; + +export const createOnboardingExtensionRequestValidator = async ( + req: OnboardingExtensionCreateRequest, + _res: OnboardingExtensionResponse, + _next: NextFunction +) => { + + const schema = joi + .object() + .strict() + .keys({ + numberOfDays: joi.number().required().min(1).messages({ + "number.base": "numberOfDays must be a number", + "any.required": "numberOfDays is required", + }), + reason: joi.string().required().messages({ + "string.empty": "reason cannot be empty", + }), + type: joi.string().valid(REQUEST_TYPE.ONBOARDING).required().messages({ + "string.empty": "type cannot be empty", + "any.required": "type is required", + }), + requestedBy: joi.string().required().messages({ + "string.empty": "requestedBy cannot be empty" + }), + username: joi.string().required().messages({ + "string.empty": "username cannot be empty" + }) + }); + + await schema.validateAsync(req.body, { abortEarly: false }); +}; From adaea252738b31ec43e4bbcd795116edbdc914a6 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 12:03:45 +0530 Subject: [PATCH 05/21] feat: added controller for handling create-onboarding-extension-request feature --- controllers/onboardingExtension.ts | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 controllers/onboardingExtension.ts diff --git a/controllers/onboardingExtension.ts b/controllers/onboardingExtension.ts new file mode 100644 index 000000000..04ff9cbcd --- /dev/null +++ b/controllers/onboardingExtension.ts @@ -0,0 +1,99 @@ +import { + ERROR_WHILE_CREATING_REQUEST, + LOG_ACTION, + REQUEST_ALREADY_PENDING, + REQUEST_CREATED_SUCCESSFULLY, + REQUEST_LOG_TYPE, + REQUEST_STATE, + REQUEST_TYPE, +} from "../constants/requests"; +import { CustomResponse } from "../typeDefinitions/global"; +import { userState } from "../constants/userStatus"; +import { addLog } from "../models/logs"; +import { createRequest, getRequestByKeyValues } from "../models/requests"; +import { fetchUser } from "../models/users"; +import { getUserStatus } from "../models/userStatus"; +import { OnboardingExtension, OnboardingExtensionCreateRequest } from "../types/onboardingExtension"; + +export const createOnboardingExtensionRequestController = async (req: OnboardingExtensionCreateRequest, res: CustomResponse) => { + try { + const data = req.body; + const {user, userExists} = await fetchUser({discordId: data.requestedBy}); + + if(!userExists) { + return res.boom.notFound("User not found"); + } + + const {id, roles, username} = user as unknown as {id: string, roles: { super_user: boolean}, username: string}; + const { data: userStatus } = await getUserStatus(id); + + if(!(roles?.super_user || (userStatus.currentStatus.state === userState.ONBOARDING && username === data.username))){ + return res.boom.unauthorized("Only super user and onboarding user are authorized to create an onboarding extension request"); + } + + const userResponse = await fetchUser({username: data.username}); + + const {id: userId, discordJoinedAt} = userResponse.user as unknown as {id: string, discordJoinedAt: Date}; + + const latestExtensionRequest: OnboardingExtension = await getRequestByKeyValues({ + userId: userId, + type: REQUEST_TYPE.ONBOARDING + }) + + if(latestExtensionRequest && latestExtensionRequest.state === REQUEST_STATE.PENDING){ + return res.boom.badRequest(REQUEST_ALREADY_PENDING); + } + + const firstDeadLine = new Date(discordJoinedAt).getTime() + (31*24*60*60*1000); + let requestNumber: number; + let oldEndsOn: number; + + if(!latestExtensionRequest){ + requestNumber = 1; + oldEndsOn = firstDeadLine; + }else if(latestExtensionRequest.state === REQUEST_STATE.REJECTED) { + requestNumber = latestExtensionRequest.requestNumber + 1; + oldEndsOn = latestExtensionRequest.oldEndsOn; + }else{ + requestNumber = latestExtensionRequest.requestNumber + 1; + oldEndsOn = latestExtensionRequest.newEndsOn; + } + + const newEndsOn = Date.now(); + + const onboardingExtension = await createRequest({ + type: REQUEST_TYPE.ONBOARDING, + state: REQUEST_STATE.PENDING, + userId: userId, + requestedBy: data.username, + oldEndsOn: oldEndsOn, + newEndsOn: newEndsOn, + reason: data.reason, + requestNumber: requestNumber, + }); + + const onboardingExtensionLog = { + type: REQUEST_LOG_TYPE.REQUEST_CREATED, + meta: { + requestId: onboardingExtension.id, + action: LOG_ACTION.CREATE, + userId: userId, + createdAt: Date.now(), + }, + body: onboardingExtension, + }; + + await addLog(onboardingExtensionLog.type, onboardingExtensionLog.meta, onboardingExtensionLog.body); + + return res.status(201).json({ + message: REQUEST_CREATED_SUCCESSFULLY, + data: { + id: onboardingExtension.id, + ...onboardingExtension, + } + }) + }catch (err) { + logger.error(ERROR_WHILE_CREATING_REQUEST, err); + return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST); + } +}; \ No newline at end of file From 2c2f5fa5845357e55d5e9c0ab982baa57f8f5586 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 12:07:06 +0530 Subject: [PATCH 06/21] fix: added middleware and controller for post route to support create-onboarding-extension feature --- controllers/requests.ts | 6 +++++- middlewares/validators/requests.ts | 7 ++++++- routes/requests.ts | 4 +++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/controllers/requests.ts b/controllers/requests.ts index d4bf87179..ab0333fd0 100644 --- a/controllers/requests.ts +++ b/controllers/requests.ts @@ -13,9 +13,11 @@ import { createTaskExtensionRequest, updateTaskExtensionRequest } from "./extens import { UpdateRequest } from "../types/requests"; import { TaskRequestRequest } from "../types/taskRequests"; import { createTaskRequestController } from "./taskRequestsv2"; +import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../types/onboardingExtension"; +import { createOnboardingExtensionRequestController } from "./onboardingExtension"; export const createRequestController = async ( - req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest, + req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest, res: CustomResponse ) => { const type = req.body.type; @@ -26,6 +28,8 @@ export const createRequestController = async ( return await createTaskExtensionRequest(req as ExtensionRequestRequest, res as ExtensionRequestResponse); case REQUEST_TYPE.TASK: return await createTaskRequestController(req as TaskRequestRequest, res as CustomResponse); + case REQUEST_TYPE.ONBOARDING: + return await createOnboardingExtensionRequestController(req as OnboardingExtensionCreateRequest, res as OnboardingExtensionResponse); default: return res.boom.badRequest("Invalid request type"); } diff --git a/middlewares/validators/requests.ts b/middlewares/validators/requests.ts index 0f5c1909e..292ed010d 100644 --- a/middlewares/validators/requests.ts +++ b/middlewares/validators/requests.ts @@ -9,9 +9,11 @@ import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../types/e import { CustomResponse } from "../../typeDefinitions/global"; import { UpdateRequest } from "../../types/requests"; import { TaskRequestRequest, TaskRequestResponse } from "../../types/taskRequests"; +import { createOnboardingExtensionRequestValidator } from "./onboardingExtensionRequest"; +import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../../types/onboardingExtension"; export const createRequestsMiddleware = async ( - req: OooRequestCreateRequest|ExtensionRequestRequest | TaskRequestRequest, + req: OooRequestCreateRequest|ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest, res: CustomResponse, next: NextFunction ) => { @@ -28,6 +30,9 @@ export const createRequestsMiddleware = async ( case REQUEST_TYPE.TASK: await createTaskRequestValidator(req as TaskRequestRequest, res as TaskRequestResponse, next); break; + case REQUEST_TYPE.ONBOARDING: + await createOnboardingExtensionRequestValidator(req as OnboardingExtensionCreateRequest, res as OnboardingExtensionResponse, next); + break; default: res.boom.badRequest(`Invalid request type: ${type}`); } diff --git a/routes/requests.ts b/routes/requests.ts index 5cda581b6..f04cba0c6 100644 --- a/routes/requests.ts +++ b/routes/requests.ts @@ -6,8 +6,10 @@ const { SUPERUSER } = require("../constants/roles"); import authenticate from "../middlewares/authenticate"; import { createRequestsMiddleware,updateRequestsMiddleware,getRequestsMiddleware } from "../middlewares/validators/requests"; import { createRequestController , updateRequestController, getRequestsController} from "../controllers/requests"; +import { skipAuthenticateForOnboardingExtensionRequest } from "../middlewares/skipAuthenticateForOnboardingExtension"; +import { verifyDiscordBot } from "../middlewares/authorizeBot"; router.get("/", getRequestsMiddleware, getRequestsController); -router.post("/",authenticate, createRequestsMiddleware, createRequestController); +router.post("/", skipAuthenticateForOnboardingExtensionRequest(authenticate, verifyDiscordBot), createRequestsMiddleware, createRequestController); router.put("/:id",authenticate, authorizeRoles([SUPERUSER]), updateRequestsMiddleware, updateRequestController); module.exports = router; From 182bfbfc707b25c2ed70b6b29e0b60e322def074 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 12:38:23 +0530 Subject: [PATCH 07/21] refactor: refactor 31 days range constant --- controllers/onboardingExtension.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/controllers/onboardingExtension.ts b/controllers/onboardingExtension.ts index 04ff9cbcd..b8ecb30f4 100644 --- a/controllers/onboardingExtension.ts +++ b/controllers/onboardingExtension.ts @@ -43,8 +43,9 @@ export const createOnboardingExtensionRequestController = async (req: Onboarding if(latestExtensionRequest && latestExtensionRequest.state === REQUEST_STATE.PENDING){ return res.boom.badRequest(REQUEST_ALREADY_PENDING); } - - const firstDeadLine = new Date(discordJoinedAt).getTime() + (31*24*60*60*1000); + + const deadlineinMillisecond = 31*24*60*60*1000; + const firstDeadLine = new Date(discordJoinedAt).getTime() + deadlineinMillisecond; let requestNumber: number; let oldEndsOn: number; From a1426786a617b051f23d8fc956a7cde0d91d3d19 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 14:43:35 +0530 Subject: [PATCH 08/21] fix: fix field name for onboarding extension request types --- types/onboardingExtension.d.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/types/onboardingExtension.d.ts b/types/onboardingExtension.d.ts index b3e6db0f8..354d83487 100644 --- a/types/onboardingExtension.d.ts +++ b/types/onboardingExtension.d.ts @@ -1,7 +1,6 @@ import { Request, Response } from "express"; import { Boom } from "express-boom"; import { REQUEST_STATE, REQUEST_TYPE } from "../constants/requests"; -import { userData } from "./global"; import { RequestQuery } from "./requests"; export type OnboardingExtension = { @@ -33,10 +32,9 @@ export type OnboardingExtensionRequestQuery = RequestQuery & { } export type OnboardingExtensionResponse = Response & { - Boom: Boom + boom: Boom } export type OnboardingExtensionCreateRequest = Request & { - CreateOnboardingExtension: CreateOnboardingExtensionBody; + body: CreateOnboardingExtensionBody; query: OnboardingExtensionRequestQuery; - Boom: Boom; } \ No newline at end of file From 489a79bf4247abe6b56e510f790085aaf8957f7d Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 14:45:51 +0530 Subject: [PATCH 09/21] fix: added validation rule and messages for request body --- middlewares/validators/onboardingExtensionRequest.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/middlewares/validators/onboardingExtensionRequest.ts b/middlewares/validators/onboardingExtensionRequest.ts index 9ffb45089..638dd2196 100644 --- a/middlewares/validators/onboardingExtensionRequest.ts +++ b/middlewares/validators/onboardingExtensionRequest.ts @@ -13,22 +13,28 @@ export const createOnboardingExtensionRequestValidator = async ( .object() .strict() .keys({ - numberOfDays: joi.number().required().min(1).messages({ + numberOfDays: joi.number().required().positive().integer().min(1).messages({ "number.base": "numberOfDays must be a number", "any.required": "numberOfDays is required", + "number.positive": "numberOfDays must be positive", + "number.min": "numberOfDays must be greater than zero", + "number.integer": "numberOfDays must be a integer" }), reason: joi.string().required().messages({ "string.empty": "reason cannot be empty", + "any.required": "reason is required", }), type: joi.string().valid(REQUEST_TYPE.ONBOARDING).required().messages({ "string.empty": "type cannot be empty", "any.required": "type is required", }), requestedBy: joi.string().required().messages({ - "string.empty": "requestedBy cannot be empty" + "string.empty": "requestedBy cannot be empty", + "any.required": "requestedBy is required", }), username: joi.string().required().messages({ - "string.empty": "username cannot be empty" + "string.empty": "username cannot be empty", + "any.required": "username is required" }) }); From 636ba4560ab7ba52cad7189fc34d6eda81650dd0 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 14:47:04 +0530 Subject: [PATCH 10/21] fix: fix test name and added tests for request body validation --- test/integration/requests.test.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/integration/requests.test.ts b/test/integration/requests.test.ts index 16c601bda..a7e1cf01a 100644 --- a/test/integration/requests.test.ts +++ b/test/integration/requests.test.ts @@ -866,7 +866,7 @@ describe("/requests Onboarding Extension", () => { }) }) - it("should return 400 response for invalid request body", (done) => { + it("should return 400 response for invalid value type of numberOfDays", (done) => { chai.request(app) .post(`${postEndpoint}?dev=true`) .set("authorization", `Bearer ${botToken}`) @@ -880,6 +880,34 @@ describe("/requests Onboarding Extension", () => { }) }) + it("should return 400 response for invalid value of numberOfDays", (done) => { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({...body, numberOfDays:1.4}) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("numberOfDays must be a integer"); + expect(res.body.error).to.equal("Bad Request"); + done(); + }) + }) + + it("should return 400 response for invalid username", (done) => { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({...body, username: undefined}) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("username is required"); + expect(res.body.error).to.equal("Bad Request"); + done(); + }) + }) + it("should return 500 response when fails to create extension request", (done) => { sinon.stub(requestsQuery, "createRequest") .throws("Error while creating extension request"); From 018ce3e5854efddef75886b29c13fcf92c1538ca Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 14:48:50 +0530 Subject: [PATCH 11/21] fix: fix response type, old and new deadline calculation --- controllers/onboardingExtension.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/controllers/onboardingExtension.ts b/controllers/onboardingExtension.ts index b8ecb30f4..751f21c77 100644 --- a/controllers/onboardingExtension.ts +++ b/controllers/onboardingExtension.ts @@ -7,16 +7,16 @@ import { REQUEST_STATE, REQUEST_TYPE, } from "../constants/requests"; -import { CustomResponse } from "../typeDefinitions/global"; import { userState } from "../constants/userStatus"; import { addLog } from "../models/logs"; import { createRequest, getRequestByKeyValues } from "../models/requests"; import { fetchUser } from "../models/users"; import { getUserStatus } from "../models/userStatus"; -import { OnboardingExtension, OnboardingExtensionCreateRequest } from "../types/onboardingExtension"; +import { OnboardingExtension, OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../types/onboardingExtension"; + +export const createOnboardingExtensionRequestController = async (req: OnboardingExtensionCreateRequest, res: OnboardingExtensionResponse) => { + try {; -export const createOnboardingExtensionRequestController = async (req: OnboardingExtensionCreateRequest, res: CustomResponse) => { - try { const data = req.body; const {user, userExists} = await fetchUser({discordId: data.requestedBy}); @@ -44,14 +44,17 @@ export const createOnboardingExtensionRequestController = async (req: Onboarding return res.boom.badRequest(REQUEST_ALREADY_PENDING); } - const deadlineinMillisecond = 31*24*60*60*1000; - const firstDeadLine = new Date(discordJoinedAt).getTime() + deadlineinMillisecond; + const thirtyOneDaysInMillisecond = 31*24*60*60*1000; + const discordJoinedDateInMillisecond = new Date(discordJoinedAt).getTime(); + const numberOfDaysInMillisecond = Math.floor(data.numberOfDays)*24*60*60*1000; + let requestNumber: number; let oldEndsOn: number; + let newEndsOn: number; if(!latestExtensionRequest){ requestNumber = 1; - oldEndsOn = firstDeadLine; + oldEndsOn = discordJoinedDateInMillisecond + thirtyOneDaysInMillisecond; }else if(latestExtensionRequest.state === REQUEST_STATE.REJECTED) { requestNumber = latestExtensionRequest.requestNumber + 1; oldEndsOn = latestExtensionRequest.oldEndsOn; @@ -60,7 +63,7 @@ export const createOnboardingExtensionRequestController = async (req: Onboarding oldEndsOn = latestExtensionRequest.newEndsOn; } - const newEndsOn = Date.now(); + newEndsOn = oldEndsOn + numberOfDaysInMillisecond; const onboardingExtension = await createRequest({ type: REQUEST_TYPE.ONBOARDING, From a21ddc90e60694f42e7d001b90389dbae3756bba Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 15:05:21 +0530 Subject: [PATCH 12/21] fix: fix actual value for validator test as it was failing --- .../middlewares/onboardingExtensionRequestValidator.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts b/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts index 390528006..32cd74f55 100644 --- a/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts +++ b/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts @@ -64,7 +64,7 @@ describe("Onboarding Extension Request Validators", () => { try { await createOnboardingExtensionRequestValidator(req as any, res as any, nextSpy); } catch (error) { - expect(error.details[0].message).to.equal(`"username" is required`); + expect(error.details[0].message).to.equal(`username is required`); } }); }); From c5b461799a6aaae7d6bc3fb7f2ad645f6a18d45d Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 20:11:12 +0530 Subject: [PATCH 13/21] fix: replace username with userId --- controllers/onboardingExtension.ts | 10 +++++----- .../validators/onboardingExtensionRequest.ts | 6 +++--- middlewares/validators/requests.ts | 2 +- test/integration/requests.test.ts | 16 ++++++---------- .../onboardingExtensionRequestValidator.test.ts | 8 ++++---- types/onboardingExtension.d.ts | 2 +- 6 files changed, 20 insertions(+), 24 deletions(-) diff --git a/controllers/onboardingExtension.ts b/controllers/onboardingExtension.ts index 751f21c77..f5dd5bdb7 100644 --- a/controllers/onboardingExtension.ts +++ b/controllers/onboardingExtension.ts @@ -24,16 +24,16 @@ export const createOnboardingExtensionRequestController = async (req: Onboarding return res.boom.notFound("User not found"); } - const {id, roles, username} = user as unknown as {id: string, roles: { super_user: boolean}, username: string}; + const {id, roles, discordId} = user as unknown as {id: string, roles: { super_user: boolean}, discordId: string}; const { data: userStatus } = await getUserStatus(id); - if(!(roles?.super_user || (userStatus.currentStatus.state === userState.ONBOARDING && username === data.username))){ + if(!(roles?.super_user || (userStatus.currentStatus.state === userState.ONBOARDING && discordId === data.userId))){ return res.boom.unauthorized("Only super user and onboarding user are authorized to create an onboarding extension request"); } - const userResponse = await fetchUser({username: data.username}); + const userResponse = await fetchUser({discordId: data.userId}); - const {id: userId, discordJoinedAt} = userResponse.user as unknown as {id: string, discordJoinedAt: Date}; + const {id: userId, discordJoinedAt, username} = userResponse.user as unknown as {id: string, discordJoinedAt: Date, username: string}; const latestExtensionRequest: OnboardingExtension = await getRequestByKeyValues({ userId: userId, @@ -69,7 +69,7 @@ export const createOnboardingExtensionRequestController = async (req: Onboarding type: REQUEST_TYPE.ONBOARDING, state: REQUEST_STATE.PENDING, userId: userId, - requestedBy: data.username, + requestedBy: username, oldEndsOn: oldEndsOn, newEndsOn: newEndsOn, reason: data.reason, diff --git a/middlewares/validators/onboardingExtensionRequest.ts b/middlewares/validators/onboardingExtensionRequest.ts index 638dd2196..5518d7f61 100644 --- a/middlewares/validators/onboardingExtensionRequest.ts +++ b/middlewares/validators/onboardingExtensionRequest.ts @@ -32,9 +32,9 @@ export const createOnboardingExtensionRequestValidator = async ( "string.empty": "requestedBy cannot be empty", "any.required": "requestedBy is required", }), - username: joi.string().required().messages({ - "string.empty": "username cannot be empty", - "any.required": "username is required" + userId: joi.string().required().messages({ + "string.empty": "userId cannot be empty", + "any.required": "userId is required" }) }); diff --git a/middlewares/validators/requests.ts b/middlewares/validators/requests.ts index 292ed010d..2cf3b6983 100644 --- a/middlewares/validators/requests.ts +++ b/middlewares/validators/requests.ts @@ -41,7 +41,7 @@ export const createRequestsMiddleware = async ( } catch (error) { const errorMessages = error.details.map((detail:any) => detail.message); logger.error(`Error while validating request payload : ${errorMessages}`); - res.boom.badRequest(errorMessages); + return res.boom.badRequest(errorMessages); } }; diff --git a/test/integration/requests.test.ts b/test/integration/requests.test.ts index a7e1cf01a..84c847b4f 100644 --- a/test/integration/requests.test.ts +++ b/test/integration/requests.test.ts @@ -807,7 +807,6 @@ describe("/requests Onboarding Extension", () => { describe("POST /requests", () => { let testUserId: string; const testUserDiscordId = "654321"; - const testUserName = userData[6].username; beforeEach(async () => { testUserId = await addUser({...userData[6], discordId: testUserDiscordId, discordJoinedAt: "2023-04-06T01:47:34.488000+00:00"}); @@ -824,8 +823,8 @@ describe("/requests Onboarding Extension", () => { type: REQUEST_TYPE.ONBOARDING, numberOfDays: 5, reason: "This is the reason", - requestedBy: "11111", - username: "user-name-2" + requestedBy: testUserDiscordId, + userId: testUserDiscordId, } it("should return Feature not implemented when dev is not true", (done) => { chai.request(app) @@ -894,15 +893,15 @@ describe("/requests Onboarding Extension", () => { }) }) - it("should return 400 response for invalid username", (done) => { + it("should return 400 response for invalid userId", (done) => { chai.request(app) .post(`${postEndpoint}?dev=true`) .set("authorization", `Bearer ${botToken}`) - .send({...body, username: undefined}) + .send({...body, userId: undefined}) .end((err, res) => { if (err) return done(err); expect(res.statusCode).to.equal(400); - expect(res.body.message).to.equal("username is required"); + expect(res.body.message).to.equal("userId is required"); expect(res.body.error).to.equal("Bad Request"); done(); }) @@ -917,7 +916,6 @@ describe("/requests Onboarding Extension", () => { .set("authorization", `Bearer ${botToken}`) .send({ ...body, - username: testUserName, requestedBy:testUserDiscordId }) .end((err, res)=>{ @@ -932,7 +930,7 @@ describe("/requests Onboarding Extension", () => { chai.request(app) .post(`${postEndpoint}?dev=true`) .set("authorization", `Bearer ${botToken}`) - .send(body) + .send({...body, requestedBy: "1111"}) .end((err, res) => { if (err) return done(err); expect(res.statusCode).to.equal(404); @@ -974,7 +972,6 @@ describe("/requests Onboarding Extension", () => { .set("authorization", `Bearer ${botToken}`) .send({ ...body, - username: testUserName, requestedBy:testUserDiscordId }) .end((err, res) => { @@ -993,7 +990,6 @@ describe("/requests Onboarding Extension", () => { .set("authorization", `Bearer ${botToken}`) .send({ ...body, - username: testUserName, requestedBy:testUserDiscordId }) .end((err, res) => { diff --git a/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts b/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts index 32cd74f55..3e5c2fa1c 100644 --- a/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts +++ b/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts @@ -21,7 +21,7 @@ describe("Onboarding Extension Request Validators", () => { const requestBody:CreateOnboardingExtensionBody = { numberOfDays: 1, reason: "This is reason", - username: "user-name-2", + userId: "22222", requestedBy: "1111", type: REQUEST_TYPE.ONBOARDING } @@ -57,14 +57,14 @@ describe("Onboarding Extension Request Validators", () => { } }); - it("should not validate for an invalid request on wrong username", async () => { + it("should not validate for an invalid request on wrong userId", async () => { req = { - body: { ...requestBody, username: undefined }, + body: { ...requestBody, userId: undefined }, }; try { await createOnboardingExtensionRequestValidator(req as any, res as any, nextSpy); } catch (error) { - expect(error.details[0].message).to.equal(`username is required`); + expect(error.details[0].message).to.equal(`userId is required`); } }); }); diff --git a/types/onboardingExtension.d.ts b/types/onboardingExtension.d.ts index 354d83487..76481c936 100644 --- a/types/onboardingExtension.d.ts +++ b/types/onboardingExtension.d.ts @@ -23,7 +23,7 @@ export type CreateOnboardingExtensionBody = { type: string; numberOfDays: number; requestedBy: string; - username: string; + userId: string; reason: string; } From bf7f7a9e24cdda17d4711c489f114c2e5107b0cb Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 22:25:34 +0530 Subject: [PATCH 14/21] fix: handled edge case and change success message --- controllers/onboardingExtension.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/controllers/onboardingExtension.ts b/controllers/onboardingExtension.ts index f5dd5bdb7..f2b728596 100644 --- a/controllers/onboardingExtension.ts +++ b/controllers/onboardingExtension.ts @@ -2,7 +2,6 @@ import { ERROR_WHILE_CREATING_REQUEST, LOG_ACTION, REQUEST_ALREADY_PENDING, - REQUEST_CREATED_SUCCESSFULLY, REQUEST_LOG_TYPE, REQUEST_STATE, REQUEST_TYPE, @@ -27,11 +26,15 @@ export const createOnboardingExtensionRequestController = async (req: Onboarding const {id, roles, discordId} = user as unknown as {id: string, roles: { super_user: boolean}, discordId: string}; const { data: userStatus } = await getUserStatus(id); - if(!(roles?.super_user || (userStatus.currentStatus.state === userState.ONBOARDING && discordId === data.userId))){ + if(!(roles?.super_user || (userStatus && userStatus.currentStatus.state === userState.ONBOARDING && discordId === data.userId))){ return res.boom.unauthorized("Only super user and onboarding user are authorized to create an onboarding extension request"); } const userResponse = await fetchUser({discordId: data.userId}); + + if(!userResponse.userExists) { + return res.boom.notFound("User not found"); + } const {id: userId, discordJoinedAt, username} = userResponse.user as unknown as {id: string, discordJoinedAt: Date, username: string}; @@ -90,7 +93,7 @@ export const createOnboardingExtensionRequestController = async (req: Onboarding await addLog(onboardingExtensionLog.type, onboardingExtensionLog.meta, onboardingExtensionLog.body); return res.status(201).json({ - message: REQUEST_CREATED_SUCCESSFULLY, + message: "Onboarding extension request created successfully!", data: { id: onboardingExtension.id, ...onboardingExtension, From e903c48ee46324be393f22a8464e8f4d337d824c Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 22:33:26 +0530 Subject: [PATCH 15/21] test: fix actual message as test was failing --- test/integration/requests.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/requests.test.ts b/test/integration/requests.test.ts index 84c847b4f..88f31a21a 100644 --- a/test/integration/requests.test.ts +++ b/test/integration/requests.test.ts @@ -995,7 +995,7 @@ describe("/requests Onboarding Extension", () => { .end((err, res) => { if (err) return done(err); expect(res.statusCode).to.equal(201); - expect(res.body.message).to.equal(REQUEST_CREATED_SUCCESSFULLY); + expect(res.body.message).to.equal("Onboarding extension request created successfully!"); expect(res.body.data.requestNumber).to.equal(1); expect(res.body.data.reason).to.equal(body.reason); expect(res.body.data.state).to.equal(REQUEST_STATE.PENDING) From b7dfc5a5b5845b33ac8d3a2dd5178fa901e378cb Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 22:48:36 +0530 Subject: [PATCH 16/21] refactor: moved onboarding request tests to separate file --- test/integration/onboardingExtension.test.ts | 219 +++++++++++++++++++ test/integration/requests.test.ts | 211 ------------------ 2 files changed, 219 insertions(+), 211 deletions(-) create mode 100644 test/integration/onboardingExtension.test.ts diff --git a/test/integration/onboardingExtension.test.ts b/test/integration/onboardingExtension.test.ts new file mode 100644 index 000000000..0263d3716 --- /dev/null +++ b/test/integration/onboardingExtension.test.ts @@ -0,0 +1,219 @@ +import addUser from "../utils/addUser"; +import chai from "chai"; +const { expect } = chai; +import userDataFixture from "../fixtures/user/user"; +import sinon from "sinon"; +import chaiHttp from "chai-http"; +import cleanDb from "../utils/cleanDb"; +import { CreateOnboardingExtensionBody } from "../../types/onboardingExtension"; +import { REQUEST_ALREADY_PENDING, REQUEST_STATE, REQUEST_TYPE } from "../../constants/requests"; +const { generateToken } = require("../../test/utils/generateBotToken"); +import app from "../../server"; +import { createUserStatusWithState } from "../../utils/userStatus"; +const firestore = require("../../utils/firestore"); +const userStatusModel = firestore.collection("usersStatus"); +import * as requestsQuery from "../../models/requests" +import { userState } from "../../constants/userStatus"; +const { CLOUDFLARE_WORKER, BAD_TOKEN } = require("../../constants/bot"); +const userData = userDataFixture(); +chai.use(chaiHttp); + +describe("/requests Onboarding Extension", () => { + describe("POST /requests", () => { + let testUserId: string; + const testUserDiscordId = "654321"; + + beforeEach(async () => { + testUserId = await addUser({...userData[6], discordId: testUserDiscordId, discordJoinedAt: "2023-04-06T01:47:34.488000+00:00"}); + }) + afterEach(async ()=>{ + sinon.restore(); + await cleanDb(); + }) + const postEndpoint = "/requests"; + const botToken = generateToken({name: CLOUDFLARE_WORKER}) + const body: CreateOnboardingExtensionBody = { + type: REQUEST_TYPE.ONBOARDING, + numberOfDays: 5, + reason: "This is the reason", + requestedBy: testUserDiscordId, + userId: testUserDiscordId, + } + it("should return Feature not implemented when dev is not true", (done) => { + chai.request(app) + .post(`${postEndpoint}`) + .send(body) + .end((err, res)=>{ + if (err) return done(err); + expect(res.statusCode).to.equal(501); + expect(res.body.message).to.equal("Feature not implemented"); + done(); + }) + }) + + it("should return Invalid Request when authorization header is missing", (done) => { + chai + .request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", "") + .send(body) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("Invalid Request"); + done(); + }) + }) + + it("should return Unauthorized Bot for invalid token", (done) => { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${BAD_TOKEN}`) + .send(body) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(401); + expect(res.body.message).to.equal("Unauthorized Bot"); + done(); + }) + }) + + it("should return 400 response for invalid value type of numberOfDays", (done) => { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({...body, numberOfDays:"1"}) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("numberOfDays must be a number"); + expect(res.body.error).to.equal("Bad Request"); + done(); + }) + }) + + it("should return 400 response for invalid value of numberOfDays", (done) => { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({...body, numberOfDays:1.4}) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("numberOfDays must be a integer"); + expect(res.body.error).to.equal("Bad Request"); + done(); + }) + }) + + it("should return 400 response for invalid userId", (done) => { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({...body, userId: undefined}) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("userId is required"); + expect(res.body.error).to.equal("Bad Request"); + done(); + }) + }) + + it("should return 500 response when fails to create extension request", (done) => { + sinon.stub(requestsQuery, "createRequest") + .throws("Error while creating extension request"); + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({ + ...body, + requestedBy:testUserDiscordId + }) + .end((err, res)=>{ + if (err) return done(err); + expect(res.statusCode).to.equal(500); + expect(res.body.message).to.equal("An internal server error occurred"); + done(); + }) + }) + + it("should return 404 response when user does not exist", (done) => { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({...body, requestedBy: "1111"}) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(404); + expect(res.body.error).to.equal("Not Found"); + expect(res.body.message).to.equal("User not found"); + done(); + }) + }) + + it("should return 401 response when user is not a super user or status is not onboarding", (done)=> { + createUserStatusWithState(testUserId, userStatusModel, userState.ACTIVE); + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({ + ...body, + requestedBy:testUserDiscordId + }) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(401); + expect(res.body.error).to.equal("Unauthorized"); + done(); + }) + }) + + it("should return 400 response when a user already has a pending request", (done)=> { + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); + const extension = { + state: REQUEST_STATE.PENDING, + type: REQUEST_TYPE.ONBOARDING, + userId: testUserId, + } + + requestsQuery.createRequest(extension); + + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({ + ...body, + requestedBy:testUserDiscordId + }) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.error).to.equal("Bad Request"); + expect(res.body.message).to.equal(REQUEST_ALREADY_PENDING); + done(); + }) + }) + + it("should return 201 for successful response", (done)=> { + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({ + ...body, + requestedBy:testUserDiscordId + }) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(201); + expect(res.body.message).to.equal("Onboarding extension request created successfully!"); + expect(res.body.data.requestNumber).to.equal(1); + expect(res.body.data.reason).to.equal(body.reason); + expect(res.body.data.state).to.equal(REQUEST_STATE.PENDING) + done(); + }) + }) + }) +}); \ No newline at end of file diff --git a/test/integration/requests.test.ts b/test/integration/requests.test.ts index 88f31a21a..18348df93 100644 --- a/test/integration/requests.test.ts +++ b/test/integration/requests.test.ts @@ -29,15 +29,6 @@ import { } from "../../constants/requests"; import { updateTask } from "../../models/tasks"; import { validTaskAssignmentRequest, validTaskCreqtionRequest } from "../fixtures/taskRequests/taskRequests"; -import { CreateOnboardingExtensionBody } from "../../types/onboardingExtension"; -const { BAD_TOKEN, CLOUDFLARE_WORKER } = require("../../constants/bot"); -const { generateToken } = require("../../test/utils/generateBotToken"); -import sinon from "sinon"; -import { createUserStatusWithState } from "../../utils/userStatus"; -import { userState } from "../../constants/userStatus"; -const firestore = require("../../utils/firestore"); -const userStatusModel = firestore.collection("usersStatus"); -import * as requestsQuery from "../../models/requests" const userData = userDataFixture(); chai.use(chaiHttp); @@ -802,205 +793,3 @@ describe("/requests Task", function () { }); }); }); - -describe("/requests Onboarding Extension", () => { - describe("POST /requests", () => { - let testUserId: string; - const testUserDiscordId = "654321"; - - beforeEach(async () => { - testUserId = await addUser({...userData[6], discordId: testUserDiscordId, discordJoinedAt: "2023-04-06T01:47:34.488000+00:00"}); - }) - - afterEach(async ()=>{ - sinon.restore(); - await cleanDb(); - }) - - const postEndpoint = "/requests"; - const botToken = generateToken({name: CLOUDFLARE_WORKER}) - const body: CreateOnboardingExtensionBody = { - type: REQUEST_TYPE.ONBOARDING, - numberOfDays: 5, - reason: "This is the reason", - requestedBy: testUserDiscordId, - userId: testUserDiscordId, - } - it("should return Feature not implemented when dev is not true", (done) => { - chai.request(app) - .post(`${postEndpoint}`) - .send(body) - .end((err, res)=>{ - if (err) return done(err); - expect(res.statusCode).to.equal(501); - expect(res.body.message).to.equal("Feature not implemented"); - done(); - }) - }) - - it("should return Invalid Request when authorization header is missing", (done) => { - chai - .request(app) - .post(`${postEndpoint}?dev=true`) - .set("authorization", "") - .send(body) - .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(400); - expect(res.body.message).to.equal("Invalid Request"); - done(); - }) - }) - - it("should return Unauthorized Bot for invalid token", (done) => { - chai.request(app) - .post(`${postEndpoint}?dev=true`) - .set("authorization", `Bearer ${BAD_TOKEN}`) - .send(body) - .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(401); - expect(res.body.message).to.equal("Unauthorized Bot"); - done(); - }) - }) - - it("should return 400 response for invalid value type of numberOfDays", (done) => { - chai.request(app) - .post(`${postEndpoint}?dev=true`) - .set("authorization", `Bearer ${botToken}`) - .send({...body, numberOfDays:"1"}) - .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(400); - expect(res.body.message).to.equal("numberOfDays must be a number"); - expect(res.body.error).to.equal("Bad Request"); - done(); - }) - }) - - it("should return 400 response for invalid value of numberOfDays", (done) => { - chai.request(app) - .post(`${postEndpoint}?dev=true`) - .set("authorization", `Bearer ${botToken}`) - .send({...body, numberOfDays:1.4}) - .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(400); - expect(res.body.message).to.equal("numberOfDays must be a integer"); - expect(res.body.error).to.equal("Bad Request"); - done(); - }) - }) - - it("should return 400 response for invalid userId", (done) => { - chai.request(app) - .post(`${postEndpoint}?dev=true`) - .set("authorization", `Bearer ${botToken}`) - .send({...body, userId: undefined}) - .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(400); - expect(res.body.message).to.equal("userId is required"); - expect(res.body.error).to.equal("Bad Request"); - done(); - }) - }) - - it("should return 500 response when fails to create extension request", (done) => { - sinon.stub(requestsQuery, "createRequest") - .throws("Error while creating extension request"); - createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); - chai.request(app) - .post(`${postEndpoint}?dev=true`) - .set("authorization", `Bearer ${botToken}`) - .send({ - ...body, - requestedBy:testUserDiscordId - }) - .end((err, res)=>{ - if (err) return done(err); - expect(res.statusCode).to.equal(500); - expect(res.body.message).to.equal("An internal server error occurred"); - done(); - }) - }) - - it("should return 404 response when user does not exist", (done) => { - chai.request(app) - .post(`${postEndpoint}?dev=true`) - .set("authorization", `Bearer ${botToken}`) - .send({...body, requestedBy: "1111"}) - .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(404); - expect(res.body.error).to.equal("Not Found"); - expect(res.body.message).to.equal("User not found"); - done(); - }) - }) - - it("should return 401 response when user is not a super user or status is not onboarding", (done)=> { - createUserStatusWithState(testUserId, userStatusModel, userState.ACTIVE); - chai.request(app) - .post(`${postEndpoint}?dev=true`) - .set("authorization", `Bearer ${botToken}`) - .send({ - ...body, - requestedBy:testUserDiscordId - }) - .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(401); - expect(res.body.error).to.equal("Unauthorized"); - done(); - }) - }) - - it("should return 400 response when a user already has a pending request", (done)=> { - createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); - const extension = { - state: REQUEST_STATE.PENDING, - type: REQUEST_TYPE.ONBOARDING, - userId: testUserId, - } - - createRequest(extension); - - chai.request(app) - .post(`${postEndpoint}?dev=true`) - .set("authorization", `Bearer ${botToken}`) - .send({ - ...body, - requestedBy:testUserDiscordId - }) - .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(400); - expect(res.body.error).to.equal("Bad Request"); - expect(res.body.message).to.equal(REQUEST_ALREADY_PENDING); - done(); - }) - }) - - it("should return 201 for successful response", (done)=> { - createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); - chai.request(app) - .post(`${postEndpoint}?dev=true`) - .set("authorization", `Bearer ${botToken}`) - .send({ - ...body, - requestedBy:testUserDiscordId - }) - .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(201); - expect(res.body.message).to.equal("Onboarding extension request created successfully!"); - expect(res.body.data.requestNumber).to.equal(1); - expect(res.body.data.reason).to.equal(body.reason); - expect(res.body.data.state).to.equal(REQUEST_STATE.PENDING) - done(); - }) - }) - }) -}); \ No newline at end of file From 4dfdef48e58e0ab7710e41df69e6c7d2c6f54370 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 22:53:52 +0530 Subject: [PATCH 17/21] fix: fix lint issue --- controllers/onboardingExtension.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/controllers/onboardingExtension.ts b/controllers/onboardingExtension.ts index f2b728596..39cb4b9bd 100644 --- a/controllers/onboardingExtension.ts +++ b/controllers/onboardingExtension.ts @@ -14,7 +14,7 @@ import { getUserStatus } from "../models/userStatus"; import { OnboardingExtension, OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../types/onboardingExtension"; export const createOnboardingExtensionRequestController = async (req: OnboardingExtensionCreateRequest, res: OnboardingExtensionResponse) => { - try {; + try { const data = req.body; const {user, userExists} = await fetchUser({discordId: data.requestedBy}); @@ -41,7 +41,7 @@ export const createOnboardingExtensionRequestController = async (req: Onboarding const latestExtensionRequest: OnboardingExtension = await getRequestByKeyValues({ userId: userId, type: REQUEST_TYPE.ONBOARDING - }) + }); if(latestExtensionRequest && latestExtensionRequest.state === REQUEST_STATE.PENDING){ return res.boom.badRequest(REQUEST_ALREADY_PENDING); @@ -98,7 +98,7 @@ export const createOnboardingExtensionRequestController = async (req: Onboarding id: onboardingExtension.id, ...onboardingExtension, } - }) + }); }catch (err) { logger.error(ERROR_WHILE_CREATING_REQUEST, err); return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST); From 0eb49545dc3af570646f0197dcdc312b62ef8dd9 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Tue, 17 Dec 2024 23:15:17 +0530 Subject: [PATCH 18/21] fix: fix test name and added test for super user case --- test/integration/onboardingExtension.test.ts | 24 +++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/test/integration/onboardingExtension.test.ts b/test/integration/onboardingExtension.test.ts index 0263d3716..fdc4fae36 100644 --- a/test/integration/onboardingExtension.test.ts +++ b/test/integration/onboardingExtension.test.ts @@ -22,8 +22,10 @@ describe("/requests Onboarding Extension", () => { describe("POST /requests", () => { let testUserId: string; const testUserDiscordId = "654321"; + let testSuperUserDiscordId = "123456"; beforeEach(async () => { + await addUser({...userData[4], discordId: testSuperUserDiscordId}); testUserId = await addUser({...userData[6], discordId: testUserDiscordId, discordJoinedAt: "2023-04-06T01:47:34.488000+00:00"}); }) afterEach(async ()=>{ @@ -196,7 +198,7 @@ describe("/requests Onboarding Extension", () => { }) }) - it("should return 201 for successful response", (done)=> { + it("should return 201 for successful response when user has onboarding status", (done)=> { createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); chai.request(app) .post(`${postEndpoint}?dev=true`) @@ -215,5 +217,25 @@ describe("/requests Onboarding Extension", () => { done(); }) }) + + it("should return 201 for successful response when user is a super user", (done)=> { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({ + ...body, + userId: testUserDiscordId, + requestedBy: testSuperUserDiscordId + }) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(201); + expect(res.body.message).to.equal("Onboarding extension request created successfully!"); + expect(res.body.data.requestNumber).to.equal(1); + expect(res.body.data.reason).to.equal(body.reason); + expect(res.body.data.state).to.equal(REQUEST_STATE.PENDING) + done(); + }) + }) }) }); \ No newline at end of file From efba90c3619e1e91032228eb7d238817def6bbb6 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Fri, 20 Dec 2024 01:22:59 +0530 Subject: [PATCH 19/21] fix: fix failing test and added test for edge case --- test/integration/onboardingExtension.test.ts | 243 +++++++++++-------- 1 file changed, 147 insertions(+), 96 deletions(-) diff --git a/test/integration/onboardingExtension.test.ts b/test/integration/onboardingExtension.test.ts index fdc4fae36..f2e7cd325 100644 --- a/test/integration/onboardingExtension.test.ts +++ b/test/integration/onboardingExtension.test.ts @@ -22,7 +22,21 @@ describe("/requests Onboarding Extension", () => { describe("POST /requests", () => { let testUserId: string; const testUserDiscordId = "654321"; - let testSuperUserDiscordId = "123456"; + const testSuperUserDiscordId = "123456"; + const extensionRequest = { + state: REQUEST_STATE.APPROVED, + type: REQUEST_TYPE.ONBOARDING, + requestNumber: 1 + }; + const postEndpoint = "/requests"; + const botToken = generateToken({name: CLOUDFLARE_WORKER}) + const body: CreateOnboardingExtensionBody = { + type: REQUEST_TYPE.ONBOARDING, + numberOfDays: 5, + reason: "This is the reason", + requestedBy: testUserDiscordId, + userId: testUserDiscordId, + }; beforeEach(async () => { await addUser({...userData[4], discordId: testSuperUserDiscordId}); @@ -32,24 +46,16 @@ describe("/requests Onboarding Extension", () => { sinon.restore(); await cleanDb(); }) - const postEndpoint = "/requests"; - const botToken = generateToken({name: CLOUDFLARE_WORKER}) - const body: CreateOnboardingExtensionBody = { - type: REQUEST_TYPE.ONBOARDING, - numberOfDays: 5, - reason: "This is the reason", - requestedBy: testUserDiscordId, - userId: testUserDiscordId, - } + it("should return Feature not implemented when dev is not true", (done) => { chai.request(app) .post(`${postEndpoint}`) .send(body) .end((err, res)=>{ - if (err) return done(err); - expect(res.statusCode).to.equal(501); - expect(res.body.message).to.equal("Feature not implemented"); - done(); + if (err) return done(err); + expect(res.statusCode).to.equal(501); + expect(res.body.message).to.equal("Feature not implemented"); + done(); }) }) @@ -60,10 +66,10 @@ describe("/requests Onboarding Extension", () => { .set("authorization", "") .send(body) .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(400); - expect(res.body.message).to.equal("Invalid Request"); - done(); + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("Invalid Request"); + done(); }) }) @@ -73,10 +79,10 @@ describe("/requests Onboarding Extension", () => { .set("authorization", `Bearer ${BAD_TOKEN}`) .send(body) .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(401); - expect(res.body.message).to.equal("Unauthorized Bot"); - done(); + if (err) return done(err); + expect(res.statusCode).to.equal(401); + expect(res.body.message).to.equal("Unauthorized Bot"); + done(); }) }) @@ -86,11 +92,11 @@ describe("/requests Onboarding Extension", () => { .set("authorization", `Bearer ${botToken}`) .send({...body, numberOfDays:"1"}) .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(400); - expect(res.body.message).to.equal("numberOfDays must be a number"); - expect(res.body.error).to.equal("Bad Request"); - done(); + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("numberOfDays must be a number"); + expect(res.body.error).to.equal("Bad Request"); + done(); }) }) @@ -100,11 +106,11 @@ describe("/requests Onboarding Extension", () => { .set("authorization", `Bearer ${botToken}`) .send({...body, numberOfDays:1.4}) .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(400); - expect(res.body.message).to.equal("numberOfDays must be a integer"); - expect(res.body.error).to.equal("Bad Request"); - done(); + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("numberOfDays must be a integer"); + expect(res.body.error).to.equal("Bad Request"); + done(); }) }) @@ -114,30 +120,27 @@ describe("/requests Onboarding Extension", () => { .set("authorization", `Bearer ${botToken}`) .send({...body, userId: undefined}) .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(400); - expect(res.body.message).to.equal("userId is required"); - expect(res.body.error).to.equal("Bad Request"); - done(); + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal("userId is required"); + expect(res.body.error).to.equal("Bad Request"); + done(); }) }) it("should return 500 response when fails to create extension request", (done) => { + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); sinon.stub(requestsQuery, "createRequest") .throws("Error while creating extension request"); - createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); chai.request(app) .post(`${postEndpoint}?dev=true`) .set("authorization", `Bearer ${botToken}`) - .send({ - ...body, - requestedBy:testUserDiscordId - }) + .send(body) .end((err, res)=>{ - if (err) return done(err); - expect(res.statusCode).to.equal(500); - expect(res.body.message).to.equal("An internal server error occurred"); - done(); + if (err) return done(err); + expect(res.statusCode).to.equal(500); + expect(res.body.message).to.equal("An internal server error occurred"); + done(); }) }) @@ -145,97 +148,145 @@ describe("/requests Onboarding Extension", () => { chai.request(app) .post(`${postEndpoint}?dev=true`) .set("authorization", `Bearer ${botToken}`) - .send({...body, requestedBy: "1111"}) + .send({...body, userId: "11111"}) .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(404); - expect(res.body.error).to.equal("Not Found"); - expect(res.body.message).to.equal("User not found"); - done(); + if (err) return done(err); + expect(res.statusCode).to.equal(404); + expect(res.body.error).to.equal("Not Found"); + expect(res.body.message).to.equal("User not found"); + done(); }) }) - it("should return 401 response when user is not a super user or status is not onboarding", (done)=> { + it("should return 400 response when user's status is not onboarding", (done)=> { createUserStatusWithState(testUserId, userStatusModel, userState.ACTIVE); chai.request(app) .post(`${postEndpoint}?dev=true`) .set("authorization", `Bearer ${botToken}`) - .send({ - ...body, - requestedBy:testUserDiscordId + .send(body) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.error).to.equal("Bad Request"); + expect(res.body.message).to.equal("User does not have onboarding status"); + done(); }) + }) + + it("should return 404 response when requested-user does not exist", (done) => { + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({...body, requestedBy: "11111"}) .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(401); - expect(res.body.error).to.equal("Unauthorized"); - done(); + if (err) return done(err); + expect(res.statusCode).to.equal(404); + expect(res.body.error).to.equal("Not Found"); + expect(res.body.message).to.equal("User not found"); + done(); }) }) it("should return 400 response when a user already has a pending request", (done)=> { createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); - const extension = { - state: REQUEST_STATE.PENDING, - type: REQUEST_TYPE.ONBOARDING, - userId: testUserId, - } - - requestsQuery.createRequest(extension); + requestsQuery.createRequest({...extensionRequest, state: REQUEST_STATE.PENDING, userId: testUserId}); chai.request(app) .post(`${postEndpoint}?dev=true`) .set("authorization", `Bearer ${botToken}`) - .send({ - ...body, - requestedBy:testUserDiscordId - }) + .send(body) .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(400); - expect(res.body.error).to.equal("Bad Request"); - expect(res.body.message).to.equal(REQUEST_ALREADY_PENDING); - done(); + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.error).to.equal("Bad Request"); + expect(res.body.message).to.equal(REQUEST_ALREADY_PENDING); + done(); }) }) - - it("should return 201 for successful response when user has onboarding status", (done)=> { + + it("should return 201 for successful response when user and requestedUser are same and has onboarding status", (done)=> { createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); chai.request(app) .post(`${postEndpoint}?dev=true`) .set("authorization", `Bearer ${botToken}`) - .send({ - ...body, - requestedBy:testUserDiscordId - }) + .send(body) .end((err, res) => { - if (err) return done(err); - expect(res.statusCode).to.equal(201); - expect(res.body.message).to.equal("Onboarding extension request created successfully!"); - expect(res.body.data.requestNumber).to.equal(1); - expect(res.body.data.reason).to.equal(body.reason); - expect(res.body.data.state).to.equal(REQUEST_STATE.PENDING) - done(); + if (err) return done(err); + expect(res.statusCode).to.equal(201); + expect(res.body.message).to.equal("Onboarding extension request created successfully!"); + expect(res.body.data.requestNumber).to.equal(1); + expect(res.body.data.reason).to.equal(body.reason); + expect(res.body.data.state).to.equal(REQUEST_STATE.PENDING) + done(); }) }) - it("should return 201 for successful response when user is a super user", (done)=> { + it("should return 201 for successful response when requested-user is a super-user and user has onboarding status", (done)=> { + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); chai.request(app) .post(`${postEndpoint}?dev=true`) .set("authorization", `Bearer ${botToken}`) .send({ ...body, - userId: testUserDiscordId, requestedBy: testSuperUserDiscordId }) .end((err, res) => { - if (err) return done(err); + if (err) return done(err); + expect(res.statusCode).to.equal(201); + expect(res.body.message).to.equal("Onboarding extension request created successfully!"); + expect(res.body.data.requestNumber).to.equal(1); + expect(res.body.data.reason).to.equal(body.reason); + expect(res.body.data.state).to.equal(REQUEST_STATE.PENDING) + done(); + }) + }) + + it("should return 201 response when latest extension request is approved", async () => { + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); + const latestApprovedExtension = await requestsQuery.createRequest({ + ...extensionRequest, + userId: testUserId, + state: REQUEST_STATE.APPROVED, + newEndsOn: Date.now(), + oldEndsOn: Date.now() - 24*60*60*1000, + }); + + const res = await chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send(body); + expect(res.statusCode).to.equal(201); expect(res.body.message).to.equal("Onboarding extension request created successfully!"); - expect(res.body.data.requestNumber).to.equal(1); + expect(res.body.data.requestNumber).to.equal(2); expect(res.body.data.reason).to.equal(body.reason); - expect(res.body.data.state).to.equal(REQUEST_STATE.PENDING) - done(); - }) + expect(res.body.data.state).to.equal(REQUEST_STATE.PENDING); + expect(res.body.data.oldEndsOn).to.equal(latestApprovedExtension.newEndsOn); + expect(res.body.data.newEndsOn).to.equal(latestApprovedExtension.newEndsOn + (body.numberOfDays*24*60*60*1000)); + }) + + it("should return 201 response when latest extension request is rejected", async () => { + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); + const latestRejectedExtension = await requestsQuery.createRequest({ + ...extensionRequest, + state: REQUEST_STATE.REJECTED, + userId: testUserId, + newEndsOn: Date.now(), + oldEndsOn: Date.now() - 24*60*60*1000, + }); + const res = await chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send(body) + + expect(res.statusCode).to.equal(201); + expect(res.body.message).to.equal("Onboarding extension request created successfully!"); + expect(res.body.data.requestNumber).to.equal(2); + expect(res.body.data.reason).to.equal(body.reason);; + expect(res.body.data.state).to.equal(REQUEST_STATE.PENDING); + expect(res.body.data.oldEndsOn).to.equal(latestRejectedExtension.oldEndsOn); + expect(res.body.data.newEndsOn).to.equal(latestRejectedExtension.oldEndsOn + (body.numberOfDays*24*60*60*1000)); }) }) }); \ No newline at end of file From 201071ad837428a693e33bb6ef83595f74465057 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Fri, 20 Dec 2024 01:23:15 +0530 Subject: [PATCH 20/21] fix: added missing field in user type --- typeDefinitions/users.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/typeDefinitions/users.ts b/typeDefinitions/users.ts index d74fa4a6c..489cd48c0 100644 --- a/typeDefinitions/users.ts +++ b/typeDefinitions/users.ts @@ -1,4 +1,5 @@ export type User = { + id?: string username?: string; first_name?: string; last_name?: string; @@ -17,7 +18,8 @@ export type User = { roles?: { member?: boolean; in_discord?: boolean; - }; + super_user?: boolean; + } tokens?: { githubAccessToken?: string; }; @@ -29,4 +31,4 @@ export type User = { }; incompleteUserDetails?: boolean; nickname_synced?: boolean; -}; +}; \ No newline at end of file From 35b1f44a479b1ce24ad1a6b802e62070fefd6fa9 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Fri, 20 Dec 2024 01:27:10 +0530 Subject: [PATCH 21/21] fix: refactor and handled case when super user request for non-onbarding user --- controllers/onboardingExtension.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/controllers/onboardingExtension.ts b/controllers/onboardingExtension.ts index 39cb4b9bd..86bcfe5a4 100644 --- a/controllers/onboardingExtension.ts +++ b/controllers/onboardingExtension.ts @@ -11,32 +11,37 @@ import { addLog } from "../models/logs"; import { createRequest, getRequestByKeyValues } from "../models/requests"; import { fetchUser } from "../models/users"; import { getUserStatus } from "../models/userStatus"; +import { User } from "../typeDefinitions/users"; import { OnboardingExtension, OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../types/onboardingExtension"; export const createOnboardingExtensionRequestController = async (req: OnboardingExtensionCreateRequest, res: OnboardingExtensionResponse) => { try { const data = req.body; - const {user, userExists} = await fetchUser({discordId: data.requestedBy}); - + const {user, userExists} = await fetchUser({discordId: data.userId}); + if(!userExists) { return res.boom.notFound("User not found"); } - const {id, roles, discordId} = user as unknown as {id: string, roles: { super_user: boolean}, discordId: string}; - const { data: userStatus } = await getUserStatus(id); + const { id: userId, discordId: userDiscordId, discordJoinedAt, username} = user as User; + const { data: userStatus } = await getUserStatus(userId); - if(!(roles?.super_user || (userStatus && userStatus.currentStatus.state === userState.ONBOARDING && discordId === data.userId))){ - return res.boom.unauthorized("Only super user and onboarding user are authorized to create an onboarding extension request"); + if(!userStatus || userStatus.currentStatus.state != userState.ONBOARDING){ + return res.boom.badRequest("User does not have onboarding status"); } - const userResponse = await fetchUser({discordId: data.userId}); + const requestedUserResponse = await fetchUser({discordId: data.requestedBy}); - if(!userResponse.userExists) { + if(!requestedUserResponse.userExists) { return res.boom.notFound("User not found"); } - const {id: userId, discordJoinedAt, username} = userResponse.user as unknown as {id: string, discordJoinedAt: Date, username: string}; + const {roles, discordId: requestedUserDiscordId} = requestedUserResponse.user as User; + + if(!(roles?.super_user || (requestedUserDiscordId === userDiscordId))){ + return res.boom.unauthorized("Only super user and onboarding user are authorized to create an onboarding extension request"); + } const latestExtensionRequest: OnboardingExtension = await getRequestByKeyValues({ userId: userId,