From 439658f389df2d785066ad0d634029876fc10cd6 Mon Sep 17 00:00:00 2001 From: Vikas Singh <59792866+vikasosmium@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:54:44 +0530 Subject: [PATCH 1/3] Updated /users/:userId PATCH Endpoint (#2316) * updated userId route for self patch request based on userid * removed console line * added feature flag * return res fix --- controllers/users.js | 28 ++ middlewares/conditionalMiddleware.ts | 10 + routes/users.js | 5 +- test/integration/users.test.js | 382 +++++++++++++++++- .../middlewares/conditionalMiddleware.test.ts | 64 +++ 5 files changed, 474 insertions(+), 15 deletions(-) create mode 100644 middlewares/conditionalMiddleware.ts create mode 100644 test/unit/middlewares/conditionalMiddleware.test.ts diff --git a/controllers/users.js b/controllers/users.js index 88abb1eda..6cbebe7b4 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -36,6 +36,7 @@ const config = require("config"); const { generateUniqueUsername } = require("../services/users"); const userService = require("../services/users"); const discordDeveloperRoleId = config.get("discordDeveloperRoleId"); +const usersCollection = firestore.collection("users"); const verifyUser = async (req, res) => { const userId = req.userData.id; @@ -714,6 +715,12 @@ const updateUser = async (req, res) => { const { id: profileDiffId, message } = req.body; const devFeatureFlag = req.query.dev === "true"; let profileDiffData; + + const userDoc = await usersCollection.doc(req.params.userId).get(); + if (!userDoc.exists) { + return res.boom.notFound("The User doesn't exist."); + } + if (devFeatureFlag) { profileDiffData = await profileDiffsQuery.fetchProfileDiffUnobfuscated(profileDiffId); } else { @@ -1096,6 +1103,26 @@ const updateUsernames = async (req, res) => { } }; +const updateProfile = async (req, res) => { + try { + const { id: currentUserId, roles = {} } = req.userData; + const isSelf = req.params.userId === currentUserId; + const isSuperUser = roles[ROLES.SUPERUSER]; + const profile = req.query.profile === "true"; + + if (isSelf && profile && req.query.dev === "true") { + return await updateSelf(req, res); + } else if (isSuperUser) { + return await updateUser(req, res); + } + + return res.boom.badRequest("Invalid Request."); + } catch (err) { + logger.error(`Error in updateUserStatusController: ${err}`); + return res.boom.badImplementation("An unexpected error occurred."); + } +}; + module.exports = { verifyUser, generateChaincode, @@ -1128,4 +1155,5 @@ module.exports = { isDeveloper, getIdentityStats, updateUsernames, + updateProfile, }; diff --git a/middlewares/conditionalMiddleware.ts b/middlewares/conditionalMiddleware.ts new file mode 100644 index 000000000..3d1249e80 --- /dev/null +++ b/middlewares/conditionalMiddleware.ts @@ -0,0 +1,10 @@ +const conditionalMiddleware = (validator) => { + return async (req, res, next) => { + if (req.params.userId === req.userData.id && req.query.profile === "true") { + return validator(req, res, next); + } + next(); + }; +}; + +module.exports = conditionalMiddleware; diff --git a/routes/users.js b/routes/users.js index e38c2ca31..921b139f6 100644 --- a/routes/users.js +++ b/routes/users.js @@ -14,12 +14,13 @@ const { Services } = require("../constants/bot"); const authenticateProfile = require("../middlewares/authenticateProfile"); const { devFlagMiddleware } = require("../middlewares/devFlag"); const { userAuthorization } = require("../middlewares/userAuthorization"); +const conditionalMiddleware = require("../middlewares/conditionalMiddleware"); router.post("/", authorizeAndAuthenticate([ROLES.SUPERUSER], [Services.CRON_JOB_HANDLER]), users.markUnverified); router.post("/update-in-discord", authenticate, authorizeRoles([SUPERUSER]), users.setInDiscordScript); router.post("/verify", authenticate, users.verifyUser); router.get("/userId/:userId", users.getUserById); -router.patch("/self", authenticate, userValidator.updateUser, users.updateSelf); +router.patch("/self", authenticate, userValidator.updateUser, users.updateSelf); // this route is being deprecated soon, please use alternate available `/users/:userId?profile=true` PATCH endpoint. router.get("/", authenticateProfile(authenticate), userValidator.getUsers, users.getUsers); router.get("/self", authenticate, users.getSelfDetails); router.get("/isDeveloper", authenticate, users.isDeveloper); @@ -75,7 +76,7 @@ router.patch( router.get("/picture/:id", authenticate, authorizeRoles([SUPERUSER]), users.getUserImageForVerification); router.patch("/profileURL", authenticate, userValidator.updateProfileURL, users.profileURL); router.patch("/rejectDiff", authenticate, authorizeRoles([SUPERUSER]), users.rejectProfileDiff); -router.patch("/:userId", authenticate, authorizeRoles([SUPERUSER]), users.updateUser); +router.patch("/:userId", authenticate, conditionalMiddleware(userValidator.updateUser), users.updateProfile); router.get("/suggestedUsers/:skillId", authenticate, authorizeRoles([SUPERUSER]), users.getSuggestedUsers); module.exports = router; router.post("/batch-username-update", authenticate, authorizeRoles([SUPERUSER]), users.updateUsernames); diff --git a/test/integration/users.test.js b/test/integration/users.test.js index f607e0d61..8ba547d3f 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1759,16 +1759,6 @@ describe("Users", function () { .set("cookie", `${cookieName}=${superUserAuthToken}`) .send({ id: `${profileDiffsId}`, - first_name: "Ankur", - last_name: "Narkhede", - yoe: 0, - company: "", - designation: "AO", - github_id: "ankur1337", - linkedin_id: "ankurnarkhede", - twitter_id: "ankur909", - instagram_id: "", - website: "", message: "", }) .end((err, res) => { @@ -1792,9 +1782,9 @@ describe("Users", function () { return done(err); } - expect(res).to.have.status(401); - expect(res.body.error).to.be.equal("Unauthorized"); - expect(res.body.message).to.be.equal("You are not authorized for this action."); + expect(res).to.have.status(400); + expect(res.body.error).to.be.equal("Bad Request"); + expect(res.body.message).to.be.equal("Invalid Request."); return done(); }); }); @@ -1819,6 +1809,372 @@ describe("Users", function () { }); }); + describe("PATCH /users/:userId?profile=true", function () { + beforeEach(function () { + fetchStub = Sinon.stub(global, "fetch"); + fetchStub.returns( + Promise.resolve({ + status: 200, + json: () => Promise.resolve(getDiscordMembers), + }) + ); + }); + + afterEach(function () { + Sinon.restore(); + }); + + it("Should update the user", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + first_name: "Test first_name", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(204); + + return done(); + }); + }); + + it("Should update the user status", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + status: "ooo", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(204); + + return done(); + }); + }); + + it("Should update the username with valid username", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + username: "validUsername123", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(204); + + return done(); + }); + }); + + it("Should allow updating user role when in_discord is not present and is not developer", function (done) { + addUser(newUser).then((newUserId) => { + const newUserJwt = authService.generateAuthToken({ userId: newUserId }); + chai + .request(app) + .patch(`/users/${newUserId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${newUserJwt}`) + .send({ + roles: { + maven: true, + }, + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(204); + return done(); + }); + }); + }); + + it("Should not remove old roles when updating user roles", async function () { + const newUserId = await addUser(newUser); + const newUserJwt = authService.generateAuthToken({ userId: newUserId }); + + const getUserResponseBeforeUpdate = await chai + .request(app) + .get(`/users?profile=true`) + .set("cookie", `${cookieName}=${newUserJwt}`); + + expect(getUserResponseBeforeUpdate).to.have.status(200); + expect(getUserResponseBeforeUpdate.body.roles).to.not.have.property("maven"); + expect(getUserResponseBeforeUpdate.body.roles.in_discord).to.equal(false); + + const updateRolesResponse = await chai + .request(app) + .patch(`/users/${newUserId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${newUserJwt}`) + .send({ + roles: { + maven: true, + }, + }); + + expect(updateRolesResponse).to.have.status(204); + + const getUserResponseAfterUpdate = await chai + .request(app) + .get(`/users?profile=true`) + .set("cookie", `${cookieName}=${newUserJwt}`); + + expect(getUserResponseAfterUpdate).to.have.status(200); + expect(getUserResponseAfterUpdate.body.roles).to.have.property("maven"); + expect(getUserResponseAfterUpdate.body.roles.maven).to.equal(true); + expect(getUserResponseAfterUpdate.body.roles.in_discord).to.equal(false); + }); + + it("Should not update the user roles when user has in_discord and developer true", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + roles: { + maven: true, + }, + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(403); + + return done(); + }); + }); + + it("Should return 400 for invalid status value", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + status: "blah", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + expect(res.body).to.be.an("object"); + expect(res.body).to.eql({ + statusCode: 400, + error: "Bad Request", + message: '"status" must be one of [ooo, idle, active, onboarding]', + }); + + return done(); + }); + }); + + it("Should return 400 if required roles is missing", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + roles: { + in_discord: false, + developer: true, + }, + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + + return done(); + }); + }); + + it("Should return 400 if invalid roles", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + roles: { + archived: "false", + in_discord: false, + developer: true, + }, + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + + return done(); + }); + }); + + it("Should return 400 for invalid username", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + username: "@invalidUser-name", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + expect(res.body).to.be.an("object"); + expect(res.body).to.eql({ + statusCode: 400, + error: "Bad Request", + message: "Username must be between 4 and 32 characters long and contain only letters or numbers.", + }); + + return done(); + }); + }); + + it("Should update the social id with valid social id", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + twitter_id: "Valid_twitterId", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(204); + return done(); + }); + }); + + it("Should return 400 for invalid Twitter ID", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + twitter_id: "invalid@twitter_id", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + expect(res.body).to.be.an("object"); + expect(res.body).to.eql({ + statusCode: 400, + error: "Bad Request", + message: "Invalid Twitter ID. ID should not contain special character @ or spaces", + }); + + return done(); + }); + }); + + it("Should return 400 for invalid Linkedin ID", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + linkedin_id: "invalid@linkedin_id", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + expect(res.body).to.be.an("object"); + expect(res.body).to.eql({ + statusCode: 400, + error: "Bad Request", + message: "Invalid Linkedin ID. ID should not contain special character @ or spaces", + }); + + return done(); + }); + }); + + it("Should return 400 for invalid instagram ID", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + instagram_id: "invalid@instagram_id", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + expect(res.body).to.be.an("object"); + expect(res.body).to.eql({ + statusCode: 400, + error: "Bad Request", + message: "Invalid Instagram ID. ID should not contain special character @ or spaces", + }); + + return done(); + }); + }); + + it("Should return 400 is space is included in the social ID", function (done) { + chai + .request(app) + .patch(`/users/${userId}?profile=true&dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ + linkedin_id: "Linkedin 123", + }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + expect(res.body).to.be.an("object"); + expect(res.body).to.eql({ + statusCode: 400, + error: "Bad Request", + message: "Invalid Linkedin ID. ID should not contain special character @ or spaces", + }); + + return done(); + }); + }); + }); + describe("GET /users/chaincode", function () { it("Should save the userId and timestamp in firestore collection and return the document ID as chaincode in response", function (done) { chai diff --git a/test/unit/middlewares/conditionalMiddleware.test.ts b/test/unit/middlewares/conditionalMiddleware.test.ts new file mode 100644 index 000000000..442cfe1c5 --- /dev/null +++ b/test/unit/middlewares/conditionalMiddleware.test.ts @@ -0,0 +1,64 @@ +import chai from "chai"; +import sinon from "sinon"; +const { expect } = chai; +const conditionalMiddleware = require("../../../middlewares/conditionalMiddleware"); +const authService = require("../../../services/authService"); +const addUser = require("../../utils/addUser"); + +describe("conditional Middleware", function () { + let req, res, next, validatorStub, middleware; + + beforeEach(async function () { + const userId = await addUser(); + validatorStub = sinon.spy(); + middleware = conditionalMiddleware(validatorStub); + + req = { + params: { userId }, + query: {}, + userData: { id: userId }, + }; + res = { + boom: { + unauthorized: sinon.spy(), + forbidden: sinon.spy(), + badRequest: sinon.spy(), + }, + }; + next = sinon.spy(); + }); + + it("should call the validator when profile query is true", async function () { + req.query.profile = "true"; + await middleware(req, res, next); + + expect(validatorStub.calledOnceWith(req, res, next)).to.equal(true); + expect(next.calledOnce).to.equal(false); + }); + + it("should call next when profile query is not true", async function () { + req.query.profile = "false"; + + await middleware(req, res, next); + + expect(validatorStub.called).to.equal(false); + expect(next.calledOnce).to.equal(true); + }); + + it("should call next when profile query is missing", async function () { + await middleware(req, res, next); + + expect(validatorStub.called).to.equal(false); + expect(next.calledOnce).to.equal(true); + }); + + it("should call next when userData.id does not match params.userId", async function () { + req.params.userId = "anotherUserId"; + req.query.profile = "true"; + + await middleware(req, res, next); + + expect(validatorStub.called).to.equal(false); + expect(next.calledOnce).to.equal(true); + }); +}); From ff06c9721e7398378164c424660b27218d725684 Mon Sep 17 00:00:00 2001 From: Vikas Singh <59792866+vikasosmium@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:55:20 +0530 Subject: [PATCH 2/3] Added /tasks/assign/:userId PATCH Endpoint (#2317) * added one patch route for self task assignment based on skillset * test case assertion fix * test case assertion fix --- routes/tasks.js | 13 +++- test/fixtures/tasks/tasks.js | 18 +++++ test/fixtures/user/user.js | 27 ++++++++ test/integration/tasks.test.js | 111 ++++++++++++++++++++++++++++++- test/unit/models/tasks.test.js | 4 +- test/unit/services/tasks.test.js | 4 +- test/unit/services/users.test.js | 4 +- 7 files changed, 173 insertions(+), 8 deletions(-) diff --git a/routes/tasks.js b/routes/tasks.js index 99c2fca6e..1d4c6858f 100644 --- a/routes/tasks.js +++ b/routes/tasks.js @@ -17,6 +17,8 @@ const { cacheResponse, invalidateCache } = require("../utils/cache"); const { ALL_TASKS } = require("../constants/cacheKeys"); const { verifyCronJob } = require("../middlewares/authorizeBot"); const { CLOUDFLARE_WORKER, CRON_JOB_HANDLER } = require("../constants/bot"); +const { devFlagMiddleware } = require("../middlewares/devFlag"); +const { userAuthorization } = require("../middlewares/userAuthorization"); const oldAuthorizationMiddleware = authorizeRoles([APPOWNER, SUPERUSER]); const newAuthorizationMiddleware = authorizeAndAuthenticate( @@ -64,7 +66,16 @@ router.patch( tasks.updateTaskStatus, assignTask ); -router.patch("/assign/self", authenticate, invalidateCache({ invalidationKeys: [ALL_TASKS] }), tasks.assignTask); +router.patch("/assign/self", authenticate, invalidateCache({ invalidationKeys: [ALL_TASKS] }), tasks.assignTask); // this route is being deprecated in favor of /assign/:userId. + +router.patch( + "/assign/:userId", + authenticate, + devFlagMiddleware, + userAuthorization, + invalidateCache({ invalidationKeys: [ALL_TASKS] }), + tasks.assignTask +); router.get("/users/discord", verifyCronJob, getUsersValidator, tasks.getUsersHandler); diff --git a/test/fixtures/tasks/tasks.js b/test/fixtures/tasks/tasks.js index 1d5efa978..ed8532c81 100644 --- a/test/fixtures/tasks/tasks.js +++ b/test/fixtures/tasks/tasks.js @@ -140,5 +140,23 @@ module.exports = () => { createdAt: 1644753600, updatedAt: 1644753600, }, + { + title: "Test task", + type: "feature", + endsOn: 1234, + startedOn: 4567, + status: "AVAILABLE", + percentCompleted: 0, + category: "FRONTEND", + level: 3, + participants: [], + completionAward: { [DINERO]: 3, [NEELAM]: 300 }, + lossRate: { [DINERO]: 1 }, + priority: "HIGH", + isNoteworthy: true, + assignee: false, + createdAt: 1644753600, + updatedAt: 1644753600, + }, ]; }; diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index 4adfc24a2..96817bd77 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -441,5 +441,32 @@ module.exports = () => { url: "https://res.cloudinary.com/realdevsquad/image/upload/v1667685133/profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar.jpg", }, }, + { + username: "vikasinghdon", + first_name: "Vikas", + last_name: "Singh", + discordId: "yashu_yashu", + yoe: 10, + img: "./img.png", + linkedin_id: "destroyer", + github_id: "pickme", + github_display_name: "Vikas Singh", + phone: "1234567890", + email: "awesome@vikas.com", + joined_discord: "2024-07-16T18:21:09.278000+00:00", + status: "idle", + tokens: { + githubAccessToken: "githubAccessToken", + }, + roles: { + super_user: false, + archived: false, + in_discord: true, + }, + picture: { + publicId: "", + url: "", + }, + }, ]; }; diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 550618dda..c8fe646ea 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -28,9 +28,15 @@ const { logType } = require("../../constants/logs"); const { INTERNAL_SERVER_ERROR } = require("../../constants/errorMessages"); const tasksService = require("../../services/tasks"); chai.use(chaiHttp); +const tags = require("../../models/tags"); +const levels = require("../../models/levels"); +const items = require("../../models/items"); +const taskController = require("../../controllers/tasks"); const appOwner = userData[3]; const superUser = userData[4]; +const genZUser = userData[20]; +const testUser = userData[2]; let jwt, superUserJwt; const { createProgressDocument } = require("../../models/progresses"); @@ -77,14 +83,40 @@ const taskData = [ }, ]; +const tagData = { + reason: "adding skills to users", + name: "EMBER", + type: "SKILL", + createdBy: "", + date: new Date().getTime(), +}; + +const itemData = { + itemId: "", + itemType: "TASK", + tagPayload: [ + { + tagId: "", + levelId: "", + }, + ], +}; + +const levelData = { + name: "1", + value: 1, +}; + describe("Tasks", function () { - let taskId1, taskId; + let taskId1, taskId, testUserId, testUserjwt; before(async function () { const userId = await addUser(appOwner); const superUserId = await addUser(superUser); + testUserId = await addUser(testUser); jwt = authService.generateAuthToken({ userId }); superUserJwt = authService.generateAuthToken({ userId: superUserId }); + testUserjwt = authService.generateAuthToken({ userId: testUserId }); // Add the active task taskId = (await tasks.updateTask(taskData[0])).taskId; @@ -1699,4 +1731,81 @@ describe("Tasks", function () { expect(res.body.message).to.be.equal(INTERNAL_SERVER_ERROR); }); }); + + describe("PATCH /tasks/assign/:userId", function () { + let taskData, genZUserJwt, genZUserId; + + beforeEach(async function () { + genZUserId = await addUser(genZUser); + genZUserJwt = authService.generateAuthToken({ userId: genZUserId }); + taskData = tasksData[8]; + }); + + afterEach(async function () { + await cleanDb(); + sinon.restore(); + }); + + it("Should not assign a task to the user if they do not have status idle", async function () { + await tasks.updateTask(taskData); + + const res = await chai + .request(app) + .patch(`/tasks/assign/${testUserId}?dev=true`) + .set("cookie", `${cookieName}=${testUserjwt}`) + .send(); + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Task cannot be assigned to users with active or OOO status"); + }); + + it("Should not assign a task to the user if task doesn't exist", async function () { + await tasks.updateTask(taskData); + + const res = await chai + .request(app) + .patch(`/tasks/assign/${genZUserId}?dev=true`) + .set("cookie", `${cookieName}=${genZUserJwt}`) + .send(); + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Task not found"); + }); + + it("Should assign task to the user if their status is idle and task is available", async function () { + const taskAdd = await tasks.updateTask(taskData); + const levelAdd = await levels.addLevel(levelData); + + tagData.createdBy = genZUserId; + const tagAdd = await tags.addTag(tagData); + + itemData.itemId = taskAdd.taskId; + itemData.tagPayload[0].tagId = tagAdd.id; + itemData.tagPayload[0].levelId = levelAdd.id; + + await items.addTagsToItem(itemData); + + const res = await chai + .request(app) + .patch(`/tasks/assign/${genZUserId}?dev=true`) + .set("cookie", `${cookieName}=${genZUserJwt}`) + .send(); + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Task assigned"); + }); + + it("Should throw an error if Firestore batch operations fail", async function () { + sinon.stub(taskController, "assignTask").rejects(new Error(INTERNAL_SERVER_ERROR)); + + const res = await chai + .request(app) + .patch(`/tasks/assign/${genZUserId}?dev=true`) + .set("cookie", `${cookieName}=${genZUserJwt}`) + .send(); + + expect(res).to.have.status(500); + expect(res.body.message).to.be.equal(INTERNAL_SERVER_ERROR); + }); + }); }); diff --git a/test/unit/models/tasks.test.js b/test/unit/models/tasks.test.js index 45dc241f6..dc66a228b 100644 --- a/test/unit/models/tasks.test.js +++ b/test/unit/models/tasks.test.js @@ -336,8 +336,8 @@ describe("tasks", function () { it("Should update task status COMPLETED to DONE", async function () { const res = await tasks.updateTaskStatus(); - expect(res.totalTasks).to.be.equal(8); - expect(res.totalUpdatedStatus).to.be.equal(8); + expect(res.totalTasks).to.be.equal(9); + expect(res.totalUpdatedStatus).to.be.equal(9); }); it("should throw an error if firebase batch operation fails", async function () { diff --git a/test/unit/services/tasks.test.js b/test/unit/services/tasks.test.js index 278180cde..39cd70651 100644 --- a/test/unit/services/tasks.test.js +++ b/test/unit/services/tasks.test.js @@ -53,7 +53,7 @@ describe("Tasks services", function () { const res = await updateTaskStatusToDone(tasks); expect(res).to.deep.equal({ - totalUpdatedStatus: 8, + totalUpdatedStatus: 9, totalOperationsFailed: 0, updatedTaskDetails: taskDetails, failedTaskDetails: [], @@ -73,7 +73,7 @@ describe("Tasks services", function () { expect(res).to.deep.equal({ totalUpdatedStatus: 0, - totalOperationsFailed: 8, + totalOperationsFailed: 9, updatedTaskDetails: [], failedTaskDetails: taskDetails, }); diff --git a/test/unit/services/users.test.js b/test/unit/services/users.test.js index f0be625ef..148cb7916 100644 --- a/test/unit/services/users.test.js +++ b/test/unit/services/users.test.js @@ -61,7 +61,7 @@ describe("Users services", function () { expect(res).to.deep.equal({ message: "Successfully completed batch updates", - totalUsersArchived: 20, + totalUsersArchived: 21, totalOperationsFailed: 0, updatedUserDetails: userDetails, failedUserDetails: [], @@ -82,7 +82,7 @@ describe("Users services", function () { expect(res).to.deep.equal({ message: "Firebase batch operation failed", totalUsersArchived: 0, - totalOperationsFailed: 20, + totalOperationsFailed: 21, updatedUserDetails: [], failedUserDetails: userDetails, }); From cb683fba0169d8d261f7da90c1e7fcf5f36788df Mon Sep 17 00:00:00 2001 From: Vikas Singh <59792866+vikasosmium@users.noreply.github.com> Date: Fri, 27 Dec 2024 23:19:34 +0530 Subject: [PATCH 3/3] Added task status PATCH endpoint (#2320) * added one new patch route for tasks status updates * added one new patch route for tasks status updates --- routes/tasks.js | 9 + test/integration/tasks.test.js | 331 +++++++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+) diff --git a/routes/tasks.js b/routes/tasks.js index 1d4c6858f..4422a2a1d 100644 --- a/routes/tasks.js +++ b/routes/tasks.js @@ -65,6 +65,15 @@ router.patch( updateSelfTask, tasks.updateTaskStatus, assignTask +); // this route is being deprecated in favor of /tasks/:id/status. +router.patch( + "/:id/status", + authenticate, + devFlagMiddleware, + invalidateCache({ invalidationKeys: [ALL_TASKS] }), + updateSelfTask, + tasks.updateTaskStatus, + assignTask ); router.patch("/assign/self", authenticate, invalidateCache({ invalidationKeys: [ALL_TASKS] }), tasks.assignTask); // this route is being deprecated in favor of /assign/:userId. diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index c8fe646ea..1d384337d 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -1326,6 +1326,337 @@ describe("Tasks", function () { }); }); + describe("PATCH /tasks/:id/status", function () { + const taskStatusData = { + status: "AVAILABLE", + percentCompleted: 50, + }; + + const taskData = { + title: "Test task", + type: "feature", + endsOn: 1234, + startedOn: 4567, + status: "VERIFIED", + percentCompleted: 10, + participants: [], + completionAward: { [DINERO]: 3, [NEELAM]: 300 }, + lossRate: { [DINERO]: 1 }, + isNoteworthy: true, + }; + + it("Should throw 400 Bad Request if the user tries to update the status of a task to AVAILABLE", function (done) { + chai + .request(app) + .patch(`/tasks/${taskId1}/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send(taskStatusData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(400); + expect(res.body).to.be.a("object"); + expect(res.body.error).to.equal("Bad Request"); + expect(res.body.message).to.equal("The value for the 'status' field is invalid."); + return done(); + }); + }); + + it("Should update the task status for given self taskid", function (done) { + taskStatusData.status = "IN_PROGRESS"; + chai + .request(app) + .patch(`/tasks/${taskId1}/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send(taskStatusData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body.taskLog).to.have.property("type"); + expect(res.body.taskLog).to.have.property("id"); + expect(res.body.taskLog.body).to.be.a("object"); + expect(res.body.taskLog.meta).to.be.a("object"); + expect(res.body.message).to.equal("Task updated successfully!"); + + expect(res.body.taskLog.body.new.status).to.equal(taskStatusData.status); + expect(res.body.taskLog.body.new.percentCompleted).to.equal(taskStatusData.percentCompleted); + return done(); + }); + }); + + it("Should update the task status for given self taskid under feature flag", function (done) { + chai + .request(app) + .patch(`/tasks/${taskId1}/status?dev=true&userStatusFlag=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ status: "DONE", percentCompleted: 100 }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body.taskLog).to.have.property("type"); + expect(res.body.taskLog).to.have.property("id"); + expect(res.body.taskLog.body).to.be.a("object"); + expect(res.body.taskLog.meta).to.be.a("object"); + expect(res.body.message).to.equal("Task updated successfully!"); + + expect(res.body.taskLog.body.new.status).to.equal("DONE"); + expect(res.body.taskLog.body.new.percentCompleted).to.equal(100); + return done(); + }); + }); + + it("Should return fail response if task data has non-acceptable status value to update the task status for given self taskid", function (done) { + chai + .request(app) + .patch(`/tasks/${taskId1}/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ ...taskStatusData, status: "invalidStatus" }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(400); + expect(res.body).to.be.a("object"); + expect(res.body.error).to.equal("Bad Request"); + return done(); + }); + }); + + it("Should return fail response if percentage is < 0 or > 100", function (done) { + chai + .request(app) + .patch(`/tasks/${taskId1}/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ ...taskStatusData, percentCompleted: -10 }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(400); + expect(res.body).to.be.a("object"); + expect(res.body.error).to.equal("Bad Request"); + return done(); + }); + }); + + it("Should return 404 if task doesnt exist", function (done) { + taskStatusData.status = "IN_PROGRESS"; + chai + .request(app) + .patch(`/tasks/wrongtaskId/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send(taskStatusData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(404); + expect(res.body.message).to.equal("Task doesn't exist"); + return done(); + }); + }); + + it("Should return Forbidden error if task is not assigned to self", async function () { + const userId = await addUser(userData[0]); + const jwt = authService.generateAuthToken({ userId }); + + const res = await chai + .request(app) + .patch(`/tasks/${taskId1}/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`); + + expect(res).to.have.status(403); + expect(res.body.message).to.equal("This task is not assigned to you"); + }); + + it("Should give error for no cookie", function (done) { + chai + .request(app) + .patch(`/tasks/${taskId1}/status?dev=true`) + .send(taskStatusData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(401); + expect(res.body.message).to.be.equal("Unauthenticated User"); + return done(); + }); + }); + + it("Should give 403 if status is already 'VERIFIED' ", async function () { + taskStatusData.status = "IN_PROGRESS"; + taskId = (await tasks.updateTask({ ...taskData, assignee: appOwner.username })).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send(taskStatusData); + + expect(res).to.have.status(403); + expect(res.body.message).to.be.equal("Status cannot be updated. Please contact admin."); + }); + + it("Should give 403 if new status is 'MERGED' ", async function () { + taskId = (await tasks.updateTask({ ...taskData, assignee: appOwner.username })).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ ...taskStatusData, status: "MERGED" }); + + expect(res.body.message).to.be.equal("Status cannot be updated. Please contact admin."); + }); + + it("Should give 403 if new status is 'BACKLOG' ", async function () { + taskId = (await tasks.updateTask({ ...taskData, assignee: appOwner.username })).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ ...taskStatusData, status: "BACKLOG" }); + + expect(res.body.message).to.be.equal("Status cannot be updated. Please contact admin."); + }); + + it("Should give 400 if percentCompleted is not 100 and new status is COMPLETED ", async function () { + taskId = (await tasks.updateTask({ ...taskData, status: "REVIEW", assignee: appOwner.username })).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ ...taskStatusData, status: "COMPLETED" }); + + expect(res).to.have.status(400); + expect(res.body.message).to.be.equal("Status cannot be updated as progress of task is not 100%."); + }); + + it("Should give 403 if current task status is DONE", async function () { + taskId = (await tasks.updateTask({ ...taskData, status: "DONE", assignee: appOwner.username })).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true&userStatusFlag=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ ...taskStatusData, status: "IN_REVIEW" }); + + expect(res.body.message).to.be.equal("Status cannot be updated. Please contact admin."); + expect(res).to.have.status(403); + }); + + it("Should give 400 if percentCompleted is not 100 and new status is VERIFIED ", async function () { + taskId = (await tasks.updateTask({ ...taskData, status: "REVIEW", assignee: appOwner.username })).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ ...taskStatusData, status: "VERIFIED" }); + + expect(res).to.have.status(400); + expect(res.body.message).to.be.equal("Status cannot be updated as progress of task is not 100%."); + }); + + it("Should give 400 if status is COMPLETED and newpercent is less than 100", async function () { + const taskData = { + title: "Test task", + type: "feature", + endsOn: 1234, + startedOn: 4567, + status: "completed", + percentCompleted: 100, + participants: [], + assignee: appOwner.username, + completionAward: { [DINERO]: 3, [NEELAM]: 300 }, + lossRate: { [DINERO]: 1 }, + isNoteworthy: true, + }; + taskId = (await tasks.updateTask(taskData)).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ percentCompleted: 80 }); + + expect(res).to.have.status(400); + expect(res.body.message).to.be.equal("Task percentCompleted can't updated as status is COMPLETED"); + }); + + it("Should give 400 if current status of task is In Progress and new status is not Blocked and both current and new percentCompleted are not 100 ", async function () { + const newDate = { ...updateTaskStatus[0], status: "IN_PROGRESS", percentCompleted: 80 }; + taskId = (await tasks.updateTask(newDate)).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true&userStatusFlag=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ status: "NEEDS_REVIEW" }); + + expect(res).to.have.status(400); + expect(res.body.message).to.be.equal( + "The status of task can not be changed from In progress until progress of task is not 100%." + ); + }); + + it("Should give 400 if new status of task is In Progress and current status of task is not Blocked and both current and new percentCompleted are not 0 ", async function () { + const newDate = { ...updateTaskStatus[0], status: "NEEDS_REVIEW", percentCompleted: 100 }; + taskId = (await tasks.updateTask(newDate)).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true&userStatusFlag=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ status: "IN_PROGRESS" }); + + expect(res).to.have.status(400); + expect(res.body.message).to.be.equal( + "The status of task can not be changed to In progress until progress of task is not 0%." + ); + }); + + it("Should give 400 if current status of task is Blocked and new status is not In Progress and both current and new percentCompleted are not 100 ", async function () { + const newDate = { ...updateTaskStatus[0], status: "BLOCKED", percentCompleted: 52 }; + taskId = (await tasks.updateTask(newDate)).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true&userStatusFlag=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ status: "NEEDS_REVIEW" }); + + expect(res).to.have.status(400); + expect(res.body.message).to.be.equal( + "The status of task can not be changed from Blocked until progress of task is not 100%." + ); + }); + + it("Should give 200 if new status of task is In Progress and current status of task is Blocked", async function () { + const newDate = { ...updateTaskStatus[0], status: "BLOCKED", percentCompleted: 56 }; + taskId = (await tasks.updateTask(newDate)).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true&userStatusFlag=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ status: "IN_PROGRESS" }); + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Task updated successfully!"); + }); + + it("Should give 200 if new status of task is Blocked and current status of task is In Progress", async function () { + const newDate = { ...updateTaskStatus[0], status: "IN_PROGRESS", percentCompleted: 59 }; + taskId = (await tasks.updateTask(newDate)).taskId; + const res = await chai + .request(app) + .patch(`/tasks/${taskId}/status?dev=true&userStatusFlag=true`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ status: "BLOCKED" }); + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Task updated successfully!"); + }); + }); + describe("GET /tasks/overdue", function () { it("Should return all the overdue Tasks", async function () { await tasks.updateTask(tasksData[0]);