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 = { diff --git a/controllers/onboardingExtension.ts b/controllers/onboardingExtension.ts new file mode 100644 index 000000000..86bcfe5a4 --- /dev/null +++ b/controllers/onboardingExtension.ts @@ -0,0 +1,111 @@ +import { + ERROR_WHILE_CREATING_REQUEST, + LOG_ACTION, + REQUEST_ALREADY_PENDING, + REQUEST_LOG_TYPE, + REQUEST_STATE, + REQUEST_TYPE, +} from "../constants/requests"; +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 { 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.userId}); + + if(!userExists) { + return res.boom.notFound("User not found"); + } + + const { id: userId, discordId: userDiscordId, discordJoinedAt, username} = user as User; + const { data: userStatus } = await getUserStatus(userId); + + if(!userStatus || userStatus.currentStatus.state != userState.ONBOARDING){ + return res.boom.badRequest("User does not have onboarding status"); + } + + const requestedUserResponse = await fetchUser({discordId: data.requestedBy}); + + if(!requestedUserResponse.userExists) { + return res.boom.notFound("User not found"); + } + + 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, + type: REQUEST_TYPE.ONBOARDING + }); + + if(latestExtensionRequest && latestExtensionRequest.state === REQUEST_STATE.PENDING){ + return res.boom.badRequest(REQUEST_ALREADY_PENDING); + } + + 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 = discordJoinedDateInMillisecond + thirtyOneDaysInMillisecond; + }else if(latestExtensionRequest.state === REQUEST_STATE.REJECTED) { + requestNumber = latestExtensionRequest.requestNumber + 1; + oldEndsOn = latestExtensionRequest.oldEndsOn; + }else{ + requestNumber = latestExtensionRequest.requestNumber + 1; + oldEndsOn = latestExtensionRequest.newEndsOn; + } + + newEndsOn = oldEndsOn + numberOfDaysInMillisecond; + + const onboardingExtension = await createRequest({ + type: REQUEST_TYPE.ONBOARDING, + state: REQUEST_STATE.PENDING, + userId: userId, + requestedBy: 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: "Onboarding extension 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 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/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..5518d7f61 --- /dev/null +++ b/middlewares/validators/onboardingExtensionRequest.ts @@ -0,0 +1,42 @@ +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().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", + "any.required": "requestedBy is required", + }), + userId: joi.string().required().messages({ + "string.empty": "userId cannot be empty", + "any.required": "userId is required" + }) + }); + + await schema.validateAsync(req.body, { abortEarly: false }); +}; diff --git a/middlewares/validators/requests.ts b/middlewares/validators/requests.ts index 0f5c1909e..2cf3b6983 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}`); } @@ -36,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/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; diff --git a/test/integration/onboardingExtension.test.ts b/test/integration/onboardingExtension.test.ts new file mode 100644 index 000000000..f2e7cd325 --- /dev/null +++ b/test/integration/onboardingExtension.test.ts @@ -0,0 +1,292 @@ +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"; + 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}); + testUserId = await addUser({...userData[6], discordId: testUserDiscordId, discordJoinedAt: "2023-04-06T01:47:34.488000+00:00"}); + }) + afterEach(async ()=>{ + sinon.restore(); + await cleanDb(); + }) + + 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) => { + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); + sinon.stub(requestsQuery, "createRequest") + .throws("Error while creating extension request"); + 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(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, 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(); + }) + }) + + 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) + .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(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); + requestsQuery.createRequest({...extensionRequest, state: REQUEST_STATE.PENDING, userId: testUserId}); + + 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(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 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) + .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(); + }) + }) + + 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, + 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(); + }) + }) + + 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(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(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 diff --git a/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts b/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts new file mode 100644 index 000000000..3e5c2fa1c --- /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", + userId: "22222", + 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 userId", async () => { + req = { + body: { ...requestBody, userId: undefined }, + }; + try { + await createOnboardingExtensionRequestValidator(req as any, res as any, nextSpy); + } catch (error) { + expect(error.details[0].message).to.equal(`userId 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 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 diff --git a/types/onboardingExtension.d.ts b/types/onboardingExtension.d.ts new file mode 100644 index 000000000..76481c936 --- /dev/null +++ b/types/onboardingExtension.d.ts @@ -0,0 +1,40 @@ +import { Request, Response } from "express"; +import { Boom } from "express-boom"; +import { REQUEST_STATE, REQUEST_TYPE } from "../constants/requests"; +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; + userId: string; + reason: string; +} + +export type OnboardingExtensionRequestQuery = RequestQuery & { + dev?: string +} + +export type OnboardingExtensionResponse = Response & { + boom: Boom +} +export type OnboardingExtensionCreateRequest = Request & { + body: CreateOnboardingExtensionBody; + query: OnboardingExtensionRequestQuery; +} \ No newline at end of file