From 4b3aedca1e0080e8a0d0eb51e316ae43fac51114 Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Thu, 2 Jan 2025 20:12:30 +0530 Subject: [PATCH 1/2] feat: Add test cases for controller and validator - Remove failing tests and fix existing tests - Add tests to check success and unexpected behaviour and fix existing tests - Replace actual messages with constants for easily maintenance - Add test for super user and request owner authorization check and fix existing failing tests - Remove un-necessary changes - Remove separate file for validator tests --- test/integration/onboardingExtension.test.ts | 247 ++++++++++++++++++- test/unit/middlewares/requests.test.ts | 78 ++++++ 2 files changed, 322 insertions(+), 3 deletions(-) diff --git a/test/integration/onboardingExtension.test.ts b/test/integration/onboardingExtension.test.ts index 541df3fd1..4c81e98b7 100644 --- a/test/integration/onboardingExtension.test.ts +++ b/test/integration/onboardingExtension.test.ts @@ -11,7 +11,13 @@ import { REQUEST_STATE, REQUEST_TYPE, ONBOARDING_REQUEST_CREATED_SUCCESSFULLY, UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST, - REQUEST_FETCHED_SUCCESSFULLY + REQUEST_FETCHED_SUCCESSFULLY, + INVALID_REQUEST_DEADLINE, + PENDING_REQUEST_UPDATED, + REQUEST_UPDATED_SUCCESSFULLY, + INVALID_REQUEST_TYPE, + REQUEST_DOES_NOT_EXIST, + UNAUTHORIZED_TO_UPDATE_REQUEST } from "../../constants/requests"; const { generateToken } = require("../../test/utils/generateBotToken"); import app from "../../server"; @@ -22,6 +28,9 @@ import * as requestsQuery from "../../models/requests" import { userState } from "../../constants/userStatus"; import { generateAuthToken } from "../../services/authService"; const { CLOUDFLARE_WORKER, BAD_TOKEN } = require("../../constants/bot"); +import * as logUtils from "../../services/logService"; +import { convertDaysToMilliseconds } from "../../utils/time"; +import { OooStatusRequest } from "../../types/oooRequest"; const userData = userDataFixture(); chai.use(chaiHttp); @@ -412,12 +421,12 @@ describe("/requests Onboarding Extension", () => { putEndpoint = `/requests/${latestExtension.id}?dev=true`; authToken = generateAuthToken({userId}); }) - + afterEach(async () => { sinon.restore(); await cleanDb(); }) - + it("should return 401 response when user is not a super user", (done) => { chai.request(app) .put(putEndpoint) @@ -597,5 +606,237 @@ describe("/requests Onboarding Extension", () => { }) }) }); + + describe("PATCH /requests", () => { + const body = { + type: REQUEST_TYPE.ONBOARDING, + newEndsOn: Date.now() + convertDaysToMilliseconds(3), + reason: "" + } + let latestValidExtension: OnboardingExtension; + let userId: string; + let invalidUserId: string; + let superUserId: string; + let patchEndpoint: string; + let authToken: string; + let latestApprovedExtension: OnboardingExtension; + let latestInvalidExtension: OnboardingExtension; + let oooRequest: OooStatusRequest; + + beforeEach(async () => { + userId = await addUser(userData[6]); + invalidUserId = await addUser(userData[0]); + superUserId = await addUser(userData[4]); + latestInvalidExtension = await requestsQuery.createRequest({ + state: REQUEST_STATE.PENDING, + type: REQUEST_TYPE.ONBOARDING, + oldEndsOn: Date.now() + convertDaysToMilliseconds(5), + userId: userId, + }); + latestValidExtension = await requestsQuery.createRequest({ + state: REQUEST_STATE.PENDING, + type: REQUEST_TYPE.ONBOARDING, + oldEndsOn: Date.now() - convertDaysToMilliseconds(3), + userId: userId + }); + latestApprovedExtension = await requestsQuery.createRequest({ + state: REQUEST_STATE.APPROVED, + type: REQUEST_TYPE.ONBOARDING, + oldEndsOn: Date.now(), + userId: userId + }); + oooRequest = await requestsQuery.createRequest({type: REQUEST_TYPE.OOO, userId: userId}); + patchEndpoint = `/requests/${latestValidExtension.id}?dev=true`; + authToken = generateAuthToken({userId}); + }) + + afterEach(async () => { + sinon.restore(); + await cleanDb(); + }) + + it("should return 400 response for incorrect type", (done) => { + chai.request(app) + .patch(patchEndpoint) + .set("authorization", `Bearer ${authToken}`) + .send({...body, type: ""}) + .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("Invalid type"); + done(); + }) + }) + + it("should return Feature not implemented when dev is not true", (done) => { + chai.request(app) + .patch(`/requests/1111?dev=false`) + .send(body) + .set("authorization", `Bearer ${authToken}`) + .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 Unauthenticated User when authorization header is missing", (done) => { + chai + .request(app) + .patch(patchEndpoint) + .set("authorization", "") + .send(body) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(401); + expect(res.body.message).to.equal("Unauthenticated User"); + done(); + }) + }) + + it("should return Unauthenticated User for invalid token", (done) => { + chai.request(app) + .patch(patchEndpoint) + .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("Unauthenticated User"); + done(); + }) + }) + + it("should return 400 response for invalid value of newEndsOn", (done) => { + chai.request(app) + .patch(patchEndpoint) + .set("authorization", `Bearer ${authToken}`) + .send({...body, newEndsOn: Date.now()}) + .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).contain(`"newEndsOn" must be greater than or equal to`) + done(); + }) + }) + + it("should return 404 response for invalid extension id", (done) => { + chai.request(app) + .patch(`/requests/1111?dev=true`) + .set("authorization", `Bearer ${authToken}`) + .send(body) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(404); + expect(res.body.message).to.equal(REQUEST_DOES_NOT_EXIST); + expect(res.body.error).to.equal("Not Found"); + done(); + }) + }) + + it("should return 403 response when super user and request owner are not updating the request", (done) => { + chai.request(app) + .patch(patchEndpoint) + .set("authorization", `Bearer ${generateAuthToken({userId: invalidUserId})}`) + .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_UPDATE_REQUEST); + done(); + }) + }) + + it("should return 400 response when request type is not onboarding", (done) => { + chai.request(app) + .patch(`/requests/${oooRequest.id}?dev=true`) + .set("authorization", `Bearer ${authToken}`) + .send(body) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal(INVALID_REQUEST_TYPE); + expect(res.body.error).to.equal("Bad Request"); + done(); + }) + }) + + it("should return 400 response when extension state is not pending", (done) => { + chai.request(app) + .patch(`/requests/${latestApprovedExtension.id}?dev=true`) + .set("authorization", `Bearer ${authToken}`) + .send(body) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal(PENDING_REQUEST_UPDATED); + expect(res.body.error).to.equal("Bad Request"); + done(); + }) + }) + + it("should return 400 response when old dealdine is greater than new deadline", (done) => { + chai.request(app) + .patch(`/requests/${latestInvalidExtension.id}?dev=true`) + .set("authorization", `Bearer ${authToken}`) + .send(body) + .end((err, res) => { + if (err) return done(err); + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.equal(INVALID_REQUEST_DEADLINE); + expect(res.body.error).to.equal("Bad Request"); + done(); + }) + }) + + it("should return 200 success response when request owner is updating the request", (done) => { + chai.request(app) + .patch(patchEndpoint) + .set("authorization", `Bearer ${authToken}`) + .send(body) + .end((err, res)=>{ + if(err) return done(err); + expect(res.statusCode).to.equal(200); + expect(res.body.message).to.equal(REQUEST_UPDATED_SUCCESSFULLY); + expect(res.body.data.id).to.equal(latestValidExtension.id); + expect(res.body.data.newEndsOn).to.equal(body.newEndsOn) + done(); + }) + }) + + it("should return 200 success response when super user is updating the request", (done) => { + chai.request(app) + .patch(patchEndpoint) + .set("authorization", `Bearer ${generateAuthToken({userId: superUserId})}`) + .send(body) + .end((err, res)=>{ + if(err) return done(err); + expect(res.statusCode).to.equal(200); + expect(res.body.message).to.equal(REQUEST_UPDATED_SUCCESSFULLY); + expect(res.body.data.id).to.equal(latestValidExtension.id); + expect(res.body.data.newEndsOn).to.equal(body.newEndsOn) + done(); + }) + }) + + + it("should return 500 response for unexpected error", (done) => { + sinon.stub(logUtils, "addLog").throws("Error") + chai.request(app) + .patch(patchEndpoint) + .send(body) + .set("authorization", `Bearer ${authToken}`) + .end((err, res)=>{ + if(err) return done(err); + expect(res.statusCode).to.equal(500); + expect(res.body.error).to.equal("Internal Server Error"); + done(); + }) + }) + }) }); diff --git a/test/unit/middlewares/requests.test.ts b/test/unit/middlewares/requests.test.ts index c57fe4baf..f1eeefffd 100644 --- a/test/unit/middlewares/requests.test.ts +++ b/test/unit/middlewares/requests.test.ts @@ -6,6 +6,7 @@ import { createRequestsMiddleware, getRequestsMiddleware, updateRequestsMiddleware, + updateRequestValidator, } from "../../../middlewares/validators/requests"; import { validOooStatusRequests, @@ -14,6 +15,9 @@ import { invalidOooStatusUpdate, } from "../../fixtures/oooRequest/oooRequest"; import { OooRequestCreateRequest, OooRequestResponse } from "../../../types/oooRequest"; +import { REQUEST_TYPE } from "../../../constants/requests"; +import { convertDaysToMilliseconds } from "../../../utils/time"; +import { updateOnboardingExtensionRequestValidator } from "../../../middlewares/validators/onboardingExtensionRequest"; describe("Create Request Validators", function () { let req: any; @@ -110,3 +114,77 @@ describe("Create Request Validators", function () { }); }); }); + +describe("updateRequestValidator", () => { + let req, res, next: sinon.SinonSpy; + + beforeEach(() => { + next = sinon.spy(); + res = { boom: { badRequest: sinon.spy() } } + }); + + afterEach(() => { + sinon.restore(); + }) + + it("should call next for correct type", async () => { + req = { body: { type: REQUEST_TYPE.ONBOARDING, newEndsOn: Date.now() + convertDaysToMilliseconds(2) } }; + await updateRequestValidator(req, res, next); + expect(next.calledOnce).to.be.true; + }) + + it("should not call next for incorrect type", async () => { + req = { body: { type: REQUEST_TYPE.OOO } }; + await updateRequestValidator(req, res, next); + expect(next.notCalled).to.be.true; + }) +}) + +describe("updateOnboardingExtensionRequestValidator", () => { + let req, res, next: sinon.SinonSpy; + + beforeEach(() => { + next = sinon.spy(); + res = { boom: { badRequest: sinon.spy() } }; + }); + + afterEach(() => { + sinon.restore(); + }) + + it("should not call next for incorrect type ", async () => { + req = { + body: { + type: REQUEST_TYPE.OOO, + newEndsOn: Date.now() + convertDaysToMilliseconds(3) + } + } + + await updateOnboardingExtensionRequestValidator(req, res, next); + expect(next.notCalled).to.be.true; + }); + + it("should not call next for incorrect newEndsOn ", async () => { + req = { + body: { + type: REQUEST_TYPE.ONBOARDING, + newEndsOn: Date.now() - convertDaysToMilliseconds(1) + } + } + + await updateOnboardingExtensionRequestValidator(req, res, next); + expect(next.notCalled).to.be.true; + }); + + it("should call next for successful validaton", async () => { + req = { + body: { + type: REQUEST_TYPE.ONBOARDING, + newEndsOn: Date.now() + convertDaysToMilliseconds(3) + } + } + + await updateOnboardingExtensionRequestValidator(req, res, next); + expect(next.calledOnce).to.be.true; + }); +}) \ No newline at end of file From c1294d28ec14a68c7f8278c41141342c6db57a7b Mon Sep 17 00:00:00 2001 From: Pankaj Sha Date: Fri, 17 Jan 2025 16:10:06 +0530 Subject: [PATCH 2/2] feat: add tests for onboarding update and validate service --- .../unit/services/onboardingExtension.test.ts | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 test/unit/services/onboardingExtension.test.ts diff --git a/test/unit/services/onboardingExtension.test.ts b/test/unit/services/onboardingExtension.test.ts new file mode 100644 index 000000000..5ee556a89 --- /dev/null +++ b/test/unit/services/onboardingExtension.test.ts @@ -0,0 +1,200 @@ +import { + INVALID_REQUEST_DEADLINE, + INVALID_REQUEST_TYPE, + PENDING_REQUEST_UPDATED, + REQUEST_DOES_NOT_EXIST, + REQUEST_STATE, + REQUEST_TYPE, + UNAUTHORIZED_TO_UPDATE_REQUEST +} from "../../../constants/requests" +import { + updateOnboardingExtensionRequest, + validateOnboardingExtensionUpdateRequest +} from "../../../services/onboardingExtension" +import { expect } from "chai" +import firestore from "../../../utils/firestore"; +import { convertDaysToMilliseconds } from "../../../utils/time"; +import cleanDb from "../../utils/cleanDb"; +const requestModel = firestore.collection("requests"); +import * as logService from "../../../services/logService"; +import sinon from "sinon"; + +describe("Test Onboarding Extension Service", () => { + let validExtensionRequest; + let validExtensionRequestDoc; + const userId = "11111"; + const errorMessage = "Unexpected error occured"; + + beforeEach(async ()=>{ + validExtensionRequest = await requestModel.add({ + type: REQUEST_TYPE.ONBOARDING, + oldEndsOn: Date.now() - convertDaysToMilliseconds(2), + state: REQUEST_STATE.PENDING, + userId, + }) + validExtensionRequestDoc = await requestModel.doc(validExtensionRequest.id).get(); + }) + + afterEach(async ()=>{ + await cleanDb(); + sinon.restore(); + }) + + describe("validateOnboardingExtensionUpdateRequest", () => { + let invalidTypeRequest; + let invalidTypeRequestDoc; + let invalidStateRequest; + let invalidStateRequestDoc; + let invalidDeadlineRequest; + let invalidDeadlineRequestDoc; + + beforeEach(async ()=>{ + invalidTypeRequest = await requestModel.add({ + type: REQUEST_TYPE.OOO, + userId, + }); + invalidTypeRequestDoc = await requestModel.doc(invalidTypeRequest.id).get(); + invalidStateRequest = await requestModel.add({ + state: REQUEST_STATE.APPROVED, + userId, + type: REQUEST_TYPE.ONBOARDING, + }) + invalidStateRequestDoc = await requestModel.doc(invalidStateRequest.id).get(); + invalidDeadlineRequest = await requestModel.add({ + type: REQUEST_TYPE.ONBOARDING, + state: REQUEST_STATE.PENDING, + oldEndsOn: Date.now() + convertDaysToMilliseconds(2), + userId, + }) + invalidDeadlineRequestDoc = await requestModel.doc(invalidDeadlineRequest.id).get(); + }) + + afterEach(async ()=>{ + await cleanDb(); + sinon.restore(); + }) + + it("should return undefined when all validation checks passes", async () => { + const response = await validateOnboardingExtensionUpdateRequest( + validExtensionRequestDoc, + validExtensionRequest.id, + true, + userId, + Date.now() + ) + expect(response).to.be.undefined; + }); + + it("should return REQUEST_DOES_NOT_EXIST error", async () => { + const response = await validateOnboardingExtensionUpdateRequest( + false, + "23345", + false, + "2341", + Date.now(), + ); + expect(response).to.not.be.undefined; + expect(response.error).to.equal(REQUEST_DOES_NOT_EXIST) + }); + + it("shoud return UNAUTHORIZED_TO_UPDATE_REQUEST error when super user and request owner are not updating request", async () => { + const response = await validateOnboardingExtensionUpdateRequest( + validExtensionRequestDoc, + validExtensionRequest.id, + false, + "2333", + Date.now() + ); + expect(response).to.be.not.undefined; + expect(response.error).to.equal(UNAUTHORIZED_TO_UPDATE_REQUEST); + }); + + it("should return INVALID_REQUEST_TYPE error", async () => { + const response = await validateOnboardingExtensionUpdateRequest( + invalidTypeRequestDoc, + invalidTypeRequest.id, + true, + userId, + Date.now() + ) + expect(response).to.be.not.undefined; + expect(response.error).to.equal(INVALID_REQUEST_TYPE); + }); + + it("should return PENDING_REQUEST_UPDATED error", async () => { + const response = await validateOnboardingExtensionUpdateRequest( + invalidStateRequestDoc, + invalidStateRequest.id, + true, + userId, + Date.now() + ) + expect(response).to.be.not.undefined; + expect(response.error).to.equal(PENDING_REQUEST_UPDATED); + }); + + it("should return INVALID_REQUEST_DEADLINE error", async () => { + const response = await validateOnboardingExtensionUpdateRequest( + invalidDeadlineRequestDoc, + invalidDeadlineRequest.id, + true, + userId, + Date.now(), + ) + expect(response).to.be.not.undefined; + expect(response.error).to.equal(INVALID_REQUEST_DEADLINE); + }); + + it("should throw error", async () => { + sinon.stub(logService, "addLog").throws(new Error(errorMessage)); + try{ + await validateOnboardingExtensionUpdateRequest( + validExtensionRequestDoc, + validExtensionRequest.id, + false, + "1111", + Date.now(), + ) + }catch(error){ + expect(error.message).to.equal(errorMessage); + } + }) + }); + + describe("updateOnboardingExtensionRequest", () => { + it("should update request", async () => { + const newDate = Date.now(); + const response = await updateOnboardingExtensionRequest( + validExtensionRequest.id, + { + reason:"test-reason", + newEndsOn: newDate, + type: REQUEST_TYPE.ONBOARDING, + }, + userId, + ); + expect(response).to.be.not.undefined; + expect(response.lastModifiedBy).to.equal(userId); + expect(response.newEndsOn).to.equal(newDate); + expect(response.reason).to.equal("test-reason"); + expect(response.updatedAt).to.equal(newDate); + }); + + it("should throw error", async () => { + sinon.stub(logService, "addLog").throws(new Error(errorMessage)); + try{ + await updateOnboardingExtensionRequest( + validExtensionRequest.id, + { + reason:"test-reason", + newEndsOn: Date.now(), + type: REQUEST_TYPE.ONBOARDING, + }, + userId, + ); + }catch(error){ + expect(error.message).to.equal(errorMessage); + } + }); + }) +}) \ No newline at end of file