Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add api to approve and reject onboarding extension requests #2324

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 79 additions & 4 deletions controllers/onboardingExtension.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import {
ERROR_WHILE_CREATING_REQUEST,
ERROR_WHILE_UPDATING_REQUEST,
LOG_ACTION,
ONBOARDING_REQUEST_CREATED_SUCCESSFULLY,
REQUEST_ALREADY_PENDING,
REQUEST_APPROVED_SUCCESSFULLY,
REQUEST_LOG_TYPE,
REQUEST_REJECTED_SUCCESSFULLY,
REQUEST_STATE,
REQUEST_TYPE,
UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST,
} from "../constants/requests";
import { userState } from "../constants/userStatus";
import { addLog } from "../services/logService";
import { createRequest, getRequestByKeyValues } from "../models/requests";
import { createRequest, getRequestByKeyValues, updateRequest } from "../models/requests";
import { fetchUser } from "../models/users";
import { getUserStatus } from "../models/userStatus";
import { User } from "../typeDefinitions/users";
import {
CreateOnboardingExtensionBody,
OnboardingExtension,
OnboardingExtensionCreateRequest,
OnboardingExtensionResponse
OnboardingExtensionResponse,
UpdateOnboardingExtensionStateRequest,
UpdateOnboardingExtensionStateRequestBody
} from "../types/onboardingExtension";
import { convertDateStringToMilliseconds, getNewDeadline } from "../utils/requests";
import { convertDaysToMilliseconds } from "../utils/time";
Expand All @@ -34,7 +39,11 @@ import { convertDaysToMilliseconds } from "../utils/time";
* @param {OnboardingExtensionResponse} res - The Express response object used to send back the response.
* @returns {Promise<OnboardingExtensionResponse>} Resolves to a response with the status and data or an error message.
*/
export const createOnboardingExtensionRequestController = async (req: OnboardingExtensionCreateRequest, res: OnboardingExtensionResponse): Promise<OnboardingExtensionResponse> => {
export const createOnboardingExtensionRequestController = async (
req: OnboardingExtensionCreateRequest,
res: OnboardingExtensionResponse )
: Promise<OnboardingExtensionResponse> => {

try {

const data = req.body as CreateOnboardingExtensionBody;
Expand Down Expand Up @@ -121,4 +130,70 @@ export const createOnboardingExtensionRequestController = async (req: Onboarding
logger.error(ERROR_WHILE_CREATING_REQUEST, err);
return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST);
}
};
};

/**
* Updates the state of an onboarding extension request.
*
* @param {UpdateOnboardingExtensionStateRequest} req - The request object containing the update details.
* @param {OnboardingExtensionResponse} res - The response object to send the result of the update.
* @returns {Promise<OnboardingExtensionResponse>} Sends the response with the result of the update operation.
*/
export const updateOnboardingExtensionRequestState = async (
req: UpdateOnboardingExtensionStateRequest,
res: OnboardingExtensionResponse )
: Promise<OnboardingExtensionResponse> => {

const dev = req.query.dev === "true";

if(!dev) return res.boom.notImplemented("Feature not implemented");

const body = req.body as UpdateOnboardingExtensionStateRequestBody;
const lastModifiedBy = req?.userData?.id;
const extensionId = req.params.id;

let requestBody: UpdateOnboardingExtensionStateRequestBody = {
state: body.state,
type: body.type,
}

if(body.message){
requestBody = { ...requestBody, message: body.message };
}

try {
const response = await updateRequest(extensionId, requestBody, lastModifiedBy, REQUEST_TYPE.ONBOARDING);

if ("error" in response) {
return res.boom.badRequest(response.error);
}

const [logType, returnMessage] = response.state === REQUEST_STATE.APPROVED
? [REQUEST_LOG_TYPE.REQUEST_APPROVED, REQUEST_APPROVED_SUCCESSFULLY]
: [REQUEST_LOG_TYPE.REQUEST_REJECTED, REQUEST_REJECTED_SUCCESSFULLY];

const requestLog = {
type: logType,
meta: {
requestId: extensionId,
action: LOG_ACTION.UPDATE,
createdBy: lastModifiedBy,
createdAt: Date.now(),
},
body: response,
};

await addLog(requestLog.type, requestLog.meta, requestLog.body);

return res.status(200).json({
message: returnMessage,
data: {
id: response.id,
...response,
},
});
}catch(error){
logger.error(ERROR_WHILE_UPDATING_REQUEST, error);
return res.boom.badImplementation(ERROR_WHILE_UPDATING_REQUEST);
}
}
6 changes: 4 additions & 2 deletions controllers/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ 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";
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse, UpdateOnboardingExtensionStateRequest } from "../types/onboardingExtension";
import { createOnboardingExtensionRequestController, updateOnboardingExtensionRequestState } from "./onboardingExtension";

export const createRequestController = async (
req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest,
Expand Down Expand Up @@ -42,6 +42,8 @@ export const updateRequestController = async (req: UpdateRequest, res: CustomRes
return await updateOooRequestController(req as UpdateRequest, res as ExtensionRequestResponse);
case REQUEST_TYPE.EXTENSION:
return await updateTaskExtensionRequest(req as UpdateRequest, res as ExtensionRequestResponse);
case REQUEST_TYPE.ONBOARDING:
return await updateOnboardingExtensionRequestState(req as unknown as UpdateOnboardingExtensionStateRequest, res as OnboardingExtensionResponse);
default:
return res.boom.badRequest("Invalid request type");
}
Expand Down
3 changes: 2 additions & 1 deletion middlewares/validators/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export const updateRequestsMiddleware = async (
.messages({
"any.only": "state must be APPROVED or REJECTED",
}),
type: joi.string().valid(REQUEST_TYPE.OOO, REQUEST_TYPE.EXTENSION).required(),
type: joi.string().valid(REQUEST_TYPE.OOO, REQUEST_TYPE.EXTENSION, REQUEST_TYPE.ONBOARDING).required(),
message: joi.string().optional()
});

try {
Expand Down
224 changes: 222 additions & 2 deletions test/integration/onboardingExtension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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 { CreateOnboardingExtensionBody, OnboardingExtension } from "../../types/onboardingExtension";
import {
REQUEST_ALREADY_PENDING,
REQUEST_STATE, REQUEST_TYPE,
Expand All @@ -19,6 +19,7 @@ const firestore = require("../../utils/firestore");
const userStatusModel = firestore.collection("usersStatus");
import * as requestsQuery from "../../models/requests"
import { userState } from "../../constants/userStatus";
import { generateAuthToken } from "../../services/authService";
const { CLOUDFLARE_WORKER, BAD_TOKEN } = require("../../constants/bot");
const userData = userDataFixture();
chai.use(chaiHttp);
Expand Down Expand Up @@ -299,4 +300,223 @@ describe("/requests Onboarding Extension", () => {
.to.equal(new Date(currentDate + (body.numberOfDays*24*60*60*1000)).toDateString());
})
})
});

describe("PUT /requests", () => {
const body = {
type: REQUEST_TYPE.ONBOARDING,
state: REQUEST_STATE.APPROVED,
message: "test-message"
};
let latestExtension: OnboardingExtension;
let userId: string;
let putEndpoint: string;
let authToken: string;
let latestApprovedExtension: OnboardingExtension;
let latestRejectedExtension: OnboardingExtension;

beforeEach(async () => {
userId = await addUser(userData[4]);
latestExtension = await requestsQuery.createRequest({
state: REQUEST_STATE.PENDING,
type: REQUEST_TYPE.ONBOARDING,
requestNumber: 1
});
latestApprovedExtension = await requestsQuery.createRequest({
state: REQUEST_STATE.APPROVED,
type: REQUEST_TYPE.ONBOARDING, requestNumber: 2
});
latestRejectedExtension = await requestsQuery.createRequest({
state: REQUEST_STATE.REJECTED,
type: REQUEST_TYPE.ONBOARDING,
requestNumber: 2
});
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)
.set("authorization", `Bearer ${generateAuthToken({userId: "111"})}`)
.send(body)
.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("You are not authorized for this action.");
done();
})
})

it("should return Invalid request type for incorrect value of type", (done) => {
chai.request(app)
.put("/requests/1111?dev=true")
.set("authorization", `Bearer ${authToken}`)
.send({...body, type: "<InvalidType>"})
.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('"type" must be one of [OOO, EXTENSION, ONBOARDING]');
done();
})
})

it("should return Feature not implemented when dev is not true", (done) => {
chai.request(app)
.put(`/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)
.put(putEndpoint)
.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)
.put(putEndpoint)
.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 state", (done) => {
chai.request(app)
.put(putEndpoint)
.set("authorization", `Bearer ${authToken}`)
.send({...body, state: REQUEST_STATE.PENDING})
.end((err, res) => {
if (err) return done(err);
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.equal("state must be APPROVED or REJECTED");
expect(res.body.error).to.equal("Bad Request");
done();
})
})

it("should return 400 response for invalid extension id", (done) => {
chai.request(app)
.put(`/requests/1111?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("Request does not exist");
expect(res.body.error).to.equal("Bad Request");
done();
})
})

it("should return 400 response when type is not onboarding and extensionId is correct", (done) => {
chai.request(app)
.put(putEndpoint)
.set("authorization", `Bearer ${authToken}`)
.send({...body, type: REQUEST_TYPE.OOO})
.end((err, res) => {
if (err) return done(err);
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.equal("Request does not exist");
expect(res.body.error).to.equal("Bad Request");
done();
})
})

it("should return 400 response when extension state is approved", (done) => {
chai.request(app)
.put(`/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("Request already approved");
expect(res.body.error).to.equal("Bad Request");
done();
})
})

it("should return 400 response when extension state is rejected", (done) => {
chai.request(app)
.put(`/requests/${latestRejectedExtension.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("Request already rejected");
expect(res.body.error).to.equal("Bad Request");
done();
})
})

it("should return 200 for success response when request is approved", (done) => {
chai.request(app)
.put(putEndpoint)
.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 approved successfully");
done();
})
})

it("should return 200 for success response when request is rejected", (done) => {
chai.request(app)
.put(putEndpoint)
.set("authorization", `Bearer ${authToken}`)
.send({...body, state: REQUEST_STATE.REJECTED})
.end((err, res) => {
if (err) return done(err);
expect(res.statusCode).to.equal(200);
expect(res.body.message).to.equal("Request rejected successfully");
done();
})
})

it("should return 500 response when fails to update extension request", (done) => {
sinon.stub(requestsQuery, "updateRequest")
.throws("Error while creating extension request");
chai.request(app)
.put(putEndpoint)
.set("authorization", `Bearer ${authToken}`)
.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");
expect(res.body.error).to.equal("Internal Server Error")
done();
})
})
})
});

Loading
Loading