diff --git a/test/integration/onboardingExtension.test.ts b/test/integration/onboardingExtension.test.ts new file mode 100644 index 000000000..454cc9688 --- /dev/null +++ b/test/integration/onboardingExtension.test.ts @@ -0,0 +1,302 @@ +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, + ONBOARDING_REQUEST_CREATED_SUCCESSFULLY, + UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST +} 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; + let testUserIdForInvalidDiscordJoinedDate: string; + let testUserDiscordIdForInvalidDiscordJoinedDate: string = "54321"; + + const testUserDiscordId: string = "654321"; + 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", + userId: testUserDiscordId, + }; + + beforeEach(async () => { + testUserId = await addUser({ + ...userData[6], + discordId: testUserDiscordId, + discordJoinedAt: "2023-04-06T01:47:34.488000+00:00" + }); + testUserIdForInvalidDiscordJoinedDate = await addUser({ + ...userData[1], + discordId: testUserDiscordIdForInvalidDiscordJoinedDate, + discordJoinedAt: "2023-04-06T01" + }); + }); + + afterEach(async ()=>{ + sinon.restore(); + await cleanDb(); + }) + + it("should not call verifyDiscordBot and return 401 response when extension type is not onboarding", (done)=> { + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .send({...body, type: REQUEST_TYPE.OOO}) + .end((err, res)=>{ + if(err) return done(err); + expect(res.statusCode).to.equal(401); + expect(res.body.error).to.equal("Unauthorized"); + expect(res.body.message).to.equal("Unauthenticated User"); + done(); + }) + }) + + 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 500 response when discordJoinedAt date string is invalid", (done) => { + createUserStatusWithState(testUserIdForInvalidDiscordJoinedDate, userStatusModel, userState.ONBOARDING); + chai.request(app) + .post(`${postEndpoint}?dev=true`) + .set("authorization", `Bearer ${botToken}`) + .send({...body, userId: testUserDiscordIdForInvalidDiscordJoinedDate}) + .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 403 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(403); + expect(res.body.error).to.equal("Forbidden"); + expect(res.body.message).to.equal(UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST); + 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 has onboarding state", (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_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 previous 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() + 2*24*60*60*1000, + 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_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 previous latest extension request is rejected", async () => { + createUserStatusWithState(testUserId, userStatusModel, userState.ONBOARDING); + const currentDate = Date.now(); + const latestRejectedExtension = await requestsQuery.createRequest({ + ...extensionRequest, + state: REQUEST_STATE.REJECTED, + userId: testUserId, + newEndsOn: currentDate, + oldEndsOn: currentDate - 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_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(new Date(res.body.data.newEndsOn).toDateString()) + .to.equal(new Date(currentDate + (body.numberOfDays*24*60*60*1000)).toDateString()); + }) + }) +}); \ 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..e00ef300c --- /dev/null +++ b/test/unit/middlewares/onboardingExtensionRequestValidator.test.ts @@ -0,0 +1,70 @@ +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", + 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..b613fc3c6 --- /dev/null +++ b/test/unit/middlewares/skipAuthenticateForOnboardingExtension.test.ts @@ -0,0 +1,47 @@ +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; + let middleware; + beforeEach(() => { + authenticate = sinon.spy(); + verifyDiscordBot = sinon.spy(); + middleware = skipAuthenticateForOnboardingExtensionRequest(authenticate, verifyDiscordBot); + req = { + body:{}, + query:{}, + }, + res = {} + }); + + it("should call authenticate when type is not onboarding", () => { + req.body.type = REQUEST_TYPE.TASK + 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; + + 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; + + 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/test/unit/utils/requests.test.ts b/test/unit/utils/requests.test.ts new file mode 100644 index 000000000..d585cd095 --- /dev/null +++ b/test/unit/utils/requests.test.ts @@ -0,0 +1,45 @@ +import { convertDateStringToMilliseconds, getNewDeadline } from "../../../utils/requests" +import { convertDaysToMilliseconds } from "../../../utils/time"; +import {expect} from "chai"; + +describe("Test getNewDeadline", () => { + const currentDate = Date.now(); + const millisecondsInTwoDays = convertDaysToMilliseconds(2); + let oldEndsOn = currentDate - millisecondsInTwoDays; + const numberOfDaysInMillisecond = convertDaysToMilliseconds(5); + + it("should return correct new deadline when old deadline has been missed", () => { + const res = getNewDeadline(currentDate, oldEndsOn, numberOfDaysInMillisecond); + expect(res).to.equal(currentDate + numberOfDaysInMillisecond); + }) + + it("shoudl return correct new deadline when old deadline has not been missed", () => { + oldEndsOn += millisecondsInTwoDays; + const res = getNewDeadline(currentDate, oldEndsOn, numberOfDaysInMillisecond); + expect(res).to.equal(oldEndsOn + numberOfDaysInMillisecond); + }) +}) + +describe("Test convertDateStringInMilliseconds", () => { + const validDateString = "2024-10-17T16:10:52.668000+00:00"; + const invalidDateString = "2024-10-17T16"; + const emptyDateString = ""; + + it("should return isDate equal false for invalid date string", () => { + const res = convertDateStringToMilliseconds(invalidDateString); + expect(res.isDate).to.equal(false); + expect(res.milliseconds).to.equal(undefined); + }) + + it("should return isDate equal false for empty date string", () => { + const res = convertDateStringToMilliseconds(emptyDateString); + expect(res.isDate).to.equal(false); + expect(res.milliseconds).to.equal(undefined); + }) + + it("should return isDate equal true for valid date string", () => { + const res = convertDateStringToMilliseconds(validDateString); + expect(res.isDate).to.equal(true); + expect(res.milliseconds).to.equal(Date.parse(validDateString)); + }) +}) \ No newline at end of file