Skip to content

Commit 5170a23

Browse files
pankajjsyesyash
authored andcommitted
feat: Add feature to handle onboarding extension request approval or rejection state (#2324)
- Add constant and types for update request state - Add controller to update onboarding status - Add dev query in request and implemented feature flag - Change request body when reason is undefined - Add onboarding type in schema for request body validation - Fix actual message in assert statement - Add tests to validate the controller - Remove destructure body - Fix string type spelling - Add jsDoc for controller to maintain good practise - Add tests to handle more edge case and fix test name - Fix lint issue - Refactor type name for better readability and fix field name - Add message field in request schema, refactor controller and test to handle message field - Refactor function name to define its purpose - Import addLog from service file - Fix jsDoc documentation - Fix lint issue - Send 404 response when request does not exist - Remove createdAt field because log stores timestamp by default
1 parent eed35b6 commit 5170a23

File tree

6 files changed

+334
-14
lines changed

6 files changed

+334
-14
lines changed

controllers/onboardingExtension.ts

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
import {
22
ERROR_WHILE_CREATING_REQUEST,
3+
ERROR_WHILE_UPDATING_REQUEST,
34
LOG_ACTION,
45
ONBOARDING_REQUEST_CREATED_SUCCESSFULLY,
56
REQUEST_ALREADY_PENDING,
7+
REQUEST_APPROVED_SUCCESSFULLY,
8+
REQUEST_DOES_NOT_EXIST,
69
REQUEST_LOG_TYPE,
10+
REQUEST_REJECTED_SUCCESSFULLY,
711
REQUEST_STATE,
812
REQUEST_TYPE,
913
UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST,
1014
} from "../constants/requests";
1115
import { userState } from "../constants/userStatus";
1216
import { addLog } from "../services/logService";
13-
import { createRequest, getRequestByKeyValues } from "../models/requests";
17+
import { createRequest, getRequestByKeyValues, updateRequest } from "../models/requests";
1418
import { fetchUser } from "../models/users";
1519
import { getUserStatus } from "../models/userStatus";
1620
import { User } from "../typeDefinitions/users";
1721
import {
1822
CreateOnboardingExtensionBody,
1923
OnboardingExtension,
2024
OnboardingExtensionCreateRequest,
21-
OnboardingExtensionResponse
25+
OnboardingExtensionResponse,
26+
UpdateOnboardingExtensionStateRequest,
27+
UpdateOnboardingExtensionStateRequestBody
2228
} from "../types/onboardingExtension";
2329
import { convertDateStringToMilliseconds, getNewDeadline } from "../utils/requests";
2430
import { convertDaysToMilliseconds } from "../utils/time";
@@ -34,7 +40,11 @@ import { convertDaysToMilliseconds } from "../utils/time";
3440
* @param {OnboardingExtensionResponse} res - The Express response object used to send back the response.
3541
* @returns {Promise<OnboardingExtensionResponse>} Resolves to a response with the status and data or an error message.
3642
*/
37-
export const createOnboardingExtensionRequestController = async (req: OnboardingExtensionCreateRequest, res: OnboardingExtensionResponse): Promise<OnboardingExtensionResponse> => {
43+
export const createOnboardingExtensionRequestController = async (
44+
req: OnboardingExtensionCreateRequest,
45+
res: OnboardingExtensionResponse )
46+
: Promise<OnboardingExtensionResponse> => {
47+
3848
try {
3949

4050
const data = req.body as CreateOnboardingExtensionBody;
@@ -121,4 +131,72 @@ export const createOnboardingExtensionRequestController = async (req: Onboarding
121131
logger.error(ERROR_WHILE_CREATING_REQUEST, err);
122132
return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST);
123133
}
124-
};
134+
};
135+
136+
/**
137+
* Updates the state of an onboarding extension request.
138+
*
139+
* @param {UpdateOnboardingExtensionStateRequest} req - The request object containing the update details.
140+
* @param {OnboardingExtensionResponse} res - The response object to send the result of the update.
141+
* @returns {Promise<OnboardingExtensionResponse>} Sends the response with the result of the update operation.
142+
*/
143+
export const updateOnboardingExtensionRequestState = async (
144+
req: UpdateOnboardingExtensionStateRequest,
145+
res: OnboardingExtensionResponse )
146+
: Promise<OnboardingExtensionResponse> => {
147+
148+
const dev = req.query.dev === "true";
149+
150+
if(!dev) return res.boom.notImplemented("Feature not implemented");
151+
152+
const body = req.body as UpdateOnboardingExtensionStateRequestBody;
153+
const lastModifiedBy = req?.userData?.id;
154+
const extensionId = req.params.id;
155+
156+
let requestBody: UpdateOnboardingExtensionStateRequestBody = {
157+
state: body.state,
158+
type: body.type,
159+
}
160+
161+
if(body.message){
162+
requestBody = { ...requestBody, message: body.message };
163+
}
164+
165+
try {
166+
const response = await updateRequest(extensionId, requestBody, lastModifiedBy, REQUEST_TYPE.ONBOARDING);
167+
168+
if ("error" in response) {
169+
if (response.error === REQUEST_DOES_NOT_EXIST) {
170+
return res.boom.notFound(response.error);
171+
}
172+
return res.boom.badRequest(response.error);
173+
}
174+
175+
const [logType, returnMessage] = response.state === REQUEST_STATE.APPROVED
176+
? [REQUEST_LOG_TYPE.REQUEST_APPROVED, REQUEST_APPROVED_SUCCESSFULLY]
177+
: [REQUEST_LOG_TYPE.REQUEST_REJECTED, REQUEST_REJECTED_SUCCESSFULLY];
178+
179+
const requestLog = {
180+
type: logType,
181+
meta: {
182+
requestId: extensionId,
183+
action: LOG_ACTION.UPDATE,
184+
createdBy: lastModifiedBy,
185+
},
186+
body: response,
187+
};
188+
189+
await addLog(requestLog.type, requestLog.meta, requestLog.body);
190+
191+
return res.status(200).json({
192+
message: returnMessage,
193+
data: {
194+
id: response.id,
195+
...response,
196+
},
197+
});
198+
}catch(error){
199+
logger.error(ERROR_WHILE_UPDATING_REQUEST, error);
200+
return res.boom.badImplementation(ERROR_WHILE_UPDATING_REQUEST);
201+
}
202+
}

controllers/requests.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { createTaskExtensionRequest, updateTaskExtensionRequest } from "./extens
1313
import { UpdateRequest } from "../types/requests";
1414
import { TaskRequestRequest } from "../types/taskRequests";
1515
import { createTaskRequestController } from "./taskRequestsv2";
16-
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../types/onboardingExtension";
17-
import { createOnboardingExtensionRequestController } from "./onboardingExtension";
16+
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse, UpdateOnboardingExtensionStateRequest } from "../types/onboardingExtension";
17+
import { createOnboardingExtensionRequestController, updateOnboardingExtensionRequestState } from "./onboardingExtension";
1818

1919
export const createRequestController = async (
2020
req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest,
@@ -42,6 +42,8 @@ export const updateRequestController = async (req: UpdateRequest, res: CustomRes
4242
return await updateOooRequestController(req as UpdateRequest, res as ExtensionRequestResponse);
4343
case REQUEST_TYPE.EXTENSION:
4444
return await updateTaskExtensionRequest(req as UpdateRequest, res as ExtensionRequestResponse);
45+
case REQUEST_TYPE.ONBOARDING:
46+
return await updateOnboardingExtensionRequestState(req as unknown as UpdateOnboardingExtensionStateRequest, res as OnboardingExtensionResponse);
4547
default:
4648
return res.boom.badRequest("Invalid request type");
4749
}

middlewares/validators/requests.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export const updateRequestsMiddleware = async (
6565
.messages({
6666
"any.only": "state must be APPROVED or REJECTED",
6767
}),
68-
type: joi.string().valid(REQUEST_TYPE.OOO, REQUEST_TYPE.EXTENSION).required(),
68+
type: joi.string().valid(REQUEST_TYPE.OOO, REQUEST_TYPE.EXTENSION, REQUEST_TYPE.ONBOARDING).required(),
69+
message: joi.string().optional()
6970
});
7071

7172
try {

test/integration/onboardingExtension.test.ts

Lines changed: 221 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import userDataFixture from "../fixtures/user/user";
55
import sinon from "sinon";
66
import chaiHttp from "chai-http";
77
import cleanDb from "../utils/cleanDb";
8-
import { CreateOnboardingExtensionBody } from "../../types/onboardingExtension";
8+
import { CreateOnboardingExtensionBody, OnboardingExtension } from "../../types/onboardingExtension";
99
import {
1010
REQUEST_ALREADY_PENDING,
1111
REQUEST_STATE, REQUEST_TYPE,
@@ -20,6 +20,7 @@ const firestore = require("../../utils/firestore");
2020
const userStatusModel = firestore.collection("usersStatus");
2121
import * as requestsQuery from "../../models/requests"
2222
import { userState } from "../../constants/userStatus";
23+
import { generateAuthToken } from "../../services/authService";
2324
const { CLOUDFLARE_WORKER, BAD_TOKEN } = require("../../constants/bot");
2425
const userData = userDataFixture();
2526
chai.use(chaiHttp);
@@ -378,4 +379,223 @@ describe("/requests Onboarding Extension", () => {
378379
});
379380
});
380381
});
382+
383+
describe("PUT /requests", () => {
384+
const body = {
385+
type: REQUEST_TYPE.ONBOARDING,
386+
state: REQUEST_STATE.APPROVED,
387+
message: "test-message"
388+
};
389+
let latestExtension: OnboardingExtension;
390+
let userId: string;
391+
let putEndpoint: string;
392+
let authToken: string;
393+
let latestApprovedExtension: OnboardingExtension;
394+
let latestRejectedExtension: OnboardingExtension;
395+
396+
beforeEach(async () => {
397+
userId = await addUser(userData[4]);
398+
latestExtension = await requestsQuery.createRequest({
399+
state: REQUEST_STATE.PENDING,
400+
type: REQUEST_TYPE.ONBOARDING,
401+
requestNumber: 1
402+
});
403+
latestApprovedExtension = await requestsQuery.createRequest({
404+
state: REQUEST_STATE.APPROVED,
405+
type: REQUEST_TYPE.ONBOARDING, requestNumber: 2
406+
});
407+
latestRejectedExtension = await requestsQuery.createRequest({
408+
state: REQUEST_STATE.REJECTED,
409+
type: REQUEST_TYPE.ONBOARDING,
410+
requestNumber: 2
411+
});
412+
putEndpoint = `/requests/${latestExtension.id}?dev=true`;
413+
authToken = generateAuthToken({userId});
414+
})
415+
416+
afterEach(async () => {
417+
sinon.restore();
418+
await cleanDb();
419+
})
420+
421+
it("should return 401 response when user is not a super user", (done) => {
422+
chai.request(app)
423+
.put(putEndpoint)
424+
.set("authorization", `Bearer ${generateAuthToken({userId: "111"})}`)
425+
.send(body)
426+
.end((err, res) => {
427+
if(err) return done(err);
428+
expect(res.statusCode).to.equal(401);
429+
expect(res.body.error).to.equal("Unauthorized");
430+
expect(res.body.message).to.equal("You are not authorized for this action.");
431+
done();
432+
})
433+
})
434+
435+
it("should return Invalid request type for incorrect value of type", (done) => {
436+
chai.request(app)
437+
.put("/requests/1111?dev=true")
438+
.set("authorization", `Bearer ${authToken}`)
439+
.send({...body, type: "<InvalidType>"})
440+
.end((err, res)=>{
441+
if(err) return done(err);
442+
expect(res.statusCode).to.equal(400);
443+
expect(res.body.error).to.equal("Bad Request");
444+
expect(res.body.message).to.equal('"type" must be one of [OOO, EXTENSION, ONBOARDING]');
445+
done();
446+
})
447+
})
448+
449+
it("should return Feature not implemented when dev is not true", (done) => {
450+
chai.request(app)
451+
.put(`/requests/1111?dev=false`)
452+
.send(body)
453+
.set("authorization", `Bearer ${authToken}`)
454+
.end((err, res)=>{
455+
if (err) return done(err);
456+
expect(res.statusCode).to.equal(501);
457+
expect(res.body.message).to.equal("Feature not implemented");
458+
done();
459+
})
460+
})
461+
462+
it("should return Unauthenticated User when authorization header is missing", (done) => {
463+
chai.request(app)
464+
.put(putEndpoint)
465+
.set("authorization", "")
466+
.send(body)
467+
.end((err, res) => {
468+
if (err) return done(err);
469+
expect(res.statusCode).to.equal(401);
470+
expect(res.body.message).to.equal("Unauthenticated User");
471+
done();
472+
})
473+
})
474+
475+
it("should return Unauthenticated User for invalid token", (done) => {
476+
chai.request(app)
477+
.put(putEndpoint)
478+
.set("authorization", `Bearer ${BAD_TOKEN}`)
479+
.send(body)
480+
.end((err, res) => {
481+
if (err) return done(err);
482+
expect(res.statusCode).to.equal(401);
483+
expect(res.body.message).to.equal("Unauthenticated User");
484+
done();
485+
})
486+
})
487+
488+
it("should return 400 response for invalid value of state", (done) => {
489+
chai.request(app)
490+
.put(putEndpoint)
491+
.set("authorization", `Bearer ${authToken}`)
492+
.send({...body, state: REQUEST_STATE.PENDING})
493+
.end((err, res) => {
494+
if (err) return done(err);
495+
expect(res.statusCode).to.equal(400);
496+
expect(res.body.message).to.equal("state must be APPROVED or REJECTED");
497+
expect(res.body.error).to.equal("Bad Request");
498+
done();
499+
})
500+
})
501+
502+
it("should return 404 response for invalid extension id", (done) => {
503+
chai.request(app)
504+
.put(`/requests/1111?dev=true`)
505+
.set("authorization", `Bearer ${authToken}`)
506+
.send(body)
507+
.end((err, res) => {
508+
if (err) return done(err);
509+
expect(res.statusCode).to.equal(404);
510+
expect(res.body.message).to.equal("Request does not exist");
511+
expect(res.body.error).to.equal("Not Found");
512+
done();
513+
})
514+
})
515+
516+
it("should return 400 response when type is not onboarding and extensionId is correct", (done) => {
517+
chai.request(app)
518+
.put(putEndpoint)
519+
.set("authorization", `Bearer ${authToken}`)
520+
.send({...body, type: REQUEST_TYPE.OOO})
521+
.end((err, res) => {
522+
if (err) return done(err);
523+
expect(res.statusCode).to.equal(400);
524+
expect(res.body.message).to.equal("Request does not exist");
525+
expect(res.body.error).to.equal("Bad Request");
526+
done();
527+
})
528+
})
529+
530+
it("should return 400 response when extension state is approved", (done) => {
531+
chai.request(app)
532+
.put(`/requests/${latestApprovedExtension.id}?dev=true`)
533+
.set("authorization", `Bearer ${authToken}`)
534+
.send(body)
535+
.end((err, res) => {
536+
if (err) return done(err);
537+
expect(res.statusCode).to.equal(400);
538+
expect(res.body.message).to.equal("Request already approved");
539+
expect(res.body.error).to.equal("Bad Request");
540+
done();
541+
})
542+
})
543+
544+
it("should return 400 response when extension state is rejected", (done) => {
545+
chai.request(app)
546+
.put(`/requests/${latestRejectedExtension.id}?dev=true`)
547+
.set("authorization", `Bearer ${authToken}`)
548+
.send(body)
549+
.end((err, res) => {
550+
if (err) return done(err);
551+
expect(res.statusCode).to.equal(400);
552+
expect(res.body.message).to.equal("Request already rejected");
553+
expect(res.body.error).to.equal("Bad Request");
554+
done();
555+
})
556+
})
557+
558+
it("should return 200 for success response when request is approved", (done) => {
559+
chai.request(app)
560+
.put(putEndpoint)
561+
.set("authorization", `Bearer ${authToken}`)
562+
.send(body)
563+
.end((err, res) => {
564+
if (err) return done(err);
565+
expect(res.statusCode).to.equal(200);
566+
expect(res.body.message).to.equal("Request approved successfully");
567+
done();
568+
})
569+
})
570+
571+
it("should return 200 for success response when request is rejected", (done) => {
572+
chai.request(app)
573+
.put(putEndpoint)
574+
.set("authorization", `Bearer ${authToken}`)
575+
.send({...body, state: REQUEST_STATE.REJECTED})
576+
.end((err, res) => {
577+
if (err) return done(err);
578+
expect(res.statusCode).to.equal(200);
579+
expect(res.body.message).to.equal("Request rejected successfully");
580+
done();
581+
})
582+
})
583+
584+
it("should return 500 response when fails to update extension request", (done) => {
585+
sinon.stub(requestsQuery, "updateRequest")
586+
.throws("Error while creating extension request");
587+
chai.request(app)
588+
.put(putEndpoint)
589+
.set("authorization", `Bearer ${authToken}`)
590+
.send(body)
591+
.end((err, res)=>{
592+
if (err) return done(err);
593+
expect(res.statusCode).to.equal(500);
594+
expect(res.body.message).to.equal("An internal server error occurred");
595+
expect(res.body.error).to.equal("Internal Server Error")
596+
done();
597+
})
598+
})
599+
});
381600
});
601+

0 commit comments

Comments
 (0)