From 8dffd72cc91e26ebc8ffaf9dcc8c1d38231bb17c Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sat, 13 May 2023 15:38:29 +0530 Subject: [PATCH 001/105] api created --- controllers/tasks.js | 20 ++++++++++++++++- models/tasks.js | 21 ++++++++++++++++++ routes/tasks.js | 1 + test/integration/tasks.test.js | 40 ++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index b64414ed9..72492d891 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -4,7 +4,7 @@ const { addLog } = require("../models/logs"); const { USER_STATUS } = require("../constants/users"); const { addOrUpdate, getRdsUserInfoByGitHubUsername } = require("../models/users"); const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING } = TASK_STATUS_OLD; -const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED } = TASK_STATUS; +const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED, MERGED, COMPLETED, RELEASED, VERIFIED } = TASK_STATUS; const { INTERNAL_SERVER_ERROR, SOMETHING_WENT_WRONG } = require("../constants/errorMessages"); /** * Creates new task @@ -294,6 +294,23 @@ const assignTask = async (req, res) => { } }; +const currentOverdueTasks = async (req, res) => { + try { + const overdueTasks = await tasks.getAllOverDueTasks(); + const overdueTasksFiltered = overdueTasks.filter( + (task) => + task.status !== MERGED || task.status !== COMPLETED || task.status !== RELEASED || task.status !== VERIFIED + ); + return res.json({ + message: "Overdue Tasks returned successfully!", + overdueTasks: overdueTasksFiltered, + }); + } catch (err) { + logger.error(`Error while fetching overdue tasks : ${err}`); + return res.boom.badImplementation("An internal server error occured"); + } +}; + module.exports = { addNewTask, fetchTasks, @@ -304,4 +321,5 @@ module.exports = { updateTaskStatus, overdueTasks, assignTask, + currentOverdueTasks, }; diff --git a/models/tasks.js b/models/tasks.js index e333ce93a..3db9f381e 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -341,6 +341,26 @@ const overdueTasks = async (overDueTasks) => { throw err; } }; + +/** + * Fetch all overdue tasks objects + * @param overdueTasks : tasks which are overdue + * @return {Promsie} + */ +const getAllOverDueTasks = async () => { + try { + const currentTime = Math.floor(Date.now() / 1000); + const overdueTasksSnapshot = await tasksModel.where("endsOn", "<", currentTime).get(); + const tasks = buildTasks(overdueTasksSnapshot); + const promises = tasks.map((task) => fromFirestoreData(task)); + const overDueTasks = await Promise.all(promises); + return overDueTasks; + } catch (err) { + logger.error("error getting all overdue tasks", err); + throw err; + } +}; + module.exports = { updateTask, fetchTasks, @@ -353,4 +373,5 @@ module.exports = { fetchSkillLevelTask, overdueTasks, fetchTaskByIssueId, + getAllOverDueTasks, }; diff --git a/routes/tasks.js b/routes/tasks.js index 62d8fae20..6aac0efd7 100644 --- a/routes/tasks.js +++ b/routes/tasks.js @@ -13,6 +13,7 @@ router.get("/self", authenticate, tasks.getSelfTasks); router.get("/overdue", authenticate, authorizeRoles([SUPERUSER]), tasks.overdueTasks); router.post("/", authenticate, authorizeRoles([APPOWNER, SUPERUSER]), createTask, tasks.addNewTask); router.patch("/:id", authenticate, authorizeRoles([APPOWNER, SUPERUSER]), updateTask, tasks.updateTask); +router.get("/overdue/current", authenticate, authorizeRoles([SUPERUSER]), tasks.currentOverdueTasks); router.get("/:id/details", tasks.getTask); router.get("/:username", tasks.getUserTasks); router.patch("/self/:id", authenticate, updateSelfTask, tasks.updateTaskStatus, assignTask); diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index df1b5cdb2..07b28b77a 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -80,6 +80,46 @@ describe("Tasks", function () { sinon.restore(); }); + describe("GET /tasks/overdue/current", function () { + it("Should return all the current overdue Tasks", function (done) { + chai + .request(app) + .get("/tasks/overdue/current") + .set("cookie", `${cookieName}=${superUserJwt}`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Overdue Tasks returned successfully!"); + expect(res.body.overdueTasks[0].id).to.be.oneOf([taskId, taskId1]); + expect(res.body.overdueTasks[1].id).to.be.oneOf([taskId, taskId1]); + return done(); + }); + }); + it("Should return 401 if someone other than superuser logged in", function (done) { + chai + .request(app) + .get(`/tasks/overdue/current`) + .set("cookie", `${cookieName}=${jwt}`) + .end((err, res) => { + if (err) { + return done(); + } + + expect(res).to.have.status(401); + expect(res.body).to.be.an("object"); + expect(res.body).to.eql({ + statusCode: 401, + error: "Unauthorized", + message: "You are not authorized for this action.", + }); + + return done(); + }); + }); + }); + describe("POST /tasks - creates a new task", function () { it("Should return success response after adding the task", function (done) { chai From 2c63ca55e38ce93eaab3b120d7f6f52775670eeb Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sat, 13 May 2023 15:49:45 +0530 Subject: [PATCH 002/105] done suggested changes --- controllers/tasks.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index 72492d891..590b32fca 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -297,10 +297,8 @@ const assignTask = async (req, res) => { const currentOverdueTasks = async (req, res) => { try { const overdueTasks = await tasks.getAllOverDueTasks(); - const overdueTasksFiltered = overdueTasks.filter( - (task) => - task.status !== MERGED || task.status !== COMPLETED || task.status !== RELEASED || task.status !== VERIFIED - ); + const overdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED]; + const overdueTasksFiltered = overdueTasks.filter((task) => !overdueTasksStatus.includes(task.status)); return res.json({ message: "Overdue Tasks returned successfully!", overdueTasks: overdueTasksFiltered, From aa1702eb774fa7cca5b8c4cdc8df8f3638ec7e18 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sat, 13 May 2023 23:37:20 +0530 Subject: [PATCH 003/105] added one more check --- controllers/tasks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index 590b32fca..7ddc2ce42 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -4,7 +4,7 @@ const { addLog } = require("../models/logs"); const { USER_STATUS } = require("../constants/users"); const { addOrUpdate, getRdsUserInfoByGitHubUsername } = require("../models/users"); const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING } = TASK_STATUS_OLD; -const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED, MERGED, COMPLETED, RELEASED, VERIFIED } = TASK_STATUS; +const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED, MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE } = TASK_STATUS; const { INTERNAL_SERVER_ERROR, SOMETHING_WENT_WRONG } = require("../constants/errorMessages"); /** * Creates new task @@ -297,7 +297,7 @@ const assignTask = async (req, res) => { const currentOverdueTasks = async (req, res) => { try { const overdueTasks = await tasks.getAllOverDueTasks(); - const overdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED]; + const overdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; const overdueTasksFiltered = overdueTasks.filter((task) => !overdueTasksStatus.includes(task.status)); return res.json({ message: "Overdue Tasks returned successfully!", From 2ca00683b9e8030c9842f6553ccc0be7098dc823 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sun, 14 May 2023 02:28:06 +0530 Subject: [PATCH 004/105] changes in the update task api --- middlewares/validators/tasks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middlewares/validators/tasks.js b/middlewares/validators/tasks.js index b2907fea1..e0d270e5c 100644 --- a/middlewares/validators/tasks.js +++ b/middlewares/validators/tasks.js @@ -83,7 +83,7 @@ const updateTask = async (req, res, next) => { .string() .valid(...TASK_STATUS_ENUM, ...Object.values(TASK_STATUS_OLD)) .optional(), - assignee: joi.string().optional(), + assignee: joi.alternatives().try(joi.string().optional(), joi.valid(null)), percentCompleted: joi.number().optional(), dependsOn: joi.array().items(joi.string()).optional(), participants: joi.array().items(joi.string()).optional(), From 786097f815f690dbab2fb37f0dd5e46047f616d5 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Fri, 21 Jul 2023 18:38:55 +0530 Subject: [PATCH 005/105] initial commit --- test/integration/users.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration/users.test.js b/test/integration/users.test.js index ddcb5fd97..ce660169d 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1492,4 +1492,6 @@ describe("Users", function () { }); }); }); + + describe("PATCH /update-archived", function () {}); }); From 5881d16f40f215c74132ce41d79f7dabfd41b2ce Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sat, 22 Jul 2023 18:30:11 +0530 Subject: [PATCH 006/105] test/added integration tests for route /update-archived --- test/integration/users.test.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/integration/users.test.js b/test/integration/users.test.js index ce660169d..5dd4a5f34 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1493,5 +1493,23 @@ describe("Users", function () { }); }); - describe("PATCH /update-archived", function () {}); + describe("PATCH /update-archived", function () { + it("returns successful response", function (done) { + chai + .request(app) + .patch("/users/update-archived") + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.message).to.equal( + "Successfully updated users archived role to true if in_discord role is false" + ); + return done(); + }); + }); + }); }); From 02004d4dab0aa902bfe26f012e91035335a52cf4 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sat, 22 Jul 2023 18:31:17 +0530 Subject: [PATCH 007/105] test/added tests for function archiveUserIfNotInDiscord --- test/unit/models/users.test.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 7c07f57e4..c279a7740 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -220,4 +220,34 @@ describe("users", function () { expect(data[0]).to.have.all.keys([...Object.keys(joinData[0]), "id"]); }); }); + + describe("archive user if not in discord", function () { + beforeEach(async function () { + const addUsersPromises = []; + userDataArray.forEach((user) => { + const userData = { + ...user, + roles: { + ...user.roles, + in_discord: false, + archived: user?.roles?.archived || false, + }, + }; + addUsersPromises.push(userModel.add(userData)); + }); + + await Promise.all(addUsersPromises); + }); + + it("should update archived role to true if in_discord is false", async function () { + await users.archiveUserIfNotInDiscord(); + + const updatedUsers = await users.fetchAllUsers(); + + updatedUsers.forEach((user) => { + expect(user.roles.in_discord).to.be.equal(false); + expect(user.roles.archived).to.be.equal(true); + }); + }); + }); }); From 8872d02bd82755ced93af729ac1ea903f0f2146f Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sat, 22 Jul 2023 18:44:32 +0530 Subject: [PATCH 008/105] refactor: overdue and extension requests APIs --- controllers/extensionRequests.js | 71 +++++++++++++++++++++++++++++++- controllers/tasks.js | 4 +- routes/extensionRequests.js | 7 ++++ test/integration/tasks.test.js | 1 + 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index f3ed89d69..d1cb3fb6e 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -1,7 +1,7 @@ const extensionRequestsQuery = require("../models/extensionRequests"); const { addLog } = require("../models/logs"); const tasks = require("../models/tasks"); -const { getUsername } = require("../utils/users"); +const { getUsername, getUserId } = require("../utils/users"); const { EXTENSION_REQUEST_STATUS } = require("../constants/extensionRequests"); const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** @@ -225,6 +225,74 @@ const updateExtensionRequestStatus = async (req, res) => { } }; +/** + * Create ETA extension Request by Admin + * + * @param req {Object} - Express request object + * @param res {Object} - Express response object + */ +const createTaskExtensionRequestAdmin = async (req, res) => { + try { + const extensionBody = req.body; + + if (!req.userData.roles?.super_user) { + return res.boom.forbidden("Only Super User can create an extension request for this task."); + } + + const assigneeUsername = extensionBody.assignee; + const assigneeId = await getUserId(extensionBody.assignee); + extensionBody.assignee = assigneeId; + const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); + if (!task) { + return res.boom.badRequest("Task with this id or taskid doesn't exist."); + } + if (task.endsOn >= extensionBody.newEndsOn) { + return res.boom.badRequest("The value for newEndsOn should be greater than the previous ETA"); + } + if (extensionBody.oldEndsOn !== task.endsOn) { + extensionBody.oldEndsOn = task.endsOn; + } + if (task.assignee !== assigneeUsername) { + return res.boom.badRequest("This task is assigned to some different user"); + } + + const prevExtensionRequest = await extensionRequestsQuery.fetchExtensionRequests({ + taskId: extensionBody.taskId, + assignee: extensionBody.assignee, + }); + if (prevExtensionRequest.length) { + return res.boom.forbidden("An extension request for this task already exists."); + } + + const extensionRequest = await extensionRequestsQuery.createExtensionRequest(extensionBody); + + const extensionLog = { + type: "extensionRequests", + meta: { + taskId: extensionBody.taskId, + createdBy: req.userData.id, + }, + body: { + extensionRequestId: extensionRequest.id, + oldEndsOn: task.endsOn, + newEndsOn: extensionBody.newEndsOn, + assignee: extensionBody.assignee, + status: EXTENSION_REQUEST_STATUS.PENDING, + }, + }; + + await addLog(extensionLog.type, extensionLog.meta, extensionLog.body); + + return res.json({ + message: "Extension Request created successfully!", + extensionRequest: { ...extensionBody, id: extensionRequest.id }, + }); + } catch (err) { + logger.error(`Error while creating new extension request: ${err}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + module.exports = { createTaskExtensionRequest, fetchExtensionRequests, @@ -232,4 +300,5 @@ module.exports = { getSelfExtensionRequests, updateExtensionRequest, updateExtensionRequestStatus, + createTaskExtensionRequestAdmin, }; diff --git a/controllers/tasks.js b/controllers/tasks.js index 7ddc2ce42..4470bf30f 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -298,7 +298,9 @@ const currentOverdueTasks = async (req, res) => { try { const overdueTasks = await tasks.getAllOverDueTasks(); const overdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; - const overdueTasksFiltered = overdueTasks.filter((task) => !overdueTasksStatus.includes(task.status)); + const overdueTasksFiltered = overdueTasks.filter( + (task) => !overdueTasksStatus.includes(task.status) && task.assignee + ); return res.json({ message: "Overdue Tasks returned successfully!", overdueTasks: overdueTasksFiltered, diff --git a/routes/extensionRequests.js b/routes/extensionRequests.js index 91fd05f07..d4d8606a4 100644 --- a/routes/extensionRequests.js +++ b/routes/extensionRequests.js @@ -11,6 +11,13 @@ const { } = require("../middlewares/validators/extensionRequests"); router.post("/", authenticate, createExtensionRequest, extensionRequests.createTaskExtensionRequest); +router.post( + "/admin", + authenticate, + authorizeRoles([SUPERUSER]), + createExtensionRequest, + extensionRequests.createTaskExtensionRequestAdmin +); router.get("/", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.fetchExtensionRequests); router.get("/self", authenticate, extensionRequests.getSelfExtensionRequests); router.get("/:id", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.getExtensionRequest); diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 07b28b77a..06c72414d 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -61,6 +61,7 @@ describe("Tasks", function () { completionAward: { [DINERO]: 3, [NEELAM]: 300 }, lossRate: { [DINERO]: 1 }, isNoteworthy: false, + assignee: appOwner.username, }, ]; From 6cc53cc4db43d09e5930c0fa2e12c07883bf3bbf Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sat, 22 Jul 2023 19:49:36 +0530 Subject: [PATCH 009/105] feat/added route controller and model function --- controllers/users.js | 10 ++++++++++ models/users.js | 25 +++++++++++++++++++++++++ routes/users.js | 1 + 3 files changed, 36 insertions(+) diff --git a/controllers/users.js b/controllers/users.js index e4379aa9e..0cfebe94e 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -618,6 +618,15 @@ const updateRoles = async (req, res) => { } }; +const archiveUserIfNotInDiscord = async (req, res) => { + try { + await userQuery.archiveUserIfNotInDiscord(); + return res.json({ message: "Successfully updated users archived role to true if in_discord role is false" }); + } catch (error) { + return res.status(500).json({ message: INTERNAL_SERVER_ERROR }); + } +}; + module.exports = { verifyUser, generateChaincode, @@ -643,4 +652,5 @@ module.exports = { setInDiscordScript, markUnverified, updateRoles, + archiveUserIfNotInDiscord, }; diff --git a/models/users.js b/models/users.js index c4e3240db..9a9f56a78 100644 --- a/models/users.js +++ b/models/users.js @@ -572,6 +572,30 @@ const fetchAllUsers = async () => { return users; }; +const archiveUserIfNotInDiscord = async () => { + const users = await fetchAllUsers(); + const updateUserPromises = []; + + users.forEach((user) => { + const isUserInDiscord = user?.roles?.in_discord; + const id = user.id; + if (!isUserInDiscord) { + const userData = { + ...user, + roles: { + ...user.roles, + archived: true, + }, + }; + updateUserPromises.push(userModel.doc(id).update(userData)); + } + // eslint-disable-next-line no-console + console.log("the token is", user); + }); + + await Promise.all(updateUserPromises); +}; + module.exports = { addOrUpdate, fetchPaginatedUsers, @@ -592,4 +616,5 @@ module.exports = { getUserImageForVerification, getDiscordUsers, fetchAllUsers, + archiveUserIfNotInDiscord, }; diff --git a/routes/users.js b/routes/users.js index 7bf85c54b..4b0cfad77 100644 --- a/routes/users.js +++ b/routes/users.js @@ -32,6 +32,7 @@ router.patch( userValidator.validateUpdateRoles, users.updateRoles ); +router.patch("/update-archived", authenticate, authorizeRoles([SUPERUSER]), users.archiveUserIfNotInDiscord); // upload.single('profile') -> multer inmemory storage of file for type multipart/form-data router.post("/picture", authenticate, checkIsVerifiedDiscord, upload.single("profile"), users.postUserPicture); From 44e07029e157848e833212c65fd6541402ae0cff Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sat, 22 Jul 2023 23:32:48 +0530 Subject: [PATCH 010/105] remove redundant code --- models/users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/users.js b/models/users.js index 9a9f56a78..7b5388926 100644 --- a/models/users.js +++ b/models/users.js @@ -148,6 +148,8 @@ const fetchPaginatedUsers = async (query) => { const doc = (query.next || query.prev) && (await userModel.doc(query.next || query.prev).get()); let dbQuery = userModel.where("roles.archived", "==", false).orderBy("username"); + // eslint-disable-next-line no-console + console.log("the users are", query); if (query.prev) { dbQuery = dbQuery.limitToLast(size); @@ -589,8 +591,6 @@ const archiveUserIfNotInDiscord = async () => { }; updateUserPromises.push(userModel.doc(id).update(userData)); } - // eslint-disable-next-line no-console - console.log("the token is", user); }); await Promise.all(updateUserPromises); From ca735c52ad3c1d91ef746e7d08b914bc027c1679 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sun, 23 Jul 2023 00:44:57 +0530 Subject: [PATCH 011/105] removed redundant code --- models/users.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/models/users.js b/models/users.js index 7b5388926..4ac8012e3 100644 --- a/models/users.js +++ b/models/users.js @@ -148,8 +148,6 @@ const fetchPaginatedUsers = async (query) => { const doc = (query.next || query.prev) && (await userModel.doc(query.next || query.prev).get()); let dbQuery = userModel.where("roles.archived", "==", false).orderBy("username"); - // eslint-disable-next-line no-console - console.log("the users are", query); if (query.prev) { dbQuery = dbQuery.limitToLast(size); From d225c0f4d818b703acdea8b160d3461ba6b42b22 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Wed, 26 Jul 2023 01:36:06 +0530 Subject: [PATCH 012/105] FIX: resolved comments --- controllers/users.js | 7 +++++-- models/users.js | 38 ++++++++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/controllers/users.js b/controllers/users.js index bee951514..858816a4e 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -615,8 +615,11 @@ const updateRoles = async (req, res) => { const archiveUserIfNotInDiscord = async (req, res) => { try { - await userQuery.archiveUserIfNotInDiscord(); - return res.json({ message: "Successfully updated users archived role to true if in_discord role is false" }); + const data = await userQuery.archiveUserIfNotInDiscord(); + return res.json({ + message: "Successfully updated users archived role to true if in_discord role is false", + data, + }); } catch (error) { return res.status(500).json({ message: INTERNAL_SERVER_ERROR }); } diff --git a/models/users.js b/models/users.js index 4ac8012e3..ab0dc5afd 100644 --- a/models/users.js +++ b/models/users.js @@ -573,25 +573,35 @@ const fetchAllUsers = async () => { }; const archiveUserIfNotInDiscord = async () => { - const users = await fetchAllUsers(); - const updateUserPromises = []; - - users.forEach((user) => { - const isUserInDiscord = user?.roles?.in_discord; - const id = user.id; - if (!isUserInDiscord) { - const userData = { - ...user, + try { + const snapshot = await userModel.where("roles.in_discord", "==", false).get(); + const batch = firestore.batch(); + const summary = { + totalUsersWithArchivedRoleUpdated: 0, + }; + + snapshot.forEach((user) => { + const id = user.id; + const userData = user.data(); + + const updatedUserData = { + ...userData, roles: { - ...user.roles, + ...userData.roles, archived: true, }, }; - updateUserPromises.push(userModel.doc(id).update(userData)); - } - }); - await Promise.all(updateUserPromises); + batch.update(userModel.doc(id), updatedUserData); + summary.totalUsersWithArchivedRoleUpdated++; + }); + + await batch.commit(); + return summary; + } catch (error) { + logger.error(`error in updating Users archived role ${error}`); + return { status: 500, message: "Users archived role couldn't be updated Successfully." }; + } }; module.exports = { From db54d13df149783f0845cb8c217bd2ca2482687c Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Wed, 26 Jul 2023 02:41:31 +0530 Subject: [PATCH 013/105] FIX: merge conflicts --- test/integration/users.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/users.test.js b/test/integration/users.test.js index 3b391e560..dd6b70bc7 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1547,6 +1547,6 @@ describe("Users", function () { expect(res.body.usersFound).to.be.equal(3); return done(); }); - }); + }); }); }); From ca2e1f6d74ffa2b09288c3afaea5d5e788d36506 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Wed, 26 Jul 2023 12:12:37 +0530 Subject: [PATCH 014/105] FIX: resolved comments --- models/users.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/models/users.js b/models/users.js index 3587f21bc..eb37729c4 100644 --- a/models/users.js +++ b/models/users.js @@ -583,17 +583,20 @@ const archiveUserIfNotInDiscord = async () => { snapshot.forEach((user) => { const id = user.id; const userData = user.data(); + const isArchived = userData?.roles?.archived; - const updatedUserData = { - ...userData, - roles: { - ...userData.roles, - archived: true, - }, - }; - - batch.update(userModel.doc(id), updatedUserData); - summary.totalUsersWithArchivedRoleUpdated++; + if (!isArchived) { + const updatedUserData = { + ...userData, + roles: { + ...userData.roles, + archived: true, + }, + }; + + batch.update(userModel.doc(id), updatedUserData); + summary.totalUsersWithArchivedRoleUpdated++; + } }); await batch.commit(); From 40b60c546d6c9dd15afe47b173a7a453f6d827d6 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Wed, 26 Jul 2023 13:00:10 +0530 Subject: [PATCH 015/105] TEST: updated tests --- test/integration/users.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/integration/users.test.js b/test/integration/users.test.js index dd6b70bc7..d886633ff 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1515,6 +1515,7 @@ describe("Users", function () { } expect(res).to.have.status(200); + expect(res.body.data).to.have.property("totalUsersWithArchivedRoleUpdated"); expect(res.body.message).to.equal( "Successfully updated users archived role to true if in_discord role is false" ); From 2378f7d0da0a5fd0ef3bfc6f9357045627de05c2 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Wed, 26 Jul 2023 23:16:39 +0530 Subject: [PATCH 016/105] REFACTOR: updated variable names --- models/users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/users.js b/models/users.js index eb37729c4..e42fdb0ee 100644 --- a/models/users.js +++ b/models/users.js @@ -577,7 +577,7 @@ const archiveUserIfNotInDiscord = async () => { const snapshot = await userModel.where("roles.in_discord", "==", false).get(); const batch = firestore.batch(); const summary = { - totalUsersWithArchivedRoleUpdated: 0, + totalUsersArchived: 0, }; snapshot.forEach((user) => { @@ -595,7 +595,7 @@ const archiveUserIfNotInDiscord = async () => { }; batch.update(userModel.doc(id), updatedUserData); - summary.totalUsersWithArchivedRoleUpdated++; + summary.totalUsersArchived++; } }); From 3bb006af1562bda832a67753948a9cdd9391fc20 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Wed, 26 Jul 2023 23:47:03 +0530 Subject: [PATCH 017/105] TEST: assert total users made archived --- test/integration/users.test.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/integration/users.test.js b/test/integration/users.test.js index d886633ff..cc9ff747e 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1504,6 +1504,18 @@ describe("Users", function () { }); describe("PATCH /update-archived", function () { + beforeEach(async function () { + const rolesToBeAdded = { + archived: false, + in_discord: false, + }; + await addUser({ ...userData[0], roles: rolesToBeAdded }); + }); + + afterEach(async function () { + await cleanDb(); + }); + it("returns successful response", function (done) { chai .request(app) @@ -1515,7 +1527,8 @@ describe("Users", function () { } expect(res).to.have.status(200); - expect(res.body.data).to.have.property("totalUsersWithArchivedRoleUpdated"); + expect(res.body.data).to.have.property("totalUsersArchived"); + expect(res.body.data.totalUsersArchived).to.be.greaterThan(0); expect(res.body.message).to.equal( "Successfully updated users archived role to true if in_discord role is false" ); From b5caca62d13940e5b8bf26c53639d08d415efdbd Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Thu, 27 Jul 2023 00:02:23 +0530 Subject: [PATCH 018/105] REFACTOR: updated route --- routes/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/users.js b/routes/users.js index 27374f393..ff3a2b104 100644 --- a/routes/users.js +++ b/routes/users.js @@ -34,7 +34,7 @@ router.patch( userValidator.validateUpdateRoles, users.updateRoles ); -router.patch("/update-archived", authenticate, authorizeRoles([SUPERUSER]), users.archiveUserIfNotInDiscord); +router.patch("/archived", authenticate, authorizeRoles([SUPERUSER]), users.archiveUserIfNotInDiscord); // upload.single('profile') -> multer inmemory storage of file for type multipart/form-data router.post("/picture", authenticate, checkIsVerifiedDiscord, upload.single("profile"), users.postUserPicture); From da99471b9f29e24e321b9c0181bd7566e1a77213 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Thu, 27 Jul 2023 00:08:04 +0530 Subject: [PATCH 019/105] TEST: updated route --- test/integration/users.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/users.test.js b/test/integration/users.test.js index cc9ff747e..d4186ca65 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1503,7 +1503,7 @@ describe("Users", function () { }); }); - describe("PATCH /update-archived", function () { + describe("PATCH /archived", function () { beforeEach(async function () { const rolesToBeAdded = { archived: false, @@ -1519,7 +1519,7 @@ describe("Users", function () { it("returns successful response", function (done) { chai .request(app) - .patch("/users/update-archived") + .patch("/users/archived") .set("cookie", `${cookieName}=${superUserAuthToken}`) .end((err, res) => { if (err) { From 9b2946aef513aa9a9433f8b41935622dfab057ee Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Thu, 27 Jul 2023 12:43:05 +0530 Subject: [PATCH 020/105] FEAT: handles batch updates more that 500 --- constants/users.js | 3 +++ models/users.js | 53 ++++++++++++++++++++++++++++++---------------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/constants/users.js b/constants/users.js index 3a11cf768..9670d1f3d 100644 --- a/constants/users.js +++ b/constants/users.js @@ -19,9 +19,12 @@ const ALLOWED_FILTER_PARAMS = { ROLE: ["role"], }; +const DOCUMENT_WRITE_SIZE = 500; + module.exports = { profileStatus, USER_STATUS, ALLOWED_FILTER_PARAMS, USER_SENSITIVE_DATA, + DOCUMENT_WRITE_SIZE, }; diff --git a/models/users.js b/models/users.js index e42fdb0ee..0a9625c67 100644 --- a/models/users.js +++ b/models/users.js @@ -7,8 +7,8 @@ const walletConstants = require("../constants/wallets"); const firestore = require("../utils/firestore"); const { fetchWallet, createWallet } = require("../models/wallets"); const { updateUserStatus } = require("../models/userStatus"); -const { arraysHaveCommonItem } = require("../utils/array"); -const { ALLOWED_FILTER_PARAMS } = require("../constants/users"); +const { arraysHaveCommonItem, chunks } = require("../utils/array"); +const { ALLOWED_FILTER_PARAMS, DOCUMENT_WRITE_SIZE } = require("../constants/users"); const { userState } = require("../constants/userStatus"); const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); const ROLES = require("../constants/roles"); @@ -575,7 +575,7 @@ const fetchAllUsers = async () => { const archiveUserIfNotInDiscord = async () => { try { const snapshot = await userModel.where("roles.in_discord", "==", false).get(); - const batch = firestore.batch(); + const usersNotInDiscord = []; const summary = { totalUsersArchived: 0, }; @@ -583,27 +583,44 @@ const archiveUserIfNotInDiscord = async () => { snapshot.forEach((user) => { const id = user.id; const userData = user.data(); - const isArchived = userData?.roles?.archived; + usersNotInDiscord.push({ ...userData, id }); + }); - if (!isArchived) { - const updatedUserData = { - ...userData, - roles: { - ...userData.roles, - archived: true, - }, - }; - - batch.update(userModel.doc(id), updatedUserData); - summary.totalUsersArchived++; - } + const userNotInDiscordChunks = chunks(usersNotInDiscord, DOCUMENT_WRITE_SIZE); + const batchUpdateArchived = userNotInDiscordChunks.map((users) => { + const batch = firestore.batch(); + users.forEach((user) => { + const id = user.id; + const isArchived = user?.roles?.archived; + + if (!isArchived) { + const updatedUserData = { + ...user, + roles: { + ...user.roles, + archived: true, + }, + }; + + batch.update(userModel.doc(id), updatedUserData); + summary.totalUsersArchived++; + } + }); + return batch; + }); + + const batchUpdatedPromise = []; + + batchUpdateArchived.forEach((batch) => { + const result = batch.commit(); + batchUpdatedPromise.push(result); }); - await batch.commit(); + await Promise.all(batchUpdatedPromise); return summary; } catch (error) { logger.error(`error in updating Users archived role ${error}`); - return { status: 500, message: "Users archived role couldn't be updated Successfully." }; + return { status: 500, message: `${error} Users archived role couldn't be updated Successfully.` }; } }; From 8e06bc80fe038b5c62134ed06a197fe17cb86955 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Thu, 27 Jul 2023 12:46:50 +0530 Subject: [PATCH 021/105] TEST: updated tests --- test/unit/models/users.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 741bb2af8..a1f75461d 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -187,7 +187,8 @@ describe("users", function () { beforeEach(async function () { const addUsersPromises = []; userDataArray.forEach((user) => { - addUsersPromises.push(userModel.add(user)); + const id = String(user.id); + addUsersPromises.push(userModel.add({ ...user, id })); }); await Promise.all(addUsersPromises); }); @@ -230,7 +231,7 @@ describe("users", function () { roles: { ...user.roles, in_discord: false, - archived: user?.roles?.archived || false, + archived: false, }, }; addUsersPromises.push(userModel.add(userData)); From aab3d73041f5744f3778e3bf2ed4b637789a2c85 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Thu, 27 Jul 2023 22:26:46 +0530 Subject: [PATCH 022/105] TEST: assert for failed operation added --- test/integration/users.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration/users.test.js b/test/integration/users.test.js index d4186ca65..3af5fb825 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1528,7 +1528,9 @@ describe("Users", function () { expect(res).to.have.status(200); expect(res.body.data).to.have.property("totalUsersArchived"); + expect(res.body.data).to.have.property("totalOperationsFailed"); expect(res.body.data.totalUsersArchived).to.be.greaterThan(0); + expect(res.body.data.totalOperationsFailed).to.be.equal(0); expect(res.body.message).to.equal( "Successfully updated users archived role to true if in_discord role is false" ); From 3c2ae5461594c6351ac8deb9e47c4cf0d36920d0 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Thu, 27 Jul 2023 22:38:25 +0530 Subject: [PATCH 023/105] FIX: added error handling for failed batch updates --- models/users.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/models/users.js b/models/users.js index 0a9625c67..113e79a26 100644 --- a/models/users.js +++ b/models/users.js @@ -578,6 +578,7 @@ const archiveUserIfNotInDiscord = async () => { const usersNotInDiscord = []; const summary = { totalUsersArchived: 0, + totalOperationsFailed: 0, }; snapshot.forEach((user) => { @@ -603,24 +604,24 @@ const archiveUserIfNotInDiscord = async () => { }; batch.update(userModel.doc(id), updatedUserData); - summary.totalUsersArchived++; } }); return batch; }); - const batchUpdatedPromise = []; - - batchUpdateArchived.forEach((batch) => { - const result = batch.commit(); - batchUpdatedPromise.push(result); - }); - - await Promise.all(batchUpdatedPromise); + for (const batch of batchUpdateArchived) { + try { + await batch.commit(); + summary.totalUsersArchived += batch._ops.length; + } catch (err) { + summary.totalOperationsFailed += batch._ops.length; + logger.error("Firebase batch Operation Failed!"); + } + } return summary; } catch (error) { logger.error(`error in updating Users archived role ${error}`); - return { status: 500, message: `${error} Users archived role couldn't be updated Successfully.` }; + throw error; } }; From 56ed77083db72b232507537020c47f81c5e0af5b Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Thu, 27 Jul 2023 22:42:34 +0530 Subject: [PATCH 024/105] REFACTOR: removed redundant changes --- test/unit/models/users.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index a1f75461d..f2909d3e9 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -187,8 +187,7 @@ describe("users", function () { beforeEach(async function () { const addUsersPromises = []; userDataArray.forEach((user) => { - const id = String(user.id); - addUsersPromises.push(userModel.add({ ...user, id })); + addUsersPromises.push(userModel.add(user)); }); await Promise.all(addUsersPromises); }); From 65a1a0c7b73b87b62704236f91df10e74e40a7eb Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sun, 30 Jul 2023 00:56:57 +0530 Subject: [PATCH 025/105] TEST: fetched only users relavant in testing --- test/unit/models/users.test.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index f2909d3e9..98399958b 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -242,11 +242,12 @@ describe("users", function () { it("should update archived role to true if in_discord is false", async function () { await users.archiveUserIfNotInDiscord(); - const updatedUsers = await users.fetchAllUsers(); + const updatedUsers = await userModel.where("roles.in_discord", "==", false).get(); updatedUsers.forEach((user) => { - expect(user.roles.in_discord).to.be.equal(false); - expect(user.roles.archived).to.be.equal(true); + const userData = user.data(); + expect(userData.roles.in_discord).to.be.equal(false); + expect(userData.roles.archived).to.be.equal(true); }); }); }); From 768ee5cf9c500bfcc70c7c8d12b06b97f9616e7c Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sun, 30 Jul 2023 01:01:04 +0530 Subject: [PATCH 026/105] TEST: updated users integration tests --- test/integration/users.test.js | 48 +++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/test/integration/users.test.js b/test/integration/users.test.js index 3af5fb825..84f3518bd 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1504,19 +1504,27 @@ describe("Users", function () { }); describe("PATCH /archived", function () { + let userId1; + let userId2; + let userId3; + let userId4; + beforeEach(async function () { const rolesToBeAdded = { archived: false, in_discord: false, }; - await addUser({ ...userData[0], roles: rolesToBeAdded }); + userId1 = await addUser({ ...userData[0], roles: rolesToBeAdded }); + userId2 = await addUser({ ...userData[1], roles: rolesToBeAdded }); + userId3 = await addUser({ ...userData[2], roles: rolesToBeAdded }); + userId4 = await addUser({ ...userData[3], roles: rolesToBeAdded }); }); afterEach(async function () { await cleanDb(); }); - it("returns successful response", function (done) { + it("should returns successful response", function (done) { chai .request(app) .patch("/users/archived") @@ -1529,7 +1537,9 @@ describe("Users", function () { expect(res).to.have.status(200); expect(res.body.data).to.have.property("totalUsersArchived"); expect(res.body.data).to.have.property("totalOperationsFailed"); - expect(res.body.data.totalUsersArchived).to.be.greaterThan(0); + expect(res.body.data).to.have.property("totalUsers"); + expect(res.body.data.totalUsersArchived).to.be.equal(4); + expect(res.body.data.totalUsers).to.be.equal(4); expect(res.body.data.totalOperationsFailed).to.be.equal(0); expect(res.body.message).to.equal( "Successfully updated users archived role to true if in_discord role is false" @@ -1537,6 +1547,38 @@ describe("Users", function () { return done(); }); }); + + it("should return proper response if no documents are found to update", async function () { + const roles = { + archived: true, + in_discord: false, + }; + await addOrUpdate({ ...userData[0], roles }, userId1); + await addOrUpdate({ ...userData[1], roles }, userId2); + await addOrUpdate({ ...userData[2], roles }, userId3); + await addOrUpdate({ ...userData[3], roles }, userId4); + + const res = await chai.request(app).patch("/users/archived").set("cookie", `${cookieName}=${superUserAuthToken}`); + + expect(res).to.have.status(200); + expect(res.body.data).to.have.property("totalUsersArchived"); + expect(res.body.data).to.have.property("totalOperationsFailed"); + expect(res.body.data).to.have.property("totalUsers"); + expect(res.body.data.totalUsers).to.be.equal(0); + expect(res.body.data.totalUsersArchived).to.be.equal(0); + expect(res.body.data.totalOperationsFailed).to.be.equal(0); + expect(res.body.message).to.equal("Couldn't find any users currently inactive in Discord but not archived."); + }); + + it("should throw an error if firestore batch operations fail", async function () { + Sinon.stub(firestore, "batch").throws(new Error("something went wrong")); + + const res = await chai.request(app).patch(`/users/archived`).set("cookie", `${cookieName}=${superUserAuthToken}`); + + expect(res.status).to.equal(500); + const response = res.body; + expect(response.message).to.be.equal("An internal server error occurred"); + }); }); describe("PATCH /users/remove-tokens", function () { From fc31550ad186cca7acad5e3f757e5d77adc26157 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sun, 30 Jul 2023 02:55:44 +0530 Subject: [PATCH 027/105] TEST: added tests for archiver user function --- test/unit/services/discordService.test.js | 64 +++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/test/unit/services/discordService.test.js b/test/unit/services/discordService.test.js index 53d82a818..6a6554579 100644 --- a/test/unit/services/discordService.test.js +++ b/test/unit/services/discordService.test.js @@ -5,12 +5,14 @@ const { addRoleToUser, getDiscordMembers, removeRoleFromUser, + archiveInactiveDiscordUsersInBulk, } = require("../../../services/discordService"); const { fetchAllUsers } = require("../../../models/users"); const Sinon = require("sinon"); const userModel = firestore.collection("users"); const userDataArray = require("../../fixtures/user/user")(); const discordMembersArray = require("../../fixtures/discordResponse/discord-response"); +const cleanDb = require("../../utils/cleanDb"); let fetchStub; describe("Discord services", function () { describe("setInDiscordFalseScript", function () { @@ -113,4 +115,66 @@ describe("Discord services", function () { }); }); }); + + describe("archive inactive discord users in bulk", function () { + const users = []; + beforeEach(async function () { + const addUsersPromises = []; + userDataArray.forEach((user) => { + const userData = { + ...user, + roles: { + ...user.roles, + in_discord: false, + archived: false, + }, + }; + addUsersPromises.push(userModel.add(userData)); + }); + await Promise.all(addUsersPromises); + + users.length = 0; + + const snapshot = await userModel + .where("roles.in_discord", "==", false) + .where("roles.archived", "==", false) + .get(); + + snapshot.forEach((user) => { + const id = user.id; + const userData = user.data(); + users.push({ ...userData, id }); + }); + }); + + afterEach(async function () { + await cleanDb(); + Sinon.restore(); + }); + + it("Should return successful response", async function () { + const res = await archiveInactiveDiscordUsersInBulk(users); + + expect(res).deep.equal({ + message: "Successfully completed batch updates", + totalUsersArchived: 14, + totalOperationsFailed: 0, + }); + }); + + it("should return failed response", async function () { + const batchStub = Sinon.stub(firestore, "batch"); + batchStub.returns({ + update: function () {}, + }); + + const res = await archiveInactiveDiscordUsersInBulk(users); + + expect(res).deep.equal({ + message: "Firebase batch operation failed", + totalUsersArchived: 0, + totalOperationsFailed: 14, + }); + }); + }); }); From 5e58cfa4bbe557440a58b139db56d10bc842ff91 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sun, 30 Jul 2023 03:05:20 +0530 Subject: [PATCH 028/105] reolved comments --- controllers/users.js | 17 ++++++++++++-- models/users.js | 48 +++++++++++++------------------------- services/discordService.js | 31 ++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/controllers/users.js b/controllers/users.js index 8d4c0990b..3d75462be 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -625,12 +625,25 @@ const updateRoles = async (req, res) => { const archiveUserIfNotInDiscord = async (req, res) => { try { const data = await userQuery.archiveUserIfNotInDiscord(); - return res.json({ + + if (data.totalUsers === 0) { + return res.status(200).json({ + message: "Couldn't find any users currently inactive in Discord but not archived.", + data, + }); + } + + if (data.totalOperationsFailed === data.totalUsers) { + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } + + return res.status(200).json({ message: "Successfully updated users archived role to true if in_discord role is false", data, }); } catch (error) { - return res.status(500).json({ message: INTERNAL_SERVER_ERROR }); + logger.error(`Error while updating the archived role: ${error}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); } }; diff --git a/models/users.js b/models/users.js index 113e79a26..d7e8c369b 100644 --- a/models/users.js +++ b/models/users.js @@ -8,6 +8,7 @@ const firestore = require("../utils/firestore"); const { fetchWallet, createWallet } = require("../models/wallets"); const { updateUserStatus } = require("../models/userStatus"); const { arraysHaveCommonItem, chunks } = require("../utils/array"); +const { archiveInactiveDiscordUsersInBulk } = require("../services/discordService"); const { ALLOWED_FILTER_PARAMS, DOCUMENT_WRITE_SIZE } = require("../constants/users"); const { userState } = require("../constants/userStatus"); const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); @@ -574,13 +575,18 @@ const fetchAllUsers = async () => { const archiveUserIfNotInDiscord = async () => { try { - const snapshot = await userModel.where("roles.in_discord", "==", false).get(); + const snapshot = await userModel.where("roles.in_discord", "==", false).where("roles.archived", "==", false).get(); const usersNotInDiscord = []; - const summary = { + let summary = { + totalUsers: snapshot.size, totalUsersArchived: 0, totalOperationsFailed: 0, }; + if (snapshot.size === 0) { + return summary; + } + snapshot.forEach((user) => { const id = user.id; const userData = user.data(); @@ -588,39 +594,17 @@ const archiveUserIfNotInDiscord = async () => { }); const userNotInDiscordChunks = chunks(usersNotInDiscord, DOCUMENT_WRITE_SIZE); - const batchUpdateArchived = userNotInDiscordChunks.map((users) => { - const batch = firestore.batch(); - users.forEach((user) => { - const id = user.id; - const isArchived = user?.roles?.archived; - - if (!isArchived) { - const updatedUserData = { - ...user, - roles: { - ...user.roles, - archived: true, - }, - }; - - batch.update(userModel.doc(id), updatedUserData); - } - }); - return batch; - }); - - for (const batch of batchUpdateArchived) { - try { - await batch.commit(); - summary.totalUsersArchived += batch._ops.length; - } catch (err) { - summary.totalOperationsFailed += batch._ops.length; - logger.error("Firebase batch Operation Failed!"); - } + for (const users of userNotInDiscordChunks) { + const res = await archiveInactiveDiscordUsersInBulk(users); + summary = { + ...summary, + totalUsersArchived: (summary.totalUsersArchived += res.totalUsersArchived), + totalOperationsFailed: (summary.totalOperationsFailed += res.totalOperationsFailed), + }; } return summary; } catch (error) { - logger.error(`error in updating Users archived role ${error}`); + logger.error(`Error in updating Users archived role: ${error}`); throw error; } }; diff --git a/services/discordService.js b/services/discordService.js index 9b6d754a5..ba5532849 100644 --- a/services/discordService.js +++ b/services/discordService.js @@ -67,9 +67,40 @@ const removeRoleFromUser = async (roleId, discordId) => { } }; +const archiveInactiveDiscordUsersInBulk = async (usersData) => { + const batch = firestore.batch(); + const summary = { + totalUsersArchived: 0, + totalOperationsFailed: 0, + }; + + usersData.forEach((user) => { + const id = user.id; + const updatedUserData = { + ...user, + roles: { + ...user.roles, + archived: true, + }, + }; + batch.update(userModel.doc(id), updatedUserData); + }); + + try { + await batch.commit(); + summary.totalUsersArchived += usersData.length; + return { message: "Successfully completed batch updates", ...summary }; + } catch (err) { + logger.error("Firebase batch Operation Failed!"); + summary.totalOperationsFailed += usersData.length; + return { message: "Firebase batch operation failed", ...summary }; + } +}; + module.exports = { getDiscordMembers, setInDiscordFalseScript, addRoleToUser, removeRoleFromUser, + archiveInactiveDiscordUsersInBulk, }; From 6d918746dfcb0ae67b18b80bba80df18d9814beb Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sun, 30 Jul 2023 20:18:11 +0530 Subject: [PATCH 029/105] feat: added test cases for tasks validators --- controllers/extensionRequests.js | 84 ++++------------------ routes/extensionRequests.js | 7 -- test/integration/extensionRequests.test.js | 49 +++++++++++-- test/unit/middlewares/tasks.test.js | 64 +++++++++++++++++ utils/users.js | 42 +++++++++++ 5 files changed, 164 insertions(+), 82 deletions(-) create mode 100644 test/unit/middlewares/tasks.test.js diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index d1cb3fb6e..d91d7338d 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -1,7 +1,7 @@ const extensionRequestsQuery = require("../models/extensionRequests"); const { addLog } = require("../models/logs"); const tasks = require("../models/tasks"); -const { getUsername, getUserId } = require("../utils/users"); +const { getUsername, getUsernameElseUndefined, getUserIdElseUndefined } = require("../utils/users"); const { EXTENSION_REQUEST_STATUS } = require("../constants/extensionRequests"); const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** @@ -14,11 +14,22 @@ const createTaskExtensionRequest = async (req, res) => { try { const extensionBody = req.body; + let assigneeUsername = await getUsernameElseUndefined(extensionBody.assignee); + let assigneeId = extensionBody.assignee; + if (!assigneeUsername) { + assigneeId = await getUserIdElseUndefined(extensionBody.assignee); + assigneeUsername = extensionBody.assignee; + extensionBody.assignee = assigneeId; + } + + if (!assigneeId) { + return res.boom.badRequest("User with this id or username doesn't exist."); + } + if (req.userData.id !== extensionBody.assignee && !req.userData.roles?.super_user) { return res.boom.forbidden("Only Super User can create an extension request for this task."); } - const assigneeUsername = await getUsername(extensionBody.assignee); const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); if (!task) { return res.boom.badRequest("Task with this id or taskid doesn't exist."); @@ -225,74 +236,6 @@ const updateExtensionRequestStatus = async (req, res) => { } }; -/** - * Create ETA extension Request by Admin - * - * @param req {Object} - Express request object - * @param res {Object} - Express response object - */ -const createTaskExtensionRequestAdmin = async (req, res) => { - try { - const extensionBody = req.body; - - if (!req.userData.roles?.super_user) { - return res.boom.forbidden("Only Super User can create an extension request for this task."); - } - - const assigneeUsername = extensionBody.assignee; - const assigneeId = await getUserId(extensionBody.assignee); - extensionBody.assignee = assigneeId; - const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); - if (!task) { - return res.boom.badRequest("Task with this id or taskid doesn't exist."); - } - if (task.endsOn >= extensionBody.newEndsOn) { - return res.boom.badRequest("The value for newEndsOn should be greater than the previous ETA"); - } - if (extensionBody.oldEndsOn !== task.endsOn) { - extensionBody.oldEndsOn = task.endsOn; - } - if (task.assignee !== assigneeUsername) { - return res.boom.badRequest("This task is assigned to some different user"); - } - - const prevExtensionRequest = await extensionRequestsQuery.fetchExtensionRequests({ - taskId: extensionBody.taskId, - assignee: extensionBody.assignee, - }); - if (prevExtensionRequest.length) { - return res.boom.forbidden("An extension request for this task already exists."); - } - - const extensionRequest = await extensionRequestsQuery.createExtensionRequest(extensionBody); - - const extensionLog = { - type: "extensionRequests", - meta: { - taskId: extensionBody.taskId, - createdBy: req.userData.id, - }, - body: { - extensionRequestId: extensionRequest.id, - oldEndsOn: task.endsOn, - newEndsOn: extensionBody.newEndsOn, - assignee: extensionBody.assignee, - status: EXTENSION_REQUEST_STATUS.PENDING, - }, - }; - - await addLog(extensionLog.type, extensionLog.meta, extensionLog.body); - - return res.json({ - message: "Extension Request created successfully!", - extensionRequest: { ...extensionBody, id: extensionRequest.id }, - }); - } catch (err) { - logger.error(`Error while creating new extension request: ${err}`); - return res.boom.badImplementation(INTERNAL_SERVER_ERROR); - } -}; - module.exports = { createTaskExtensionRequest, fetchExtensionRequests, @@ -300,5 +243,4 @@ module.exports = { getSelfExtensionRequests, updateExtensionRequest, updateExtensionRequestStatus, - createTaskExtensionRequestAdmin, }; diff --git a/routes/extensionRequests.js b/routes/extensionRequests.js index d4d8606a4..91fd05f07 100644 --- a/routes/extensionRequests.js +++ b/routes/extensionRequests.js @@ -11,13 +11,6 @@ const { } = require("../middlewares/validators/extensionRequests"); router.post("/", authenticate, createExtensionRequest, extensionRequests.createTaskExtensionRequest); -router.post( - "/admin", - authenticate, - authorizeRoles([SUPERUSER]), - createExtensionRequest, - extensionRequests.createTaskExtensionRequestAdmin -); router.get("/", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.fetchExtensionRequests); router.get("/self", authenticate, extensionRequests.getSelfExtensionRequests); router.get("/:id", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.getExtensionRequest); diff --git a/test/integration/extensionRequests.test.js b/test/integration/extensionRequests.test.js index d1ab13dc3..3ebe25197 100644 --- a/test/integration/extensionRequests.test.js +++ b/test/integration/extensionRequests.test.js @@ -24,7 +24,7 @@ const superUser = userData[4]; let appOwnerjwt, superUserJwt, jwt; describe("Extension Requests", function () { - let taskId1, taskId2, taskId3, extensionRequestId1, extensionRequestId2; + let taskId0, taskId1, taskId2, taskId3, extensionRequestId1, extensionRequestId2; before(async function () { const userId = await addUser(user); @@ -37,6 +37,19 @@ describe("Extension Requests", function () { jwt = authService.generateAuthToken({ userId: userId }); const taskData = [ + { + title: "Test task 1", + type: "feature", + endsOn: 1234, + startedOn: 4567, + status: "active", + percentCompleted: 10, + participants: [], + assignee: appOwner.username, + isNoteworthy: true, + completionAward: { [DINERO]: 3, [NEELAM]: 300 }, + lossRate: { [DINERO]: 1 }, + }, { title: "Test task", type: "feature", @@ -83,13 +96,14 @@ describe("Extension Requests", function () { ]; // Add the active task - taskId1 = (await tasks.updateTask(taskData[0])).taskId; + taskId0 = (await tasks.updateTask(taskData[0])).taskId; + taskId1 = (await tasks.updateTask(taskData[1])).taskId; // Add the completed task - taskId2 = (await tasks.updateTask(taskData[1])).taskId; + taskId2 = (await tasks.updateTask(taskData[2])).taskId; // Add the completed task - taskId3 = (await tasks.updateTask(taskData[2])).taskId; + taskId3 = (await tasks.updateTask(taskData[3])).taskId; const extensionRequest = { taskId: taskId3, @@ -214,6 +228,33 @@ describe("Extension Requests", function () { return done(); }); }); + it("Should return success response after adding the extension request (sending assignee username)", function (done) { + chai + .request(app) + .post("/extension-requests") + .set("cookie", `${cookieName}=${appOwnerjwt}`) + .send({ + taskId: taskId0, + title: "change ETA", + assignee: appOwner.username, + oldEndsOn: 1234, + newEndsOn: 1235, + reason: "family event", + status: "PENDING", + }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("Extension Request created successfully!"); + expect(res.body.extensionRequest).to.be.a("object"); + expect(res.body.extensionRequest.assignee).to.equal(appOwner.id); + expect(res.body.extensionRequest.status).to.equal(EXTENSION_REQUEST_STATUS.PENDING); + return done(); + }); + }); it("Should return fail response if someone try to create a extension request for someone else and is not a super user", function (done) { chai .request(app) diff --git a/test/unit/middlewares/tasks.test.js b/test/unit/middlewares/tasks.test.js new file mode 100644 index 000000000..fe738904e --- /dev/null +++ b/test/unit/middlewares/tasks.test.js @@ -0,0 +1,64 @@ +const { updateTask } = require("../../../middlewares/validators/tasks"); // Replace with the actual path to your updateTask module +const { expect } = require("chai"); +const sinon = require("sinon"); + +describe("updateTask function", function () { + // Helper function to create a request object with a specific body + const createRequest = (body) => ({ body }); + + // Helper function to create a response object with a mocked boom function + const createResponse = () => ({ + boom: { + badRequest: sinon.stub().returns({ error: true, message: "Bad Request" }), + }, + }); + + // Helper function to create a next function that simply calls done + const createNext = () => sinon.stub(); + + afterEach(function () { + sinon.restore(); + }); + + it("validates a valid request body", async function () { + const validRequestBody = { + title: "Sample Task", + purpose: "Test purposes", + type: "Sample Type", + status: "active", + isNoteworthy: true, + isCollapsed: false, + }; + + const req = createRequest(validRequestBody); + const res = createResponse(); + const next = createNext(); + + await updateTask(req, res, next); + + expect(res.boom.badRequest.calledOnce).to.be.equal(false); + expect(next.calledOnce).to.be.equal(true); + }); + + it("handles invalid request body", async function () { + const invalidRequestBody = { + // Missing required fields, or incorrect data types + title: 123, + purpose: 456, + type: true, + status: "invalid_status", + isNoteworthy: "yes", + isCollapsed: "no", + assignee: "", + }; + + const req = createRequest(invalidRequestBody); + const res = createResponse(); + const next = createNext(); + + await updateTask(req, res, next); + + expect(res.boom.badRequest.calledOnce).to.be.equal(true); + expect(next.calledOnce).to.be.equal(false); + }); +}); diff --git a/utils/users.js b/utils/users.js index f3754d74e..6935dced8 100644 --- a/utils/users.js +++ b/utils/users.js @@ -43,6 +43,46 @@ const getUsername = async (userId) => { throw error; } }; + +/** + * Used for receiving username when providing userId, if not found then returns undefined + * + * @param userId {String} - userId of the User. + * @returns username {String} - username of the same user + */ +const getUsernameElseUndefined = async (userId) => { + try { + const { + user: { username }, + } = await fetchUser({ userId }); + return username; + } catch (error) { + logger.error("Something went wrong", error); + return undefined; + } +}; + +/** + * Used for receiving userId when providing username, if not found then returns undefined + * + * @param username {String} - username of the User. + * @returns id {String} - userId of the same user + */ + +const getUserIdElseUndefined = async (username) => { + try { + const { + userExists, + user: { id }, + } = await fetchUser({ username }); + + return userExists ? id : false; + } catch (error) { + logger.error("Something went wrong", error); + return undefined; + } +}; + /** * Converts the userIds entered in the array to corresponding usernames * @param participantArray {array} : participants array to be updated @@ -152,4 +192,6 @@ module.exports = { getLowestLevelSkill, getPaginationLink, getUsernamesFromPRs, + getUsernameElseUndefined, + getUserIdElseUndefined, }; From 137ba544a6b0d009f69d286f8d174a75e65c829f Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sun, 30 Jul 2023 23:57:46 +0530 Subject: [PATCH 030/105] REFACTOR: to remove circular dependency --- models/users.js | 2 +- services/discordService.js | 31 ---------- services/users.js | 36 ++++++++++++ test/integration/users.test.js | 1 + test/unit/services/discordService.test.js | 64 -------------------- test/unit/services/users.test.js | 72 +++++++++++++++++++++++ 6 files changed, 110 insertions(+), 96 deletions(-) create mode 100644 services/users.js create mode 100644 test/unit/services/users.test.js diff --git a/models/users.js b/models/users.js index 229c62130..781add27f 100644 --- a/models/users.js +++ b/models/users.js @@ -8,7 +8,7 @@ const firestore = require("../utils/firestore"); const { fetchWallet, createWallet } = require("../models/wallets"); const { updateUserStatus } = require("../models/userStatus"); const { arraysHaveCommonItem, chunks } = require("../utils/array"); -const { archiveInactiveDiscordUsersInBulk } = require("../services/discordService"); +const { archiveInactiveDiscordUsersInBulk } = require("../services/users"); const { ALLOWED_FILTER_PARAMS, DOCUMENT_WRITE_SIZE } = require("../constants/users"); const { userState } = require("../constants/userStatus"); const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); diff --git a/services/discordService.js b/services/discordService.js index ba5532849..9b6d754a5 100644 --- a/services/discordService.js +++ b/services/discordService.js @@ -67,40 +67,9 @@ const removeRoleFromUser = async (roleId, discordId) => { } }; -const archiveInactiveDiscordUsersInBulk = async (usersData) => { - const batch = firestore.batch(); - const summary = { - totalUsersArchived: 0, - totalOperationsFailed: 0, - }; - - usersData.forEach((user) => { - const id = user.id; - const updatedUserData = { - ...user, - roles: { - ...user.roles, - archived: true, - }, - }; - batch.update(userModel.doc(id), updatedUserData); - }); - - try { - await batch.commit(); - summary.totalUsersArchived += usersData.length; - return { message: "Successfully completed batch updates", ...summary }; - } catch (err) { - logger.error("Firebase batch Operation Failed!"); - summary.totalOperationsFailed += usersData.length; - return { message: "Firebase batch operation failed", ...summary }; - } -}; - module.exports = { getDiscordMembers, setInDiscordFalseScript, addRoleToUser, removeRoleFromUser, - archiveInactiveDiscordUsersInBulk, }; diff --git a/services/users.js b/services/users.js new file mode 100644 index 000000000..656f421c1 --- /dev/null +++ b/services/users.js @@ -0,0 +1,36 @@ +const firestore = require("../utils/firestore"); +const userModel = firestore.collection("users"); + +const archiveInactiveDiscordUsersInBulk = async (usersData) => { + const batch = firestore.batch(); + const summary = { + totalUsersArchived: 0, + totalOperationsFailed: 0, + }; + + usersData.forEach((user) => { + const id = user.id; + const updatedUserData = { + ...user, + roles: { + ...user.roles, + archived: true, + }, + }; + batch.update(userModel.doc(id), updatedUserData); + }); + + try { + await batch.commit(); + summary.totalUsersArchived += usersData.length; + return { message: "Successfully completed batch updates", ...summary }; + } catch (err) { + logger.error("Firebase batch Operation Failed!"); + summary.totalOperationsFailed += usersData.length; + return { message: "Firebase batch operation failed", ...summary }; + } +}; + +module.exports = { + archiveInactiveDiscordUsersInBulk, +}; diff --git a/test/integration/users.test.js b/test/integration/users.test.js index 84f3518bd..cdc827fd5 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1522,6 +1522,7 @@ describe("Users", function () { afterEach(async function () { await cleanDb(); + Sinon.restore(); }); it("should returns successful response", function (done) { diff --git a/test/unit/services/discordService.test.js b/test/unit/services/discordService.test.js index 6a6554579..53d82a818 100644 --- a/test/unit/services/discordService.test.js +++ b/test/unit/services/discordService.test.js @@ -5,14 +5,12 @@ const { addRoleToUser, getDiscordMembers, removeRoleFromUser, - archiveInactiveDiscordUsersInBulk, } = require("../../../services/discordService"); const { fetchAllUsers } = require("../../../models/users"); const Sinon = require("sinon"); const userModel = firestore.collection("users"); const userDataArray = require("../../fixtures/user/user")(); const discordMembersArray = require("../../fixtures/discordResponse/discord-response"); -const cleanDb = require("../../utils/cleanDb"); let fetchStub; describe("Discord services", function () { describe("setInDiscordFalseScript", function () { @@ -115,66 +113,4 @@ describe("Discord services", function () { }); }); }); - - describe("archive inactive discord users in bulk", function () { - const users = []; - beforeEach(async function () { - const addUsersPromises = []; - userDataArray.forEach((user) => { - const userData = { - ...user, - roles: { - ...user.roles, - in_discord: false, - archived: false, - }, - }; - addUsersPromises.push(userModel.add(userData)); - }); - await Promise.all(addUsersPromises); - - users.length = 0; - - const snapshot = await userModel - .where("roles.in_discord", "==", false) - .where("roles.archived", "==", false) - .get(); - - snapshot.forEach((user) => { - const id = user.id; - const userData = user.data(); - users.push({ ...userData, id }); - }); - }); - - afterEach(async function () { - await cleanDb(); - Sinon.restore(); - }); - - it("Should return successful response", async function () { - const res = await archiveInactiveDiscordUsersInBulk(users); - - expect(res).deep.equal({ - message: "Successfully completed batch updates", - totalUsersArchived: 14, - totalOperationsFailed: 0, - }); - }); - - it("should return failed response", async function () { - const batchStub = Sinon.stub(firestore, "batch"); - batchStub.returns({ - update: function () {}, - }); - - const res = await archiveInactiveDiscordUsersInBulk(users); - - expect(res).deep.equal({ - message: "Firebase batch operation failed", - totalUsersArchived: 0, - totalOperationsFailed: 14, - }); - }); - }); }); diff --git a/test/unit/services/users.test.js b/test/unit/services/users.test.js new file mode 100644 index 000000000..c494e6430 --- /dev/null +++ b/test/unit/services/users.test.js @@ -0,0 +1,72 @@ +const Sinon = require("sinon"); +const { expect } = require("chai"); + +const firestore = require("../../../utils/firestore"); +const userModel = firestore.collection("users"); +const cleanDb = require("../../utils/cleanDb"); +const userDataArray = require("../../fixtures/user/user")(); +const { archiveInactiveDiscordUsersInBulk } = require("../../../services/users"); + +describe("Users services", function () { + describe("archive inactive discord users in bulk", function () { + const users = []; + beforeEach(async function () { + const addUsersPromises = []; + userDataArray.forEach((user) => { + const userData = { + ...user, + roles: { + ...user.roles, + in_discord: false, + archived: false, + }, + }; + addUsersPromises.push(userModel.add(userData)); + }); + await Promise.all(addUsersPromises); + + users.length = 0; + + const snapshot = await userModel + .where("roles.in_discord", "==", false) + .where("roles.archived", "==", false) + .get(); + + snapshot.forEach((user) => { + const id = user.id; + const userData = user.data(); + users.push({ ...userData, id }); + }); + }); + + afterEach(async function () { + await cleanDb(); + Sinon.restore(); + }); + + it("Should return successful response", async function () { + const res = await archiveInactiveDiscordUsersInBulk(users); + + expect(res).deep.equal({ + message: "Successfully completed batch updates", + totalUsersArchived: 14, + totalOperationsFailed: 0, + }); + }); + + it("should return failed response", async function () { + const batchStub = Sinon.stub(firestore, "batch"); + batchStub.returns({ + update: function () {}, + }); + + const res = await archiveInactiveDiscordUsersInBulk(users); + + expect(res).deep.equal({ + message: "Firebase batch operation failed", + totalUsersArchived: 0, + totalOperationsFailed: 14, + }); + }); + }); +}); From d8a8a4acd3cb82f5ac1874427dc9540e483c293d Mon Sep 17 00:00:00 2001 From: Pratiyush Kumar Date: Tue, 1 Aug 2023 15:13:44 +0530 Subject: [PATCH 031/105] added onBoarding status of user more than 31 days --- middlewares/validators/user.js | 1 + models/users.js | 30 +++++++++++++++++++++++++++++- utils/userStatus.js | 5 +++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/middlewares/validators/user.js b/middlewares/validators/user.js index 9e4dfdfb8..f9d559c12 100644 --- a/middlewares/validators/user.js +++ b/middlewares/validators/user.js @@ -189,6 +189,7 @@ async function validateUserQueryParams(req, res, next) { .optional(), role: joi.string().valid(ROLES.MEMBER, ROLES.INDISCORD, ROLES.ARCHIVED).optional(), verified: joi.string().optional(), + day: joi.string().min(0).max(100).optional(), }) .messages({ "object.min": "Please provide at least one filter criteria", diff --git a/models/users.js b/models/users.js index 0fdc1b67c..a89b7d420 100644 --- a/models/users.js +++ b/models/users.js @@ -6,7 +6,7 @@ const walletConstants = require("../constants/wallets"); const firestore = require("../utils/firestore"); const { fetchWallet, createWallet } = require("../models/wallets"); -const { updateUserStatus } = require("../models/userStatus"); +const { updateUserStatus, getAllUserStatus } = require("../models/userStatus"); const { arraysHaveCommonItem } = require("../utils/array"); const { ALLOWED_FILTER_PARAMS } = require("../constants/users"); const { userState } = require("../constants/userStatus"); @@ -19,6 +19,7 @@ const userStatusModel = firestore.collection("usersStatus"); const photoVerificationModel = firestore.collection("photo-verification"); const { ITEM_TAG, USER_STATE } = ALLOWED_FILTER_PARAMS; const admin = require("firebase-admin"); +const { filterUsersWithOnboardingState } = require("../utils/userStatus"); /** * Adds or updates the user data @@ -458,6 +459,10 @@ const getRdsUserInfoByGitHubUsername = async (githubUsername) => { */ const getUsersBasedOnFilter = async (query) => { + if (query.state === "ONBOARDING" && query.day) { + const fetchUsersWithOnBoardingState = await getUsersWithOnboardingState(query); + return fetchUsersWithOnBoardingState; + } const allQueryKeys = Object.keys(query); const doesTagQueryExist = arraysHaveCommonItem(ITEM_TAG, allQueryKeys); const doesStateQueryExist = arraysHaveCommonItem(USER_STATE, allQueryKeys); @@ -540,6 +545,29 @@ const getUsersBasedOnFilter = async (query) => { return []; }; +const getUsersWithOnboardingState = async (query) => { + const range = Number(query.day); + const { allUserStatus } = await getAllUserStatus(query); + const allUsersWithOnboardingState = filterUsersWithOnboardingState(allUserStatus); + const updatedOnboardingUsersWithDate = []; + const filteredUsers = []; + for (const user of allUsersWithOnboardingState) { + const result = await userModel.doc(user.userId).get(); + filteredUsers.push(result.data()); + } + filteredUsers.map((element) => { + if (element.discordJoinedAt) { + const userDiscordJoinedDate = new Date(element.discordJoinedAt); + const currentTimeStamp = new Date().getTime(); + const timeDifferenceInMilliseconds = currentTimeStamp - userDiscordJoinedDate.getTime(); + const currentAndUserJoinedDateDifference = Math.floor(timeDifferenceInMilliseconds / (1000 * 60 * 60 * 24)); + if (currentAndUserJoinedDateDifference > range) { + updatedOnboardingUsersWithDate.push(element); + } + } + return updatedOnboardingUsersWithDate; + }); +}; /** * Fetch all users * diff --git a/utils/userStatus.js b/utils/userStatus.js index d0565474a..7e1e9ce65 100644 --- a/utils/userStatus.js +++ b/utils/userStatus.js @@ -294,6 +294,10 @@ const generateErrorResponse = (message) => { }; }; +const filterUsersWithOnboardingState = (data) => { + return data.filter((item) => item.currentStatus.state === userState.ONBOARDING); +}; + module.exports = { getUserIdBasedOnRoute, getTomorrowTimeStamp, @@ -307,4 +311,5 @@ module.exports = { checkIfUserHasLiveTasks, generateErrorResponse, generateNewStatus, + filterUsersWithOnboardingState, }; From 2326828805849f1eeec2be2432d55678c66bc448 Mon Sep 17 00:00:00 2001 From: Pratiyush Kumar Date: Wed, 2 Aug 2023 14:52:30 +0530 Subject: [PATCH 032/105] finally finalized the route of the onboarding state --- middlewares/validators/user.js | 5 ++++- models/users.js | 9 +++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/middlewares/validators/user.js b/middlewares/validators/user.js index f9d559c12..324655fb4 100644 --- a/middlewares/validators/user.js +++ b/middlewares/validators/user.js @@ -189,7 +189,10 @@ async function validateUserQueryParams(req, res, next) { .optional(), role: joi.string().valid(ROLES.MEMBER, ROLES.INDISCORD, ROLES.ARCHIVED).optional(), verified: joi.string().optional(), - day: joi.string().min(0).max(100).optional(), + time: joi + .string() + .regex(/^[1-9]\d*d$/) + .optional(), }) .messages({ "object.min": "Please provide at least one filter criteria", diff --git a/models/users.js b/models/users.js index a89b7d420..80ee2bfb7 100644 --- a/models/users.js +++ b/models/users.js @@ -459,7 +459,7 @@ const getRdsUserInfoByGitHubUsername = async (githubUsername) => { */ const getUsersBasedOnFilter = async (query) => { - if (query.state === "ONBOARDING" && query.day) { + if (query.state === "ONBOARDING" && query.time) { const fetchUsersWithOnBoardingState = await getUsersWithOnboardingState(query); return fetchUsersWithOnBoardingState; } @@ -546,7 +546,8 @@ const getUsersBasedOnFilter = async (query) => { }; const getUsersWithOnboardingState = async (query) => { - const range = Number(query.day); + const time = query.time; + const range = Number(time.split("d")[0]); const { allUserStatus } = await getAllUserStatus(query); const allUsersWithOnboardingState = filterUsersWithOnboardingState(allUserStatus); const updatedOnboardingUsersWithDate = []; @@ -555,7 +556,7 @@ const getUsersWithOnboardingState = async (query) => { const result = await userModel.doc(user.userId).get(); filteredUsers.push(result.data()); } - filteredUsers.map((element) => { + filteredUsers.forEach((element) => { if (element.discordJoinedAt) { const userDiscordJoinedDate = new Date(element.discordJoinedAt); const currentTimeStamp = new Date().getTime(); @@ -565,8 +566,8 @@ const getUsersWithOnboardingState = async (query) => { updatedOnboardingUsersWithDate.push(element); } } - return updatedOnboardingUsersWithDate; }); + return updatedOnboardingUsersWithDate; }; /** * Fetch all users From 97c7fee993fb4a1eee5ae69a91daf9a3cf7ba5dc Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Wed, 2 Aug 2023 16:39:20 +0530 Subject: [PATCH 033/105] FEAT: added common route /users --- controllers/users.js | 55 +++++++++++++++++++++++++++++++++++--------- models/users.js | 2 ++ routes/users.js | 3 +-- services/users.js | 4 ++++ 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/controllers/users.js b/controllers/users.js index 1d13b9487..84ef8f79c 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -569,9 +569,9 @@ const filterUsers = async (req, res) => { } }; -const nonVerifiedDiscordUsers = async (req, res) => { +const nonVerifiedDiscordUsers = async () => { const data = await dataAccess.retrieveDiscordUsers(); - return res.json(data); + return data; }; const setInDiscordScript = async (req, res) => { @@ -625,31 +625,63 @@ const updateRoles = async (req, res) => { } }; -const archiveUserIfNotInDiscord = async (req, res) => { +const archiveUserIfNotInDiscord = async () => { try { const data = await userQuery.archiveUserIfNotInDiscord(); if (data.totalUsers === 0) { - return res.status(200).json({ + return { message: "Couldn't find any users currently inactive in Discord but not archived.", - data, - }); + summary: data, + }; } if (data.totalOperationsFailed === data.totalUsers) { - return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + throw Error("Internal server error occured"); } - return res.status(200).json({ + return { message: "Successfully updated users archived role to true if in_discord role is false", - data, - }); + summary: data, + }; } catch (error) { logger.error(`Error while updating the archived role: ${error}`); - return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + throw Error("Internal server error occured"); } }; +async function usersPatchHandler(req, res) { + try { + if (!req.body && !req.body.action) { + return res.boom.badRequest("Invalid payload"); + } + + const { action } = req.body; + + if (action === "nonVerifiedDiscordUsers") { + const data = await nonVerifiedDiscordUsers(); + return res.status(200).json(data); + } else if (action === "archiveUsersIfNotInDiscord") { + const debugParam = req.query.debug?.toLowerCase(); + const data = await archiveUserIfNotInDiscord(); + + if (debugParam === "true") { + data.summary.updatedUserIds = data.summary.updatedUserIds.slice(-3); + return res.status(200).json(data); + } else { + delete data.summary.updatedUserIds; + } + + return res.status(200).json(data); + } else { + return res.boom.badRequest("Invalid payload"); + } + } catch (error) { + logger.error("Error while handling the common route:", error); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +} + module.exports = { verifyUser, generateChaincode, @@ -677,4 +709,5 @@ module.exports = { removeTokens, updateRoles, archiveUserIfNotInDiscord, + usersPatchHandler, }; diff --git a/models/users.js b/models/users.js index 781add27f..83b87406a 100644 --- a/models/users.js +++ b/models/users.js @@ -581,6 +581,7 @@ const archiveUserIfNotInDiscord = async () => { totalUsers: snapshot.size, totalUsersArchived: 0, totalOperationsFailed: 0, + updatedUserIds: [], }; if (snapshot.size === 0) { @@ -600,6 +601,7 @@ const archiveUserIfNotInDiscord = async () => { ...summary, totalUsersArchived: (summary.totalUsersArchived += res.totalUsersArchived), totalOperationsFailed: (summary.totalOperationsFailed += res.totalOperationsFailed), + updatedUserIds: [...summary.updatedUserIds, ...res.updatedUserIds], }; } return summary; diff --git a/routes/users.js b/routes/users.js index ff3a2b104..d18a6f843 100644 --- a/routes/users.js +++ b/routes/users.js @@ -26,7 +26,7 @@ router.get("/:userId/intro", authenticate, authorizeRoles([SUPERUSER]), users.ge router.put("/self/intro", authenticate, userValidator.validateJoinData, users.addUserIntro); router.get("/:id/skills", users.getUserSkills); router.get("/:id/badges", getUserBadges); -router.patch("/", authenticate, authorizeRoles([SUPERUSER]), users.nonVerifiedDiscordUsers); +router.patch("/", authenticate, authorizeRoles([SUPERUSER]), users.usersPatchHandler); router.patch( "/:id/temporary/data", authenticate, @@ -34,7 +34,6 @@ router.patch( userValidator.validateUpdateRoles, users.updateRoles ); -router.patch("/archived", authenticate, authorizeRoles([SUPERUSER]), users.archiveUserIfNotInDiscord); // upload.single('profile') -> multer inmemory storage of file for type multipart/form-data router.post("/picture", authenticate, checkIsVerifiedDiscord, upload.single("profile"), users.postUserPicture); diff --git a/services/users.js b/services/users.js index 656f421c1..24d8c2101 100644 --- a/services/users.js +++ b/services/users.js @@ -3,9 +3,11 @@ const userModel = firestore.collection("users"); const archiveInactiveDiscordUsersInBulk = async (usersData) => { const batch = firestore.batch(); + const updatedUsers = []; const summary = { totalUsersArchived: 0, totalOperationsFailed: 0, + updatedUserIds: [], }; usersData.forEach((user) => { @@ -18,11 +20,13 @@ const archiveInactiveDiscordUsersInBulk = async (usersData) => { }, }; batch.update(userModel.doc(id), updatedUserData); + updatedUsers.push(id); }); try { await batch.commit(); summary.totalUsersArchived += usersData.length; + summary.updatedUserIds = [...updatedUsers]; return { message: "Successfully completed batch updates", ...summary }; } catch (err) { logger.error("Firebase batch Operation Failed!"); From 2b05f0e2642ccfeb59deb5f795491e8b30e1d744 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Wed, 2 Aug 2023 16:40:42 +0530 Subject: [PATCH 034/105] TEST: updated tests --- test/integration/users.test.js | 98 +++++++++++++++++++++++++------- test/unit/services/users.test.js | 4 ++ 2 files changed, 80 insertions(+), 22 deletions(-) diff --git a/test/integration/users.test.js b/test/integration/users.test.js index cdc827fd5..ce402da96 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1503,11 +1503,10 @@ describe("Users", function () { }); }); - describe("PATCH /archived", function () { + describe("PATCH /users", function () { let userId1; let userId2; let userId3; - let userId4; beforeEach(async function () { const rolesToBeAdded = { @@ -1517,7 +1516,6 @@ describe("Users", function () { userId1 = await addUser({ ...userData[0], roles: rolesToBeAdded }); userId2 = await addUser({ ...userData[1], roles: rolesToBeAdded }); userId3 = await addUser({ ...userData[2], roles: rolesToBeAdded }); - userId4 = await addUser({ ...userData[3], roles: rolesToBeAdded }); }); afterEach(async function () { @@ -1525,23 +1523,42 @@ describe("Users", function () { Sinon.restore(); }); - it("should returns successful response", function (done) { + it("should return 400 if payload is not passed correctly", function (done) { chai .request(app) - .patch("/users/archived") + .patch("/users") .set("cookie", `${cookieName}=${superUserAuthToken}`) + .send() + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + expect(res.body.message).to.equal("Invalid payload"); + return done(); + }); + }); + + it("should returns successful response for api archiveUsersIfNotInDiscord", function (done) { + chai + .request(app) + .patch("/users") + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .send({ action: "archiveUsersIfNotInDiscord" }) .end((err, res) => { if (err) { return done(err); } expect(res).to.have.status(200); - expect(res.body.data).to.have.property("totalUsersArchived"); - expect(res.body.data).to.have.property("totalOperationsFailed"); - expect(res.body.data).to.have.property("totalUsers"); - expect(res.body.data.totalUsersArchived).to.be.equal(4); - expect(res.body.data.totalUsers).to.be.equal(4); - expect(res.body.data.totalOperationsFailed).to.be.equal(0); + expect(res.body.summary).to.have.property("totalUsersArchived"); + expect(res.body.summary).to.have.property("totalOperationsFailed"); + expect(res.body.summary).to.have.property("totalUsers"); + expect(res.body.summary).to.not.have.property("updatedUserIds"); + expect(res.body.summary.totalUsersArchived).to.be.equal(3); + expect(res.body.summary.totalUsers).to.be.equal(3); + expect(res.body.summary.totalOperationsFailed).to.be.equal(0); expect(res.body.message).to.equal( "Successfully updated users archived role to true if in_discord role is false" ); @@ -1549,7 +1566,7 @@ describe("Users", function () { }); }); - it("should return proper response if no documents are found to update", async function () { + it("should return proper response if no documents are found to update for api archiveUsersIfNotInDiscord", async function () { const roles = { archived: true, in_discord: false, @@ -1557,29 +1574,66 @@ describe("Users", function () { await addOrUpdate({ ...userData[0], roles }, userId1); await addOrUpdate({ ...userData[1], roles }, userId2); await addOrUpdate({ ...userData[2], roles }, userId3); - await addOrUpdate({ ...userData[3], roles }, userId4); - const res = await chai.request(app).patch("/users/archived").set("cookie", `${cookieName}=${superUserAuthToken}`); + const res = await chai + .request(app) + .patch("/users") + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .send({ action: "archiveUsersIfNotInDiscord" }); expect(res).to.have.status(200); - expect(res.body.data).to.have.property("totalUsersArchived"); - expect(res.body.data).to.have.property("totalOperationsFailed"); - expect(res.body.data).to.have.property("totalUsers"); - expect(res.body.data.totalUsers).to.be.equal(0); - expect(res.body.data.totalUsersArchived).to.be.equal(0); - expect(res.body.data.totalOperationsFailed).to.be.equal(0); + expect(res.body.summary).to.have.property("totalUsersArchived"); + expect(res.body.summary).to.have.property("totalOperationsFailed"); + expect(res.body.summary).to.have.property("totalUsers"); + expect(res.body.summary).to.not.have.property("updatedUserIds"); + expect(res.body.summary.totalUsers).to.be.equal(0); + expect(res.body.summary.totalUsersArchived).to.be.equal(0); + expect(res.body.summary.totalOperationsFailed).to.be.equal(0); expect(res.body.message).to.equal("Couldn't find any users currently inactive in Discord but not archived."); }); - it("should throw an error if firestore batch operations fail", async function () { + it("should throw an error if firestore batch operations fail for api archiveUsersIfNotInDiscord", async function () { Sinon.stub(firestore, "batch").throws(new Error("something went wrong")); - const res = await chai.request(app).patch(`/users/archived`).set("cookie", `${cookieName}=${superUserAuthToken}`); + const res = await chai + .request(app) + .patch(`/users`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .send({ action: "archiveUsersIfNotInDiscord" }); expect(res.status).to.equal(500); const response = res.body; expect(response.message).to.be.equal("An internal server error occurred"); }); + + it("should return correct response if debug param is passed", function (done) { + const updatedUserIds = [userId1, userId2, userId3]; + + chai + .request(app) + .patch("/users?debug=true") + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .send({ action: "archiveUsersIfNotInDiscord" }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.summary).to.have.property("totalUsersArchived"); + expect(res.body.summary).to.have.property("totalOperationsFailed"); + expect(res.body.summary).to.have.property("totalUsers"); + expect(res.body.summary).to.have.property("updatedUserIds"); + expect(res.body.summary.totalUsersArchived).to.be.equal(3); + expect(res.body.summary.totalUsers).to.be.equal(3); + expect(res.body.summary.totalOperationsFailed).to.be.equal(0); + expect(res.body.summary.updatedUserIds).to.deep.equal(updatedUserIds); + expect(res.body.message).to.equal( + "Successfully updated users archived role to true if in_discord role is false" + ); + return done(); + }); + }); }); describe("PATCH /users/remove-tokens", function () { diff --git a/test/unit/services/users.test.js b/test/unit/services/users.test.js index c494e6430..91169264e 100644 --- a/test/unit/services/users.test.js +++ b/test/unit/services/users.test.js @@ -10,6 +10,7 @@ const { archiveInactiveDiscordUsersInBulk } = require("../../../services/users") describe("Users services", function () { describe("archive inactive discord users in bulk", function () { const users = []; + const userIds = []; beforeEach(async function () { const addUsersPromises = []; userDataArray.forEach((user) => { @@ -36,6 +37,7 @@ describe("Users services", function () { const id = user.id; const userData = user.data(); users.push({ ...userData, id }); + userIds.push(id); }); }); @@ -51,6 +53,7 @@ describe("Users services", function () { message: "Successfully completed batch updates", totalUsersArchived: 14, totalOperationsFailed: 0, + updatedUserIds: userIds, }); }); @@ -66,6 +69,7 @@ describe("Users services", function () { message: "Firebase batch operation failed", totalUsersArchived: 0, totalOperationsFailed: 14, + updatedUserIds: [], }); }); }); From 5336d2952cf0ef95be1905c86129db72f17356f2 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Wed, 2 Aug 2023 17:12:58 +0530 Subject: [PATCH 035/105] TEST: fix failed tests --- test/integration/users.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/integration/users.test.js b/test/integration/users.test.js index ce402da96..11b551ec4 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1607,8 +1607,6 @@ describe("Users", function () { }); it("should return correct response if debug param is passed", function (done) { - const updatedUserIds = [userId1, userId2, userId3]; - chai .request(app) .patch("/users?debug=true") @@ -1627,7 +1625,7 @@ describe("Users", function () { expect(res.body.summary.totalUsersArchived).to.be.equal(3); expect(res.body.summary.totalUsers).to.be.equal(3); expect(res.body.summary.totalOperationsFailed).to.be.equal(0); - expect(res.body.summary.updatedUserIds).to.deep.equal(updatedUserIds); + expect(res.body.summary.updatedUserIds.length).to.equal(3); expect(res.body.message).to.equal( "Successfully updated users archived role to true if in_discord role is false" ); From 35b52cb6854454ae25cf7738f7bb31503825b4eb Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Wed, 2 Aug 2023 17:17:29 +0530 Subject: [PATCH 036/105] REFACTOR: changed param to query --- controllers/users.js | 4 ++-- test/integration/users.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/controllers/users.js b/controllers/users.js index 84ef8f79c..62832ea1a 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -662,10 +662,10 @@ async function usersPatchHandler(req, res) { const data = await nonVerifiedDiscordUsers(); return res.status(200).json(data); } else if (action === "archiveUsersIfNotInDiscord") { - const debugParam = req.query.debug?.toLowerCase(); + const debugQuery = req.query.debug?.toLowerCase(); const data = await archiveUserIfNotInDiscord(); - if (debugParam === "true") { + if (debugQuery === "true") { data.summary.updatedUserIds = data.summary.updatedUserIds.slice(-3); return res.status(200).json(data); } else { diff --git a/test/integration/users.test.js b/test/integration/users.test.js index 11b551ec4..424e75635 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1606,7 +1606,7 @@ describe("Users", function () { expect(response.message).to.be.equal("An internal server error occurred"); }); - it("should return correct response if debug param is passed", function (done) { + it("should return correct response if debug query is passed for api archiveUsersIfNotInDiscord", function (done) { chai .request(app) .patch("/users?debug=true") From 2e651f285ec7a63ae103e12a4e17b52f5cc1e1e9 Mon Sep 17 00:00:00 2001 From: Ravi kumar <1002kumarravi@gmail.com> Date: Thu, 3 Aug 2023 07:11:30 +0530 Subject: [PATCH 037/105] add: github created at key in db for new joiner --- controllers/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/auth.js b/controllers/auth.js index b3073bd41..8cf7c5c65 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -49,10 +49,10 @@ const githubAuthCallback = (req, res, next) => { logger.error(err); return res.boom.unauthorized("User cannot be authenticated"); } - userData = { github_id: user.username, github_display_name: user.displayName, + github_created_at: user._json.created_at, }; const { userId, incompleteUserDetails } = await users.addOrUpdate(userData); From d7c0908ece25022bfe8c47528dcc84a717fe9107 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Thu, 3 Aug 2023 14:42:24 +0530 Subject: [PATCH 038/105] refactor: requested changes for overdue tasks API --- controllers/extensionRequests.js | 2 +- controllers/tasks.js | 6 ++--- models/tasks.js | 11 ++++----- test/integration/extensionRequests.test.js | 27 +++++++++++++++++++++- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index d91d7338d..15b2e1a19 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -27,7 +27,7 @@ const createTaskExtensionRequest = async (req, res) => { } if (req.userData.id !== extensionBody.assignee && !req.userData.roles?.super_user) { - return res.boom.forbidden("Only Super User can create an extension request for this task."); + return res.boom.forbidden("Only assigned user and super user can create an extension request for this task."); } const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); diff --git a/controllers/tasks.js b/controllers/tasks.js index aedd213ff..353979265 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -403,10 +403,10 @@ const assignTask = async (req, res) => { const currentOverdueTasks = async (req, res) => { try { - const overdueTasks = await tasks.getAllOverDueTasks(); - const overdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; + const overdueTasks = await tasks.getOverdueTasks(); + const nonOverdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; const overdueTasksFiltered = overdueTasks.filter( - (task) => !overdueTasksStatus.includes(task.status) && task.assignee + (task) => !nonOverdueTasksStatus.includes(task.status) && task.assignee ); return res.json({ message: "Overdue Tasks returned successfully!", diff --git a/models/tasks.js b/models/tasks.js index 2ce2c182c..201b88ba8 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -458,16 +458,15 @@ const overdueTasks = async (overDueTasks) => { /** * Fetch all overdue tasks objects - * @param overdueTasks : tasks which are overdue - * @return {Promsie} + * @return {Array} */ -const getAllOverDueTasks = async () => { +const getOverdueTasks = async () => { try { const currentTime = Math.floor(Date.now() / 1000); const overdueTasksSnapshot = await tasksModel.where("endsOn", "<", currentTime).get(); const tasks = buildTasks(overdueTasksSnapshot); - const promises = tasks.map((task) => fromFirestoreData(task)); - const overDueTasks = await Promise.all(promises); + const tasksPromises = tasks.map((task) => fromFirestoreData(task)); + const overDueTasks = await Promise.all(tasksPromises); return overDueTasks; } catch (err) { logger.error("error getting all overdue tasks", err); @@ -488,6 +487,6 @@ module.exports = { overdueTasks, addDependency, fetchTaskByIssueId, - getAllOverDueTasks, + getOverdueTasks, fetchPaginatedTasks, }; diff --git a/test/integration/extensionRequests.test.js b/test/integration/extensionRequests.test.js index 3ebe25197..6ea9c66f1 100644 --- a/test/integration/extensionRequests.test.js +++ b/test/integration/extensionRequests.test.js @@ -255,6 +255,29 @@ describe("Extension Requests", function () { return done(); }); }); + it("Should return failure response after adding the extension request (sending wrong assignee info)", function (done) { + chai + .request(app) + .post("/extension-requests") + .set("cookie", `${cookieName}=${appOwnerjwt}`) + .send({ + taskId: taskId0, + title: "change ETA", + assignee: "hello", + oldEndsOn: 1234, + newEndsOn: 1235, + reason: "family event", + status: "PENDING", + }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(400); + expect(res.body.message).to.equal("User with this id or username doesn't exist."); + return done(); + }); + }); it("Should return fail response if someone try to create a extension request for someone else and is not a super user", function (done) { chai .request(app) @@ -276,7 +299,9 @@ describe("Extension Requests", function () { expect(res).to.have.status(403); expect(res.body).to.be.a("object"); - expect(res.body.message).to.equal("Only Super User can create an extension request for this task."); + expect(res.body.message).to.equal( + "Only assigned user and super user can create an extension request for this task." + ); return done(); }); }); From 8d18645b9c3217fff810adf02eb134c802cab5d0 Mon Sep 17 00:00:00 2001 From: Ravi kumar <1002kumarravi@gmail.com> Date: Fri, 4 Aug 2023 11:18:28 +0530 Subject: [PATCH 039/105] fix: convert date type into unix timestamp --- controllers/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/auth.js b/controllers/auth.js index 8cf7c5c65..77aeaa040 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -52,7 +52,7 @@ const githubAuthCallback = (req, res, next) => { userData = { github_id: user.username, github_display_name: user.displayName, - github_created_at: user._json.created_at, + github_created_at: Number((new Date(user._json.created_at).getTime() / 1000).toFixed(0)), }; const { userId, incompleteUserDetails } = await users.addOrUpdate(userData); From 605ad9223730b2c0b05c7accbdfcd8076edf92a3 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sat, 5 Aug 2023 02:09:43 +0530 Subject: [PATCH 040/105] TEST: added model error test case --- test/unit/models/users.test.js | 38 +++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 137d69c96..47a27756d 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -5,6 +5,7 @@ /* eslint-disable security/detect-object-injection */ const chai = require("chai"); +const sinon = require("sinon"); const { expect } = chai; const cleanDb = require("../../utils/cleanDb"); @@ -239,10 +240,17 @@ describe("users", function () { await Promise.all(addUsersPromises); }); + afterEach(function () { + sinon.restore(); + }); + it("should update archived role to true if in_discord is false", async function () { await users.archiveUserIfNotInDiscord(); - const updatedUsers = await userModel.where("roles.in_discord", "==", false).get(); + const updatedUsers = await userModel + .where("roles.in_discord", "==", false) + .where("roles.archived", "==", false) + .get(); updatedUsers.forEach((user) => { const userData = user.data(); @@ -250,6 +258,34 @@ describe("users", function () { expect(userData.roles.archived).to.be.equal(true); }); }); + + it("should throw an error if firebase batch operation fails", async function () { + const stub = sinon.stub(firestore, "batch"); + stub.returns({ + update: function () {}, + commit: function () { + throw new Error("Firestore batch update failed"); + }, + }); + + try { + await users.archiveUserIfNotInDiscord(); + } catch (error) { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal("An internal server error occurred"); + } + + const updatedUsers = await userModel + .where("roles.in_discord", "==", false) + .where("roles.archived", "==", false) + .get(); + + updatedUsers.forEach((user) => { + const userData = user.data(); + expect(userData.roles.in_discord).to.be.equal(false); + expect(userData.roles.archived).to.be.not.equal(true); + }); + }); }); describe("remove github token from users", function () { From 763f3595dbc0623eacf79a317e74f600ecae0933 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sat, 5 Aug 2023 02:11:16 +0530 Subject: [PATCH 041/105] REFACTOR: added payload validator middleware --- middlewares/validators/user.js | 24 ++++++++- test/unit/middlewares/user-validator.test.js | 52 +++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/middlewares/validators/user.js b/middlewares/validators/user.js index 9e4dfdfb8..62091e544 100644 --- a/middlewares/validators/user.js +++ b/middlewares/validators/user.js @@ -1,7 +1,11 @@ const { customWordCountValidator } = require("../../utils/customWordCountValidator"); const joi = require("joi"); -const { USER_STATUS } = require("../../constants/users"); +const { + USER_STATUS, + USERS_PATCH_HANDLER_ACTIONS, + USERS_PATCH_HANDLER_ERROR_MESSAGES, +} = require("../../constants/users"); const ROLES = require("../../constants/roles"); const { IMAGE_VERIFICATION_TYPES } = require("../../constants/imageVerificationTypes"); const { userState } = require("../../constants/userStatus"); @@ -238,6 +242,23 @@ async function validateUpdateRoles(req, res, next) { } } +async function validateUsersPatchHandler(req, res, next) { + const requestBodySchema = joi.object({ + action: joi + .string() + .valid(USERS_PATCH_HANDLER_ACTIONS.ARCHIVE_USERS, USERS_PATCH_HANDLER_ACTIONS.NON_VERFIED_DISCORD_USERS) + .required(), + }); + + try { + await requestBodySchema.validateAsync(req.body); + next(); + } catch (error) { + logger.error("Error in validating action payload", error); + res.boom.badRequest(`${USERS_PATCH_HANDLER_ERROR_MESSAGES.VALIDATE_PAYLOAD}: ${error.message}`); + } +} + module.exports = { updateUser, updateProfileURL, @@ -246,4 +267,5 @@ module.exports = { validateUserQueryParams, validateImageVerificationQuery, validateUpdateRoles, + validateUsersPatchHandler, }; diff --git a/test/unit/middlewares/user-validator.test.js b/test/unit/middlewares/user-validator.test.js index ecbc9fb89..47a328b7b 100644 --- a/test/unit/middlewares/user-validator.test.js +++ b/test/unit/middlewares/user-validator.test.js @@ -1,5 +1,5 @@ const sinon = require("sinon"); -const { validateJoinData } = require("./../../../middlewares/validators/user"); +const { validateJoinData, validateUsersPatchHandler } = require("./../../../middlewares/validators/user"); const joinData = require("./../../fixtures/user/join"); const { expect } = require("chai"); @@ -34,4 +34,54 @@ describe("Middleware | Validators | User", function () { expect(nextSpy.calledOnce).to.be.equal(false); }); }); + + describe("User validator for usersPatchHandler", function () { + it("should call the next for api archiveUsers", async function () { + const req = {}; + + const res = { + boom: { + badRequest: () => {}, + }, + }; + + const next = sinon.spy(); + await validateUsersPatchHandler(req, res, next); + expect(next.calledOnce).to.be.equal(true); + }); + + it("should call the next for api nonVerifiedDiscordUsers", async function () { + const req = { + body: { + action: "nonVerifiedDiscordUsers", + }, + }; + + const res = {}; + + const next = sinon.spy(); + await validateUsersPatchHandler(req, res, next); + expect(next.calledOnce).to.be.equal(true); + }); + + it("should stop the propagation of next", async function () { + const req = { + body: { + action: "", + }, + }; + + const res = { + boom: { + badRequest: () => {}, + }, + }; + + const next = sinon.spy(); + await validateUsersPatchHandler(req, res, next).catch((error) => { + expect(error).to.be.an.instanceOf(Error); + }); + expect(next.calledOnce).to.be.equal(false); + }); + }); }); From 1367c184d7716a8dc9a071096c39458735b3d32c Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sat, 5 Aug 2023 02:12:23 +0530 Subject: [PATCH 042/105] REFACTOR: added error & success states constants --- constants/users.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/constants/users.js b/constants/users.js index 9670d1f3d..74bf6b32b 100644 --- a/constants/users.js +++ b/constants/users.js @@ -21,10 +21,33 @@ const ALLOWED_FILTER_PARAMS = { const DOCUMENT_WRITE_SIZE = 500; +const USERS_PATCH_HANDLER_ACTIONS = { + ARCHIVE_USERS: "archiveUsers", + NON_VERFIED_DISCORD_USERS: "nonVerifiedDiscordUsers", +}; + +const USERS_PATCH_HANDLER_ERROR_MESSAGES = { + VALIDATE_PAYLOAD: "Invalid Payload", + ARCHIVE_USERS: { + NO_USERS_DATA_TO_UPDATE: "Couldn't find any users currently inactive in Discord but not archived.", + BATCH_DATA_UPDATED_FAILED: "Firebase batch operation failed", + }, +}; + +const USERS_PATCH_HANDLER_SUCCESS_MESSAGES = { + ARCHIVE_USERS: { + SUCCESSFULLY_UPDATED_DATA: "Successfully updated users archived role to true if in_discord role is false", + SUCCESSFULLY_COMPLETED_BATCH_UPDATES: "Successfully completed batch updates", + }, +}; + module.exports = { profileStatus, USER_STATUS, ALLOWED_FILTER_PARAMS, USER_SENSITIVE_DATA, DOCUMENT_WRITE_SIZE, + USERS_PATCH_HANDLER_ACTIONS, + USERS_PATCH_HANDLER_ERROR_MESSAGES, + USERS_PATCH_HANDLER_SUCCESS_MESSAGES, }; From 081639aed23ddf0b17b68e0d333d154f519fc166 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sat, 5 Aug 2023 02:14:47 +0530 Subject: [PATCH 043/105] FEAT: added failed users details in response --- services/users.js | 24 +++++++++++++++--------- test/unit/services/users.test.js | 22 +++++++++++++--------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/services/users.js b/services/users.js index 24d8c2101..1da2d6f63 100644 --- a/services/users.js +++ b/services/users.js @@ -1,17 +1,19 @@ +const { USERS_PATCH_HANDLER_SUCCESS_MESSAGES, USERS_PATCH_HANDLER_ERROR_MESSAGES } = require("../constants/users"); const firestore = require("../utils/firestore"); const userModel = firestore.collection("users"); -const archiveInactiveDiscordUsersInBulk = async (usersData) => { +const archiveUsers = async (usersData) => { const batch = firestore.batch(); - const updatedUsers = []; + const usersBatch = []; const summary = { totalUsersArchived: 0, totalOperationsFailed: 0, - updatedUserIds: [], + updatedUserDetails: [], + failedUserDetails: [], }; usersData.forEach((user) => { - const id = user.id; + const { id, first_name: firstName, last_name: lastName } = user; const updatedUserData = { ...user, roles: { @@ -20,21 +22,25 @@ const archiveInactiveDiscordUsersInBulk = async (usersData) => { }, }; batch.update(userModel.doc(id), updatedUserData); - updatedUsers.push(id); + usersBatch.push({ id, firstName, lastName }); }); try { await batch.commit(); summary.totalUsersArchived += usersData.length; - summary.updatedUserIds = [...updatedUsers]; - return { message: "Successfully completed batch updates", ...summary }; + summary.updatedUserDetails = [...usersBatch]; + return { + message: USERS_PATCH_HANDLER_SUCCESS_MESSAGES.ARCHIVE_USERS.SUCCESSFULLY_COMPLETED_BATCH_UPDATES, + ...summary, + }; } catch (err) { logger.error("Firebase batch Operation Failed!"); summary.totalOperationsFailed += usersData.length; - return { message: "Firebase batch operation failed", ...summary }; + summary.failedUserDetails = [...usersBatch]; + return { message: USERS_PATCH_HANDLER_ERROR_MESSAGES.ARCHIVE_USERS.BATCH_DATA_UPDATED_FAILED, ...summary }; } }; module.exports = { - archiveInactiveDiscordUsersInBulk, + archiveUsers, }; diff --git a/test/unit/services/users.test.js b/test/unit/services/users.test.js index 91169264e..82170d7e4 100644 --- a/test/unit/services/users.test.js +++ b/test/unit/services/users.test.js @@ -5,12 +5,12 @@ const firestore = require("../../../utils/firestore"); const userModel = firestore.collection("users"); const cleanDb = require("../../utils/cleanDb"); const userDataArray = require("../../fixtures/user/user")(); -const { archiveInactiveDiscordUsersInBulk } = require("../../../services/users"); +const { archiveUsers } = require("../../../services/users"); describe("Users services", function () { describe("archive inactive discord users in bulk", function () { const users = []; - const userIds = []; + const userDetails = []; beforeEach(async function () { const addUsersPromises = []; userDataArray.forEach((user) => { @@ -27,6 +27,7 @@ describe("Users services", function () { await Promise.all(addUsersPromises); users.length = 0; + userDetails.length = 0; const snapshot = await userModel .where("roles.in_discord", "==", false) @@ -36,8 +37,9 @@ describe("Users services", function () { snapshot.forEach((user) => { const id = user.id; const userData = user.data(); + const { first_name: firstName, last_name: lastName } = userData; users.push({ ...userData, id }); - userIds.push(id); + userDetails.push({ id, firstName, lastName }); }); }); @@ -47,13 +49,14 @@ describe("Users services", function () { }); it("Should return successful response", async function () { - const res = await archiveInactiveDiscordUsersInBulk(users); + const res = await archiveUsers(users); - expect(res).deep.equal({ + expect(res).to.deep.equal({ message: "Successfully completed batch updates", totalUsersArchived: 14, totalOperationsFailed: 0, - updatedUserIds: userIds, + updatedUserDetails: userDetails, + failedUserDetails: [], }); }); @@ -63,13 +66,14 @@ describe("Users services", function () { update: function () {}, }); - const res = await archiveInactiveDiscordUsersInBulk(users); + const res = await archiveUsers(users); - expect(res).deep.equal({ + expect(res).to.deep.equal({ message: "Firebase batch operation failed", totalUsersArchived: 0, totalOperationsFailed: 14, - updatedUserIds: [], + updatedUserDetails: [], + failedUserDetails: userDetails, }); }); }); From a873faf599d11a1676cb909cb9615a4fb4b47c5f Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sat, 5 Aug 2023 02:16:47 +0530 Subject: [PATCH 044/105] REFACTOR: resolved comments --- controllers/users.js | 44 +++++++++++++++++----------------- models/users.js | 16 +++++++++---- routes/users.js | 8 ++++++- test/integration/users.test.js | 40 ++++++++++++++++++------------- 4 files changed, 64 insertions(+), 44 deletions(-) diff --git a/controllers/users.js b/controllers/users.js index 62832ea1a..eae23032f 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -15,6 +15,11 @@ const { addRoleToUser, getDiscordMembers } = require("../services/discordService const { fetchAllUsers } = require("../models/users"); const { getQualifiers } = require("../utils/helper"); const { getFilteredPRsOrIssues } = require("../utils/pullRequests"); +const { + USERS_PATCH_HANDLER_ACTIONS, + USERS_PATCH_HANDLER_ERROR_MESSAGES, + USERS_PATCH_HANDLER_SUCCESS_MESSAGES, +} = require("../constants/users"); const verifyUser = async (req, res) => { const userId = req.userData.id; @@ -631,53 +636,48 @@ const archiveUserIfNotInDiscord = async () => { if (data.totalUsers === 0) { return { - message: "Couldn't find any users currently inactive in Discord but not archived.", + message: USERS_PATCH_HANDLER_ERROR_MESSAGES.ARCHIVE_USERS.NO_USERS_DATA_TO_UPDATE, summary: data, }; } - if (data.totalOperationsFailed === data.totalUsers) { - throw Error("Internal server error occured"); - } - return { - message: "Successfully updated users archived role to true if in_discord role is false", + message: USERS_PATCH_HANDLER_SUCCESS_MESSAGES.ARCHIVE_USERS.SUCCESSFULLY_UPDATED_DATA, summary: data, }; } catch (error) { logger.error(`Error while updating the archived role: ${error}`); - throw Error("Internal server error occured"); + throw Error(INTERNAL_SERVER_ERROR); } }; async function usersPatchHandler(req, res) { try { - if (!req.body && !req.body.action) { - return res.boom.badRequest("Invalid payload"); - } - const { action } = req.body; + let response; - if (action === "nonVerifiedDiscordUsers") { + if (action === USERS_PATCH_HANDLER_ACTIONS.NON_VERFIED_DISCORD_USERS) { const data = await nonVerifiedDiscordUsers(); - return res.status(200).json(data); - } else if (action === "archiveUsersIfNotInDiscord") { + response = data; + } + + if (action === USERS_PATCH_HANDLER_ACTIONS.ARCHIVE_USERS) { const debugQuery = req.query.debug?.toLowerCase(); const data = await archiveUserIfNotInDiscord(); if (debugQuery === "true") { - data.summary.updatedUserIds = data.summary.updatedUserIds.slice(-3); - return res.status(200).json(data); + data.summary.updatedUserDetails = data.summary.updatedUserDetails.slice(-3); + response = data; } else { - delete data.summary.updatedUserIds; + delete data.summary.updatedUserDetails; + delete data.summary.failedUserDetails; + response = data; } - - return res.status(200).json(data); - } else { - return res.boom.badRequest("Invalid payload"); } + + return res.status(200).json(response); } catch (error) { - logger.error("Error while handling the common route:", error); + logger.error("Error while handling the users common patch route:", error); return res.boom.badImplementation(INTERNAL_SERVER_ERROR); } } diff --git a/models/users.js b/models/users.js index 83b87406a..19c05b390 100644 --- a/models/users.js +++ b/models/users.js @@ -8,7 +8,7 @@ const firestore = require("../utils/firestore"); const { fetchWallet, createWallet } = require("../models/wallets"); const { updateUserStatus } = require("../models/userStatus"); const { arraysHaveCommonItem, chunks } = require("../utils/array"); -const { archiveInactiveDiscordUsersInBulk } = require("../services/users"); +const { archiveUsers } = require("../services/users"); const { ALLOWED_FILTER_PARAMS, DOCUMENT_WRITE_SIZE } = require("../constants/users"); const { userState } = require("../constants/userStatus"); const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); @@ -20,6 +20,7 @@ const userStatusModel = firestore.collection("usersStatus"); const photoVerificationModel = firestore.collection("photo-verification"); const { ITEM_TAG, USER_STATE } = ALLOWED_FILTER_PARAMS; const admin = require("firebase-admin"); +const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** * Adds or updates the user data @@ -581,7 +582,8 @@ const archiveUserIfNotInDiscord = async () => { totalUsers: snapshot.size, totalUsersArchived: 0, totalOperationsFailed: 0, - updatedUserIds: [], + updatedUserDetails: [], + failedUserDetails: [], }; if (snapshot.size === 0) { @@ -596,14 +598,20 @@ const archiveUserIfNotInDiscord = async () => { const userNotInDiscordChunks = chunks(usersNotInDiscord, DOCUMENT_WRITE_SIZE); for (const users of userNotInDiscordChunks) { - const res = await archiveInactiveDiscordUsersInBulk(users); + const res = await archiveUsers(users); summary = { ...summary, totalUsersArchived: (summary.totalUsersArchived += res.totalUsersArchived), totalOperationsFailed: (summary.totalOperationsFailed += res.totalOperationsFailed), - updatedUserIds: [...summary.updatedUserIds, ...res.updatedUserIds], + updatedUserDetails: [...summary.updatedUserDetails, ...res.updatedUserDetails], + failedUserDetails: [...summary.failedUserDetails, ...res.failedUserDetails], }; } + + if (summary.totalOperationsFailed === summary.totalUsers) { + throw Error(INTERNAL_SERVER_ERROR); + } + return summary; } catch (error) { logger.error(`Error in updating Users archived role: ${error}`); diff --git a/routes/users.js b/routes/users.js index d18a6f843..081cddf64 100644 --- a/routes/users.js +++ b/routes/users.js @@ -26,7 +26,13 @@ router.get("/:userId/intro", authenticate, authorizeRoles([SUPERUSER]), users.ge router.put("/self/intro", authenticate, userValidator.validateJoinData, users.addUserIntro); router.get("/:id/skills", users.getUserSkills); router.get("/:id/badges", getUserBadges); -router.patch("/", authenticate, authorizeRoles([SUPERUSER]), users.usersPatchHandler); +router.patch( + "/", + authenticate, + authorizeRoles([SUPERUSER]), + userValidator.validateUsersPatchHandler, + users.usersPatchHandler +); router.patch( "/:id/temporary/data", authenticate, diff --git a/test/integration/users.test.js b/test/integration/users.test.js index 424e75635..dcc37a99b 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1535,7 +1535,7 @@ describe("Users", function () { } expect(res).to.have.status(400); - expect(res.body.message).to.equal("Invalid payload"); + expect(res.body.message).to.equal('Invalid Payload: "action" is required'); return done(); }); }); @@ -1545,16 +1545,14 @@ describe("Users", function () { .request(app) .patch("/users") .set("cookie", `${cookieName}=${superUserAuthToken}`) - .send({ action: "archiveUsersIfNotInDiscord" }) + .send({ action: "archiveUsers" }) .end((err, res) => { if (err) { return done(err); } expect(res).to.have.status(200); - expect(res.body.summary).to.have.property("totalUsersArchived"); - expect(res.body.summary).to.have.property("totalOperationsFailed"); - expect(res.body.summary).to.have.property("totalUsers"); + expect(res.body.summary).to.have.all.keys(["totalUsersArchived", "totalOperationsFailed", "totalUsers"]); expect(res.body.summary).to.not.have.property("updatedUserIds"); expect(res.body.summary.totalUsersArchived).to.be.equal(3); expect(res.body.summary.totalUsers).to.be.equal(3); @@ -1579,12 +1577,10 @@ describe("Users", function () { .request(app) .patch("/users") .set("cookie", `${cookieName}=${superUserAuthToken}`) - .send({ action: "archiveUsersIfNotInDiscord" }); + .send({ action: "archiveUsers" }); expect(res).to.have.status(200); - expect(res.body.summary).to.have.property("totalUsersArchived"); - expect(res.body.summary).to.have.property("totalOperationsFailed"); - expect(res.body.summary).to.have.property("totalUsers"); + expect(res.body.summary).to.have.all.keys(["totalUsersArchived", "totalOperationsFailed", "totalUsers"]); expect(res.body.summary).to.not.have.property("updatedUserIds"); expect(res.body.summary.totalUsers).to.be.equal(0); expect(res.body.summary.totalUsersArchived).to.be.equal(0); @@ -1593,13 +1589,19 @@ describe("Users", function () { }); it("should throw an error if firestore batch operations fail for api archiveUsersIfNotInDiscord", async function () { - Sinon.stub(firestore, "batch").throws(new Error("something went wrong")); + const stub = Sinon.stub(firestore, "batch"); + stub.returns({ + update: function () {}, + commit: function () { + throw new Error("Firestore batch commit failed!"); + }, + }); const res = await chai .request(app) .patch(`/users`) .set("cookie", `${cookieName}=${superUserAuthToken}`) - .send({ action: "archiveUsersIfNotInDiscord" }); + .send({ action: "archiveUsers" }); expect(res.status).to.equal(500); const response = res.body; @@ -1611,21 +1613,25 @@ describe("Users", function () { .request(app) .patch("/users?debug=true") .set("cookie", `${cookieName}=${superUserAuthToken}`) - .send({ action: "archiveUsersIfNotInDiscord" }) + .send({ action: "archiveUsers" }) .end((err, res) => { if (err) { return done(err); } expect(res).to.have.status(200); - expect(res.body.summary).to.have.property("totalUsersArchived"); - expect(res.body.summary).to.have.property("totalOperationsFailed"); - expect(res.body.summary).to.have.property("totalUsers"); - expect(res.body.summary).to.have.property("updatedUserIds"); + expect(res.body.summary).to.have.all.keys([ + "totalUsersArchived", + "totalOperationsFailed", + "totalUsers", + "updatedUserDetails", + "failedUserDetails", + ]); expect(res.body.summary.totalUsersArchived).to.be.equal(3); expect(res.body.summary.totalUsers).to.be.equal(3); expect(res.body.summary.totalOperationsFailed).to.be.equal(0); - expect(res.body.summary.updatedUserIds.length).to.equal(3); + expect(res.body.summary.updatedUserDetails.length).to.equal(3); + expect(res.body.summary.failedUserDetails.length).to.equal(0); expect(res.body.message).to.equal( "Successfully updated users archived role to true if in_discord role is false" ); From 49bd0d4a72295eb8eb05effb5f3b30268374d086 Mon Sep 17 00:00:00 2001 From: Manish Devrani <79859472+manish591@users.noreply.github.com> Date: Sat, 5 Aug 2023 02:41:14 +0530 Subject: [PATCH 045/105] REFACTOR: fix lint issues --- test/unit/middlewares/user-validator.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/middlewares/user-validator.test.js b/test/unit/middlewares/user-validator.test.js index ea3ee6646..36d76230a 100644 --- a/test/unit/middlewares/user-validator.test.js +++ b/test/unit/middlewares/user-validator.test.js @@ -72,13 +72,13 @@ describe("Middleware | Validators | User", function () { action: "", }, }; - + const res = { boom: { badRequest: () => {}, }, }; - + const next = sinon.spy(); await validateUsersPatchHandler(req, res, next).catch((error) => { expect(error).to.be.an.instanceOf(Error); From 0404f052fe2936090be87d9bf05992b16bc47f04 Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Sat, 5 Aug 2023 02:57:57 +0530 Subject: [PATCH 046/105] TEST: updated user service test --- test/unit/services/users.test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/unit/services/users.test.js b/test/unit/services/users.test.js index 82170d7e4..009f05014 100644 --- a/test/unit/services/users.test.js +++ b/test/unit/services/users.test.js @@ -53,7 +53,7 @@ describe("Users services", function () { expect(res).to.deep.equal({ message: "Successfully completed batch updates", - totalUsersArchived: 14, + totalUsersArchived: 15, totalOperationsFailed: 0, updatedUserDetails: userDetails, failedUserDetails: [], @@ -64,6 +64,9 @@ describe("Users services", function () { const batchStub = Sinon.stub(firestore, "batch"); batchStub.returns({ update: function () {}, + commit: function () { + throw new Error("Firebase batch operation failed"); + }, }); const res = await archiveUsers(users); @@ -71,7 +74,7 @@ describe("Users services", function () { expect(res).to.deep.equal({ message: "Firebase batch operation failed", totalUsersArchived: 0, - totalOperationsFailed: 14, + totalOperationsFailed: 15, updatedUserDetails: [], failedUserDetails: userDetails, }); From 613e4664aaff50caf615d8c9c61595d9ebbd2b6c Mon Sep 17 00:00:00 2001 From: Pratiyush Kumar Date: Sat, 5 Aug 2023 14:59:55 +0530 Subject: [PATCH 047/105] changed the variables name --- models/users.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/models/users.js b/models/users.js index 80ee2bfb7..070aad3c3 100644 --- a/models/users.js +++ b/models/users.js @@ -550,24 +550,24 @@ const getUsersWithOnboardingState = async (query) => { const range = Number(time.split("d")[0]); const { allUserStatus } = await getAllUserStatus(query); const allUsersWithOnboardingState = filterUsersWithOnboardingState(allUserStatus); - const updatedOnboardingUsersWithDate = []; + const onboardedUsersInRange = []; const filteredUsers = []; for (const user of allUsersWithOnboardingState) { const result = await userModel.doc(user.userId).get(); filteredUsers.push(result.data()); } - filteredUsers.forEach((element) => { - if (element.discordJoinedAt) { - const userDiscordJoinedDate = new Date(element.discordJoinedAt); + filteredUsers.forEach((user) => { + if (user.discordJoinedAt) { + const userDiscordJoinedDate = new Date(user.discordJoinedAt); const currentTimeStamp = new Date().getTime(); const timeDifferenceInMilliseconds = currentTimeStamp - userDiscordJoinedDate.getTime(); const currentAndUserJoinedDateDifference = Math.floor(timeDifferenceInMilliseconds / (1000 * 60 * 60 * 24)); if (currentAndUserJoinedDateDifference > range) { - updatedOnboardingUsersWithDate.push(element); + onboardedUsersInRange.push(user); } } }); - return updatedOnboardingUsersWithDate; + return onboardedUsersInRange; }; /** * Fetch all users From 1bb76e398446357ffd2179bc1c1ead73c88f926e Mon Sep 17 00:00:00 2001 From: vivek lokhande Date: Sat, 5 Aug 2023 19:15:19 +0530 Subject: [PATCH 048/105] (#1367) added fix for api response --- controllers/auth.js | 8 +++----- middlewares/validators/qrCodeAuth.js | 1 + models/qrCodeAuth.js | 7 +++++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/controllers/auth.js b/controllers/auth.js index e54cc2722..4329a381b 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -2,7 +2,7 @@ const passport = require("passport"); const users = require("../models/users"); const QrCodeAuthModel = require("../models/qrCodeAuth"); const authService = require("../services/authService"); -const { SOMETHING_WENT_WRONG, DATA_ADDED_SUCCESSFULLY, BAD_REQUEST } = require("../constants/errorMessages"); +const { SOMETHING_WENT_WRONG, DATA_ADDED_SUCCESSFULLY } = require("../constants/errorMessages"); /** * Fetches the user info from GitHub and authenticates User @@ -86,10 +86,8 @@ const storeUserDeviceInfo = async (req, res) => { const userInfo = await QrCodeAuthModel.storeUserDeviceInfo(userJson); - if (!userInfo) { - return res.status(404).json({ - message: BAD_REQUEST, - }); + if (userInfo.userExists !== undefined && !userInfo.userExists) { + return res.boom.notFound("Document not found!"); } return res.status(201).json({ diff --git a/middlewares/validators/qrCodeAuth.js b/middlewares/validators/qrCodeAuth.js index 7b549328a..83254540a 100644 --- a/middlewares/validators/qrCodeAuth.js +++ b/middlewares/validators/qrCodeAuth.js @@ -12,6 +12,7 @@ const storeUserDeviceInfo = async (req, res, next) => { next(); } catch (error) { logger.error(`Error validating newDeviceInfo payload : ${error}`); + res.boom.badRequest(error.details[0].message); } }; diff --git a/models/qrCodeAuth.js b/models/qrCodeAuth.js index 36f8b8c01..219dc1033 100644 --- a/models/qrCodeAuth.js +++ b/models/qrCodeAuth.js @@ -41,6 +41,13 @@ const storeUserDeviceInfo = async (userDeviceInfoData) => { try { const { user_id: userId } = userDeviceInfoData; const user = await userModel.doc(userId).get(); + + if (!user.data()) { + return { + userExists: false, + }; + } + if (user.data()) { await QrCodeAuthModel.doc(userId).set(userDeviceInfoData); From 510dd1d3f2b639c8a7a1987e610261e1c69e0736 Mon Sep 17 00:00:00 2001 From: vivek lokhande Date: Sat, 5 Aug 2023 19:15:42 +0530 Subject: [PATCH 049/105] (#1367) updated tests for the code --- test/integration/qrCodeAuth.test.js | 37 ++++++++++++++++++++++------- test/unit/models/qrCodeAuth.test.js | 11 +++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/test/integration/qrCodeAuth.test.js b/test/integration/qrCodeAuth.test.js index b57aa2d84..3f6fba421 100644 --- a/test/integration/qrCodeAuth.test.js +++ b/test/integration/qrCodeAuth.test.js @@ -14,6 +14,7 @@ const cookieName = config.get("userToken.cookieName"); // Import fixtures let userDeviceInfoData; let wrongUserDeviceInfoData; +let wrongUserDeviceInfoDataWithWrongDeviceInfo; let userId; const user = userData[0]; @@ -21,8 +22,10 @@ describe("QrCodeAuth", function () { describe("POST call for adding user", function () { beforeEach(async function () { userId = await addUser(user); + userDeviceInfoData = { ...userDeviceInfoDataArray[0], user_id: userId }; wrongUserDeviceInfoData = userDeviceInfoDataArray[0]; + wrongUserDeviceInfoDataWithWrongDeviceInfo = { ...userDeviceInfoDataArray[0], user_id: userId, device_info: 2 }; }); afterEach(async function () { await cleanDb(); @@ -48,21 +51,39 @@ describe("QrCodeAuth", function () { }); }); - it("Should return a 500 status code and the correct error message when an error occurs while storing user device info", function (done) { + it("should fail with 404, when the user is not found", function (done) { chai .request(app) .post("/auth/qr-code-auth") .send(wrongUserDeviceInfoData) .end((err, res) => { if (err) { - return done(); + return done(err); + } + + expect(res).to.have.status(404); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("Document not found!"); + expect(res.body.error).to.equal("Not Found"); + + return done(); + }); + }); + + it("should throw 400, if the validation of the values passed in the body does not pass", function (done) { + chai + .request(app) + .post("/auth/qr-code-auth") + .send(wrongUserDeviceInfoDataWithWrongDeviceInfo) + .end((err, res) => { + if (err) { + return done(err); } - expect(res).to.have.status(500); - expect(res.body).to.eql({ - statusCode: 500, - error: "Internal Server Error", - message: "An internal server error occurred", - }); + + expect(res).to.have.status(400); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal('"device_info" must be a string'); + expect(res.body.error).to.equal("Bad Request"); return done(); }); diff --git a/test/unit/models/qrCodeAuth.test.js b/test/unit/models/qrCodeAuth.test.js index e8097b50c..2e8260696 100644 --- a/test/unit/models/qrCodeAuth.test.js +++ b/test/unit/models/qrCodeAuth.test.js @@ -17,6 +17,7 @@ describe("mobile auth", function () { await cleanDb(); }); describe("storeUserDeviceInfo", function () { + const WRONG_USER_ID = "xynsasd"; it("should store user Id and device info of user for mobile auth", async function () { const userData = userDataArray[0]; const { userId } = await users.addOrUpdate(userData); @@ -46,6 +47,16 @@ describe("mobile auth", function () { expect(deviceId).to.be.a("string"); expect(authorizationStatus).to.be.a("string"); }); + + it("should return userExist as false when the user document is not found", async function () { + const userDeviceInfoData = { + ...userDeviceInfoDataArray[0], + user_id: WRONG_USER_ID, + authorization_status: "NOT_INIT", + }; + const response = await qrCodeAuth.storeUserDeviceInfo(userDeviceInfoData); + expect(response.userExists).to.be.equal(false); + }); }); describe("updateAuthStatus", function () { From dc52cc9f8f03f360e8d90e039ccb7c47c4e3a8e4 Mon Sep 17 00:00:00 2001 From: vivek lokhande Date: Sat, 5 Aug 2023 19:25:55 +0530 Subject: [PATCH 050/105] (#1367) address failing test for validator --- test/unit/middlewares/qrCodeAuthValidator.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/unit/middlewares/qrCodeAuthValidator.test.js b/test/unit/middlewares/qrCodeAuthValidator.test.js index e890dea0b..217e83c8d 100644 --- a/test/unit/middlewares/qrCodeAuthValidator.test.js +++ b/test/unit/middlewares/qrCodeAuthValidator.test.js @@ -40,7 +40,11 @@ describe("qrCodeAuth", function () { }, }; - const res = {}; + const res = { + boom: { + badRequest: () => {}, + }, + }; const nextSpy = Sinon.spy(); await validateAuthStatus(req, res, nextSpy); From afa09b531b831ab86240e5268b20a50804243e61 Mon Sep 17 00:00:00 2001 From: vivek lokhande Date: Sat, 5 Aug 2023 19:31:41 +0530 Subject: [PATCH 051/105] (#1367) address failing validator test --- test/unit/middlewares/qrCodeAuthValidator.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/unit/middlewares/qrCodeAuthValidator.test.js b/test/unit/middlewares/qrCodeAuthValidator.test.js index 217e83c8d..5228b6f2f 100644 --- a/test/unit/middlewares/qrCodeAuthValidator.test.js +++ b/test/unit/middlewares/qrCodeAuthValidator.test.js @@ -25,7 +25,11 @@ describe("qrCodeAuth", function () { }, }; - const res = {}; + const res = { + boom: { + badRequest: () => {}, + }, + }; const nextSpy = Sinon.spy(); await storeUserDeviceInfo(req, res, nextSpy); @@ -40,11 +44,7 @@ describe("qrCodeAuth", function () { }, }; - const res = { - boom: { - badRequest: () => {}, - }, - }; + const res = {}; const nextSpy = Sinon.spy(); await validateAuthStatus(req, res, nextSpy); From c14d25d69ba871336bbfe4a9f0cfcb91bcb0cffb Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sun, 6 Aug 2023 01:16:10 +0530 Subject: [PATCH 052/105] refactor: changed the flow for overdue tasks API --- constants/tasks.js | 1 + controllers/tasks.js | 20 +------------ models/tasks.js | 42 ++++++++++++-------------- routes/tasks.js | 1 - test/integration/tasks.test.js | 55 ++++++++++------------------------ 5 files changed, 36 insertions(+), 83 deletions(-) diff --git a/constants/tasks.js b/constants/tasks.js index 06dc326e0..37e22ca37 100644 --- a/constants/tasks.js +++ b/constants/tasks.js @@ -20,6 +20,7 @@ const TASK_STATUS = { RELEASED: "RELEASED", VERIFIED: "VERIFIED", DONE: "DONE", + OVERDUE: "OVERDUE", }; // TODO: convert this to new task status diff --git a/controllers/tasks.js b/controllers/tasks.js index 353979265..dfd36134b 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -4,7 +4,7 @@ const { addLog } = require("../models/logs"); const { USER_STATUS } = require("../constants/users"); const { addOrUpdate, getRdsUserInfoByGitHubUsername } = require("../models/users"); const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING } = TASK_STATUS_OLD; -const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED, MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE } = TASK_STATUS; +const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED } = TASK_STATUS; const { INTERNAL_SERVER_ERROR, SOMETHING_WENT_WRONG } = require("../constants/errorMessages"); const dependencyModel = require("../models/tasks"); const { transformQuery } = require("../utils/tasks"); @@ -401,23 +401,6 @@ const assignTask = async (req, res) => { } }; -const currentOverdueTasks = async (req, res) => { - try { - const overdueTasks = await tasks.getOverdueTasks(); - const nonOverdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; - const overdueTasksFiltered = overdueTasks.filter( - (task) => !nonOverdueTasksStatus.includes(task.status) && task.assignee - ); - return res.json({ - message: "Overdue Tasks returned successfully!", - overdueTasks: overdueTasksFiltered, - }); - } catch (err) { - logger.error(`Error while fetching overdue tasks : ${err}`); - return res.boom.badImplementation("An internal server error occured"); - } -}; - module.exports = { addNewTask, fetchTasks, @@ -428,5 +411,4 @@ module.exports = { updateTaskStatus, overdueTasks, assignTask, - currentOverdueTasks, }; diff --git a/models/tasks.js b/models/tasks.js index 201b88ba8..c0d4a969c 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -5,7 +5,7 @@ const dependencyModel = firestore.collection("taskDependencies"); const userUtils = require("../utils/users"); const { fromFirestoreData, toFirestoreData, buildTasks } = require("../utils/tasks"); const { TASK_TYPE, TASK_STATUS, TASK_STATUS_OLD, TASK_SIZE } = require("../constants/tasks"); -const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, COMPLETED } = TASK_STATUS; +const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, COMPLETED, MERGED, RELEASED, VERIFIED, AVAILABLE } = TASK_STATUS; const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING, OLD_COMPLETED } = TASK_STATUS_OLD; /** @@ -117,9 +117,13 @@ const getBuiltTasks = async (tasksSnapshot) => { const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, prev }) => { try { - const initialQuery = status - ? tasksModel.where("status", "==", status).orderBy("title") - : tasksModel.orderBy("title"); + let initialQuery; + if (status === TASK_STATUS.OVERDUE) { + const currentTime = Math.floor(Date.now() / 1000); + initialQuery = tasksModel.where("endsOn", "<", currentTime); + } else { + initialQuery = status ? tasksModel.where("status", "==", status).orderBy("title") : tasksModel.orderBy("title"); + } let queryDoc = initialQuery; if (prev) { @@ -148,6 +152,17 @@ const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, const nextDoc = await initialQuery.startAfter(last).limit(1).get(); const allTasks = await getBuiltTasks(snapshot); + + if (status === TASK_STATUS.OVERDUE) { + const nonOverdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; + const overdueTasks = allTasks.filter((task) => !nonOverdueTasksStatus.includes(task.status) && task.assignee); + return { + allTasks: overdueTasks, + next: nextDoc.docs[0]?.id ?? "", + prev: prevDoc.docs[0]?.id ?? "", + }; + } + return { allTasks, next: nextDoc.docs[0]?.id ?? "", @@ -456,24 +471,6 @@ const overdueTasks = async (overDueTasks) => { } }; -/** - * Fetch all overdue tasks objects - * @return {Array} - */ -const getOverdueTasks = async () => { - try { - const currentTime = Math.floor(Date.now() / 1000); - const overdueTasksSnapshot = await tasksModel.where("endsOn", "<", currentTime).get(); - const tasks = buildTasks(overdueTasksSnapshot); - const tasksPromises = tasks.map((task) => fromFirestoreData(task)); - const overDueTasks = await Promise.all(tasksPromises); - return overDueTasks; - } catch (err) { - logger.error("error getting all overdue tasks", err); - throw err; - } -}; - module.exports = { updateTask, fetchTasks, @@ -487,6 +484,5 @@ module.exports = { overdueTasks, addDependency, fetchTaskByIssueId, - getOverdueTasks, fetchPaginatedTasks, }; diff --git a/routes/tasks.js b/routes/tasks.js index 745e21dbc..c10ba28db 100644 --- a/routes/tasks.js +++ b/routes/tasks.js @@ -12,7 +12,6 @@ const { ALL_TASKS } = require("../constants/cacheKeys"); router.get("/", getTasksValidator, cacheResponse({ invalidationKey: ALL_TASKS, expiry: 10 }), tasks.fetchTasks); router.get("/self", authenticate, tasks.getSelfTasks); router.get("/overdue", authenticate, authorizeRoles([SUPERUSER]), tasks.overdueTasks); -router.get("/overdue/current", authenticate, authorizeRoles([SUPERUSER]), tasks.currentOverdueTasks); router.post( "/", authenticate, diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index eec9167c1..fa366330b 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -81,46 +81,6 @@ describe("Tasks", function () { sinon.restore(); }); - describe("GET /tasks/overdue/current", function () { - it("Should return all the current overdue Tasks", function (done) { - chai - .request(app) - .get("/tasks/overdue/current") - .set("cookie", `${cookieName}=${superUserJwt}`) - .end((err, res) => { - if (err) { - return done(err); - } - expect(res).to.have.status(200); - expect(res.body.message).to.be.equal("Overdue Tasks returned successfully!"); - expect(res.body.overdueTasks[0].id).to.be.oneOf([taskId, taskId1]); - expect(res.body.overdueTasks[1].id).to.be.oneOf([taskId, taskId1]); - return done(); - }); - }); - it("Should return 401 if someone other than superuser logged in", function (done) { - chai - .request(app) - .get(`/tasks/overdue/current`) - .set("cookie", `${cookieName}=${jwt}`) - .end((err, res) => { - if (err) { - return done(); - } - - expect(res).to.have.status(401); - expect(res.body).to.be.an("object"); - expect(res.body).to.eql({ - statusCode: 401, - error: "Unauthorized", - message: "You are not authorized for this action.", - }); - - return done(); - }); - }); - }); - describe("POST /tasks - creates a new task", function () { it("Should return success response after adding the task", function (done) { chai @@ -251,6 +211,21 @@ describe("Tasks", function () { }); }); + it("Should get all overdue tasks GET /tasks", function (done) { + chai + .request(app) + .get(`/tasks?dev=true&status=overdue`) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.tasks[0].id).to.be.oneOf([taskId, taskId1]); + return done(); + }); + }); + it("Should get tasks when correct query parameters are passed", function (done) { chai .request(app) From 183dc4877da14b5062e5d12e23d30cc49a3e597c Mon Sep 17 00:00:00 2001 From: Pratiyush Kumar Date: Sun, 6 Aug 2023 11:24:47 +0530 Subject: [PATCH 053/105] removed the filter utility function --- models/users.js | 25 ++++++++++++++++++------- utils/userStatus.js | 5 ----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/models/users.js b/models/users.js index 070aad3c3..b8a159c50 100644 --- a/models/users.js +++ b/models/users.js @@ -6,7 +6,7 @@ const walletConstants = require("../constants/wallets"); const firestore = require("../utils/firestore"); const { fetchWallet, createWallet } = require("../models/wallets"); -const { updateUserStatus, getAllUserStatus } = require("../models/userStatus"); +const { updateUserStatus } = require("../models/userStatus"); const { arraysHaveCommonItem } = require("../utils/array"); const { ALLOWED_FILTER_PARAMS } = require("../constants/users"); const { userState } = require("../constants/userStatus"); @@ -19,7 +19,6 @@ const userStatusModel = firestore.collection("usersStatus"); const photoVerificationModel = firestore.collection("photo-verification"); const { ITEM_TAG, USER_STATE } = ALLOWED_FILTER_PARAMS; const admin = require("firebase-admin"); -const { filterUsersWithOnboardingState } = require("../utils/userStatus"); /** * Adds or updates the user data @@ -546,13 +545,25 @@ const getUsersBasedOnFilter = async (query) => { }; const getUsersWithOnboardingState = async (query) => { - const time = query.time; - const range = Number(time.split("d")[0]); - const { allUserStatus } = await getAllUserStatus(query); - const allUsersWithOnboardingState = filterUsersWithOnboardingState(allUserStatus); + const allUserStatus = []; const onboardedUsersInRange = []; const filteredUsers = []; - for (const user of allUsersWithOnboardingState) { + + const time = query.time; + const range = Number(time.split("d")[0]); + + const data = await userStatusModel.where("currentStatus.state", "==", query.state).get(); + data.forEach((doc) => { + const currentUserStatus = { + id: doc.id, + userId: doc.data().userId, + currentStatus: doc.data().currentStatus, + monthlyHours: doc.data().monthlyHours, + }; + allUserStatus.push(currentUserStatus); + }); + + for (const user of allUserStatus) { const result = await userModel.doc(user.userId).get(); filteredUsers.push(result.data()); } diff --git a/utils/userStatus.js b/utils/userStatus.js index 7e1e9ce65..d0565474a 100644 --- a/utils/userStatus.js +++ b/utils/userStatus.js @@ -294,10 +294,6 @@ const generateErrorResponse = (message) => { }; }; -const filterUsersWithOnboardingState = (data) => { - return data.filter((item) => item.currentStatus.state === userState.ONBOARDING); -}; - module.exports = { getUserIdBasedOnRoute, getTomorrowTimeStamp, @@ -311,5 +307,4 @@ module.exports = { checkIfUserHasLiveTasks, generateErrorResponse, generateNewStatus, - filterUsersWithOnboardingState, }; From b1abccb502147dfb420e06ff331216bfcaa15dec Mon Sep 17 00:00:00 2001 From: Ravi kumar <1002kumarravi@gmail.com> Date: Sun, 6 Aug 2023 18:44:42 +0530 Subject: [PATCH 054/105] fix: Unix Timestamp method --- controllers/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/auth.js b/controllers/auth.js index e038e1226..9c6554f68 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -52,7 +52,7 @@ const githubAuthCallback = (req, res, next) => { userData = { github_id: user.username, github_display_name: user.displayName, - github_created_at: Number((new Date(user._json.created_at).getTime() / 1000).toFixed(0)), + github_created_at: Number(new Date(user._json.created_at).getTime()), created_at: Date.now(), updated_at: Date.now(), }; From 36edb2479e75a5e18309752848513c12189c1f4c Mon Sep 17 00:00:00 2001 From: Ravi kumar <1002kumarravi@gmail.com> Date: Sun, 6 Aug 2023 19:15:53 +0530 Subject: [PATCH 055/105] feat: test for github_created_at fields --- test/fixtures/auth/githubUserInfo.js | 44 ++++++++++++++++++++++++++++ test/fixtures/user/user.js | 15 ++++++++++ test/unit/models/users.test.js | 8 +++++ 3 files changed, 67 insertions(+) diff --git a/test/fixtures/auth/githubUserInfo.js b/test/fixtures/auth/githubUserInfo.js index 153264e0a..bdf2ec452 100644 --- a/test/fixtures/auth/githubUserInfo.js +++ b/test/fixtures/auth/githubUserInfo.js @@ -101,5 +101,49 @@ module.exports = () => { created_at: "2020-09-06T16:21:38Z", updated_at: "2023-07-26T09:29:37Z", }, + { + id: "86847625", + nodeId: "MDQ6VXNlcjg2ODQ3NjI1", + displayName: "Ravi kumar", + username: "ravikumar1002", + profileUrl: "https://github.com/ravikumar1002", + photos: [{ value: "https://avatars.githubusercontent.com/u/86847625?v=4" }], + provider: "github", + _raw: '{"login":"ravikumar1002","id":86847625,"node_id":"MDQ6VXNlcjg2ODQ3NjI1","avatar_url":"https://avatars.githubusercontent.com/u/86847625?v=4","gravatar_id":"","url":"https://api.github.com/users/ravikumar1002","html_url":"https://github.com/ravikumar1002","followers_url":"https://api.github.com/users/ravikumar1002/followers","following_url":"https://api.github.com/users/ravikumar1002/following{/other_user}","gists_url":"https://api.github.com/users/ravikumar1002/gists{/gist_id}","starred_url":"https://api.github.com/users/ravikumar1002/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ravikumar1002/subscriptions","organizations_url":"https://api.github.com/users/ravikumar1002/orgs","repos_url":"https://api.github.com/users/ravikumar1002/repos","events_url":"https://api.github.com/users/ravikumar1002/events{/privacy}","received_events_url":"https://api.github.com/users/ravikumar1002/received_events","type":"User","site_admin":false,"name":"Ravi kumar","company":null,"blog":"https://ravikumar.dev/","location":"Delhi, India","email":null,"hireable":null,"bio":"Frontend developer","twitter_username":"kumarravi1002","public_repos":47,"public_gists":0,"followers":9,"following":13,"created_at":"2021-07-02T16:40:44Z","updated_at":"2023-07-27T00:12:32Z"}', + _json: { + login: "ravikumar1002", + id: 86847625, + node_id: "MDQ6VXNlcjg2ODQ3NjI1", + avatar_url: "https://avatars.githubusercontent.com/u/86847625?v=4", + gravatar_id: "", + url: "https://api.github.com/users/ravikumar1002", + html_url: "https://github.com/ravikumar1002", + followers_url: "https://api.github.com/users/ravikumar1002/followers", + following_url: "https://api.github.com/users/ravikumar1002/following{/other_user}", + gists_url: "https://api.github.com/users/ravikumar1002/gists{/gist_id}", + starred_url: "https://api.github.com/users/ravikumar1002/starred{/owner}{/repo}", + subscriptions_url: "https://api.github.com/users/ravikumar1002/subscriptions", + organizations_url: "https://api.github.com/users/ravikumar1002/orgs", + repos_url: "https://api.github.com/users/ravikumar1002/repos", + events_url: "https://api.github.com/users/ravikumar1002/events{/privacy}", + received_events_url: "https://api.github.com/users/ravikumar1002/received_events", + type: "User", + site_admin: false, + name: "Ravi kumar", + company: null, + blog: "https://ravikumar.dev/", + location: "Delhi, India", + email: null, + hireable: null, + bio: "Frontend developer", + twitter_username: "kumarravi1002", + public_repos: 47, + public_gists: 0, + followers: 9, + following: 13, + created_at: "2021-07-02T16:40:44Z", + updated_at: "2023-07-27T00:12:32Z", + }, + }, ]; }; diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index caf2735c5..7938c3271 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -343,5 +343,20 @@ module.exports = () => { updated_at: Date.now(), created_at: Date.now(), }, + { + username: "ravikumar1002", + first_name: "ravi", + last_name: "kumar", + github_id: githubUserInfo[2].username, + github_display_name: githubUserInfo[2].displayName, + github_created_at: Number(new Date(githubUserInfo[2]._json.created_at).getTime()), + roles: { + member: false, + in_discord: true, + }, + incompleteUserDetails: false, + updated_at: Date.now(), + created_at: Date.now(), + }, ]; }; diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 42d7cad61..cf2ce3e77 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -102,6 +102,14 @@ describe("users", function () { expect(user).to.haveOwnProperty("updated_at"); expect(userExists).to.equal(true); }); + + it("It should have github_created_at fields", async function () { + const userData = userDataArray[15]; + await users.addOrUpdate(userData); + const githubUsername = "ravikumar1002"; + const { user } = await users.fetchUser({ githubUsername }); + expect(user).to.haveOwnProperty("github_created_at"); + }); }); describe("user image verification", function () { From 1b745bf9836400d546e2e11d56826064eb136343 Mon Sep 17 00:00:00 2001 From: Ravi kumar <1002kumarravi@gmail.com> Date: Mon, 7 Aug 2023 14:01:40 +0530 Subject: [PATCH 056/105] remove: extra fixtures data --- test/fixtures/auth/githubUserInfo.js | 44 ---------------------------- test/fixtures/user/user.js | 16 +--------- test/unit/models/users.test.js | 4 +-- 3 files changed, 3 insertions(+), 61 deletions(-) diff --git a/test/fixtures/auth/githubUserInfo.js b/test/fixtures/auth/githubUserInfo.js index bdf2ec452..153264e0a 100644 --- a/test/fixtures/auth/githubUserInfo.js +++ b/test/fixtures/auth/githubUserInfo.js @@ -101,49 +101,5 @@ module.exports = () => { created_at: "2020-09-06T16:21:38Z", updated_at: "2023-07-26T09:29:37Z", }, - { - id: "86847625", - nodeId: "MDQ6VXNlcjg2ODQ3NjI1", - displayName: "Ravi kumar", - username: "ravikumar1002", - profileUrl: "https://github.com/ravikumar1002", - photos: [{ value: "https://avatars.githubusercontent.com/u/86847625?v=4" }], - provider: "github", - _raw: '{"login":"ravikumar1002","id":86847625,"node_id":"MDQ6VXNlcjg2ODQ3NjI1","avatar_url":"https://avatars.githubusercontent.com/u/86847625?v=4","gravatar_id":"","url":"https://api.github.com/users/ravikumar1002","html_url":"https://github.com/ravikumar1002","followers_url":"https://api.github.com/users/ravikumar1002/followers","following_url":"https://api.github.com/users/ravikumar1002/following{/other_user}","gists_url":"https://api.github.com/users/ravikumar1002/gists{/gist_id}","starred_url":"https://api.github.com/users/ravikumar1002/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ravikumar1002/subscriptions","organizations_url":"https://api.github.com/users/ravikumar1002/orgs","repos_url":"https://api.github.com/users/ravikumar1002/repos","events_url":"https://api.github.com/users/ravikumar1002/events{/privacy}","received_events_url":"https://api.github.com/users/ravikumar1002/received_events","type":"User","site_admin":false,"name":"Ravi kumar","company":null,"blog":"https://ravikumar.dev/","location":"Delhi, India","email":null,"hireable":null,"bio":"Frontend developer","twitter_username":"kumarravi1002","public_repos":47,"public_gists":0,"followers":9,"following":13,"created_at":"2021-07-02T16:40:44Z","updated_at":"2023-07-27T00:12:32Z"}', - _json: { - login: "ravikumar1002", - id: 86847625, - node_id: "MDQ6VXNlcjg2ODQ3NjI1", - avatar_url: "https://avatars.githubusercontent.com/u/86847625?v=4", - gravatar_id: "", - url: "https://api.github.com/users/ravikumar1002", - html_url: "https://github.com/ravikumar1002", - followers_url: "https://api.github.com/users/ravikumar1002/followers", - following_url: "https://api.github.com/users/ravikumar1002/following{/other_user}", - gists_url: "https://api.github.com/users/ravikumar1002/gists{/gist_id}", - starred_url: "https://api.github.com/users/ravikumar1002/starred{/owner}{/repo}", - subscriptions_url: "https://api.github.com/users/ravikumar1002/subscriptions", - organizations_url: "https://api.github.com/users/ravikumar1002/orgs", - repos_url: "https://api.github.com/users/ravikumar1002/repos", - events_url: "https://api.github.com/users/ravikumar1002/events{/privacy}", - received_events_url: "https://api.github.com/users/ravikumar1002/received_events", - type: "User", - site_admin: false, - name: "Ravi kumar", - company: null, - blog: "https://ravikumar.dev/", - location: "Delhi, India", - email: null, - hireable: null, - bio: "Frontend developer", - twitter_username: "kumarravi1002", - public_repos: 47, - public_gists: 0, - followers: 9, - following: 13, - created_at: "2021-07-02T16:40:44Z", - updated_at: "2023-07-27T00:12:32Z", - }, - }, ]; }; diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index 7938c3271..e9746fd39 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -19,6 +19,7 @@ module.exports = () => { linkedin_id: "ankurnarkhede", github_id: githubUserInfo[0].username, github_display_name: githubUserInfo[0].displayName, + github_created_at: Number(new Date(githubUserInfo[0]._json.created_at).getTime()), isMember: true, phone: "1234567890", email: "abc@gmail.com", @@ -343,20 +344,5 @@ module.exports = () => { updated_at: Date.now(), created_at: Date.now(), }, - { - username: "ravikumar1002", - first_name: "ravi", - last_name: "kumar", - github_id: githubUserInfo[2].username, - github_display_name: githubUserInfo[2].displayName, - github_created_at: Number(new Date(githubUserInfo[2]._json.created_at).getTime()), - roles: { - member: false, - in_discord: true, - }, - incompleteUserDetails: false, - updated_at: Date.now(), - created_at: Date.now(), - }, ]; }; diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index cf2ce3e77..4fb868b21 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -104,9 +104,9 @@ describe("users", function () { }); it("It should have github_created_at fields", async function () { - const userData = userDataArray[15]; + const userData = userDataArray[0]; await users.addOrUpdate(userData); - const githubUsername = "ravikumar1002"; + const githubUsername = "ankur"; const { user } = await users.fetchUser({ githubUsername }); expect(user).to.haveOwnProperty("github_created_at"); }); From 3488b2c0ce68c6c8f6a1123858e6c259e39875c4 Mon Sep 17 00:00:00 2001 From: Pratiyush Kumar Date: Tue, 8 Aug 2023 07:23:16 +0530 Subject: [PATCH 057/105] added integration test case --- test/fixtures/user/user.js | 1 + test/integration/usersFilter.test.js | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index a88d923e1..1911205a2 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -63,6 +63,7 @@ module.exports = () => { github_id: "cartmanishere", linkedin_id: "pranav-gajjewar", twitter_id: "PGajjewar", + discordJoinedAt: "2023-04-06T01:47:34.488000+00:00", phone: "1234567891", email: "pgajjewar@gmail.com", roles: { diff --git a/test/integration/usersFilter.test.js b/test/integration/usersFilter.test.js index 9e0f5ed97..c5a9f156c 100644 --- a/test/integration/usersFilter.test.js +++ b/test/integration/usersFilter.test.js @@ -177,6 +177,29 @@ describe("Filter Users", function () { }); }); + it("Should search users based on Onboarding state and discord join more then 31 days", function (done) { + chai + .request(app) + .get("/users/search") + .query({ state: "ONBOARDING", time: "31d" }) + .set("cookie", `${cookieName}=${jwt}`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body).to.be.a("object"); + expect(res.body.count).to.be.a("number"); + expect(res.body.message).to.equal("Users found successfully!"); + expect(res.body.users).to.be.a("array"); + expect(res.body.users.length).to.equal(1); + res.body.users.forEach((user) => { + expect(user).to.have.property("discordJoinedAt"); + }); + return done(); + }); + }); + it("Should search users based on Tag", function (done) { chai .request(app) From b0b54d2f242ceb817e4835c51ba435af9e012aa8 Mon Sep 17 00:00:00 2001 From: Pratiyush Kumar Date: Tue, 8 Aug 2023 09:11:59 +0530 Subject: [PATCH 058/105] commiting unit testing --- models/users.js | 1 - test/fixtures/userStatus/userStatus.js | 87 ++++++++++++++++++++++++++ test/unit/models/users.test.js | 37 +++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/models/users.js b/models/users.js index b8a159c50..efd6c4bc8 100644 --- a/models/users.js +++ b/models/users.js @@ -551,7 +551,6 @@ const getUsersWithOnboardingState = async (query) => { const time = query.time; const range = Number(time.split("d")[0]); - const data = await userStatusModel.where("currentStatus.state", "==", query.state).get(); data.forEach((doc) => { const currentUserStatus = { diff --git a/test/fixtures/userStatus/userStatus.js b/test/fixtures/userStatus/userStatus.js index 9fc7c8bff..f7bdb5e40 100644 --- a/test/fixtures/userStatus/userStatus.js +++ b/test/fixtures/userStatus/userStatus.js @@ -74,6 +74,89 @@ const activeStatus = { }, }; +const usersData = [ + { + id: 0, + discordJoinedAt: "2023-04-06T01:47:34.488000+00:00", + first_name: "Mayank", + last_name: "Kumar", + }, + { + id: 1, + discordJoinedAt: "2023-03-24T01:47:34.488000+00:00", + first_name: "Rohan", + last_name: "Kumar", + }, + { + id: 2, + discordJoinedAt: "2023-07-04T01:47:34.488000+00:00", + first_name: "Raj", + last_name: "Kumar", + }, +]; + +const allUserStatus = [ + { + id: 1, + userId: "WxmZvzkPO5R6Fv1BjKdn", + currentStatus: { + until: "", + updatedAt: 1690243200000, + from: 1687737600000, + state: "ONBOARDING", + }, + }, + { + id: 2, + userId: "dKmOiIfCb8oWS6qryrSQ", + currentStatus: { + from: "1690633905", + state: "ONBOARDING", + updatedAt: "1690633905", + }, + }, + { + id: 3, + userId: "si9xTbdk93jrouu3SZGg", + currentStatus: { + from: "1690633905", + state: "ACTIVE", + updatedAt: "1690633905", + }, + }, +]; + +const allUserStatusWithOnBoardingStatus = [ + { + id: 1, + currentStatus: { + until: "", + updatedAt: 1690243200000, + from: 1687737600000, + state: "ONBOARDING", + }, + }, + { + id: 2, + currentStatus: { + from: "1690633905", + state: "ONBOARDING", + updatedAt: "1690633905", + }, + }, +]; + +const allUsersWithoutONBOARDINGSTATUS = [ + { + id: 3, + currentStatus: { + from: "1690633905", + state: "ACTIVE", + updatedAt: "1690633905", + }, + }, +]; + const generateUserStatusData = (state, updatedAt, from, until = "", message = "") => { return { currentStatus: { @@ -137,4 +220,8 @@ module.exports = { activeStatus, generateStatusDataForCancelOOO, generateStatusDataForState, + usersData, + allUserStatus, + allUserStatusWithOnBoardingStatus, + allUsersWithoutONBOARDINGSTATUS, }; diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index e379a8f59..6fa6a5f2c 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -11,7 +11,9 @@ const cleanDb = require("../../utils/cleanDb"); const users = require("../../../models/users"); const firestore = require("../../../utils/firestore"); const { userPhotoVerificationData, newUserPhotoVerificationData } = require("../../fixtures/user/photo-verification"); +const { allUserStatus, usersData, allUserStatusWithOnBoardingStatus } = require("../../fixtures/userStatus/userStatus"); const userModel = firestore.collection("users"); +const userStatusModel = firestore.collection("userStatus"); const joinModel = firestore.collection("applicants"); const userDataArray = require("../../fixtures/user/user")(); const joinData = require("../../fixtures/user/join")(); @@ -279,4 +281,39 @@ describe("users", function () { }); }); }); + describe("getUsersBasedOnFilter", function () { + beforeEach(async function () { + const addUsersPromises = []; + const userStatusPromises = []; + for (const user of usersData) { + addUsersPromises.push(await userModel.add(user)); + } + for (const element of allUserStatus) { + userStatusPromises.push(await userStatusModel.add(element)); + } + await Promise.all([...addUsersPromises, ...userStatusPromises]); + + const allUserDocs = await userModel.get(); + allUserDocs.forEach(function (doc) { + // console.log("doc.data", doc.data()); + }); + + const allStatusDocs = await userStatusModel.get(); + allStatusDocs.forEach(function (doc) { + // console.log("doc.data", doc.data()); + }); + }); + + afterEach(async function () { + await cleanDb(); + }); + it("should render users with onboarding state and time as 31days", async function () { + const query = { + state: "ONBOARDING", + time: "31d", + }; + const result = await users.getUsersBasedOnFilter(query); + expect(result).to.deep.equal(allUserStatusWithOnBoardingStatus); + }); + }); }); From 2e2702723edd31e6858443063dbfee6c266eef1c Mon Sep 17 00:00:00 2001 From: Pratiyush Kumar Date: Wed, 9 Aug 2023 08:52:02 +0530 Subject: [PATCH 059/105] unit testing is done --- test/fixtures/user/user.js | 3 + test/fixtures/userStatus/userStatus.js | 87 -------------------------- test/unit/models/users.test.js | 38 +++++------ 3 files changed, 18 insertions(+), 110 deletions(-) diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index 1911205a2..cb926e710 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -22,6 +22,7 @@ module.exports = () => { isMember: true, phone: "1234567890", email: "abc@gmail.com", + discordJoinedAt: "2023-04-06T01:47:34.488000+00:00", joined_discord: "2023-01-13T18:21:09.278000+00:00", roles: { member: true, @@ -47,6 +48,7 @@ module.exports = () => { github_id: "whydonti", linkedin_id: "nikhil-bhandarkar", twitter_id: "whatifi", + discordJoinedAt: "2023-04-06T01:47:34.488000+00:00", phone: "1234567891", email: "abc1@gmail.com", picture: { @@ -83,6 +85,7 @@ module.exports = () => { linkedin_id: "sagarbajpai", github_id: "sagarbajpai", github_display_name: "Sagar Bajpai", + discordJoinedAt: "2023-04-06T01:47:34.488000+00:00", phone: "1234567890", email: "abc@gmail.com", status: "active", diff --git a/test/fixtures/userStatus/userStatus.js b/test/fixtures/userStatus/userStatus.js index f7bdb5e40..9fc7c8bff 100644 --- a/test/fixtures/userStatus/userStatus.js +++ b/test/fixtures/userStatus/userStatus.js @@ -74,89 +74,6 @@ const activeStatus = { }, }; -const usersData = [ - { - id: 0, - discordJoinedAt: "2023-04-06T01:47:34.488000+00:00", - first_name: "Mayank", - last_name: "Kumar", - }, - { - id: 1, - discordJoinedAt: "2023-03-24T01:47:34.488000+00:00", - first_name: "Rohan", - last_name: "Kumar", - }, - { - id: 2, - discordJoinedAt: "2023-07-04T01:47:34.488000+00:00", - first_name: "Raj", - last_name: "Kumar", - }, -]; - -const allUserStatus = [ - { - id: 1, - userId: "WxmZvzkPO5R6Fv1BjKdn", - currentStatus: { - until: "", - updatedAt: 1690243200000, - from: 1687737600000, - state: "ONBOARDING", - }, - }, - { - id: 2, - userId: "dKmOiIfCb8oWS6qryrSQ", - currentStatus: { - from: "1690633905", - state: "ONBOARDING", - updatedAt: "1690633905", - }, - }, - { - id: 3, - userId: "si9xTbdk93jrouu3SZGg", - currentStatus: { - from: "1690633905", - state: "ACTIVE", - updatedAt: "1690633905", - }, - }, -]; - -const allUserStatusWithOnBoardingStatus = [ - { - id: 1, - currentStatus: { - until: "", - updatedAt: 1690243200000, - from: 1687737600000, - state: "ONBOARDING", - }, - }, - { - id: 2, - currentStatus: { - from: "1690633905", - state: "ONBOARDING", - updatedAt: "1690633905", - }, - }, -]; - -const allUsersWithoutONBOARDINGSTATUS = [ - { - id: 3, - currentStatus: { - from: "1690633905", - state: "ACTIVE", - updatedAt: "1690633905", - }, - }, -]; - const generateUserStatusData = (state, updatedAt, from, until = "", message = "") => { return { currentStatus: { @@ -220,8 +137,4 @@ module.exports = { activeStatus, generateStatusDataForCancelOOO, generateStatusDataForState, - usersData, - allUserStatus, - allUserStatusWithOnBoardingStatus, - allUsersWithoutONBOARDINGSTATUS, }; diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 6fa6a5f2c..9ef1a8c21 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -11,14 +11,16 @@ const cleanDb = require("../../utils/cleanDb"); const users = require("../../../models/users"); const firestore = require("../../../utils/firestore"); const { userPhotoVerificationData, newUserPhotoVerificationData } = require("../../fixtures/user/photo-verification"); -const { allUserStatus, usersData, allUserStatusWithOnBoardingStatus } = require("../../fixtures/userStatus/userStatus"); +const { generateStatusDataForState } = require("../../fixtures/userStatus/userStatus"); const userModel = firestore.collection("users"); -const userStatusModel = firestore.collection("userStatus"); +const userStatusModel = firestore.collection("usersStatus"); const joinModel = firestore.collection("applicants"); const userDataArray = require("../../fixtures/user/user")(); const joinData = require("../../fixtures/user/join")(); const photoVerificationModel = firestore.collection("photo-verification"); - +const userData = require("../../fixtures/user/user"); +const addUser = require("../../utils/addUser"); +const { userState } = require("../../../constants/userStatus"); /** * Test the model functions and validate the data stored */ @@ -282,26 +284,16 @@ describe("users", function () { }); }); describe("getUsersBasedOnFilter", function () { - beforeEach(async function () { - const addUsersPromises = []; - const userStatusPromises = []; - for (const user of usersData) { - addUsersPromises.push(await userModel.add(user)); - } - for (const element of allUserStatus) { - userStatusPromises.push(await userStatusModel.add(element)); - } - await Promise.all([...addUsersPromises, ...userStatusPromises]); + let [userId0, userId1, userId2] = []; - const allUserDocs = await userModel.get(); - allUserDocs.forEach(function (doc) { - // console.log("doc.data", doc.data()); - }); - - const allStatusDocs = await userStatusModel.get(); - allStatusDocs.forEach(function (doc) { - // console.log("doc.data", doc.data()); - }); + beforeEach(async function () { + const userArr = userData(); + userId0 = await addUser(userArr[0]); + userId1 = await addUser(userArr[1]); + userId2 = await addUser(userArr[2]); + await userStatusModel.doc("userStatus000").set(generateStatusDataForState(userId0, userState.ONBOARDING)); + await userStatusModel.doc("userStatus001").set(generateStatusDataForState(userId1, userState.ONBOARDING)); + await userStatusModel.doc("userStatus002").set(generateStatusDataForState(userId2, userState.IDLE)); }); afterEach(async function () { @@ -313,7 +305,7 @@ describe("users", function () { time: "31d", }; const result = await users.getUsersBasedOnFilter(query); - expect(result).to.deep.equal(allUserStatusWithOnBoardingStatus); + expect(result.length).to.equal(2); }); }); }); From dcfc76a69e3417a34bd3b4ecbad767e5ba3970ee Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Fri, 11 Aug 2023 19:30:08 +0530 Subject: [PATCH 060/105] resolved conflicts --- constants/users.js | 1 - 1 file changed, 1 deletion(-) diff --git a/constants/users.js b/constants/users.js index 2fbc3db00..f96dbd19e 100644 --- a/constants/users.js +++ b/constants/users.js @@ -43,7 +43,6 @@ module.exports = { profileStatus, USER_STATUS, ALLOWED_FILTER_PARAMS, - USER_SENSITIVE_DATA, DOCUMENT_WRITE_SIZE, USERS_PATCH_HANDLER_ACTIONS, USERS_PATCH_HANDLER_ERROR_MESSAGES, From f412ba528ebd82536962e1f9aa6d81872f3830af Mon Sep 17 00:00:00 2001 From: Manish Devrani Date: Fri, 11 Aug 2023 19:57:36 +0530 Subject: [PATCH 061/105] TEST: fix failing test --- test/unit/services/users.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/services/users.test.js b/test/unit/services/users.test.js index 009f05014..c3c060b47 100644 --- a/test/unit/services/users.test.js +++ b/test/unit/services/users.test.js @@ -53,7 +53,7 @@ describe("Users services", function () { expect(res).to.deep.equal({ message: "Successfully completed batch updates", - totalUsersArchived: 15, + totalUsersArchived: 16, totalOperationsFailed: 0, updatedUserDetails: userDetails, failedUserDetails: [], @@ -74,7 +74,7 @@ describe("Users services", function () { expect(res).to.deep.equal({ message: "Firebase batch operation failed", totalUsersArchived: 0, - totalOperationsFailed: 15, + totalOperationsFailed: 16, updatedUserDetails: [], failedUserDetails: userDetails, }); From 5bded4331e573d492ec9abc1a7a8df55d4406dae Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Fri, 11 Aug 2023 22:33:07 +0530 Subject: [PATCH 062/105] refactor: made the overdue tasks API behind the feature flag --- models/tasks.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/tasks.js b/models/tasks.js index c0d4a969c..a578b01e6 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -115,10 +115,10 @@ const getBuiltTasks = async (tasksSnapshot) => { return taskList; }; -const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, prev }) => { +const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, prev, dev = false }) => { try { let initialQuery; - if (status === TASK_STATUS.OVERDUE) { + if (status === TASK_STATUS.OVERDUE && dev) { const currentTime = Math.floor(Date.now() / 1000); initialQuery = tasksModel.where("endsOn", "<", currentTime); } else { @@ -153,7 +153,7 @@ const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, const allTasks = await getBuiltTasks(snapshot); - if (status === TASK_STATUS.OVERDUE) { + if (status === TASK_STATUS.OVERDUE && dev) { const nonOverdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; const overdueTasks = allTasks.filter((task) => !nonOverdueTasksStatus.includes(task.status) && task.assignee); return { From 89838a53e4224402f720296c214fb9da2a8c5d53 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sat, 22 Jul 2023 18:44:32 +0530 Subject: [PATCH 063/105] refactor: overdue and extension requests APIs --- controllers/extensionRequests.js | 71 +++++++++++++++++++++++++++++++- routes/extensionRequests.js | 7 ++++ test/integration/tasks.test.js | 1 + 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index f3ed89d69..d1cb3fb6e 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -1,7 +1,7 @@ const extensionRequestsQuery = require("../models/extensionRequests"); const { addLog } = require("../models/logs"); const tasks = require("../models/tasks"); -const { getUsername } = require("../utils/users"); +const { getUsername, getUserId } = require("../utils/users"); const { EXTENSION_REQUEST_STATUS } = require("../constants/extensionRequests"); const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** @@ -225,6 +225,74 @@ const updateExtensionRequestStatus = async (req, res) => { } }; +/** + * Create ETA extension Request by Admin + * + * @param req {Object} - Express request object + * @param res {Object} - Express response object + */ +const createTaskExtensionRequestAdmin = async (req, res) => { + try { + const extensionBody = req.body; + + if (!req.userData.roles?.super_user) { + return res.boom.forbidden("Only Super User can create an extension request for this task."); + } + + const assigneeUsername = extensionBody.assignee; + const assigneeId = await getUserId(extensionBody.assignee); + extensionBody.assignee = assigneeId; + const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); + if (!task) { + return res.boom.badRequest("Task with this id or taskid doesn't exist."); + } + if (task.endsOn >= extensionBody.newEndsOn) { + return res.boom.badRequest("The value for newEndsOn should be greater than the previous ETA"); + } + if (extensionBody.oldEndsOn !== task.endsOn) { + extensionBody.oldEndsOn = task.endsOn; + } + if (task.assignee !== assigneeUsername) { + return res.boom.badRequest("This task is assigned to some different user"); + } + + const prevExtensionRequest = await extensionRequestsQuery.fetchExtensionRequests({ + taskId: extensionBody.taskId, + assignee: extensionBody.assignee, + }); + if (prevExtensionRequest.length) { + return res.boom.forbidden("An extension request for this task already exists."); + } + + const extensionRequest = await extensionRequestsQuery.createExtensionRequest(extensionBody); + + const extensionLog = { + type: "extensionRequests", + meta: { + taskId: extensionBody.taskId, + createdBy: req.userData.id, + }, + body: { + extensionRequestId: extensionRequest.id, + oldEndsOn: task.endsOn, + newEndsOn: extensionBody.newEndsOn, + assignee: extensionBody.assignee, + status: EXTENSION_REQUEST_STATUS.PENDING, + }, + }; + + await addLog(extensionLog.type, extensionLog.meta, extensionLog.body); + + return res.json({ + message: "Extension Request created successfully!", + extensionRequest: { ...extensionBody, id: extensionRequest.id }, + }); + } catch (err) { + logger.error(`Error while creating new extension request: ${err}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + module.exports = { createTaskExtensionRequest, fetchExtensionRequests, @@ -232,4 +300,5 @@ module.exports = { getSelfExtensionRequests, updateExtensionRequest, updateExtensionRequestStatus, + createTaskExtensionRequestAdmin, }; diff --git a/routes/extensionRequests.js b/routes/extensionRequests.js index 91fd05f07..d4d8606a4 100644 --- a/routes/extensionRequests.js +++ b/routes/extensionRequests.js @@ -11,6 +11,13 @@ const { } = require("../middlewares/validators/extensionRequests"); router.post("/", authenticate, createExtensionRequest, extensionRequests.createTaskExtensionRequest); +router.post( + "/admin", + authenticate, + authorizeRoles([SUPERUSER]), + createExtensionRequest, + extensionRequests.createTaskExtensionRequestAdmin +); router.get("/", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.fetchExtensionRequests); router.get("/self", authenticate, extensionRequests.getSelfExtensionRequests); router.get("/:id", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.getExtensionRequest); diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 906f108de..509d9ea55 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -61,6 +61,7 @@ describe("Tasks", function () { completionAward: { [DINERO]: 3, [NEELAM]: 300 }, lossRate: { [DINERO]: 1 }, isNoteworthy: false, + assignee: appOwner.username, }, ]; From 94d51d8bd4622e6e944f121edb528530819a191f Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sun, 30 Jul 2023 20:18:11 +0530 Subject: [PATCH 064/105] feat: added test cases for tasks validators --- controllers/extensionRequests.js | 84 ++++------------------ routes/extensionRequests.js | 7 -- test/integration/extensionRequests.test.js | 49 +++++++++++-- test/unit/middlewares/tasks.test.js | 64 +++++++++++++++++ utils/users.js | 40 +++++++++++ 5 files changed, 162 insertions(+), 82 deletions(-) create mode 100644 test/unit/middlewares/tasks.test.js diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index d1cb3fb6e..d91d7338d 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -1,7 +1,7 @@ const extensionRequestsQuery = require("../models/extensionRequests"); const { addLog } = require("../models/logs"); const tasks = require("../models/tasks"); -const { getUsername, getUserId } = require("../utils/users"); +const { getUsername, getUsernameElseUndefined, getUserIdElseUndefined } = require("../utils/users"); const { EXTENSION_REQUEST_STATUS } = require("../constants/extensionRequests"); const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** @@ -14,11 +14,22 @@ const createTaskExtensionRequest = async (req, res) => { try { const extensionBody = req.body; + let assigneeUsername = await getUsernameElseUndefined(extensionBody.assignee); + let assigneeId = extensionBody.assignee; + if (!assigneeUsername) { + assigneeId = await getUserIdElseUndefined(extensionBody.assignee); + assigneeUsername = extensionBody.assignee; + extensionBody.assignee = assigneeId; + } + + if (!assigneeId) { + return res.boom.badRequest("User with this id or username doesn't exist."); + } + if (req.userData.id !== extensionBody.assignee && !req.userData.roles?.super_user) { return res.boom.forbidden("Only Super User can create an extension request for this task."); } - const assigneeUsername = await getUsername(extensionBody.assignee); const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); if (!task) { return res.boom.badRequest("Task with this id or taskid doesn't exist."); @@ -225,74 +236,6 @@ const updateExtensionRequestStatus = async (req, res) => { } }; -/** - * Create ETA extension Request by Admin - * - * @param req {Object} - Express request object - * @param res {Object} - Express response object - */ -const createTaskExtensionRequestAdmin = async (req, res) => { - try { - const extensionBody = req.body; - - if (!req.userData.roles?.super_user) { - return res.boom.forbidden("Only Super User can create an extension request for this task."); - } - - const assigneeUsername = extensionBody.assignee; - const assigneeId = await getUserId(extensionBody.assignee); - extensionBody.assignee = assigneeId; - const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); - if (!task) { - return res.boom.badRequest("Task with this id or taskid doesn't exist."); - } - if (task.endsOn >= extensionBody.newEndsOn) { - return res.boom.badRequest("The value for newEndsOn should be greater than the previous ETA"); - } - if (extensionBody.oldEndsOn !== task.endsOn) { - extensionBody.oldEndsOn = task.endsOn; - } - if (task.assignee !== assigneeUsername) { - return res.boom.badRequest("This task is assigned to some different user"); - } - - const prevExtensionRequest = await extensionRequestsQuery.fetchExtensionRequests({ - taskId: extensionBody.taskId, - assignee: extensionBody.assignee, - }); - if (prevExtensionRequest.length) { - return res.boom.forbidden("An extension request for this task already exists."); - } - - const extensionRequest = await extensionRequestsQuery.createExtensionRequest(extensionBody); - - const extensionLog = { - type: "extensionRequests", - meta: { - taskId: extensionBody.taskId, - createdBy: req.userData.id, - }, - body: { - extensionRequestId: extensionRequest.id, - oldEndsOn: task.endsOn, - newEndsOn: extensionBody.newEndsOn, - assignee: extensionBody.assignee, - status: EXTENSION_REQUEST_STATUS.PENDING, - }, - }; - - await addLog(extensionLog.type, extensionLog.meta, extensionLog.body); - - return res.json({ - message: "Extension Request created successfully!", - extensionRequest: { ...extensionBody, id: extensionRequest.id }, - }); - } catch (err) { - logger.error(`Error while creating new extension request: ${err}`); - return res.boom.badImplementation(INTERNAL_SERVER_ERROR); - } -}; - module.exports = { createTaskExtensionRequest, fetchExtensionRequests, @@ -300,5 +243,4 @@ module.exports = { getSelfExtensionRequests, updateExtensionRequest, updateExtensionRequestStatus, - createTaskExtensionRequestAdmin, }; diff --git a/routes/extensionRequests.js b/routes/extensionRequests.js index d4d8606a4..91fd05f07 100644 --- a/routes/extensionRequests.js +++ b/routes/extensionRequests.js @@ -11,13 +11,6 @@ const { } = require("../middlewares/validators/extensionRequests"); router.post("/", authenticate, createExtensionRequest, extensionRequests.createTaskExtensionRequest); -router.post( - "/admin", - authenticate, - authorizeRoles([SUPERUSER]), - createExtensionRequest, - extensionRequests.createTaskExtensionRequestAdmin -); router.get("/", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.fetchExtensionRequests); router.get("/self", authenticate, extensionRequests.getSelfExtensionRequests); router.get("/:id", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.getExtensionRequest); diff --git a/test/integration/extensionRequests.test.js b/test/integration/extensionRequests.test.js index d1ab13dc3..3ebe25197 100644 --- a/test/integration/extensionRequests.test.js +++ b/test/integration/extensionRequests.test.js @@ -24,7 +24,7 @@ const superUser = userData[4]; let appOwnerjwt, superUserJwt, jwt; describe("Extension Requests", function () { - let taskId1, taskId2, taskId3, extensionRequestId1, extensionRequestId2; + let taskId0, taskId1, taskId2, taskId3, extensionRequestId1, extensionRequestId2; before(async function () { const userId = await addUser(user); @@ -37,6 +37,19 @@ describe("Extension Requests", function () { jwt = authService.generateAuthToken({ userId: userId }); const taskData = [ + { + title: "Test task 1", + type: "feature", + endsOn: 1234, + startedOn: 4567, + status: "active", + percentCompleted: 10, + participants: [], + assignee: appOwner.username, + isNoteworthy: true, + completionAward: { [DINERO]: 3, [NEELAM]: 300 }, + lossRate: { [DINERO]: 1 }, + }, { title: "Test task", type: "feature", @@ -83,13 +96,14 @@ describe("Extension Requests", function () { ]; // Add the active task - taskId1 = (await tasks.updateTask(taskData[0])).taskId; + taskId0 = (await tasks.updateTask(taskData[0])).taskId; + taskId1 = (await tasks.updateTask(taskData[1])).taskId; // Add the completed task - taskId2 = (await tasks.updateTask(taskData[1])).taskId; + taskId2 = (await tasks.updateTask(taskData[2])).taskId; // Add the completed task - taskId3 = (await tasks.updateTask(taskData[2])).taskId; + taskId3 = (await tasks.updateTask(taskData[3])).taskId; const extensionRequest = { taskId: taskId3, @@ -214,6 +228,33 @@ describe("Extension Requests", function () { return done(); }); }); + it("Should return success response after adding the extension request (sending assignee username)", function (done) { + chai + .request(app) + .post("/extension-requests") + .set("cookie", `${cookieName}=${appOwnerjwt}`) + .send({ + taskId: taskId0, + title: "change ETA", + assignee: appOwner.username, + oldEndsOn: 1234, + newEndsOn: 1235, + reason: "family event", + status: "PENDING", + }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("Extension Request created successfully!"); + expect(res.body.extensionRequest).to.be.a("object"); + expect(res.body.extensionRequest.assignee).to.equal(appOwner.id); + expect(res.body.extensionRequest.status).to.equal(EXTENSION_REQUEST_STATUS.PENDING); + return done(); + }); + }); it("Should return fail response if someone try to create a extension request for someone else and is not a super user", function (done) { chai .request(app) diff --git a/test/unit/middlewares/tasks.test.js b/test/unit/middlewares/tasks.test.js new file mode 100644 index 000000000..fe738904e --- /dev/null +++ b/test/unit/middlewares/tasks.test.js @@ -0,0 +1,64 @@ +const { updateTask } = require("../../../middlewares/validators/tasks"); // Replace with the actual path to your updateTask module +const { expect } = require("chai"); +const sinon = require("sinon"); + +describe("updateTask function", function () { + // Helper function to create a request object with a specific body + const createRequest = (body) => ({ body }); + + // Helper function to create a response object with a mocked boom function + const createResponse = () => ({ + boom: { + badRequest: sinon.stub().returns({ error: true, message: "Bad Request" }), + }, + }); + + // Helper function to create a next function that simply calls done + const createNext = () => sinon.stub(); + + afterEach(function () { + sinon.restore(); + }); + + it("validates a valid request body", async function () { + const validRequestBody = { + title: "Sample Task", + purpose: "Test purposes", + type: "Sample Type", + status: "active", + isNoteworthy: true, + isCollapsed: false, + }; + + const req = createRequest(validRequestBody); + const res = createResponse(); + const next = createNext(); + + await updateTask(req, res, next); + + expect(res.boom.badRequest.calledOnce).to.be.equal(false); + expect(next.calledOnce).to.be.equal(true); + }); + + it("handles invalid request body", async function () { + const invalidRequestBody = { + // Missing required fields, or incorrect data types + title: 123, + purpose: 456, + type: true, + status: "invalid_status", + isNoteworthy: "yes", + isCollapsed: "no", + assignee: "", + }; + + const req = createRequest(invalidRequestBody); + const res = createResponse(); + const next = createNext(); + + await updateTask(req, res, next); + + expect(res.boom.badRequest.calledOnce).to.be.equal(true); + expect(next.calledOnce).to.be.equal(false); + }); +}); diff --git a/utils/users.js b/utils/users.js index e3145c775..eb3a9699b 100644 --- a/utils/users.js +++ b/utils/users.js @@ -43,6 +43,46 @@ const getUsername = async (userId) => { throw error; } }; + +/** + * Used for receiving username when providing userId, if not found then returns undefined + * + * @param userId {String} - userId of the User. + * @returns username {String} - username of the same user + */ +const getUsernameElseUndefined = async (userId) => { + try { + const { + user: { username }, + } = await fetchUser({ userId }); + return username; + } catch (error) { + logger.error("Something went wrong", error); + return undefined; + } +}; + +/** + * Used for receiving userId when providing username, if not found then returns undefined + * + * @param username {String} - username of the User. + * @returns id {String} - userId of the same user + */ + +const getUserIdElseUndefined = async (username) => { + try { + const { + userExists, + user: { id }, + } = await fetchUser({ username }); + + return userExists ? id : false; + } catch (error) { + logger.error("Something went wrong", error); + return undefined; + } +}; + /** * Converts the userIds entered in the array to corresponding usernames * @param participantArray {array} : participants array to be updated From f3ce39e5feecf3d4fb33af613044433eaeb6c40b Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Thu, 3 Aug 2023 14:42:24 +0530 Subject: [PATCH 065/105] refactor: requested changes for overdue tasks API --- controllers/extensionRequests.js | 2 +- test/integration/extensionRequests.test.js | 27 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index d91d7338d..15b2e1a19 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -27,7 +27,7 @@ const createTaskExtensionRequest = async (req, res) => { } if (req.userData.id !== extensionBody.assignee && !req.userData.roles?.super_user) { - return res.boom.forbidden("Only Super User can create an extension request for this task."); + return res.boom.forbidden("Only assigned user and super user can create an extension request for this task."); } const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); diff --git a/test/integration/extensionRequests.test.js b/test/integration/extensionRequests.test.js index 3ebe25197..6ea9c66f1 100644 --- a/test/integration/extensionRequests.test.js +++ b/test/integration/extensionRequests.test.js @@ -255,6 +255,29 @@ describe("Extension Requests", function () { return done(); }); }); + it("Should return failure response after adding the extension request (sending wrong assignee info)", function (done) { + chai + .request(app) + .post("/extension-requests") + .set("cookie", `${cookieName}=${appOwnerjwt}`) + .send({ + taskId: taskId0, + title: "change ETA", + assignee: "hello", + oldEndsOn: 1234, + newEndsOn: 1235, + reason: "family event", + status: "PENDING", + }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(400); + expect(res.body.message).to.equal("User with this id or username doesn't exist."); + return done(); + }); + }); it("Should return fail response if someone try to create a extension request for someone else and is not a super user", function (done) { chai .request(app) @@ -276,7 +299,9 @@ describe("Extension Requests", function () { expect(res).to.have.status(403); expect(res.body).to.be.a("object"); - expect(res.body.message).to.equal("Only Super User can create an extension request for this task."); + expect(res.body.message).to.equal( + "Only assigned user and super user can create an extension request for this task." + ); return done(); }); }); From eac8ed8de05f1636b8231fba39a5431af7a373d7 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sun, 6 Aug 2023 01:16:10 +0530 Subject: [PATCH 066/105] refactor: changed the flow for overdue tasks API --- constants/tasks.js | 1 + models/tasks.js | 23 +++++++++++++++++++---- test/integration/tasks.test.js | 15 +++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/constants/tasks.js b/constants/tasks.js index 06dc326e0..37e22ca37 100644 --- a/constants/tasks.js +++ b/constants/tasks.js @@ -20,6 +20,7 @@ const TASK_STATUS = { RELEASED: "RELEASED", VERIFIED: "VERIFIED", DONE: "DONE", + OVERDUE: "OVERDUE", }; // TODO: convert this to new task status diff --git a/models/tasks.js b/models/tasks.js index 88bd98cd6..e3bcb4797 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -5,7 +5,7 @@ const dependencyModel = firestore.collection("taskDependencies"); const userUtils = require("../utils/users"); const { fromFirestoreData, toFirestoreData, buildTasks } = require("../utils/tasks"); const { TASK_TYPE, TASK_STATUS, TASK_STATUS_OLD, TASK_SIZE } = require("../constants/tasks"); -const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, COMPLETED } = TASK_STATUS; +const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, COMPLETED, MERGED, RELEASED, VERIFIED, AVAILABLE } = TASK_STATUS; const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING, OLD_COMPLETED } = TASK_STATUS_OLD; /** @@ -121,9 +121,13 @@ const getBuiltTasks = async (tasksSnapshot, searchTerm) => { const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, prev }) => { try { - const initialQuery = status - ? tasksModel.where("status", "==", status).orderBy("title") - : tasksModel.orderBy("title"); + let initialQuery; + if (status === TASK_STATUS.OVERDUE) { + const currentTime = Math.floor(Date.now() / 1000); + initialQuery = tasksModel.where("endsOn", "<", currentTime); + } else { + initialQuery = status ? tasksModel.where("status", "==", status).orderBy("title") : tasksModel.orderBy("title"); + } let queryDoc = initialQuery; if (prev) { @@ -152,6 +156,17 @@ const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, const nextDoc = await initialQuery.startAfter(last).limit(1).get(); const allTasks = await getBuiltTasks(snapshot); + + if (status === TASK_STATUS.OVERDUE) { + const nonOverdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; + const overdueTasks = allTasks.filter((task) => !nonOverdueTasksStatus.includes(task.status) && task.assignee); + return { + allTasks: overdueTasks, + next: nextDoc.docs[0]?.id ?? "", + prev: prevDoc.docs[0]?.id ?? "", + }; + } + return { allTasks, next: nextDoc.docs[0]?.id ?? "", diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 509d9ea55..67a211cd4 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -211,6 +211,21 @@ describe("Tasks", function () { }); }); + it("Should get all overdue tasks GET /tasks", function (done) { + chai + .request(app) + .get(`/tasks?dev=true&status=overdue`) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.tasks[0].id).to.be.oneOf([taskId, taskId1]); + return done(); + }); + }); + it("Should get tasks when correct query parameters are passed", function (done) { chai .request(app) From 98dfd8c94c9f7eac26a5cc5d911a75fbeaaa05cd Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Fri, 11 Aug 2023 22:33:07 +0530 Subject: [PATCH 067/105] refactor: made the overdue tasks API behind the feature flag --- models/tasks.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/tasks.js b/models/tasks.js index e3bcb4797..2e66173a1 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -119,10 +119,10 @@ const getBuiltTasks = async (tasksSnapshot, searchTerm) => { return taskList; }; -const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, prev }) => { +const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, prev, dev = false }) => { try { let initialQuery; - if (status === TASK_STATUS.OVERDUE) { + if (status === TASK_STATUS.OVERDUE && dev) { const currentTime = Math.floor(Date.now() / 1000); initialQuery = tasksModel.where("endsOn", "<", currentTime); } else { @@ -157,7 +157,7 @@ const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, const allTasks = await getBuiltTasks(snapshot); - if (status === TASK_STATUS.OVERDUE) { + if (status === TASK_STATUS.OVERDUE && dev) { const nonOverdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; const overdueTasks = allTasks.filter((task) => !nonOverdueTasksStatus.includes(task.status) && task.assignee); return { From f0df5845e5714afa198225fbc94abcee4fbadc2a Mon Sep 17 00:00:00 2001 From: vivek lokhande Date: Sat, 12 Aug 2023 12:11:27 +0530 Subject: [PATCH 068/105] (#1367) added dataAccessLayer to fetch user --- controllers/auth.js | 10 ++++++---- models/qrCodeAuth.js | 19 +++---------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/controllers/auth.js b/controllers/auth.js index d310a66db..5b14ab96a 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -2,8 +2,9 @@ const passport = require("passport"); const users = require("../models/users"); const QrCodeAuthModel = require("../models/qrCodeAuth"); const authService = require("../services/authService"); +const dataAccess = require("../services/dataAccessLayer"); const { SOMETHING_WENT_WRONG, DATA_ADDED_SUCCESSFULLY } = require("../constants/errorMessages"); - +const USER_DOES_NOT_EXIST_ERROR = "User does not exist!"; /** * Makes authentication call to GitHub statergy * @@ -111,11 +112,12 @@ const storeUserDeviceInfo = async (req, res) => { authorization_status: "NOT_INIT", }; - const userInfo = await QrCodeAuthModel.storeUserDeviceInfo(userJson); + const userInfoData = await dataAccess.retrieveUsers({ id: userJson.user_id }); - if (userInfo.userExists !== undefined && !userInfo.userExists) { - return res.boom.notFound("Document not found!"); + if (!userInfoData.userExists) { + return res.boom.notFound(USER_DOES_NOT_EXIST_ERROR); } + const userInfo = await QrCodeAuthModel.storeUserDeviceInfo(userJson); return res.status(201).json({ ...userInfo, diff --git a/models/qrCodeAuth.js b/models/qrCodeAuth.js index de6e1d18d..96b49f024 100644 --- a/models/qrCodeAuth.js +++ b/models/qrCodeAuth.js @@ -1,7 +1,6 @@ const firestore = require("../utils/firestore"); const QrCodeAuthModel = firestore.collection("QrCodeAuth"); -const userModel = firestore.collection("users"); -const USER_DOES_NOT_EXIST_ERROR = "User does not exist."; + /** * Stores the user device info * @@ -40,21 +39,9 @@ const updateStatus = async (userId, authStatus = "NOT_INIT") => { const storeUserDeviceInfo = async (userDeviceInfoData) => { try { const { user_id: userId } = userDeviceInfoData; - const user = await userModel.doc(userId).get(); - - if (!user.data()) { - return { - userExists: false, - }; - } + await QrCodeAuthModel.doc(userId).set(userDeviceInfoData); - if (user.data()) { - await QrCodeAuthModel.doc(userId).set(userDeviceInfoData); - - return { userDeviceInfoData }; - } else { - throw new Error(USER_DOES_NOT_EXIST_ERROR); - } + return { userDeviceInfoData }; } catch (err) { logger.error("Error in storing user device info.", err); throw err; From 9803654f75ea057b689c156d38f656c570fc2131 Mon Sep 17 00:00:00 2001 From: vivek lokhande Date: Sat, 12 Aug 2023 12:27:33 +0530 Subject: [PATCH 069/105] (#1368) updated model tests --- test/integration/qrCodeAuth.test.js | 9 +++++---- test/unit/models/qrCodeAuth.test.js | 11 ----------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/test/integration/qrCodeAuth.test.js b/test/integration/qrCodeAuth.test.js index e04538d5b..ac788d747 100644 --- a/test/integration/qrCodeAuth.test.js +++ b/test/integration/qrCodeAuth.test.js @@ -10,11 +10,12 @@ const qrCodeAuthModel = require("../../models/qrCodeAuth"); const authService = require("../../services/authService"); const config = require("config"); const cookieName = config.get("userToken.cookieName"); +const USER_DOES_NOT_EXIST_ERROR = "User does not exist!"; // Import fixtures let userDeviceInfoData; let wrongUserDeviceInfoData; -let wrongUserDeviceInfoDataWithWrongDeviceInfo; +let wrongUserIdDeviceInfo; let userId; const user = userData[0]; @@ -25,7 +26,7 @@ describe("QrCodeAuth", function () { userDeviceInfoData = { ...userDeviceInfoDataArray[0], user_id: userId }; wrongUserDeviceInfoData = userDeviceInfoDataArray[0]; - wrongUserDeviceInfoDataWithWrongDeviceInfo = { ...userDeviceInfoDataArray[0], user_id: userId, device_info: 2 }; + wrongUserIdDeviceInfo = { ...userDeviceInfoDataArray[0], user_id: userId, device_info: 2 }; }); afterEach(async function () { await cleanDb(); @@ -63,7 +64,7 @@ describe("QrCodeAuth", function () { expect(res).to.have.status(404); expect(res.body).to.be.a("object"); - expect(res.body.message).to.equal("Document not found!"); + expect(res.body.message).to.equal(USER_DOES_NOT_EXIST_ERROR); expect(res.body.error).to.equal("Not Found"); return done(); @@ -74,7 +75,7 @@ describe("QrCodeAuth", function () { chai .request(app) .post("/auth/qr-code-auth") - .send(wrongUserDeviceInfoDataWithWrongDeviceInfo) + .send(wrongUserIdDeviceInfo) .end((err, res) => { if (err) { return done(err); diff --git a/test/unit/models/qrCodeAuth.test.js b/test/unit/models/qrCodeAuth.test.js index 4091a041c..8a3e98b54 100644 --- a/test/unit/models/qrCodeAuth.test.js +++ b/test/unit/models/qrCodeAuth.test.js @@ -17,7 +17,6 @@ describe("mobile auth", function () { await cleanDb(); }); describe("storeUserDeviceInfo", function () { - const WRONG_USER_ID = "xynsasd"; it("should store user Id and device info of user for mobile auth", async function () { const userData = userDataArray[0]; const { userId } = await users.addOrUpdate(userData); @@ -47,16 +46,6 @@ describe("mobile auth", function () { expect(deviceId).to.be.a("string"); expect(authorizationStatus).to.be.a("string"); }); - - it("should return userExist as false when the user document is not found", async function () { - const userDeviceInfoData = { - ...userDeviceInfoDataArray[0], - user_id: WRONG_USER_ID, - authorization_status: "NOT_INIT", - }; - const response = await qrCodeAuth.storeUserDeviceInfo(userDeviceInfoData); - expect(response.userExists).to.be.equal(false); - }); }); describe("updateAuthStatus", function () { From b1e0d54818f59332cb2a3822d4af1ca9cedbfe30 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sat, 22 Jul 2023 18:44:32 +0530 Subject: [PATCH 070/105] refactor: overdue and extension requests APIs --- controllers/extensionRequests.js | 71 +++++++++++++++++++++++++++++++- routes/extensionRequests.js | 7 ++++ test/integration/tasks.test.js | 1 + 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index f3ed89d69..d1cb3fb6e 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -1,7 +1,7 @@ const extensionRequestsQuery = require("../models/extensionRequests"); const { addLog } = require("../models/logs"); const tasks = require("../models/tasks"); -const { getUsername } = require("../utils/users"); +const { getUsername, getUserId } = require("../utils/users"); const { EXTENSION_REQUEST_STATUS } = require("../constants/extensionRequests"); const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** @@ -225,6 +225,74 @@ const updateExtensionRequestStatus = async (req, res) => { } }; +/** + * Create ETA extension Request by Admin + * + * @param req {Object} - Express request object + * @param res {Object} - Express response object + */ +const createTaskExtensionRequestAdmin = async (req, res) => { + try { + const extensionBody = req.body; + + if (!req.userData.roles?.super_user) { + return res.boom.forbidden("Only Super User can create an extension request for this task."); + } + + const assigneeUsername = extensionBody.assignee; + const assigneeId = await getUserId(extensionBody.assignee); + extensionBody.assignee = assigneeId; + const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); + if (!task) { + return res.boom.badRequest("Task with this id or taskid doesn't exist."); + } + if (task.endsOn >= extensionBody.newEndsOn) { + return res.boom.badRequest("The value for newEndsOn should be greater than the previous ETA"); + } + if (extensionBody.oldEndsOn !== task.endsOn) { + extensionBody.oldEndsOn = task.endsOn; + } + if (task.assignee !== assigneeUsername) { + return res.boom.badRequest("This task is assigned to some different user"); + } + + const prevExtensionRequest = await extensionRequestsQuery.fetchExtensionRequests({ + taskId: extensionBody.taskId, + assignee: extensionBody.assignee, + }); + if (prevExtensionRequest.length) { + return res.boom.forbidden("An extension request for this task already exists."); + } + + const extensionRequest = await extensionRequestsQuery.createExtensionRequest(extensionBody); + + const extensionLog = { + type: "extensionRequests", + meta: { + taskId: extensionBody.taskId, + createdBy: req.userData.id, + }, + body: { + extensionRequestId: extensionRequest.id, + oldEndsOn: task.endsOn, + newEndsOn: extensionBody.newEndsOn, + assignee: extensionBody.assignee, + status: EXTENSION_REQUEST_STATUS.PENDING, + }, + }; + + await addLog(extensionLog.type, extensionLog.meta, extensionLog.body); + + return res.json({ + message: "Extension Request created successfully!", + extensionRequest: { ...extensionBody, id: extensionRequest.id }, + }); + } catch (err) { + logger.error(`Error while creating new extension request: ${err}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + module.exports = { createTaskExtensionRequest, fetchExtensionRequests, @@ -232,4 +300,5 @@ module.exports = { getSelfExtensionRequests, updateExtensionRequest, updateExtensionRequestStatus, + createTaskExtensionRequestAdmin, }; diff --git a/routes/extensionRequests.js b/routes/extensionRequests.js index 91fd05f07..d4d8606a4 100644 --- a/routes/extensionRequests.js +++ b/routes/extensionRequests.js @@ -11,6 +11,13 @@ const { } = require("../middlewares/validators/extensionRequests"); router.post("/", authenticate, createExtensionRequest, extensionRequests.createTaskExtensionRequest); +router.post( + "/admin", + authenticate, + authorizeRoles([SUPERUSER]), + createExtensionRequest, + extensionRequests.createTaskExtensionRequestAdmin +); router.get("/", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.fetchExtensionRequests); router.get("/self", authenticate, extensionRequests.getSelfExtensionRequests); router.get("/:id", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.getExtensionRequest); diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 906f108de..509d9ea55 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -61,6 +61,7 @@ describe("Tasks", function () { completionAward: { [DINERO]: 3, [NEELAM]: 300 }, lossRate: { [DINERO]: 1 }, isNoteworthy: false, + assignee: appOwner.username, }, ]; From b6f2124782e1422f5a07c09657b36f5f3850207a Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sun, 30 Jul 2023 20:18:11 +0530 Subject: [PATCH 071/105] feat: added test cases for tasks validators --- controllers/extensionRequests.js | 84 ++++------------------ routes/extensionRequests.js | 7 -- test/integration/extensionRequests.test.js | 49 +++++++++++-- test/unit/middlewares/tasks.test.js | 64 +++++++++++++++++ utils/users.js | 40 +++++++++++ 5 files changed, 162 insertions(+), 82 deletions(-) create mode 100644 test/unit/middlewares/tasks.test.js diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index d1cb3fb6e..d91d7338d 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -1,7 +1,7 @@ const extensionRequestsQuery = require("../models/extensionRequests"); const { addLog } = require("../models/logs"); const tasks = require("../models/tasks"); -const { getUsername, getUserId } = require("../utils/users"); +const { getUsername, getUsernameElseUndefined, getUserIdElseUndefined } = require("../utils/users"); const { EXTENSION_REQUEST_STATUS } = require("../constants/extensionRequests"); const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** @@ -14,11 +14,22 @@ const createTaskExtensionRequest = async (req, res) => { try { const extensionBody = req.body; + let assigneeUsername = await getUsernameElseUndefined(extensionBody.assignee); + let assigneeId = extensionBody.assignee; + if (!assigneeUsername) { + assigneeId = await getUserIdElseUndefined(extensionBody.assignee); + assigneeUsername = extensionBody.assignee; + extensionBody.assignee = assigneeId; + } + + if (!assigneeId) { + return res.boom.badRequest("User with this id or username doesn't exist."); + } + if (req.userData.id !== extensionBody.assignee && !req.userData.roles?.super_user) { return res.boom.forbidden("Only Super User can create an extension request for this task."); } - const assigneeUsername = await getUsername(extensionBody.assignee); const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); if (!task) { return res.boom.badRequest("Task with this id or taskid doesn't exist."); @@ -225,74 +236,6 @@ const updateExtensionRequestStatus = async (req, res) => { } }; -/** - * Create ETA extension Request by Admin - * - * @param req {Object} - Express request object - * @param res {Object} - Express response object - */ -const createTaskExtensionRequestAdmin = async (req, res) => { - try { - const extensionBody = req.body; - - if (!req.userData.roles?.super_user) { - return res.boom.forbidden("Only Super User can create an extension request for this task."); - } - - const assigneeUsername = extensionBody.assignee; - const assigneeId = await getUserId(extensionBody.assignee); - extensionBody.assignee = assigneeId; - const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); - if (!task) { - return res.boom.badRequest("Task with this id or taskid doesn't exist."); - } - if (task.endsOn >= extensionBody.newEndsOn) { - return res.boom.badRequest("The value for newEndsOn should be greater than the previous ETA"); - } - if (extensionBody.oldEndsOn !== task.endsOn) { - extensionBody.oldEndsOn = task.endsOn; - } - if (task.assignee !== assigneeUsername) { - return res.boom.badRequest("This task is assigned to some different user"); - } - - const prevExtensionRequest = await extensionRequestsQuery.fetchExtensionRequests({ - taskId: extensionBody.taskId, - assignee: extensionBody.assignee, - }); - if (prevExtensionRequest.length) { - return res.boom.forbidden("An extension request for this task already exists."); - } - - const extensionRequest = await extensionRequestsQuery.createExtensionRequest(extensionBody); - - const extensionLog = { - type: "extensionRequests", - meta: { - taskId: extensionBody.taskId, - createdBy: req.userData.id, - }, - body: { - extensionRequestId: extensionRequest.id, - oldEndsOn: task.endsOn, - newEndsOn: extensionBody.newEndsOn, - assignee: extensionBody.assignee, - status: EXTENSION_REQUEST_STATUS.PENDING, - }, - }; - - await addLog(extensionLog.type, extensionLog.meta, extensionLog.body); - - return res.json({ - message: "Extension Request created successfully!", - extensionRequest: { ...extensionBody, id: extensionRequest.id }, - }); - } catch (err) { - logger.error(`Error while creating new extension request: ${err}`); - return res.boom.badImplementation(INTERNAL_SERVER_ERROR); - } -}; - module.exports = { createTaskExtensionRequest, fetchExtensionRequests, @@ -300,5 +243,4 @@ module.exports = { getSelfExtensionRequests, updateExtensionRequest, updateExtensionRequestStatus, - createTaskExtensionRequestAdmin, }; diff --git a/routes/extensionRequests.js b/routes/extensionRequests.js index d4d8606a4..91fd05f07 100644 --- a/routes/extensionRequests.js +++ b/routes/extensionRequests.js @@ -11,13 +11,6 @@ const { } = require("../middlewares/validators/extensionRequests"); router.post("/", authenticate, createExtensionRequest, extensionRequests.createTaskExtensionRequest); -router.post( - "/admin", - authenticate, - authorizeRoles([SUPERUSER]), - createExtensionRequest, - extensionRequests.createTaskExtensionRequestAdmin -); router.get("/", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.fetchExtensionRequests); router.get("/self", authenticate, extensionRequests.getSelfExtensionRequests); router.get("/:id", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.getExtensionRequest); diff --git a/test/integration/extensionRequests.test.js b/test/integration/extensionRequests.test.js index d1ab13dc3..3ebe25197 100644 --- a/test/integration/extensionRequests.test.js +++ b/test/integration/extensionRequests.test.js @@ -24,7 +24,7 @@ const superUser = userData[4]; let appOwnerjwt, superUserJwt, jwt; describe("Extension Requests", function () { - let taskId1, taskId2, taskId3, extensionRequestId1, extensionRequestId2; + let taskId0, taskId1, taskId2, taskId3, extensionRequestId1, extensionRequestId2; before(async function () { const userId = await addUser(user); @@ -37,6 +37,19 @@ describe("Extension Requests", function () { jwt = authService.generateAuthToken({ userId: userId }); const taskData = [ + { + title: "Test task 1", + type: "feature", + endsOn: 1234, + startedOn: 4567, + status: "active", + percentCompleted: 10, + participants: [], + assignee: appOwner.username, + isNoteworthy: true, + completionAward: { [DINERO]: 3, [NEELAM]: 300 }, + lossRate: { [DINERO]: 1 }, + }, { title: "Test task", type: "feature", @@ -83,13 +96,14 @@ describe("Extension Requests", function () { ]; // Add the active task - taskId1 = (await tasks.updateTask(taskData[0])).taskId; + taskId0 = (await tasks.updateTask(taskData[0])).taskId; + taskId1 = (await tasks.updateTask(taskData[1])).taskId; // Add the completed task - taskId2 = (await tasks.updateTask(taskData[1])).taskId; + taskId2 = (await tasks.updateTask(taskData[2])).taskId; // Add the completed task - taskId3 = (await tasks.updateTask(taskData[2])).taskId; + taskId3 = (await tasks.updateTask(taskData[3])).taskId; const extensionRequest = { taskId: taskId3, @@ -214,6 +228,33 @@ describe("Extension Requests", function () { return done(); }); }); + it("Should return success response after adding the extension request (sending assignee username)", function (done) { + chai + .request(app) + .post("/extension-requests") + .set("cookie", `${cookieName}=${appOwnerjwt}`) + .send({ + taskId: taskId0, + title: "change ETA", + assignee: appOwner.username, + oldEndsOn: 1234, + newEndsOn: 1235, + reason: "family event", + status: "PENDING", + }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("Extension Request created successfully!"); + expect(res.body.extensionRequest).to.be.a("object"); + expect(res.body.extensionRequest.assignee).to.equal(appOwner.id); + expect(res.body.extensionRequest.status).to.equal(EXTENSION_REQUEST_STATUS.PENDING); + return done(); + }); + }); it("Should return fail response if someone try to create a extension request for someone else and is not a super user", function (done) { chai .request(app) diff --git a/test/unit/middlewares/tasks.test.js b/test/unit/middlewares/tasks.test.js new file mode 100644 index 000000000..fe738904e --- /dev/null +++ b/test/unit/middlewares/tasks.test.js @@ -0,0 +1,64 @@ +const { updateTask } = require("../../../middlewares/validators/tasks"); // Replace with the actual path to your updateTask module +const { expect } = require("chai"); +const sinon = require("sinon"); + +describe("updateTask function", function () { + // Helper function to create a request object with a specific body + const createRequest = (body) => ({ body }); + + // Helper function to create a response object with a mocked boom function + const createResponse = () => ({ + boom: { + badRequest: sinon.stub().returns({ error: true, message: "Bad Request" }), + }, + }); + + // Helper function to create a next function that simply calls done + const createNext = () => sinon.stub(); + + afterEach(function () { + sinon.restore(); + }); + + it("validates a valid request body", async function () { + const validRequestBody = { + title: "Sample Task", + purpose: "Test purposes", + type: "Sample Type", + status: "active", + isNoteworthy: true, + isCollapsed: false, + }; + + const req = createRequest(validRequestBody); + const res = createResponse(); + const next = createNext(); + + await updateTask(req, res, next); + + expect(res.boom.badRequest.calledOnce).to.be.equal(false); + expect(next.calledOnce).to.be.equal(true); + }); + + it("handles invalid request body", async function () { + const invalidRequestBody = { + // Missing required fields, or incorrect data types + title: 123, + purpose: 456, + type: true, + status: "invalid_status", + isNoteworthy: "yes", + isCollapsed: "no", + assignee: "", + }; + + const req = createRequest(invalidRequestBody); + const res = createResponse(); + const next = createNext(); + + await updateTask(req, res, next); + + expect(res.boom.badRequest.calledOnce).to.be.equal(true); + expect(next.calledOnce).to.be.equal(false); + }); +}); diff --git a/utils/users.js b/utils/users.js index e3145c775..eb3a9699b 100644 --- a/utils/users.js +++ b/utils/users.js @@ -43,6 +43,46 @@ const getUsername = async (userId) => { throw error; } }; + +/** + * Used for receiving username when providing userId, if not found then returns undefined + * + * @param userId {String} - userId of the User. + * @returns username {String} - username of the same user + */ +const getUsernameElseUndefined = async (userId) => { + try { + const { + user: { username }, + } = await fetchUser({ userId }); + return username; + } catch (error) { + logger.error("Something went wrong", error); + return undefined; + } +}; + +/** + * Used for receiving userId when providing username, if not found then returns undefined + * + * @param username {String} - username of the User. + * @returns id {String} - userId of the same user + */ + +const getUserIdElseUndefined = async (username) => { + try { + const { + userExists, + user: { id }, + } = await fetchUser({ username }); + + return userExists ? id : false; + } catch (error) { + logger.error("Something went wrong", error); + return undefined; + } +}; + /** * Converts the userIds entered in the array to corresponding usernames * @param participantArray {array} : participants array to be updated From 6c81de87c831a01fd69f13034b83c916cba0a5a0 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Thu, 3 Aug 2023 14:42:24 +0530 Subject: [PATCH 072/105] refactor: requested changes for overdue tasks API --- controllers/extensionRequests.js | 2 +- test/integration/extensionRequests.test.js | 27 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index d91d7338d..15b2e1a19 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -27,7 +27,7 @@ const createTaskExtensionRequest = async (req, res) => { } if (req.userData.id !== extensionBody.assignee && !req.userData.roles?.super_user) { - return res.boom.forbidden("Only Super User can create an extension request for this task."); + return res.boom.forbidden("Only assigned user and super user can create an extension request for this task."); } const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); diff --git a/test/integration/extensionRequests.test.js b/test/integration/extensionRequests.test.js index 3ebe25197..6ea9c66f1 100644 --- a/test/integration/extensionRequests.test.js +++ b/test/integration/extensionRequests.test.js @@ -255,6 +255,29 @@ describe("Extension Requests", function () { return done(); }); }); + it("Should return failure response after adding the extension request (sending wrong assignee info)", function (done) { + chai + .request(app) + .post("/extension-requests") + .set("cookie", `${cookieName}=${appOwnerjwt}`) + .send({ + taskId: taskId0, + title: "change ETA", + assignee: "hello", + oldEndsOn: 1234, + newEndsOn: 1235, + reason: "family event", + status: "PENDING", + }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(400); + expect(res.body.message).to.equal("User with this id or username doesn't exist."); + return done(); + }); + }); it("Should return fail response if someone try to create a extension request for someone else and is not a super user", function (done) { chai .request(app) @@ -276,7 +299,9 @@ describe("Extension Requests", function () { expect(res).to.have.status(403); expect(res.body).to.be.a("object"); - expect(res.body.message).to.equal("Only Super User can create an extension request for this task."); + expect(res.body.message).to.equal( + "Only assigned user and super user can create an extension request for this task." + ); return done(); }); }); From 074de65431eb08c286a89bd625cafe135fc7ed59 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sun, 6 Aug 2023 01:16:10 +0530 Subject: [PATCH 073/105] refactor: changed the flow for overdue tasks API --- constants/tasks.js | 1 + models/tasks.js | 23 +++++++++++++++++++---- test/integration/tasks.test.js | 15 +++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/constants/tasks.js b/constants/tasks.js index 06dc326e0..37e22ca37 100644 --- a/constants/tasks.js +++ b/constants/tasks.js @@ -20,6 +20,7 @@ const TASK_STATUS = { RELEASED: "RELEASED", VERIFIED: "VERIFIED", DONE: "DONE", + OVERDUE: "OVERDUE", }; // TODO: convert this to new task status diff --git a/models/tasks.js b/models/tasks.js index 88bd98cd6..e3bcb4797 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -5,7 +5,7 @@ const dependencyModel = firestore.collection("taskDependencies"); const userUtils = require("../utils/users"); const { fromFirestoreData, toFirestoreData, buildTasks } = require("../utils/tasks"); const { TASK_TYPE, TASK_STATUS, TASK_STATUS_OLD, TASK_SIZE } = require("../constants/tasks"); -const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, COMPLETED } = TASK_STATUS; +const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, COMPLETED, MERGED, RELEASED, VERIFIED, AVAILABLE } = TASK_STATUS; const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING, OLD_COMPLETED } = TASK_STATUS_OLD; /** @@ -121,9 +121,13 @@ const getBuiltTasks = async (tasksSnapshot, searchTerm) => { const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, prev }) => { try { - const initialQuery = status - ? tasksModel.where("status", "==", status).orderBy("title") - : tasksModel.orderBy("title"); + let initialQuery; + if (status === TASK_STATUS.OVERDUE) { + const currentTime = Math.floor(Date.now() / 1000); + initialQuery = tasksModel.where("endsOn", "<", currentTime); + } else { + initialQuery = status ? tasksModel.where("status", "==", status).orderBy("title") : tasksModel.orderBy("title"); + } let queryDoc = initialQuery; if (prev) { @@ -152,6 +156,17 @@ const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, const nextDoc = await initialQuery.startAfter(last).limit(1).get(); const allTasks = await getBuiltTasks(snapshot); + + if (status === TASK_STATUS.OVERDUE) { + const nonOverdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; + const overdueTasks = allTasks.filter((task) => !nonOverdueTasksStatus.includes(task.status) && task.assignee); + return { + allTasks: overdueTasks, + next: nextDoc.docs[0]?.id ?? "", + prev: prevDoc.docs[0]?.id ?? "", + }; + } + return { allTasks, next: nextDoc.docs[0]?.id ?? "", diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 509d9ea55..67a211cd4 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -211,6 +211,21 @@ describe("Tasks", function () { }); }); + it("Should get all overdue tasks GET /tasks", function (done) { + chai + .request(app) + .get(`/tasks?dev=true&status=overdue`) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.tasks[0].id).to.be.oneOf([taskId, taskId1]); + return done(); + }); + }); + it("Should get tasks when correct query parameters are passed", function (done) { chai .request(app) From e52ccfc7f4bb9fe1e10774fb8945f8ca818954b1 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Fri, 11 Aug 2023 22:33:07 +0530 Subject: [PATCH 074/105] refactor: made the overdue tasks API behind the feature flag --- models/tasks.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/tasks.js b/models/tasks.js index e3bcb4797..2e66173a1 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -119,10 +119,10 @@ const getBuiltTasks = async (tasksSnapshot, searchTerm) => { return taskList; }; -const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, prev }) => { +const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, prev, dev = false }) => { try { let initialQuery; - if (status === TASK_STATUS.OVERDUE) { + if (status === TASK_STATUS.OVERDUE && dev) { const currentTime = Math.floor(Date.now() / 1000); initialQuery = tasksModel.where("endsOn", "<", currentTime); } else { @@ -157,7 +157,7 @@ const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, const allTasks = await getBuiltTasks(snapshot); - if (status === TASK_STATUS.OVERDUE) { + if (status === TASK_STATUS.OVERDUE && dev) { const nonOverdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; const overdueTasks = allTasks.filter((task) => !nonOverdueTasksStatus.includes(task.status) && task.assignee); return { From 9433039004c716d91faa792b9d55a6ba7e0057ac Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sat, 13 May 2023 15:38:29 +0530 Subject: [PATCH 075/105] api created --- controllers/tasks.js | 20 ++++++++++++++++- models/tasks.js | 20 +++++++++++++++++ test/integration/tasks.test.js | 40 ++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index b3918308b..c69fc8066 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -4,7 +4,7 @@ const { addLog } = require("../models/logs"); const { USER_STATUS } = require("../constants/users"); const { addOrUpdate, getRdsUserInfoByGitHubUsername } = require("../models/users"); const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING } = TASK_STATUS_OLD; -const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED } = TASK_STATUS; +const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED, MERGED, COMPLETED, RELEASED, VERIFIED } = TASK_STATUS; const { INTERNAL_SERVER_ERROR, SOMETHING_WENT_WRONG } = require("../constants/errorMessages"); const dependencyModel = require("../models/tasks"); const { transformQuery } = require("../utils/tasks"); @@ -424,6 +424,23 @@ const assignTask = async (req, res) => { } }; +const currentOverdueTasks = async (req, res) => { + try { + const overdueTasks = await tasks.getAllOverDueTasks(); + const overdueTasksFiltered = overdueTasks.filter( + (task) => + task.status !== MERGED || task.status !== COMPLETED || task.status !== RELEASED || task.status !== VERIFIED + ); + return res.json({ + message: "Overdue Tasks returned successfully!", + overdueTasks: overdueTasksFiltered, + }); + } catch (err) { + logger.error(`Error while fetching overdue tasks : ${err}`); + return res.boom.badImplementation("An internal server error occured"); + } +}; + module.exports = { addNewTask, fetchTasks, @@ -434,4 +451,5 @@ module.exports = { updateTaskStatus, overdueTasks, assignTask, + currentOverdueTasks, }; diff --git a/models/tasks.js b/models/tasks.js index 2e66173a1..dd474bef1 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -474,6 +474,26 @@ const overdueTasks = async (overDueTasks) => { throw err; } }; + +/** + * Fetch all overdue tasks objects + * @param overdueTasks : tasks which are overdue + * @return {Promsie} + */ +const getAllOverDueTasks = async () => { + try { + const currentTime = Math.floor(Date.now() / 1000); + const overdueTasksSnapshot = await tasksModel.where("endsOn", "<", currentTime).get(); + const tasks = buildTasks(overdueTasksSnapshot); + const promises = tasks.map((task) => fromFirestoreData(task)); + const overDueTasks = await Promise.all(promises); + return overDueTasks; + } catch (err) { + logger.error("error getting all overdue tasks", err); + throw err; + } +}; + module.exports = { updateTask, fetchTasks, diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 67a211cd4..4b44cccf8 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -81,6 +81,46 @@ describe("Tasks", function () { sinon.restore(); }); + describe("GET /tasks/overdue/current", function () { + it("Should return all the current overdue Tasks", function (done) { + chai + .request(app) + .get("/tasks/overdue/current") + .set("cookie", `${cookieName}=${superUserJwt}`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Overdue Tasks returned successfully!"); + expect(res.body.overdueTasks[0].id).to.be.oneOf([taskId, taskId1]); + expect(res.body.overdueTasks[1].id).to.be.oneOf([taskId, taskId1]); + return done(); + }); + }); + it("Should return 401 if someone other than superuser logged in", function (done) { + chai + .request(app) + .get(`/tasks/overdue/current`) + .set("cookie", `${cookieName}=${jwt}`) + .end((err, res) => { + if (err) { + return done(); + } + + expect(res).to.have.status(401); + expect(res.body).to.be.an("object"); + expect(res.body).to.eql({ + statusCode: 401, + error: "Unauthorized", + message: "You are not authorized for this action.", + }); + + return done(); + }); + }); + }); + describe("POST /tasks - creates a new task", function () { it("Should return success response after adding the task", function (done) { chai From 0e6e8c4254469805eae297a0aeb9c4c61b4d848d Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sat, 13 May 2023 15:49:45 +0530 Subject: [PATCH 076/105] done suggested changes --- controllers/tasks.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index c69fc8066..30d5b6216 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -427,10 +427,8 @@ const assignTask = async (req, res) => { const currentOverdueTasks = async (req, res) => { try { const overdueTasks = await tasks.getAllOverDueTasks(); - const overdueTasksFiltered = overdueTasks.filter( - (task) => - task.status !== MERGED || task.status !== COMPLETED || task.status !== RELEASED || task.status !== VERIFIED - ); + const overdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED]; + const overdueTasksFiltered = overdueTasks.filter((task) => !overdueTasksStatus.includes(task.status)); return res.json({ message: "Overdue Tasks returned successfully!", overdueTasks: overdueTasksFiltered, From 20bb7c2789950b30221646b046ea31f3711ffd47 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sat, 13 May 2023 23:37:20 +0530 Subject: [PATCH 077/105] added one more check --- controllers/tasks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index 30d5b6216..13db0e631 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -4,7 +4,7 @@ const { addLog } = require("../models/logs"); const { USER_STATUS } = require("../constants/users"); const { addOrUpdate, getRdsUserInfoByGitHubUsername } = require("../models/users"); const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING } = TASK_STATUS_OLD; -const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED, MERGED, COMPLETED, RELEASED, VERIFIED } = TASK_STATUS; +const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED, MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE } = TASK_STATUS; const { INTERNAL_SERVER_ERROR, SOMETHING_WENT_WRONG } = require("../constants/errorMessages"); const dependencyModel = require("../models/tasks"); const { transformQuery } = require("../utils/tasks"); @@ -427,7 +427,7 @@ const assignTask = async (req, res) => { const currentOverdueTasks = async (req, res) => { try { const overdueTasks = await tasks.getAllOverDueTasks(); - const overdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED]; + const overdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; const overdueTasksFiltered = overdueTasks.filter((task) => !overdueTasksStatus.includes(task.status)); return res.json({ message: "Overdue Tasks returned successfully!", From 224b2c9bce733f981c101a979e5c2209618f23e5 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sat, 22 Jul 2023 18:44:32 +0530 Subject: [PATCH 078/105] refactor: overdue and extension requests APIs --- controllers/extensionRequests.js | 69 ++++++++++++++++++++++++++++++++ controllers/tasks.js | 4 +- routes/extensionRequests.js | 7 ++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index 15b2e1a19..1d8ef60eb 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -236,6 +236,74 @@ const updateExtensionRequestStatus = async (req, res) => { } }; +/** + * Create ETA extension Request by Admin + * + * @param req {Object} - Express request object + * @param res {Object} - Express response object + */ +const createTaskExtensionRequestAdmin = async (req, res) => { + try { + const extensionBody = req.body; + + if (!req.userData.roles?.super_user) { + return res.boom.forbidden("Only Super User can create an extension request for this task."); + } + + const assigneeUsername = extensionBody.assignee; + const assigneeId = await getUserId(extensionBody.assignee); + extensionBody.assignee = assigneeId; + const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); + if (!task) { + return res.boom.badRequest("Task with this id or taskid doesn't exist."); + } + if (task.endsOn >= extensionBody.newEndsOn) { + return res.boom.badRequest("The value for newEndsOn should be greater than the previous ETA"); + } + if (extensionBody.oldEndsOn !== task.endsOn) { + extensionBody.oldEndsOn = task.endsOn; + } + if (task.assignee !== assigneeUsername) { + return res.boom.badRequest("This task is assigned to some different user"); + } + + const prevExtensionRequest = await extensionRequestsQuery.fetchExtensionRequests({ + taskId: extensionBody.taskId, + assignee: extensionBody.assignee, + }); + if (prevExtensionRequest.length) { + return res.boom.forbidden("An extension request for this task already exists."); + } + + const extensionRequest = await extensionRequestsQuery.createExtensionRequest(extensionBody); + + const extensionLog = { + type: "extensionRequests", + meta: { + taskId: extensionBody.taskId, + createdBy: req.userData.id, + }, + body: { + extensionRequestId: extensionRequest.id, + oldEndsOn: task.endsOn, + newEndsOn: extensionBody.newEndsOn, + assignee: extensionBody.assignee, + status: EXTENSION_REQUEST_STATUS.PENDING, + }, + }; + + await addLog(extensionLog.type, extensionLog.meta, extensionLog.body); + + return res.json({ + message: "Extension Request created successfully!", + extensionRequest: { ...extensionBody, id: extensionRequest.id }, + }); + } catch (err) { + logger.error(`Error while creating new extension request: ${err}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + module.exports = { createTaskExtensionRequest, fetchExtensionRequests, @@ -243,4 +311,5 @@ module.exports = { getSelfExtensionRequests, updateExtensionRequest, updateExtensionRequestStatus, + createTaskExtensionRequestAdmin, }; diff --git a/controllers/tasks.js b/controllers/tasks.js index 13db0e631..0d440da71 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -428,7 +428,9 @@ const currentOverdueTasks = async (req, res) => { try { const overdueTasks = await tasks.getAllOverDueTasks(); const overdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; - const overdueTasksFiltered = overdueTasks.filter((task) => !overdueTasksStatus.includes(task.status)); + const overdueTasksFiltered = overdueTasks.filter( + (task) => !overdueTasksStatus.includes(task.status) && task.assignee + ); return res.json({ message: "Overdue Tasks returned successfully!", overdueTasks: overdueTasksFiltered, diff --git a/routes/extensionRequests.js b/routes/extensionRequests.js index 91fd05f07..d4d8606a4 100644 --- a/routes/extensionRequests.js +++ b/routes/extensionRequests.js @@ -11,6 +11,13 @@ const { } = require("../middlewares/validators/extensionRequests"); router.post("/", authenticate, createExtensionRequest, extensionRequests.createTaskExtensionRequest); +router.post( + "/admin", + authenticate, + authorizeRoles([SUPERUSER]), + createExtensionRequest, + extensionRequests.createTaskExtensionRequestAdmin +); router.get("/", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.fetchExtensionRequests); router.get("/self", authenticate, extensionRequests.getSelfExtensionRequests); router.get("/:id", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.getExtensionRequest); From 2fba8cd75d912070329e7728be22137580820187 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sun, 30 Jul 2023 20:18:11 +0530 Subject: [PATCH 079/105] feat: added test cases for tasks validators --- controllers/extensionRequests.js | 69 -------------------------------- routes/extensionRequests.js | 7 ---- 2 files changed, 76 deletions(-) diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index 1d8ef60eb..15b2e1a19 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -236,74 +236,6 @@ const updateExtensionRequestStatus = async (req, res) => { } }; -/** - * Create ETA extension Request by Admin - * - * @param req {Object} - Express request object - * @param res {Object} - Express response object - */ -const createTaskExtensionRequestAdmin = async (req, res) => { - try { - const extensionBody = req.body; - - if (!req.userData.roles?.super_user) { - return res.boom.forbidden("Only Super User can create an extension request for this task."); - } - - const assigneeUsername = extensionBody.assignee; - const assigneeId = await getUserId(extensionBody.assignee); - extensionBody.assignee = assigneeId; - const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); - if (!task) { - return res.boom.badRequest("Task with this id or taskid doesn't exist."); - } - if (task.endsOn >= extensionBody.newEndsOn) { - return res.boom.badRequest("The value for newEndsOn should be greater than the previous ETA"); - } - if (extensionBody.oldEndsOn !== task.endsOn) { - extensionBody.oldEndsOn = task.endsOn; - } - if (task.assignee !== assigneeUsername) { - return res.boom.badRequest("This task is assigned to some different user"); - } - - const prevExtensionRequest = await extensionRequestsQuery.fetchExtensionRequests({ - taskId: extensionBody.taskId, - assignee: extensionBody.assignee, - }); - if (prevExtensionRequest.length) { - return res.boom.forbidden("An extension request for this task already exists."); - } - - const extensionRequest = await extensionRequestsQuery.createExtensionRequest(extensionBody); - - const extensionLog = { - type: "extensionRequests", - meta: { - taskId: extensionBody.taskId, - createdBy: req.userData.id, - }, - body: { - extensionRequestId: extensionRequest.id, - oldEndsOn: task.endsOn, - newEndsOn: extensionBody.newEndsOn, - assignee: extensionBody.assignee, - status: EXTENSION_REQUEST_STATUS.PENDING, - }, - }; - - await addLog(extensionLog.type, extensionLog.meta, extensionLog.body); - - return res.json({ - message: "Extension Request created successfully!", - extensionRequest: { ...extensionBody, id: extensionRequest.id }, - }); - } catch (err) { - logger.error(`Error while creating new extension request: ${err}`); - return res.boom.badImplementation(INTERNAL_SERVER_ERROR); - } -}; - module.exports = { createTaskExtensionRequest, fetchExtensionRequests, @@ -311,5 +243,4 @@ module.exports = { getSelfExtensionRequests, updateExtensionRequest, updateExtensionRequestStatus, - createTaskExtensionRequestAdmin, }; diff --git a/routes/extensionRequests.js b/routes/extensionRequests.js index d4d8606a4..91fd05f07 100644 --- a/routes/extensionRequests.js +++ b/routes/extensionRequests.js @@ -11,13 +11,6 @@ const { } = require("../middlewares/validators/extensionRequests"); router.post("/", authenticate, createExtensionRequest, extensionRequests.createTaskExtensionRequest); -router.post( - "/admin", - authenticate, - authorizeRoles([SUPERUSER]), - createExtensionRequest, - extensionRequests.createTaskExtensionRequestAdmin -); router.get("/", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.fetchExtensionRequests); router.get("/self", authenticate, extensionRequests.getSelfExtensionRequests); router.get("/:id", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.getExtensionRequest); From fc8101e992dafc84b4b67cd8072692404bdcbd53 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Thu, 3 Aug 2023 14:42:24 +0530 Subject: [PATCH 080/105] refactor: requested changes for overdue tasks API --- controllers/tasks.js | 6 +++--- models/tasks.js | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index 0d440da71..39a13cda2 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -426,10 +426,10 @@ const assignTask = async (req, res) => { const currentOverdueTasks = async (req, res) => { try { - const overdueTasks = await tasks.getAllOverDueTasks(); - const overdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; + const overdueTasks = await tasks.getOverdueTasks(); + const nonOverdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; const overdueTasksFiltered = overdueTasks.filter( - (task) => !overdueTasksStatus.includes(task.status) && task.assignee + (task) => !nonOverdueTasksStatus.includes(task.status) && task.assignee ); return res.json({ message: "Overdue Tasks returned successfully!", diff --git a/models/tasks.js b/models/tasks.js index dd474bef1..2df01b0a9 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -477,16 +477,15 @@ const overdueTasks = async (overDueTasks) => { /** * Fetch all overdue tasks objects - * @param overdueTasks : tasks which are overdue - * @return {Promsie} + * @return {Array} */ -const getAllOverDueTasks = async () => { +const getOverdueTasks = async () => { try { const currentTime = Math.floor(Date.now() / 1000); const overdueTasksSnapshot = await tasksModel.where("endsOn", "<", currentTime).get(); const tasks = buildTasks(overdueTasksSnapshot); - const promises = tasks.map((task) => fromFirestoreData(task)); - const overDueTasks = await Promise.all(promises); + const tasksPromises = tasks.map((task) => fromFirestoreData(task)); + const overDueTasks = await Promise.all(tasksPromises); return overDueTasks; } catch (err) { logger.error("error getting all overdue tasks", err); From 3432d30e96174a9752d23a5ca2a270960e72dd33 Mon Sep 17 00:00:00 2001 From: ivinayakg Date: Sun, 6 Aug 2023 01:16:10 +0530 Subject: [PATCH 081/105] refactor: changed the flow for overdue tasks API --- controllers/tasks.js | 20 +---------------- models/tasks.js | 18 --------------- test/integration/tasks.test.js | 40 ---------------------------------- 3 files changed, 1 insertion(+), 77 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index 39a13cda2..b3918308b 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -4,7 +4,7 @@ const { addLog } = require("../models/logs"); const { USER_STATUS } = require("../constants/users"); const { addOrUpdate, getRdsUserInfoByGitHubUsername } = require("../models/users"); const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING } = TASK_STATUS_OLD; -const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED, MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE } = TASK_STATUS; +const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, ASSIGNED } = TASK_STATUS; const { INTERNAL_SERVER_ERROR, SOMETHING_WENT_WRONG } = require("../constants/errorMessages"); const dependencyModel = require("../models/tasks"); const { transformQuery } = require("../utils/tasks"); @@ -424,23 +424,6 @@ const assignTask = async (req, res) => { } }; -const currentOverdueTasks = async (req, res) => { - try { - const overdueTasks = await tasks.getOverdueTasks(); - const nonOverdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; - const overdueTasksFiltered = overdueTasks.filter( - (task) => !nonOverdueTasksStatus.includes(task.status) && task.assignee - ); - return res.json({ - message: "Overdue Tasks returned successfully!", - overdueTasks: overdueTasksFiltered, - }); - } catch (err) { - logger.error(`Error while fetching overdue tasks : ${err}`); - return res.boom.badImplementation("An internal server error occured"); - } -}; - module.exports = { addNewTask, fetchTasks, @@ -451,5 +434,4 @@ module.exports = { updateTaskStatus, overdueTasks, assignTask, - currentOverdueTasks, }; diff --git a/models/tasks.js b/models/tasks.js index 2df01b0a9..1a5f5cf53 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -475,24 +475,6 @@ const overdueTasks = async (overDueTasks) => { } }; -/** - * Fetch all overdue tasks objects - * @return {Array} - */ -const getOverdueTasks = async () => { - try { - const currentTime = Math.floor(Date.now() / 1000); - const overdueTasksSnapshot = await tasksModel.where("endsOn", "<", currentTime).get(); - const tasks = buildTasks(overdueTasksSnapshot); - const tasksPromises = tasks.map((task) => fromFirestoreData(task)); - const overDueTasks = await Promise.all(tasksPromises); - return overDueTasks; - } catch (err) { - logger.error("error getting all overdue tasks", err); - throw err; - } -}; - module.exports = { updateTask, fetchTasks, diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 4b44cccf8..67a211cd4 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -81,46 +81,6 @@ describe("Tasks", function () { sinon.restore(); }); - describe("GET /tasks/overdue/current", function () { - it("Should return all the current overdue Tasks", function (done) { - chai - .request(app) - .get("/tasks/overdue/current") - .set("cookie", `${cookieName}=${superUserJwt}`) - .end((err, res) => { - if (err) { - return done(err); - } - expect(res).to.have.status(200); - expect(res.body.message).to.be.equal("Overdue Tasks returned successfully!"); - expect(res.body.overdueTasks[0].id).to.be.oneOf([taskId, taskId1]); - expect(res.body.overdueTasks[1].id).to.be.oneOf([taskId, taskId1]); - return done(); - }); - }); - it("Should return 401 if someone other than superuser logged in", function (done) { - chai - .request(app) - .get(`/tasks/overdue/current`) - .set("cookie", `${cookieName}=${jwt}`) - .end((err, res) => { - if (err) { - return done(); - } - - expect(res).to.have.status(401); - expect(res.body).to.be.an("object"); - expect(res.body).to.eql({ - statusCode: 401, - error: "Unauthorized", - message: "You are not authorized for this action.", - }); - - return done(); - }); - }); - }); - describe("POST /tasks - creates a new task", function () { it("Should return success response after adding the task", function (done) { chai From eeb0dff44184cf7489884fa6d13a4a5ae8ff2f91 Mon Sep 17 00:00:00 2001 From: Pratiyush Kumar Date: Sun, 13 Aug 2023 14:15:43 +0530 Subject: [PATCH 082/105] changed the function working and made the getUsersBasedOnFilter generic --- models/users.js | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/models/users.js b/models/users.js index efd6c4bc8..c303ecc50 100644 --- a/models/users.js +++ b/models/users.js @@ -458,10 +458,6 @@ const getRdsUserInfoByGitHubUsername = async (githubUsername) => { */ const getUsersBasedOnFilter = async (query) => { - if (query.state === "ONBOARDING" && query.time) { - const fetchUsersWithOnBoardingState = await getUsersWithOnboardingState(query); - return fetchUsersWithOnBoardingState; - } const allQueryKeys = Object.keys(query); const doesTagQueryExist = arraysHaveCommonItem(ITEM_TAG, allQueryKeys); const doesStateQueryExist = arraysHaveCommonItem(USER_STATE, allQueryKeys); @@ -508,6 +504,10 @@ const getUsersBasedOnFilter = async (query) => { const userRefs = finalItems.map((itemId) => userModel.doc(itemId)); const userDocs = (await firestore.getAll(...userRefs)).map((doc) => ({ id: doc.id, ...doc.data() })); const filteredUserDocs = userDocs.filter((doc) => !doc.roles?.archived); + if (query.time) { + const fetchUsersWithOnBoardingState = await getUsersWithOnboardingState(filteredUserDocs, stateItems, query.time); + return fetchUsersWithOnBoardingState; + } return filteredUserDocs; } @@ -541,31 +541,16 @@ const getUsersBasedOnFilter = async (query) => { return filteredUsers.filter((user) => !user.roles?.archived); } + return []; }; -const getUsersWithOnboardingState = async (query) => { - const allUserStatus = []; - const onboardedUsersInRange = []; - const filteredUsers = []; - - const time = query.time; +const getUsersWithOnboardingState = async (filteredUserDocs, stateItems, time) => { + const UsersInRange = []; const range = Number(time.split("d")[0]); - const data = await userStatusModel.where("currentStatus.state", "==", query.state).get(); - data.forEach((doc) => { - const currentUserStatus = { - id: doc.id, - userId: doc.data().userId, - currentStatus: doc.data().currentStatus, - monthlyHours: doc.data().monthlyHours, - }; - allUserStatus.push(currentUserStatus); + const filteredUsers = filteredUserDocs.filter((userDoc) => { + return stateItems.some((stateItem) => stateItem.userId === userDoc.id); }); - - for (const user of allUserStatus) { - const result = await userModel.doc(user.userId).get(); - filteredUsers.push(result.data()); - } filteredUsers.forEach((user) => { if (user.discordJoinedAt) { const userDiscordJoinedDate = new Date(user.discordJoinedAt); @@ -573,11 +558,11 @@ const getUsersWithOnboardingState = async (query) => { const timeDifferenceInMilliseconds = currentTimeStamp - userDiscordJoinedDate.getTime(); const currentAndUserJoinedDateDifference = Math.floor(timeDifferenceInMilliseconds / (1000 * 60 * 60 * 24)); if (currentAndUserJoinedDateDifference > range) { - onboardedUsersInRange.push(user); + UsersInRange.push(user); } } }); - return onboardedUsersInRange; + return UsersInRange; }; /** * Fetch all users From 7c8c560baedbe58d2dab35ea041019f5ea4e1445 Mon Sep 17 00:00:00 2001 From: shreya-mishra Date: Thu, 10 Aug 2023 21:07:44 +0530 Subject: [PATCH 083/105] completed validation of verify status --- controllers/auth.js | 25 ++++++++++++++++++++++++- middlewares/validators/qrCodeAuth.js | 15 +++++++++++++++ models/qrCodeAuth.js | 12 ++++++++++-- routes/auth.js | 6 ++++++ test/unit/models/qrCodeAuth.test.js | 2 +- 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/controllers/auth.js b/controllers/auth.js index 9c6554f68..b1ad1458d 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -151,7 +151,7 @@ const updateAuthStatus = async (req, res) => { const fetchUserDeviceInfo = async (req, res) => { try { const deviceId = req.query.device_id; - const userDeviceInfoData = await QrCodeAuthModel.retrieveUserDeviceInfo(deviceId); + const userDeviceInfoData = await QrCodeAuthModel.retrieveUserDeviceInfo({ deviceId }); if (!userDeviceInfoData.userExists) { return res.boom.notFound(`User with id ${deviceId} does not exist.`); } @@ -165,6 +165,28 @@ const fetchUserDeviceInfo = async (req, res) => { } }; +const fetchDeviceDetails = async (req, res) => { + try { + const userId = req.query.user_id; + const userDeviceInfoData = await QrCodeAuthModel.retrieveUserDeviceInfo({userId}); + console.log('userDeviceInfoData', userDeviceInfoData); + if (!userDeviceInfoData.userExists) { + // return res.boom.notFound(`User with id ${userId} does not exist.`); + return res.json({ + message: "Authentication document does not Exists", + // data: { device_info: userDeviceInfoData.data.device_info }, + }); + } + return res.json({ + message: "Authentication document Exists", + data: { device_info: userDeviceInfoData.data.device_info }, + }); + } catch (error) { + logger.error(`Error while fetching user device info: ${error}`); + return res.boom.badImplementation(SOMETHING_WENT_WRONG); + } +} + module.exports = { githubAuthLogin, githubAuthCallback, @@ -172,4 +194,5 @@ module.exports = { storeUserDeviceInfo, updateAuthStatus, fetchUserDeviceInfo, + fetchDeviceDetails, }; diff --git a/middlewares/validators/qrCodeAuth.js b/middlewares/validators/qrCodeAuth.js index 8aea8b3e7..0d3943ba9 100644 --- a/middlewares/validators/qrCodeAuth.js +++ b/middlewares/validators/qrCodeAuth.js @@ -46,8 +46,23 @@ const validateFetchingUserDocument = async (req, res, next) => { } }; +const validateFetchingUserDeviceStatus = async (req, res, next) => { + const schema = joi.object().strict().keys({ + user_id: joi.string().required(), + }); + + try { + await schema.validateAsync(req.query); + next(); + } catch (error) { + logger.error(`Invalid Query Parameters Passed`); + res.boom.badRequest(`Invalid Query Parameters Passed`); + } +}; + module.exports = { storeUserDeviceInfo, validateAuthStatus, validateFetchingUserDocument, + validateFetchingUserDeviceStatus, }; diff --git a/models/qrCodeAuth.js b/models/qrCodeAuth.js index 0bbdadbac..390d460a8 100644 --- a/models/qrCodeAuth.js +++ b/models/qrCodeAuth.js @@ -54,9 +54,17 @@ const storeUserDeviceInfo = async (userDeviceInfoData) => { } }; -const retrieveUserDeviceInfo = async (deviceId) => { +const retrieveUserDeviceInfo = async ({ deviceId, userId }) => { + let queryDocument; try { - const queryDocument = await QrCodeAuthModel.where("device_id", "==", deviceId).get(); + if (deviceId) { + queryDocument = await QrCodeAuthModel.where("device_id", "==", deviceId).get(); + } + else if (userId) { + queryDocument = await QrCodeAuthModel.where("user_id", "==", userId) + .where("authorization_status", "==", "NOT_INIT") + .get(); + } const userData = queryDocument.docs[0]; if (!userData) { diff --git a/routes/auth.js b/routes/auth.js index 110d6c34c..5ffd833d8 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -13,6 +13,12 @@ router.get("/signout", auth.signout); router.get("/qr-code-auth", userDeviceInfoValidator.validateFetchingUserDocument, auth.fetchUserDeviceInfo); +router.get( + "/qr-code-auth-getDeviceInfo", + userDeviceInfoValidator.validateFetchingUserDeviceStatus, + auth.fetchDeviceDetails +); + router.post("/qr-code-auth", userDeviceInfoValidator.storeUserDeviceInfo, auth.storeUserDeviceInfo); router.patch( diff --git a/test/unit/models/qrCodeAuth.test.js b/test/unit/models/qrCodeAuth.test.js index 8a3e98b54..d4dda9f3a 100644 --- a/test/unit/models/qrCodeAuth.test.js +++ b/test/unit/models/qrCodeAuth.test.js @@ -92,7 +92,7 @@ describe("mobile auth", function () { }; await qrCodeAuth.storeUserDeviceInfo(userDeviceInfoData); - const response = await qrCodeAuth.retrieveUserDeviceInfo(userDeviceInfoData.device_id); + const response = await qrCodeAuth.retrieveUserDeviceInfo({deviceId : userDeviceInfoData.device_id}); const userDeviceInfo = response.data; const { user_id: userID, From 82addc6386316d5d21214ec658a91d5acd658a6f Mon Sep 17 00:00:00 2001 From: Lakshay Manchanda Date: Fri, 11 Aug 2023 18:56:35 +0530 Subject: [PATCH 084/105] tests for model and middleware --- controllers/auth.js | 11 ++--- models/qrCodeAuth.js | 3 +- .../middlewares/qrCodeAuthValidator.test.js | 36 +++++++++++++++++ test/unit/models/qrCodeAuth.test.js | 40 ++++++++++++++++++- 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/controllers/auth.js b/controllers/auth.js index b1ad1458d..91ffcf4f2 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -168,14 +168,9 @@ const fetchUserDeviceInfo = async (req, res) => { const fetchDeviceDetails = async (req, res) => { try { const userId = req.query.user_id; - const userDeviceInfoData = await QrCodeAuthModel.retrieveUserDeviceInfo({userId}); - console.log('userDeviceInfoData', userDeviceInfoData); + const userDeviceInfoData = await QrCodeAuthModel.retrieveUserDeviceInfo({ userId }); if (!userDeviceInfoData.userExists) { - // return res.boom.notFound(`User with id ${userId} does not exist.`); - return res.json({ - message: "Authentication document does not Exists", - // data: { device_info: userDeviceInfoData.data.device_info }, - }); + return res.boom.notFound(`User with id ${userId} does not exist.`); } return res.json({ message: "Authentication document Exists", @@ -185,7 +180,7 @@ const fetchDeviceDetails = async (req, res) => { logger.error(`Error while fetching user device info: ${error}`); return res.boom.badImplementation(SOMETHING_WENT_WRONG); } -} +}; module.exports = { githubAuthLogin, diff --git a/models/qrCodeAuth.js b/models/qrCodeAuth.js index 390d460a8..dc3882039 100644 --- a/models/qrCodeAuth.js +++ b/models/qrCodeAuth.js @@ -59,8 +59,7 @@ const retrieveUserDeviceInfo = async ({ deviceId, userId }) => { try { if (deviceId) { queryDocument = await QrCodeAuthModel.where("device_id", "==", deviceId).get(); - } - else if (userId) { + } else if (userId) { queryDocument = await QrCodeAuthModel.where("user_id", "==", userId) .where("authorization_status", "==", "NOT_INIT") .get(); diff --git a/test/unit/middlewares/qrCodeAuthValidator.test.js b/test/unit/middlewares/qrCodeAuthValidator.test.js index 63606b5c9..55d23896e 100644 --- a/test/unit/middlewares/qrCodeAuthValidator.test.js +++ b/test/unit/middlewares/qrCodeAuthValidator.test.js @@ -3,6 +3,7 @@ const { validateAuthStatus, storeUserDeviceInfo, validateFetchingUserDocument, + validateFetchingUserDeviceStatus, } = require("../../../middlewares/validators/qrCodeAuth"); const { expect } = require("chai"); const { userDeviceInfoDataArray } = require("../../fixtures/qrCodeAuth/qrCodeAuth"); @@ -104,4 +105,39 @@ describe("qrCodeAuth", function () { expect(nextSpy.callCount).to.be.equal(0); }); }); + + describe("test get call validator with userId", function () { + it("Allows request to pass on valid params", async function () { + const req = { + query: { + user_id: "USER_ID", + }, + }; + + const res = {}; + + const nextSpy = Sinon.spy(); + await validateFetchingUserDeviceStatus(req, res, nextSpy); + expect(nextSpy.callCount).to.be.equal(1); + }); + + it("Does not allow request to pass on invalid params", async function () { + const req = { + query: { + device_id: "ID", + device_type: "DEVICE_TYPE", + }, + }; + + const res = { + boom: { + badRequest: () => {}, + }, + }; + + const nextSpy = Sinon.spy(); + await validateFetchingUserDeviceStatus(req, res, nextSpy); + expect(nextSpy.callCount).to.be.equal(0); + }); + }); }); diff --git a/test/unit/models/qrCodeAuth.test.js b/test/unit/models/qrCodeAuth.test.js index d4dda9f3a..add397171 100644 --- a/test/unit/models/qrCodeAuth.test.js +++ b/test/unit/models/qrCodeAuth.test.js @@ -92,7 +92,45 @@ describe("mobile auth", function () { }; await qrCodeAuth.storeUserDeviceInfo(userDeviceInfoData); - const response = await qrCodeAuth.retrieveUserDeviceInfo({deviceId : userDeviceInfoData.device_id}); + const response = await qrCodeAuth.retrieveUserDeviceInfo({ deviceId: userDeviceInfoData.device_id }); + const userDeviceInfo = response.data; + const { + user_id: userID, + device_info: deviceInfo, + device_id: deviceId, + authorization_status: authorizationStatus, + access_token: accessToken, + } = userDeviceInfo; + + const data = (await qrCodeAuthModel.doc(userId).get()).data(); + + Object.keys(userDeviceInfo).forEach((key) => { + expect(userDeviceInfo[key]).to.deep.equal(data[key]); + }); + + expect(response).to.be.an("object"); + expect(userID).to.be.a("string"); + expect(deviceInfo).to.be.a("string"); + expect(deviceId).to.be.a("string"); + expect(authorizationStatus).to.be.a("string"); + expect(accessToken).to.be.a("string"); + }); + }); + + describe("retrieveUserDeviceInfo with userId", function () { + it("should fetch the user device info for mobile auth", async function () { + const userData = userDataArray[0]; + const { userId } = await users.addOrUpdate(userData); + + const userDeviceInfoData = { + ...userDeviceInfoDataArray[0], + user_id: userId, + authorization_status: "NOT_INIT", + access_token: "ACCESS_TOKEN", + }; + + await qrCodeAuth.storeUserDeviceInfo(userDeviceInfoData); + const response = await qrCodeAuth.retrieveUserDeviceInfo({ userId: userDeviceInfoData.user_id }); const userDeviceInfo = response.data; const { user_id: userID, From c589152d100e0012ff610de3cbbdf3393a587056 Mon Sep 17 00:00:00 2001 From: shreya-mishra Date: Fri, 11 Aug 2023 20:45:16 +0530 Subject: [PATCH 085/105] resolved the comments --- controllers/auth.js | 6 +++--- middlewares/validators/qrCodeAuth.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/controllers/auth.js b/controllers/auth.js index 91ffcf4f2..b3da47643 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -150,7 +150,7 @@ const updateAuthStatus = async (req, res) => { const fetchUserDeviceInfo = async (req, res) => { try { - const deviceId = req.query.device_id; + const { device_id: deviceId } = req.query; const userDeviceInfoData = await QrCodeAuthModel.retrieveUserDeviceInfo({ deviceId }); if (!userDeviceInfoData.userExists) { return res.boom.notFound(`User with id ${deviceId} does not exist.`); @@ -167,14 +167,14 @@ const fetchUserDeviceInfo = async (req, res) => { const fetchDeviceDetails = async (req, res) => { try { - const userId = req.query.user_id; + const { user_id: userId } = req.query; const userDeviceInfoData = await QrCodeAuthModel.retrieveUserDeviceInfo({ userId }); if (!userDeviceInfoData.userExists) { return res.boom.notFound(`User with id ${userId} does not exist.`); } return res.json({ message: "Authentication document Exists", - data: { device_info: userDeviceInfoData.data.device_info }, + data: { device_info: userDeviceInfoData.data?.device_info }, }); } catch (error) { logger.error(`Error while fetching user device info: ${error}`); diff --git a/middlewares/validators/qrCodeAuth.js b/middlewares/validators/qrCodeAuth.js index 0d3943ba9..ff678f825 100644 --- a/middlewares/validators/qrCodeAuth.js +++ b/middlewares/validators/qrCodeAuth.js @@ -41,8 +41,8 @@ const validateFetchingUserDocument = async (req, res, next) => { await schema.validateAsync(req.query); next(); } catch (error) { - logger.error(`Invalid Query Parameters Passed`); - res.boom.badRequest(`Invalid Query Parameters Passed`); + logger.error("Invalid Query Parameters Passed"); + res.boom.badRequest("Invalid Query Parameters Passed"); } }; @@ -55,8 +55,8 @@ const validateFetchingUserDeviceStatus = async (req, res, next) => { await schema.validateAsync(req.query); next(); } catch (error) { - logger.error(`Invalid Query Parameters Passed`); - res.boom.badRequest(`Invalid Query Parameters Passed`); + logger.error("Invalid Query Parameters Passed"); + res.boom.badRequest("Invalid Query Parameters Passed"); } }; From a3d4e3573b3e9d0db4a5d94f0d8cb5dc16550caf Mon Sep 17 00:00:00 2001 From: shreya-mishra Date: Thu, 10 Aug 2023 21:07:44 +0530 Subject: [PATCH 086/105] completed validation of verify status --- middlewares/validators/qrCodeAuth.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/middlewares/validators/qrCodeAuth.js b/middlewares/validators/qrCodeAuth.js index ff678f825..860dfb402 100644 --- a/middlewares/validators/qrCodeAuth.js +++ b/middlewares/validators/qrCodeAuth.js @@ -60,6 +60,20 @@ const validateFetchingUserDeviceStatus = async (req, res, next) => { } }; +const validateFetchingUserDeviceStatus = async (req, res, next) => { + const schema = joi.object().strict().keys({ + user_id: joi.string().required(), + }); + + try { + await schema.validateAsync(req.query); + next(); + } catch (error) { + logger.error(`Invalid Query Parameters Passed`); + res.boom.badRequest(`Invalid Query Parameters Passed`); + } +}; + module.exports = { storeUserDeviceInfo, validateAuthStatus, From c00c82561a0799d392b6ffc675599f9a836076dc Mon Sep 17 00:00:00 2001 From: shreya-mishra Date: Fri, 11 Aug 2023 20:45:16 +0530 Subject: [PATCH 087/105] resolved the comments --- middlewares/validators/qrCodeAuth.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/middlewares/validators/qrCodeAuth.js b/middlewares/validators/qrCodeAuth.js index 860dfb402..ff678f825 100644 --- a/middlewares/validators/qrCodeAuth.js +++ b/middlewares/validators/qrCodeAuth.js @@ -60,20 +60,6 @@ const validateFetchingUserDeviceStatus = async (req, res, next) => { } }; -const validateFetchingUserDeviceStatus = async (req, res, next) => { - const schema = joi.object().strict().keys({ - user_id: joi.string().required(), - }); - - try { - await schema.validateAsync(req.query); - next(); - } catch (error) { - logger.error(`Invalid Query Parameters Passed`); - res.boom.badRequest(`Invalid Query Parameters Passed`); - } -}; - module.exports = { storeUserDeviceInfo, validateAuthStatus, From 3a4e95b8b70966eabf613e45e9eb16c0ca5a7b5b Mon Sep 17 00:00:00 2001 From: shreya-mishra Date: Sat, 12 Aug 2023 17:15:54 +0530 Subject: [PATCH 088/105] sync with develop From f46faeb7408d793c342618d2501e32b147aac49a Mon Sep 17 00:00:00 2001 From: shreya-mishra Date: Sun, 13 Aug 2023 00:41:26 +0530 Subject: [PATCH 089/105] resolved the comments and added generateUniqueToken action --- controllers/auth.js | 9 +++++++-- middlewares/validators/qrCodeAuth.js | 15 --------------- models/qrCodeAuth.js | 6 +++--- routes/auth.js | 6 +----- utils/generateUniqueToken.js | 12 ++++++++++++ 5 files changed, 23 insertions(+), 25 deletions(-) create mode 100644 utils/generateUniqueToken.js diff --git a/controllers/auth.js b/controllers/auth.js index b3da47643..d9c2bde4e 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -3,6 +3,7 @@ const users = require("../models/users"); const QrCodeAuthModel = require("../models/qrCodeAuth"); const authService = require("../services/authService"); const { SOMETHING_WENT_WRONG, DATA_ADDED_SUCCESSFULLY, BAD_REQUEST } = require("../constants/errorMessages"); +const { generateUniqueToken } = require("../utils/generateUniqueToken"); /** * Makes authentication call to GitHub statergy @@ -132,7 +133,11 @@ const updateAuthStatus = async (req, res) => { try { const userId = req.userData.id; const authStatus = req.params.authorization_status; - const result = await QrCodeAuthModel.updateStatus(userId, authStatus); + let token; + if (authStatus === "AUTHORIZED") { + token = await generateUniqueToken(); + } + const result = await QrCodeAuthModel.updateStatus(userId, authStatus, token); if (!result.userExists) { return res.boom.notFound("Document not found!"); @@ -167,7 +172,7 @@ const fetchUserDeviceInfo = async (req, res) => { const fetchDeviceDetails = async (req, res) => { try { - const { user_id: userId } = req.query; + const userId = req.userData.id; const userDeviceInfoData = await QrCodeAuthModel.retrieveUserDeviceInfo({ userId }); if (!userDeviceInfoData.userExists) { return res.boom.notFound(`User with id ${userId} does not exist.`); diff --git a/middlewares/validators/qrCodeAuth.js b/middlewares/validators/qrCodeAuth.js index ff678f825..223c56964 100644 --- a/middlewares/validators/qrCodeAuth.js +++ b/middlewares/validators/qrCodeAuth.js @@ -46,23 +46,8 @@ const validateFetchingUserDocument = async (req, res, next) => { } }; -const validateFetchingUserDeviceStatus = async (req, res, next) => { - const schema = joi.object().strict().keys({ - user_id: joi.string().required(), - }); - - try { - await schema.validateAsync(req.query); - next(); - } catch (error) { - logger.error("Invalid Query Parameters Passed"); - res.boom.badRequest("Invalid Query Parameters Passed"); - } -}; - module.exports = { storeUserDeviceInfo, validateAuthStatus, validateFetchingUserDocument, - validateFetchingUserDeviceStatus, }; diff --git a/models/qrCodeAuth.js b/models/qrCodeAuth.js index dc3882039..695edfa1b 100644 --- a/models/qrCodeAuth.js +++ b/models/qrCodeAuth.js @@ -9,7 +9,7 @@ const USER_DOES_NOT_EXIST_ERROR = "User does not exist."; * @return {Promise<{userDeviceInfoData|Object}>} */ -const updateStatus = async (userId, authStatus = "NOT_INIT") => { +const updateStatus = async (userId, authStatus = "NOT_INIT", token) => { try { const authData = await QrCodeAuthModel.doc(userId).get(); @@ -22,8 +22,8 @@ const updateStatus = async (userId, authStatus = "NOT_INIT") => { await QrCodeAuthModel.doc(userId).set({ ...authData.data(), authorization_status: authStatus, + token: `${token}`, }); - return { userExists: true, data: { @@ -64,7 +64,7 @@ const retrieveUserDeviceInfo = async ({ deviceId, userId }) => { .where("authorization_status", "==", "NOT_INIT") .get(); } - const userData = queryDocument.docs[0]; + const userData = queryDocument?.docs[0]; if (!userData) { return { diff --git a/routes/auth.js b/routes/auth.js index 5ffd833d8..6e2a91931 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -13,11 +13,7 @@ router.get("/signout", auth.signout); router.get("/qr-code-auth", userDeviceInfoValidator.validateFetchingUserDocument, auth.fetchUserDeviceInfo); -router.get( - "/qr-code-auth-getDeviceInfo", - userDeviceInfoValidator.validateFetchingUserDeviceStatus, - auth.fetchDeviceDetails -); +router.get("/device", authenticate, auth.fetchDeviceDetails); router.post("/qr-code-auth", userDeviceInfoValidator.storeUserDeviceInfo, auth.storeUserDeviceInfo); diff --git a/utils/generateUniqueToken.js b/utils/generateUniqueToken.js new file mode 100644 index 000000000..3d98a0c98 --- /dev/null +++ b/utils/generateUniqueToken.js @@ -0,0 +1,12 @@ +const crypto = require("crypto"); + +export const generateUniqueToken = async () => { + const uuidToken = crypto.randomUUID(); + const randomNumber = Math.floor(Math.random() * 1000000); + const generationTime = Date.now(); + const encoder = new TextEncoder(); + const encodedString = encoder.encode(uuidToken + randomNumber + generationTime); + const hash = await crypto.subtle.digest("SHA-256", encodedString); + const token = [...new Uint8Array(hash)].map((x) => x.toString(16).padStart(2, "0")).join(""); + return token; +}; From 228ca2710e4e2b28ba8b83c5e13c35b3cf35d552 Mon Sep 17 00:00:00 2001 From: Lakshay Manchanda Date: Sun, 13 Aug 2023 15:17:00 +0530 Subject: [PATCH 090/105] fixed failing tests --- middlewares/validators/qrCodeAuth.js | 15 +++++++++++++++ routes/auth.js | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/middlewares/validators/qrCodeAuth.js b/middlewares/validators/qrCodeAuth.js index 223c56964..ff678f825 100644 --- a/middlewares/validators/qrCodeAuth.js +++ b/middlewares/validators/qrCodeAuth.js @@ -46,8 +46,23 @@ const validateFetchingUserDocument = async (req, res, next) => { } }; +const validateFetchingUserDeviceStatus = async (req, res, next) => { + const schema = joi.object().strict().keys({ + user_id: joi.string().required(), + }); + + try { + await schema.validateAsync(req.query); + next(); + } catch (error) { + logger.error("Invalid Query Parameters Passed"); + res.boom.badRequest("Invalid Query Parameters Passed"); + } +}; + module.exports = { storeUserDeviceInfo, validateAuthStatus, validateFetchingUserDocument, + validateFetchingUserDeviceStatus, }; diff --git a/routes/auth.js b/routes/auth.js index 6e2a91931..ee85ede86 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -13,7 +13,7 @@ router.get("/signout", auth.signout); router.get("/qr-code-auth", userDeviceInfoValidator.validateFetchingUserDocument, auth.fetchUserDeviceInfo); -router.get("/device", authenticate, auth.fetchDeviceDetails); +router.get("/device", userDeviceInfoValidator.validateFetchingUserDeviceStatus, authenticate, auth.fetchDeviceDetails); router.post("/qr-code-auth", userDeviceInfoValidator.storeUserDeviceInfo, auth.storeUserDeviceInfo); From 967acea12b4a068ed26e2327a3dfb180865c8041 Mon Sep 17 00:00:00 2001 From: Lakshay Manchanda Date: Sun, 13 Aug 2023 23:20:07 +0530 Subject: [PATCH 091/105] Validation after authentication --- routes/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/auth.js b/routes/auth.js index ee85ede86..8ddae9dfa 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -13,7 +13,7 @@ router.get("/signout", auth.signout); router.get("/qr-code-auth", userDeviceInfoValidator.validateFetchingUserDocument, auth.fetchUserDeviceInfo); -router.get("/device", userDeviceInfoValidator.validateFetchingUserDeviceStatus, authenticate, auth.fetchDeviceDetails); +router.get("/device", authenticate, userDeviceInfoValidator.validateFetchingUserDeviceStatus, auth.fetchDeviceDetails); router.post("/qr-code-auth", userDeviceInfoValidator.storeUserDeviceInfo, auth.storeUserDeviceInfo); From 62eedaf2e2fd375838185af73dda3f39b8c25bf5 Mon Sep 17 00:00:00 2001 From: Lakshay Manchanda Date: Sun, 13 Aug 2023 23:30:12 +0530 Subject: [PATCH 092/105] fixing code --- middlewares/validators/qrCodeAuth.js | 15 -------- routes/auth.js | 2 +- .../middlewares/qrCodeAuthValidator.test.js | 36 ------------------- 3 files changed, 1 insertion(+), 52 deletions(-) diff --git a/middlewares/validators/qrCodeAuth.js b/middlewares/validators/qrCodeAuth.js index ff678f825..223c56964 100644 --- a/middlewares/validators/qrCodeAuth.js +++ b/middlewares/validators/qrCodeAuth.js @@ -46,23 +46,8 @@ const validateFetchingUserDocument = async (req, res, next) => { } }; -const validateFetchingUserDeviceStatus = async (req, res, next) => { - const schema = joi.object().strict().keys({ - user_id: joi.string().required(), - }); - - try { - await schema.validateAsync(req.query); - next(); - } catch (error) { - logger.error("Invalid Query Parameters Passed"); - res.boom.badRequest("Invalid Query Parameters Passed"); - } -}; - module.exports = { storeUserDeviceInfo, validateAuthStatus, validateFetchingUserDocument, - validateFetchingUserDeviceStatus, }; diff --git a/routes/auth.js b/routes/auth.js index 8ddae9dfa..6e2a91931 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -13,7 +13,7 @@ router.get("/signout", auth.signout); router.get("/qr-code-auth", userDeviceInfoValidator.validateFetchingUserDocument, auth.fetchUserDeviceInfo); -router.get("/device", authenticate, userDeviceInfoValidator.validateFetchingUserDeviceStatus, auth.fetchDeviceDetails); +router.get("/device", authenticate, auth.fetchDeviceDetails); router.post("/qr-code-auth", userDeviceInfoValidator.storeUserDeviceInfo, auth.storeUserDeviceInfo); diff --git a/test/unit/middlewares/qrCodeAuthValidator.test.js b/test/unit/middlewares/qrCodeAuthValidator.test.js index 55d23896e..63606b5c9 100644 --- a/test/unit/middlewares/qrCodeAuthValidator.test.js +++ b/test/unit/middlewares/qrCodeAuthValidator.test.js @@ -3,7 +3,6 @@ const { validateAuthStatus, storeUserDeviceInfo, validateFetchingUserDocument, - validateFetchingUserDeviceStatus, } = require("../../../middlewares/validators/qrCodeAuth"); const { expect } = require("chai"); const { userDeviceInfoDataArray } = require("../../fixtures/qrCodeAuth/qrCodeAuth"); @@ -105,39 +104,4 @@ describe("qrCodeAuth", function () { expect(nextSpy.callCount).to.be.equal(0); }); }); - - describe("test get call validator with userId", function () { - it("Allows request to pass on valid params", async function () { - const req = { - query: { - user_id: "USER_ID", - }, - }; - - const res = {}; - - const nextSpy = Sinon.spy(); - await validateFetchingUserDeviceStatus(req, res, nextSpy); - expect(nextSpy.callCount).to.be.equal(1); - }); - - it("Does not allow request to pass on invalid params", async function () { - const req = { - query: { - device_id: "ID", - device_type: "DEVICE_TYPE", - }, - }; - - const res = { - boom: { - badRequest: () => {}, - }, - }; - - const nextSpy = Sinon.spy(); - await validateFetchingUserDeviceStatus(req, res, nextSpy); - expect(nextSpy.callCount).to.be.equal(0); - }); - }); }); From 5826e2a12d583300272ddc72c22f51102fbdf505 Mon Sep 17 00:00:00 2001 From: gauravsinhaweb Date: Mon, 14 Aug 2023 04:08:41 +0530 Subject: [PATCH 093/105] Refactor: keeping body optional in archiving details in the archiving process --- controllers/members.js | 7 +------ test/integration/members.test.js | 18 ------------------ 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/controllers/members.js b/controllers/members.js index f44f8e0ea..1ece7d3bc 100644 --- a/controllers/members.js +++ b/controllers/members.js @@ -87,18 +87,13 @@ const archiveMembers = async (req, res) => { const superUserId = req.userData.id; const { reason } = req.body; const roles = req?.userData?.roles; - const isReasonNullOrUndefined = !reason; - const isReasonEmptyOrWhitespace = /^\s*$/.test(reason); - if (isReasonNullOrUndefined || isReasonEmptyOrWhitespace) { - return res.boom.badRequest("Reason is required"); - } if (user?.userExists) { const successObject = await members.addArchiveRoleToMembers(user.user.id); if (successObject.isArchived) { return res.boom.badRequest("User is already archived"); } const body = { - reason: reason, + reason: reason || "", archived_user: { user_id: user.user.id, username: user.user.username, diff --git a/test/integration/members.test.js b/test/integration/members.test.js index ee74b0689..5da4a06aa 100644 --- a/test/integration/members.test.js +++ b/test/integration/members.test.js @@ -307,24 +307,6 @@ describe("Members", function () { return done(); }); }); - it("Should return 400 if body is empty", function (done) { - chai - .request(app) - .patch(`/members/archiveMembers/${userToBeArchived.username}`) - .set("cookie", `${cookieName}=${jwt}`) - .send({}) - .end((err, res) => { - if (err) { - return done(err); - } - - expect(res).to.have.status(400); - expect(res.body).to.be.a("object"); - expect(res.body.message).to.equal("Reason is required"); - - return done(); - }); - }); it("Should archive the user", function (done) { addUser(userToBeArchived).then(() => { chai From de9f9096e541d3c8cbc061916379f5289ec7c54d Mon Sep 17 00:00:00 2001 From: vivek lokhande Date: Mon, 14 Aug 2023 12:15:41 +0530 Subject: [PATCH 094/105] update error message --- constants/errorMessages.js | 1 + controllers/auth.js | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/constants/errorMessages.js b/constants/errorMessages.js index c7c7c4959..66a2477d0 100644 --- a/constants/errorMessages.js +++ b/constants/errorMessages.js @@ -8,4 +8,5 @@ module.exports = { BAD_REQUEST: "BAD_REQUEST", INVALID_QUERY_PARAM: "Invalid Query Parameters Passed", FILE_TOO_LARGE: (size) => `File too large, max accepted size is ${size} MB`, + USER_DOES_NOT_EXIST_ERROR: "User does not exist!", }; diff --git a/controllers/auth.js b/controllers/auth.js index 5b14ab96a..711c943ed 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -3,8 +3,12 @@ const users = require("../models/users"); const QrCodeAuthModel = require("../models/qrCodeAuth"); const authService = require("../services/authService"); const dataAccess = require("../services/dataAccessLayer"); -const { SOMETHING_WENT_WRONG, DATA_ADDED_SUCCESSFULLY } = require("../constants/errorMessages"); -const USER_DOES_NOT_EXIST_ERROR = "User does not exist!"; +const { + SOMETHING_WENT_WRONG, + DATA_ADDED_SUCCESSFULLY, + USER_DOES_NOT_EXIST_ERROR, +} = require("../constants/errorMessages"); + /** * Makes authentication call to GitHub statergy * From 3dfe137529402b44df80eea55d546b542216d76f Mon Sep 17 00:00:00 2001 From: Pratiyush Kumar Date: Tue, 15 Aug 2023 15:42:05 +0530 Subject: [PATCH 095/105] added onBoarding condition --- models/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/users.js b/models/users.js index c303ecc50..447d61a27 100644 --- a/models/users.js +++ b/models/users.js @@ -504,7 +504,7 @@ const getUsersBasedOnFilter = async (query) => { const userRefs = finalItems.map((itemId) => userModel.doc(itemId)); const userDocs = (await firestore.getAll(...userRefs)).map((doc) => ({ id: doc.id, ...doc.data() })); const filteredUserDocs = userDocs.filter((doc) => !doc.roles?.archived); - if (query.time) { + if (query.time & (query.state === "ONBOARDING")) { const fetchUsersWithOnBoardingState = await getUsersWithOnboardingState(filteredUserDocs, stateItems, query.time); return fetchUsersWithOnBoardingState; } From 8d6b972620f4a172933a5b5da4ca71352fd3fc69 Mon Sep 17 00:00:00 2001 From: Pratiyush Kumar Date: Tue, 15 Aug 2023 15:53:33 +0530 Subject: [PATCH 096/105] fixed prettier error and added logical and --- models/users.js | 2 +- test/unit/models/users.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/models/users.js b/models/users.js index 1017ec520..33f3c8295 100644 --- a/models/users.js +++ b/models/users.js @@ -508,7 +508,7 @@ const getUsersBasedOnFilter = async (query) => { const userRefs = finalItems.map((itemId) => userModel.doc(itemId)); const userDocs = (await firestore.getAll(...userRefs)).map((doc) => ({ id: doc.id, ...doc.data() })); const filteredUserDocs = userDocs.filter((doc) => !doc.roles?.archived); - if (query.time & (query.state === "ONBOARDING")) { + if (query.time && query.state === "ONBOARDING") { const fetchUsersWithOnBoardingState = await getUsersWithOnboardingState(filteredUserDocs, stateItems, query.time); return fetchUsersWithOnBoardingState; } diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 4ac168a3c..680bce719 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -393,8 +393,8 @@ describe("users", function () { }; const result = await users.getUsersBasedOnFilter(query); expect(result.length).to.equal(2); - }) - }) + }); + }); describe("fetch users by id", function () { let allIds = []; before(async function () { From 965ef9a053ae65f87fc6f52de14b4902858d37de Mon Sep 17 00:00:00 2001 From: Pratiyush Kumar Date: Wed, 16 Aug 2023 13:53:13 +0530 Subject: [PATCH 097/105] validator test cases added and corrected spelling mistake --- models/users.js | 14 +++++++++----- test/unit/middlewares/users-validator.test.js | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/models/users.js b/models/users.js index 33f3c8295..4f7addd2b 100644 --- a/models/users.js +++ b/models/users.js @@ -509,7 +509,11 @@ const getUsersBasedOnFilter = async (query) => { const userDocs = (await firestore.getAll(...userRefs)).map((doc) => ({ id: doc.id, ...doc.data() })); const filteredUserDocs = userDocs.filter((doc) => !doc.roles?.archived); if (query.time && query.state === "ONBOARDING") { - const fetchUsersWithOnBoardingState = await getUsersWithOnboardingState(filteredUserDocs, stateItems, query.time); + const fetchUsersWithOnBoardingState = await getUsersWithOnboardingStateInRange( + filteredUserDocs, + stateItems, + query.time + ); return fetchUsersWithOnBoardingState; } return filteredUserDocs; @@ -549,8 +553,8 @@ const getUsersBasedOnFilter = async (query) => { return []; }; -const getUsersWithOnboardingState = async (filteredUserDocs, stateItems, time) => { - const UsersInRange = []; +const getUsersWithOnboardingStateInRange = async (filteredUserDocs, stateItems, time) => { + const usersInRange = []; const range = Number(time.split("d")[0]); const filteredUsers = filteredUserDocs.filter((userDoc) => { return stateItems.some((stateItem) => stateItem.userId === userDoc.id); @@ -562,11 +566,11 @@ const getUsersWithOnboardingState = async (filteredUserDocs, stateItems, time) = const timeDifferenceInMilliseconds = currentTimeStamp - userDiscordJoinedDate.getTime(); const currentAndUserJoinedDateDifference = Math.floor(timeDifferenceInMilliseconds / (1000 * 60 * 60 * 24)); if (currentAndUserJoinedDateDifference > range) { - UsersInRange.push(user); + usersInRange.push(user); } } }); - return UsersInRange; + return usersInRange; }; /** * Fetch all users diff --git a/test/unit/middlewares/users-validator.test.js b/test/unit/middlewares/users-validator.test.js index 615ee4cec..d260d58ed 100644 --- a/test/unit/middlewares/users-validator.test.js +++ b/test/unit/middlewares/users-validator.test.js @@ -1,5 +1,5 @@ const Sinon = require("sinon"); -const { validateUpdateRoles } = require("../../../middlewares/validators/user"); +const { validateUpdateRoles, validateUserQueryParams } = require("../../../middlewares/validators/user"); const { expect } = require("chai"); describe("Test the roles update validator", function () { @@ -15,6 +15,19 @@ describe("Test the roles update validator", function () { expect(nextSpy.callCount).to.be.equal(1); }); + it("Allows the request for time as 31d", async function () { + const req = { + query: { + state: "ONBOARDING", + time: "31d", + }, + }; + const res = {}; + const nextSpy = Sinon.spy(); + await validateUserQueryParams(req, res, nextSpy); + expect(nextSpy.callCount).to.be.equal(1); + }); + it("Allows the request to pass with archived property", async function () { const req = { body: { From 6fd6414e1b5dec3e1ba8dc8ae5ca727f90a48b47 Mon Sep 17 00:00:00 2001 From: Ritik Jaiswal <57758447+RitikJaiswal75@users.noreply.github.com> Date: Wed, 16 Aug 2023 22:15:20 +0530 Subject: [PATCH 098/105] fix the staging for archived users (#1421) --- middlewares/validators/staging.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middlewares/validators/staging.js b/middlewares/validators/staging.js index e9180826e..66f43addc 100644 --- a/middlewares/validators/staging.js +++ b/middlewares/validators/staging.js @@ -4,7 +4,7 @@ const validateUserRoles = async (req, res, next) => { const schema = joi.object().strict().keys({ super_user: joi.boolean().optional(), member: joi.boolean().optional(), - archive: joi.boolean().optional(), + archived: joi.boolean().optional(), in_discord: joi.boolean().optional(), }); From 5a2ade7d24a0d52373c159425789dd3844cc8d76 Mon Sep 17 00:00:00 2001 From: FMK2312 Date: Thu, 17 Aug 2023 17:01:28 +0530 Subject: [PATCH 099/105] removing one time script - remove tokens --- controllers/users.js | 20 -------------------- models/users.js | 15 --------------- routes/users.js | 1 - 3 files changed, 36 deletions(-) diff --git a/controllers/users.js b/controllers/users.js index 4d1b7c980..a22f8748c 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -589,25 +589,6 @@ const setInDiscordScript = async (req, res) => { } }; -const removeTokens = async (req, res) => { - try { - const users = await userQuery.fetchUsersWithToken(); - - if (!users.length) { - return res.status(404).json({ message: "No users found with github Token!" }); - } - - await userQuery.removeGitHubToken(users); - - return res.status(200).json({ - message: "Github Token removed from all users!", - usersFound: users.length, - }); - } catch (err) { - return res.boom.badImplementation({ message: INTERNAL_SERVER_ERROR }); - } -}; - const updateRoles = async (req, res) => { try { const result = await dataAccess.retrieveUsers({ id: req.params.id }); @@ -707,7 +688,6 @@ module.exports = { nonVerifiedDiscordUsers, setInDiscordScript, markUnverified, - removeTokens, updateRoles, archiveUserIfNotInDiscord, usersPatchHandler, diff --git a/models/users.js b/models/users.js index ac105a4ce..1aba21f9f 100644 --- a/models/users.js +++ b/models/users.js @@ -620,20 +620,6 @@ const archiveUserIfNotInDiscord = async () => { throw error; } }; - -const fetchUsersWithToken = async () => { - try { - const users = []; - const usersRef = await userModel.where("tokens", "!=", false).get(); - usersRef.forEach((user) => { - users.push(userModel.doc(user.id)); - }); - return users; - } catch (err) { - logger.error(`Error while fetching all users with tokens field: ${err}`); - return []; - } -}; /** * * @param {[string]} userIds Array id's of user @@ -736,7 +722,6 @@ module.exports = { getDiscordUsers, fetchAllUsers, archiveUserIfNotInDiscord, - fetchUsersWithToken, removeGitHubToken, getUsersByRole, fetchUserByIds, diff --git a/routes/users.js b/routes/users.js index 2229d39ed..6e6a78e60 100644 --- a/routes/users.js +++ b/routes/users.js @@ -19,7 +19,6 @@ router.get("/self", authenticate, users.getSelfDetails); router.get("/isUsernameAvailable/:username", authenticate, users.getUsernameAvailabilty); router.get("/chaincode", authenticate, users.generateChaincode); router.get("/search", userValidator.validateUserQueryParams, users.filterUsers); -router.post("/tokens", authenticate, authorizeRoles([SUPERUSER]), users.removeTokens); router.get("/:username", users.getUser); router.get("/:userId/intro", authenticate, authorizeRoles([SUPERUSER]), users.getUserIntro); From cf38384f6d143b892477b19dd6ad21e3318238e4 Mon Sep 17 00:00:00 2001 From: FMK2312 Date: Thu, 17 Aug 2023 17:31:35 +0530 Subject: [PATCH 100/105] forgot to remove tests --- test/integration/users.test.js | 27 ------------------------ test/unit/models/users.test.js | 38 ---------------------------------- 2 files changed, 65 deletions(-) diff --git a/test/integration/users.test.js b/test/integration/users.test.js index cbe8c5f90..df0277b4a 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -1751,31 +1751,4 @@ describe("Users", function () { }); }); }); - - describe("POST /users/tokens", function () { - before(async function () { - await addOrUpdate(userData[0]); - await addOrUpdate(userData[1]); - await addOrUpdate(userData[2]); - await addOrUpdate(userData[3]); - }); - after(async function () { - await cleanDb(); - }); - it("should remove all the users with token field", function (done) { - chai - .request(app) - .post("/users/tokens") - .set("Cookie", `${cookieName}=${superUserAuthToken}`) - .end((err, res) => { - if (err) { - return done(err); - } - expect(res).to.have.status(200); - expect(res.body.message).to.be.equal("Github Token removed from all users!"); - expect(res.body.usersFound).to.be.equal(3); - return done(); - }); - }); - }); }); diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index f51678139..2a0d445fa 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -306,44 +306,6 @@ describe("users", function () { }); }); }); - - describe("remove github token from users", function () { - beforeEach(async function () { - const addUsersPromises = []; - userDataArray.forEach((user) => { - addUsersPromises.push(userModel.add(user)); - }); - await Promise.all(addUsersPromises); - }); - - afterEach(async function () { - await cleanDb(); - }); - - it("return array of users", async function () { - const data = await users.fetchUsersWithToken(); - expect(data).to.be.not.equal(null); - }); - it('removes token field from user"s data', async function () { - const userRef = await users.fetchUsersWithToken(); - const dataBefore = await userRef[1].get(); - const beforeRemoval = Object.keys(dataBefore.data()).includes("tokens"); - expect(beforeRemoval).to.be.equal(true); - await users.removeGitHubToken(userRef); - const dataAfter = await userRef[1].get(); - const afterRemoval = Object.keys(dataAfter.data()).includes("tokens"); - expect(afterRemoval).to.be.equal(false); - }); - - it("throws error if id is not found in db", async function () { - try { - await users.removeGitHubToken("1223"); - } catch (error) { - expect(error).to.be.instanceOf(Error); - } - }); - }); - describe("get users by roles", function () { beforeEach(async function () { const addUsersPromises = []; From ff04020571f0ebde69a51967c7798801a8f0c64e Mon Sep 17 00:00:00 2001 From: kotesh_Mudila Date: Tue, 22 Aug 2023 09:43:11 +0530 Subject: [PATCH 101/105] API endpoint to update Discord usernames for backend users (#1378) * Add a sample route to test it's working * FEAT:Add functionality to update discord Nickname * CHORE: remove console.log's * TEST: Added for discord-nickname-update controller * TEST: add message object accessible * TEST: Added for setUserDiscordNickname * REFACTOR: added a fixture, removed console.log's --- controllers/users.js | 33 ++++++++- routes/users.js | 7 ++ services/discordService.js | 19 ++++++ .../discordResponse/discord-response.js | 10 +++ test/integration/users.test.js | 68 ++++++++++++++++++- test/unit/services/discordService.test.js | 24 +++++++ 6 files changed, 159 insertions(+), 2 deletions(-) diff --git a/controllers/users.js b/controllers/users.js index a22f8748c..453a8a3e0 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -9,7 +9,7 @@ const dataAccess = require("../services/dataAccessLayer"); const logger = require("../utils/logger"); const { SOMETHING_WENT_WRONG, INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); const { getPaginationLink, getUsernamesFromPRs, getRoleToUpdate } = require("../utils/users"); -const { setInDiscordFalseScript } = require("../services/discordService"); +const { setInDiscordFalseScript, setUserDiscordNickname } = require("../services/discordService"); const { generateDiscordProfileImageUrl } = require("../utils/discord-actions"); const { addRoleToUser, getDiscordMembers } = require("../services/discordService"); const { fetchAllUsers } = require("../models/users"); @@ -315,6 +315,36 @@ const verifyUserImage = async (req, res) => { } }; +/** + * Patch Update user nickname + * + * @param req {Object} - Express request object + * @param res {Object} - Express response object + */ + +const updateDiscordUserNickname = async (req, res) => { + const { userId } = req.params; + try { + const userToBeUpdated = await dataAccess.retrieveUsers({ id: userId }); + const { discordId, username } = userToBeUpdated.user; + if (!discordId) { + throw new Error("user not verified"); + } + const response = await setUserDiscordNickname(username, discordId); + + return res.json({ + userAffected: { + userId, + username, + discordId, + }, + message: response, + }); + } catch (err) { + logger.error(`Error while updating nickname: ${err}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; const markUnverified = async (req, res) => { try { const [usersInRdsDiscordServer, allRdsLoggedInUsers] = await Promise.all([getDiscordMembers(), fetchAllUsers()]); @@ -689,6 +719,7 @@ module.exports = { setInDiscordScript, markUnverified, updateRoles, + updateDiscordUserNickname, archiveUserIfNotInDiscord, usersPatchHandler, }; diff --git a/routes/users.js b/routes/users.js index 6e6a78e60..2d4d379c5 100644 --- a/routes/users.js +++ b/routes/users.js @@ -19,6 +19,13 @@ router.get("/self", authenticate, users.getSelfDetails); router.get("/isUsernameAvailable/:username", authenticate, users.getUsernameAvailabilty); router.get("/chaincode", authenticate, users.generateChaincode); router.get("/search", userValidator.validateUserQueryParams, users.filterUsers); +router.patch( + "/:userId/update-nickname", + authenticate, + authorizeRoles([SUPERUSER]), + checkIsVerifiedDiscord, + users.updateDiscordUserNickname +); router.get("/:username", users.getUser); router.get("/:userId/intro", authenticate, authorizeRoles([SUPERUSER]), users.getUserIntro); diff --git a/services/discordService.js b/services/discordService.js index bc28be382..b7eb2aa2f 100644 --- a/services/discordService.js +++ b/services/discordService.js @@ -68,9 +68,28 @@ const removeRoleFromUser = async (roleId, discordId) => { } }; +const setUserDiscordNickname = async (userName, discordId) => { + try { + const authToken = await generateAuthTokenForCloudflare(); + + const response = await ( + await fetch(`${DISCORD_BASE_URL}/guild/member`, { + method: "PATCH", + body: JSON.stringify({ userName, discordId }), + headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` }, + }) + ).json(); + return response; + } catch (err) { + logger.error("Error in updating discord Nickname", err); + throw err; + } +}; + module.exports = { getDiscordMembers, setInDiscordFalseScript, addRoleToUser, removeRoleFromUser, + setUserDiscordNickname, }; diff --git a/test/fixtures/discordResponse/discord-response.js b/test/fixtures/discordResponse/discord-response.js index ad9e4699f..2d34e03dd 100644 --- a/test/fixtures/discordResponse/discord-response.js +++ b/test/fixtures/discordResponse/discord-response.js @@ -89,7 +89,17 @@ const usersFromRds = [ }, ]; +const updatedNicknameResponse = { + userAffected: { + userId: "X0H3paYveEWh7Q2fPhor", + username: "test-name-007", + discordId: "1123566257019568232", + }, + message: "User nickname changed successfully", +}; + module.exports = { getDiscordMembers, usersFromRds, + updatedNicknameResponse, }; diff --git a/test/integration/users.test.js b/test/integration/users.test.js index df0277b4a..5d90c29bf 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -15,7 +15,7 @@ const superUser = userData[4]; const searchParamValues = require("../fixtures/user/search")(); const config = require("config"); -const { getDiscordMembers } = require("../fixtures/discordResponse/discord-response"); +const { getDiscordMembers, updatedNicknameResponse } = require("../fixtures/discordResponse/discord-response"); const joinData = require("../fixtures/user/join"); const { userStatusDataAfterSignup, @@ -45,6 +45,7 @@ describe("Users", function () { let superUserId; let superUserAuthToken; let userId = ""; + let fetchStub; beforeEach(async function () { userId = await addUser(); @@ -1751,4 +1752,69 @@ describe("Users", function () { }); }); }); + describe("PATCH /:userId/update-nickname", function () { + beforeEach(async function () { + fetchStub = Sinon.stub(global, "fetch"); + userId = await addUser(userData[0]); + }); + afterEach(async function () { + await cleanDb(); + Sinon.restore(); + }); + it("returns 200 for successfully updating nickname with patch method", function (done) { + fetchStub.returns( + Promise.resolve({ + status: 200, + json: () => Promise.resolve(updatedNicknameResponse), + }) + ); + chai + .request(app) + .patch(`/users/${userId}/update-nickname`) + .set("Cookie", `${cookieName}=${superUserAuthToken}`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body.message.message).to.be.equal("User nickname changed successfully"); + return done(); + }); + }); + }); + + describe("test discord actions of nickname for unverified user", function () { + beforeEach(async function () { + fetchStub = Sinon.stub(global, "fetch"); + const superUser = userData[4]; + userId = await addUser(userData[2]); + superUserId = await addUser(superUser); + superUserAuthToken = authService.generateAuthToken({ userId: superUserId }); + }); + afterEach(async function () { + await cleanDb(); + Sinon.restore(); + }); + it("throw error if discordId is not present and user is not verified", function (done) { + fetchStub.returns({ + update: function () {}, + commit: function () { + throw new Error("User not verified"); + }, + }); + chai + .request(app) + .patch(`/users/${userId}/update-nickname`) + .set("Cookie", `${cookieName}=${superUserAuthToken}`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(500); + const response = res.body; + expect(response.message).to.be.equal("An internal server error occurred"); + return done(); + }); + }); + }); }); diff --git a/test/unit/services/discordService.test.js b/test/unit/services/discordService.test.js index 18ca58d5d..8b4708db7 100644 --- a/test/unit/services/discordService.test.js +++ b/test/unit/services/discordService.test.js @@ -5,6 +5,7 @@ const { addRoleToUser, getDiscordMembers, removeRoleFromUser, + setUserDiscordNickname, } = require("../../../services/discordService"); const { fetchAllUsers } = require("../../../models/users"); const Sinon = require("sinon"); @@ -114,4 +115,27 @@ describe("Discord services", function () { }); }); }); + + describe("change user nickname on discord", function () { + beforeEach(function () { + fetchStub = Sinon.stub(global, "fetch"); + }); + + afterEach(function () { + fetchStub.restore(); + }); + + it("makes a API call to update the user's discord nickname ", async function () { + fetchStub.returns( + Promise.resolve({ + status: 200, + json: () => Promise.resolve({ message: "done" }), + }) + ); + + const response = await setUserDiscordNickname("aMYlI7sxQ4JMPwiqLQlp", "username"); + + expect(response.message).to.be.equal("done"); + }); + }); }); From 1105c7b247c824a52c9b9b9409cfb6bb9ad07adf Mon Sep 17 00:00:00 2001 From: Prakash Date: Tue, 22 Aug 2023 16:59:46 +0530 Subject: [PATCH 102/105] Revert "Revert "Dev to Main Sync"" This reverts commit 3423a5f024eec35eb388ce30123a52af022d6762. --- constants/userDataLevels.js | 21 ++++ constants/users.js | 3 - controllers/discordactions.js | 19 +++- controllers/events.js | 74 +++++++++++- controllers/members.js | 26 ++++- controllers/users.js | 11 +- middlewares/authenticate.js | 7 +- middlewares/validators/events.js | 46 ++++++++ models/discordactions.js | 31 +++++ models/events.js | 106 ++++++++++++++++++ models/logs.js | 22 +++- models/users.js | 26 +++++ routes/events.js | 2 + services/dataAccessLayer.js | 81 ++++++++----- .../fixtures/discordactions/discordactions.js | 6 +- test/fixtures/logs/archievedUsers.js | 68 +++++++++++ test/fixtures/user/user.js | 29 ++++- test/integration/discord.test.js | 31 +++++ test/integration/discordactions.test.js | 55 +++++++++ test/integration/members.test.js | 69 ++++++++---- test/integration/tasks.test.js | 2 +- test/integration/users.test.js | 25 ----- test/integration/usersFilter.test.js | 1 - test/unit/models/discordactions.test.js | 37 ++++++ test/unit/models/events.test.js | 64 +++++++++-- test/unit/models/logs.test.js | 84 ++++++++++++++ test/unit/models/users.test.js | 32 +++++- test/unit/services/dataAccessLayer.test.js | 103 ++++++++++++----- 28 files changed, 933 insertions(+), 148 deletions(-) create mode 100644 constants/userDataLevels.js create mode 100644 test/fixtures/logs/archievedUsers.js diff --git a/constants/userDataLevels.js b/constants/userDataLevels.js new file mode 100644 index 000000000..a907e265a --- /dev/null +++ b/constants/userDataLevels.js @@ -0,0 +1,21 @@ +const ACCESS_LEVEL = { + PUBLIC: "public", + INTERNAL: "internal", + PRIVATE: "private", + CONFIDENTIAL: "confidential", +}; + +const ROLE_LEVEL = { + private: ["super_user"], + internal: ["super_user"], + confidential: ["super_user"], +}; + +const KEYS_NOT_ALLOWED = { + public: ["email", "phone", "chaincode"], + internal: ["phone", "chaincode"], + private: ["chaincode"], + confidential: [], +}; + +module.exports = { ACCESS_LEVEL, KEYS_NOT_ALLOWED, ROLE_LEVEL }; diff --git a/constants/users.js b/constants/users.js index 3a11cf768..eaf6c73ac 100644 --- a/constants/users.js +++ b/constants/users.js @@ -4,8 +4,6 @@ const profileStatus = { NOT_APPROVED: "NOT APPROVED", }; -const USER_SENSITIVE_DATA = ["phone", "email", "chaincode", "tokens"]; - const USER_STATUS = { OOO: "ooo", IDLE: "idle", @@ -23,5 +21,4 @@ module.exports = { profileStatus, USER_STATUS, ALLOWED_FILTER_PARAMS, - USER_SENSITIVE_DATA, }; diff --git a/controllers/discordactions.js b/controllers/discordactions.js index a24bf665b..f91da7504 100644 --- a/controllers/discordactions.js +++ b/controllers/discordactions.js @@ -3,6 +3,7 @@ const admin = require("firebase-admin"); const config = require("config"); const jwt = require("jsonwebtoken"); const discordRolesModel = require("../models/discordactions"); +const { retrieveUsers } = require("../services/dataAccessLayer"); /** * Creates a role @@ -66,9 +67,25 @@ const createGroupRole = async (req, res) => { const getAllGroupRoles = async (req, res) => { try { const { groups } = await discordRolesModel.getAllGroupRoles(); + const groupsWithMemberCount = await discordRolesModel.getNumberOfMemberForGroups(groups); + const groupCreatorIds = groupsWithMemberCount.reduce((ids, group) => { + ids.add(group.createdBy); + return ids; + }, new Set()); + const groupCreatorsDetails = await retrieveUsers({ userIds: Array.from(groupCreatorIds) }); + const groupsWithUserDetails = groupsWithMemberCount.map((group) => { + const groupCreator = groupCreatorsDetails[group.createdBy]; + return { + ...group, + firstName: groupCreator.first_name, + lastName: groupCreator.last_name, + image: groupCreator.picture?.url, + }; + }); + return res.json({ message: "Roles fetched successfully!", - groups, + groups: groupsWithUserDetails, }); } catch (err) { logger.error(`Error while getting roles: ${err}`); diff --git a/controllers/events.js b/controllers/events.js index b0b77bbdc..1429d65d7 100644 --- a/controllers/events.js +++ b/controllers/events.js @@ -1,9 +1,11 @@ -/* eslint-disable camelcase */ const { GET_ALL_EVENTS_LIMIT_MIN, UNWANTED_PROPERTIES_FROM_100MS } = require("../constants/events"); +const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); + const { EventTokenService, EventAPIService } = require("../services"); -const { removeUnwantedProperties } = require("../utils/events"); const eventQuery = require("../models/events"); + const logger = require("../utils/logger"); +const { removeUnwantedProperties } = require("../utils/events"); const tokenService = new EventTokenService(); const apiService = new EventAPIService(tokenService); @@ -100,7 +102,7 @@ const joinEvent = async (req, res) => { }); } catch (error) { logger.error({ error }); - return res.status(500).send("Internal Server Error"); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); } }; @@ -193,6 +195,70 @@ const endActiveEvent = async (req, res) => { } }; +/** + * Adds a peer to an event. + * + * @async + * @function + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @returns {Promise} The JSON response with the added peer data and a success message. + * @throws {Object} The JSON response with an error message if an error occurred while adding the peer. + */ +const addPeerToEvent = async (req, res) => { + try { + const data = await eventQuery.addPeerToEvent({ + peerId: req.body.peerId, + name: req.body.name, + role: req.body.role, + joinedAt: req.body.joinedAt, + eventId: req.params.id, + }); + return res.status(200).json({ + data, + message: `Selected Participant is added to the event.`, + }); + } catch (error) { + logger.error({ error }); + return res.status(500).json({ + error: error.code, + message: "You can't add selected Participant. Please ask Admin or Host for help.", + }); + } +}; + +/** + * Kicks out a peer from an event. + * + * @async + * @function + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @returns {Promise} The JSON response with a success message if the peer is successfully kicked out. + * @throws {Object} The JSON response with an error message if an error occurred while kicking out the peer. + */ +const kickoutPeer = async (req, res) => { + const { id } = req.params; + const payload = { + peer_id: req.body.peerId, + reason: req.body.reason, + }; + + try { + await apiService.post(`/active-rooms/${id}/remove-peers`, payload); + await eventQuery.kickoutPeer({ eventId: id, peerId: payload.peer_id, reason: req.body.reason }); + return res.status(200).json({ + message: `Selected Participant is removed from event.`, + }); + } catch (error) { + logger.error({ error }); + return res.status(500).json({ + error: error.code, + message: "You can't remove selected Participant from Remove, Please ask Admin or Host for help.", + }); + } +}; + module.exports = { createEvent, getAllEvents, @@ -200,4 +266,6 @@ module.exports = { getEventById, updateEvent, endActiveEvent, + addPeerToEvent, + kickoutPeer, }; diff --git a/controllers/members.js b/controllers/members.js index abb6af3f5..f44f8e0ea 100644 --- a/controllers/members.js +++ b/controllers/members.js @@ -1,8 +1,9 @@ const ROLES = require("../constants/roles"); const members = require("../models/members"); const tasks = require("../models/tasks"); -const { SOMETHING_WENT_WRONG } = require("../constants/errorMessages"); +const { SOMETHING_WENT_WRONG, INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); const dataAccess = require("../services/dataAccessLayer"); +const { addLog } = require("../models/logs"); /** * Fetches the data about our members * @@ -83,17 +84,38 @@ const archiveMembers = async (req, res) => { try { const { username } = req.params; const user = await dataAccess.retrieveUsers({ username }); + const superUserId = req.userData.id; + const { reason } = req.body; + const roles = req?.userData?.roles; + const isReasonNullOrUndefined = !reason; + const isReasonEmptyOrWhitespace = /^\s*$/.test(reason); + if (isReasonNullOrUndefined || isReasonEmptyOrWhitespace) { + return res.boom.badRequest("Reason is required"); + } if (user?.userExists) { const successObject = await members.addArchiveRoleToMembers(user.user.id); if (successObject.isArchived) { return res.boom.badRequest("User is already archived"); } + const body = { + reason: reason, + archived_user: { + user_id: user.user.id, + username: user.user.username, + }, + archived_by: { + user_id: superUserId, + roles: roles, + }, + }; + + addLog("archived-details", {}, body); return res.status(204).send(); } return res.boom.notFound("User doesn't exist"); } catch (err) { logger.error(`Error while retriving contributions ${err}`); - return res.boom.badImplementation(SOMETHING_WENT_WRONG); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); } }; diff --git a/controllers/users.js b/controllers/users.js index 1a990dd9d..00d26a1dc 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -101,9 +101,10 @@ const getUsers = async (req, res) => { } const data = await dataAccess.retrieveUsers({ query: req.query }); + return res.json({ message: "Users returned successfully!", - users: data.allUsers, + users: data.users, links: { next: data.nextId ? getPaginationLink(req.query, "next", data.nextId) : "", prev: data.prevId ? getPaginationLink(req.query, "prev", data.prevId) : "", @@ -205,10 +206,9 @@ const getUsernameAvailabilty = async (req, res) => { const getSelfDetails = async (req, res) => { try { if (req.userData) { - if (req.query.private) { - return res.send(req.userData); - } - const user = await dataAccess.retrieveUsers({ userdata: req.userData }); + const user = await dataAccess.retrieveUsers({ + userdata: req.userData, + }); return res.send(user); } return res.boom.notFound("User doesn't exist"); @@ -407,6 +407,7 @@ const updateUser = async (req, res) => { const generateChaincode = async (req, res) => { try { const { id } = req.userData; + const chaincode = await chaincodeQuery.storeChaincode(id); await userQuery.addOrUpdate({ chaincode }, id); return res.json({ diff --git a/middlewares/authenticate.js b/middlewares/authenticate.js index d72b2c71f..667d41b32 100644 --- a/middlewares/authenticate.js +++ b/middlewares/authenticate.js @@ -1,5 +1,5 @@ const authService = require("../services/authService"); -const users = require("../models/users"); +const dataAccess = require("../services/dataAccessLayer"); /** * Middleware to check if the user has been restricted. If user is restricted, @@ -54,7 +54,7 @@ module.exports = async (req, res, next) => { const { userId } = authService.verifyAuthToken(token); // add user data to `req.userData` for further use - const userData = await users.fetchUser({ userId }); + const userData = await dataAccess.retrieveUsers({ id: userId }); req.userData = userData.user; return checkRestricted(req, res, next); @@ -79,8 +79,7 @@ module.exports = async (req, res, next) => { }); // add user data to `req.userData` for further use - req.userData = await users.fetchUser({ userId }); - + req.userData = await dataAccess.retrieveUsers({ id: userId }); return checkRestricted(req, res, next); } else { return res.boom.unauthorized("Unauthenticated User"); diff --git a/middlewares/validators/events.js b/middlewares/validators/events.js index c5196f2c7..10e479fc3 100644 --- a/middlewares/validators/events.js +++ b/middlewares/validators/events.js @@ -100,6 +100,50 @@ const endActiveEvent = async (req, res, next) => { } }; +const addPeerToEvent = async (req, res, next) => { + const { id } = req.params; + const { peerId, name, role, joinedAt } = req.body; + + const schema = joi.object({ + peerId: joi.string().required(), + name: joi.string().required(), + id: joi.string().required(), + role: joi.string().required(), + joinedAt: joi.date().required(), + }); + + const validationOptions = { abortEarly: false }; + + try { + await schema.validateAsync({ peerId, name, id, role, joinedAt }, validationOptions); + next(); + } catch (error) { + logger.error(`Error while adding a peer to the event: ${error}`); + res.boom.badRequest(error.details[0].message); + } +}; + +const kickoutPeer = async (req, res, next) => { + const { id } = req.params; + const { peerId, reason } = req.body; + + const schema = joi.object({ + id: joi.string().required(), + peerId: joi.string().required(), + reason: joi.string().required(), + }); + + const validationOptions = { abortEarly: false }; + + try { + await schema.validateAsync({ id, peerId, reason }, validationOptions); + next(); + } catch (error) { + logger.error(`We encountered some error while removing selected Participant from event: ${error}`); + res.boom.badRequest(error.details[0].message); + } +}; + module.exports = { createEvent, getAllEvents, @@ -107,4 +151,6 @@ module.exports = { getEventById, updateEvent, endActiveEvent, + addPeerToEvent, + kickoutPeer, }; diff --git a/models/discordactions.js b/models/discordactions.js index cb93d3336..c8e30230d 100644 --- a/models/discordactions.js +++ b/models/discordactions.js @@ -114,10 +114,41 @@ const updateDiscordImageForVerification = async (userDiscordId) => { } }; +const getNumberOfMemberForGroups = async (groups = []) => { + try { + if (!groups.length) { + return []; + } + const roleIds = groups.map((group) => group.roleid); + + const snapshots = await memberRoleModel.where("roleid", "in", roleIds).get(); + const roleCount = {}; + + snapshots.forEach((doc) => { + const roleToMemberMapping = doc.data(); + + if (roleCount[roleToMemberMapping.roleid]) { + roleCount[roleToMemberMapping.roleid] += 1; + } else { + roleCount[roleToMemberMapping.roleid] = 1; + } + }); + + return groups.map((group) => ({ + ...group, + memberCount: roleCount[group.roleid] || 0, + })); + } catch (err) { + logger.error("Error while counting members for each group", err); + throw err; + } +}; + module.exports = { createNewRole, getAllGroupRoles, addGroupRoleToMember, isGroupRoleExists, updateDiscordImageForVerification, + getNumberOfMemberForGroups, }; diff --git a/models/events.js b/models/events.js index b78f2fbfe..46077ced2 100644 --- a/models/events.js +++ b/models/events.js @@ -1,7 +1,9 @@ +const Firestore = require("@google-cloud/firestore"); const firestore = require("../utils/firestore"); const logger = require("../utils/logger"); const eventModel = firestore.collection("events"); +const peerModel = firestore.collection("peers"); /** * Creates a new event document in Firestore and returns the data for the created document. @@ -66,8 +68,112 @@ const endActiveEvent = async ({ id, reason, lock }) => { } }; +/** + * Adds a peer to an event in the Firestore database. + * @async + * @function + * @param {Object} peerData - The data of the peer to be added. + * @param {string} peerData.name - The name of the peer. + * @param {string} peerData.eventId - The unique identifier of the event the peer is being added to. + * @param {string} peerData.role - The role of the peer in the event. + * @param {Date} peerData.joinedAt - The timestamp indicating when the peer joined the event. + * @returns {Promise} The data of the added peer. + * @throws {Error} If an error occurs while adding the peer to the event. + */ + +const addPeerToEvent = async (peerData) => { + try { + const batch = firestore.batch(); + + const peerRef = peerModel.doc(peerData.peerId); + const peerDocSnapshot = await peerRef.get(); + + if (!peerDocSnapshot.exists) { + // If the peer document doesn't exist, create a new one + const peerDocData = { + peerId: peerData.peerId, + name: peerData.name, + joinedEvents: [ + { + event_id: peerData.eventId, + role: peerData.role, + joined_at: peerData.joinedAt, + }, + ], + }; + batch.set(peerRef, peerDocData); + } else { + // If the peer document exists, update the joinedEvents array + batch.update(peerRef, { + joinedEvents: Firestore.FieldValue.arrayUnion({ + event_id: peerData.eventId, + role: peerData.role, + joined_at: peerData.joinedAt, + }), + }); + } + + const eventRef = eventModel.doc(peerData.eventId); + batch.update(eventRef, { + peers: Firestore.FieldValue.arrayUnion(peerRef.id), + }); + + await batch.commit(); + + const updatedPeerSnapshot = await peerRef.get(); + return updatedPeerSnapshot.data(); + } catch (error) { + logger.error("Error in adding peer to the event", error); + throw error; + } +}; + +/** + * Removes a peer from an event and marks them as kicked out in the Firestore database. + * @async + * @function + * @param {Object} params - The parameters for kicking out the peer. + * @param {string} params.eventId - The unique identifier of the event from which the peer is being kicked out. + * @param {string} params.peerId - The unique identifier of the peer being kicked out. + * @param {string} params.reason - The reason for kicking out the peer from the event. + * @returns {Promise} The updated data of the kicked-out peer. + * @throws {Error} If the peer is not found or is not part of the specified event. + */ +const kickoutPeer = async ({ eventId, peerId, reason }) => { + try { + const peerRef = peerModel.doc(peerId); + const peerSnapshot = await peerRef.get(); + + if (!peerSnapshot.exists) { + throw new Error("Participant not found"); + } + + const peerData = peerSnapshot.data(); + const joinedEvents = peerData.joinedEvents; + + const eventIndex = joinedEvents.findIndex((event) => event.event_id === eventId); + if (eventIndex === -1) { + throw new Error("Participant is not part of the specified event"); + } + + const updatedJoinedEvents = joinedEvents.map((event, index) => + index === eventIndex ? { ...event, left_at: new Date(), reason: reason, isKickedout: true } : event + ); + + await peerRef.update({ joinedEvents: updatedJoinedEvents }); + + const updatedPeerSnapshot = await peerRef.get(); + return updatedPeerSnapshot.data(); + } catch (error) { + logger.error("Error in removing peer from the event.", error); + throw error; + } +}; + module.exports = { createEvent, updateEvent, endActiveEvent, + addPeerToEvent, + kickoutPeer, }; diff --git a/models/logs.js b/models/logs.js index 80daf676f..8779d0601 100644 --- a/models/logs.js +++ b/models/logs.js @@ -3,6 +3,7 @@ const { getBeforeHourTime } = require("../utils/time"); const logsModel = firestore.collection("logs"); const admin = require("firebase-admin"); const { logType } = require("../constants/logs"); +const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** * Adds log @@ -22,7 +23,7 @@ const addLog = async (type, meta, body) => { return await logsModel.add(log); } catch (err) { logger.error("Error in adding log", err); - throw err; + throw new Error(INTERNAL_SERVER_ERROR); } }; @@ -42,14 +43,27 @@ const fetchLogs = async (query, param) => { } }); - const { limit, lastDocId } = query; + const { limit, lastDocId, userId } = query; let lastDoc; const limitDocuments = Number(limit); if (lastDocId) { lastDoc = await logsModel.doc(lastDocId).get(); } - + if (userId) { + const logsSnapshot = await logsModel + .where("type", "==", param) + .where("body.archived_user.user_id", "==", userId) + .orderBy("timestamp", "desc") + .get(); + const logs = []; + logsSnapshot.forEach((doc) => { + logs.push({ + ...doc.data(), + }); + }); + return logs; + } const logsSnapshotQuery = call.orderBy("timestamp", "desc").startAfter(lastDoc ?? ""); const snapshot = limit ? await logsSnapshotQuery.limit(limitDocuments).get() @@ -64,7 +78,7 @@ const fetchLogs = async (query, param) => { return logs; } catch (err) { logger.error("Error in adding log", err); - throw err; + throw new Error(INTERNAL_SERVER_ERROR); } }; diff --git a/models/users.js b/models/users.js index 5aeac0924..9915d2baf 100644 --- a/models/users.js +++ b/models/users.js @@ -584,6 +584,31 @@ const fetchUsersWithToken = async () => { return users; } catch (err) { logger.error(`Error while fetching all users with tokens field: ${err}`); + return []; + } +}; +/** + * + * @param {[string]} userIds Array id's of user + * @returns Object containing the details of the users whose userId was provided. + */ +const fetchUserByIds = async (userIds = []) => { + if (userIds.length === 0) { + return {}; + } + try { + const users = {}; + const usersRefs = userIds.map((docId) => userModel.doc(docId)); + const documents = await firestore.getAll(...usersRefs); + documents.forEach((snapshot) => { + if (snapshot.exists) { + users[snapshot.id] = snapshot.data(); + } + }); + + return users; + } catch (err) { + logger.error("Error retrieving user data", err); throw err; } }; @@ -666,4 +691,5 @@ module.exports = { fetchUsersWithToken, removeGitHubToken, getUsersByRole, + fetchUserByIds, }; diff --git a/routes/events.js b/routes/events.js index 9c5df3116..5a2b2ce8c 100644 --- a/routes/events.js +++ b/routes/events.js @@ -10,5 +10,7 @@ router.post("/join", eventsValidator.joinEvent, events.joinEvent); router.get("/:id", eventsValidator.getEventById, events.getEventById); router.patch("/", authenticate, eventsValidator.updateEvent, events.updateEvent); router.patch("/end", authenticate, eventsValidator.endActiveEvent, events.endActiveEvent); +router.post("/:id/peers", authenticate, eventsValidator.addPeerToEvent, events.addPeerToEvent); +router.patch("/:id/peers/kickout", authenticate, eventsValidator.kickoutPeer, events.kickoutPeer); module.exports = router; diff --git a/services/dataAccessLayer.js b/services/dataAccessLayer.js index 83c4ef3bd..4810e1bbf 100644 --- a/services/dataAccessLayer.js +++ b/services/dataAccessLayer.js @@ -1,8 +1,17 @@ const userQuery = require("../models/users"); const members = require("../models/members"); -const { USER_SENSITIVE_DATA } = require("../constants/users"); +const { ROLE_LEVEL, KEYS_NOT_ALLOWED, ACCESS_LEVEL } = require("../constants/userDataLevels"); -const retrieveUsers = async ({ id = null, username = null, usernames = null, query = null, userdata }) => { +const retrieveUsers = async ({ + id = null, + username = null, + usernames = null, + query = null, + userdata, + level = ACCESS_LEVEL.PUBLIC, + role = null, + userIds = [], +}) => { if (id || username) { let result; if (id != null) { @@ -10,66 +19,87 @@ const retrieveUsers = async ({ id = null, username = null, usernames = null, que } else { result = await userQuery.fetchUser({ username: username }); } - removeSensitiveInfo(result.user); + const user = levelSpecificAccess(result.user, level, role); + result.user = user; return result; } else if (usernames) { const { users } = await userQuery.fetchUsers(usernames); - users.forEach((element) => { - removeSensitiveInfo(element); + const result = []; + users.forEach((userdata) => { + const user = levelSpecificAccess(userdata, level, role); + result.push(user); }); - return users; + return result; + } else if (userIds.length > 0) { + const userDetails = await userQuery.fetchUserByIds(userIds); + Object.keys(userDetails).forEach((userId) => { + removeSensitiveInfo(userDetails[userId]); + }); + return userDetails; } else if (query) { const { allUsers, nextId, prevId } = await userQuery.fetchPaginatedUsers(query); - allUsers.forEach((element) => { - removeSensitiveInfo(element); + const users = []; + allUsers.forEach((userdata) => { + const user = levelSpecificAccess(userdata, level, role); + users.push(user); }); - return { allUsers, nextId, prevId }; + return { users, nextId, prevId }; } else { - removeSensitiveInfo(userdata); - return userdata; + const result = await userQuery.fetchUser({ userId: userdata.id }); + return levelSpecificAccess(result.user, level, role); } }; -const retrieveDiscordUsers = async () => { +const retrieveDiscordUsers = async (level = ACCESS_LEVEL.PUBLIC, role = null) => { const users = await userQuery.getDiscordUsers(); - users.forEach((element) => { - removeSensitiveInfo(element); + const usersData = []; + users.forEach((userdata) => { + const user = levelSpecificAccess(userdata, level, role); + usersData.push(user); }); - return users; + return usersData; }; const retreiveFilteredUsers = async (query) => { const users = await userQuery.getUsersBasedOnFilter(query); - users.forEach((element) => { - removeSensitiveInfo(element); + users.forEach((userdata) => { + removeSensitiveInfo(userdata); }); return users; }; const retrieveMembers = async (query) => { const allUsers = await members.fetchUsers(query); - allUsers.forEach((element) => { - removeSensitiveInfo(element); + allUsers.forEach((userdata) => { + removeSensitiveInfo(userdata); }); return allUsers; }; const retrieveUsersWithRole = async (role) => { const users = await members.fetchUsersWithRole(role); - users.forEach((element) => { - removeSensitiveInfo(element); + users.forEach((userdata) => { + removeSensitiveInfo(userdata); }); return users; }; -const removeSensitiveInfo = function (obj) { - for (let i = 0; i < USER_SENSITIVE_DATA.length; i++) { - if (Object.prototype.hasOwnProperty.call(obj, USER_SENSITIVE_DATA[i])) { - delete obj[USER_SENSITIVE_DATA[i]]; +const removeSensitiveInfo = function (obj, level = ACCESS_LEVEL.PUBLIC) { + for (let i = 0; i < KEYS_NOT_ALLOWED[level].length; i++) { + if (Object.prototype.hasOwnProperty.call(obj, KEYS_NOT_ALLOWED[level][i])) { + delete obj[KEYS_NOT_ALLOWED[level][i]]; } } }; +const levelSpecificAccess = (user, level = ACCESS_LEVEL.PUBLIC, role = null) => { + if (level === ACCESS_LEVEL.PUBLIC || ROLE_LEVEL[level].includes(role)) { + removeSensitiveInfo(user, level); + return user; + } + return "unauthorized"; +}; + module.exports = { retrieveUsers, removeSensitiveInfo, @@ -77,4 +107,5 @@ module.exports = { retrieveMembers, retrieveUsersWithRole, retreiveFilteredUsers, + levelSpecificAccess, }; diff --git a/test/fixtures/discordactions/discordactions.js b/test/fixtures/discordactions/discordactions.js index f14c57b52..0a84b617d 100644 --- a/test/fixtures/discordactions/discordactions.js +++ b/test/fixtures/discordactions/discordactions.js @@ -1,7 +1,7 @@ const groupData = [ - { id: "1", name: "Group 1" }, - { id: "2", name: "Group 2" }, - { id: "3", name: "Group 3" }, + { rolename: "Group 1", roleid: 1 }, + { rolename: "Group 2", roleid: 2 }, + { rolename: "Group 3", roleid: 3 }, ]; const roleData = { diff --git a/test/fixtures/logs/archievedUsers.js b/test/fixtures/logs/archievedUsers.js new file mode 100644 index 000000000..66f9b3497 --- /dev/null +++ b/test/fixtures/logs/archievedUsers.js @@ -0,0 +1,68 @@ +const archivedUserDetailsModal = [ + { + type: "archived-details", + meta: {}, + body: { + reason: "test reason", + archived_user: { user_id: "R5kljdsleH4Gr2t7tvr0Z", username: "testUser1" }, + archived_by: { + user_id: "ReMyuklislajwooncVL", + roles: { + in_discord: true, + super_user: false, + member: true, + archived: false, + }, + }, + }, + timestamp: { + _seconds: 1657193216, + _nanoseconds: 912000000, + }, + }, + { + type: "archived-details", + meta: {}, + body: { + reason: "test reason", + archived_user: { user_id: "R5kljdsleH4Gr2t7tvr0Z", username: "testUser1" }, + archived_by: { + user_id: "ReMyuklislajwooncVL", + roles: { + in_discord: true, + super_user: false, + member: true, + archived: false, + }, + }, + }, + timestamp: { + _seconds: 1657193216, + _nanoseconds: 912000000, + }, + }, + { + type: "archived-details", + meta: {}, + body: { + reason: "test reason", + archived_user: { user_id: "Efskee4Gr2t7tvr0Z", username: "testUser2" }, + archived_by: { + user_id: "ReMyuklislajwooncVL", + roles: { + in_discord: true, + super_user: false, + member: true, + archived: false, + }, + }, + }, + timestamp: { + _seconds: 1657193216, + _nanoseconds: 912000000, + }, + }, +]; +module.exports = { + archivedUserDetailsModal, +}; diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index caf2735c5..2123328f4 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -267,12 +267,6 @@ module.exports = () => { linkedin_id: "testuser1", github_id: "testuser1", github_display_name: "Test User", - phone: "1234567890", - email: "tu@gmail.com", - chaincode: "1234", - tokens: { - githubAccessToken: "githubAccessToken", - }, roles: { member: true, }, @@ -329,6 +323,29 @@ module.exports = () => { twitter_id: "ramsingh123", linkedin_id: "ramsingh123", }, + { + username: "testuser3", + first_name: "test3", + last_name: "user3", + yoe: 1, + img: "./img.png", + linkedin_id: "testuser1", + github_id: "testuser", + github_display_name: "Test User 3", + phone: "1234567890", + email: "abcd@gmail.com", + chaincode: "12345", + tokens: { + githubAccessToken: "githubAccessToken", + }, + roles: { + member: true, + }, + picture: { + publicId: "profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar", + url: "https://res.cloudinary.com/realdevsquad/image/upload/v1667685133/profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar.jpg", + }, + }, { username: "sahsisunny", first_name: "sunny", diff --git a/test/integration/discord.test.js b/test/integration/discord.test.js index 5147efee3..9a801157c 100644 --- a/test/integration/discord.test.js +++ b/test/integration/discord.test.js @@ -8,6 +8,14 @@ const authService = require("../../services/authService"); const userData = require("../fixtures/user/user")(); const { requestRoleData } = require("../fixtures/discordactions/discordactions"); +const firestore = require("../../utils/firestore"); +const discordRoleModel = firestore.collection("discord-roles"); +const userModel = firestore.collection("users"); + +const { addGroupRoleToMember } = require("../../models/discordactions"); + +const { groupData } = require("../fixtures/discordactions/discordactions"); + const cookieName = config.get("userToken.cookieName"); let userId; @@ -59,6 +67,29 @@ describe("test discord actions", function () { const user = { ...userData[4], discordId: "123456789" }; userId = await addUser(user); jwt = authService.generateAuthToken({ userId }); + + let allIds = []; + + const addUsersPromises = userData.map((user) => userModel.add({ ...user })); + const responses = await Promise.all(addUsersPromises); + allIds = responses.map((response) => response.id); + + const addRolesPromises = [ + discordRoleModel.add({ roleid: groupData[0].roleid, rolename: groupData[0].rolename, createdBy: allIds[1] }), + discordRoleModel.add({ roleid: groupData[1].roleid, rolename: groupData[1].rolename, createdBy: allIds[0] }), + ]; + await Promise.all(addRolesPromises); + + const addGroupRolesPromises = [ + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[0] }), + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[1] }), + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[1] }), + addGroupRoleToMember({ roleid: groupData[1].roleid, userid: allIds[0] }), + ]; + await Promise.all(addGroupRolesPromises); + }); + afterEach(async function () { + await cleanDb(); }); it("returns 200 for active users get method", function (done) { diff --git a/test/integration/discordactions.test.js b/test/integration/discordactions.test.js index 74ec16085..928091bc5 100644 --- a/test/integration/discordactions.test.js +++ b/test/integration/discordactions.test.js @@ -16,6 +16,11 @@ const cookieName = config.get("userToken.cookieName"); const firestore = require("../../utils/firestore"); const { userPhotoVerificationData } = require("../fixtures/user/photo-verification"); const photoVerificationModel = firestore.collection("photo-verification"); +const discordRoleModel = firestore.collection("discord-roles"); +const userModel = firestore.collection("users"); + +const { groupData } = require("../fixtures/discordactions/discordactions"); +const { addGroupRoleToMember } = require("../../models/discordactions"); chai.use(chaiHttp); describe("Discord actions", function () { @@ -85,4 +90,54 @@ describe("Discord actions", function () { }); }); }); + + describe("GET /discord-actions/groups", function () { + before(async function () { + let allIds = []; + + const addUsersPromises = userData.map((user) => userModel.add({ ...user })); + const responses = await Promise.all(addUsersPromises); + allIds = responses.map((response) => response.id); + + const addRolesPromises = [ + discordRoleModel.add({ roleid: groupData[0].roleid, rolename: groupData[0].rolename, createdBy: allIds[1] }), + discordRoleModel.add({ roleid: groupData[1].roleid, rolename: groupData[1].rolename, createdBy: allIds[0] }), + ]; + await Promise.all(addRolesPromises); + + const addGroupRolesPromises = [ + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[0] }), + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[1] }), + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[1] }), + addGroupRoleToMember({ roleid: groupData[1].roleid, userid: allIds[0] }), + ]; + await Promise.all(addGroupRolesPromises); + }); + + after(async function () { + await cleanDb(); + }); + + it("should successfully return all groups detail", function (done) { + chai + .request(app) + .get(`/discord-actions/groups`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body).to.be.an("object"); + // Verify presence of specific properties in each group + const expectedProps = ["roleid", "rolename", "memberCount", "firstName", "lastName", "image"]; + res.body.groups.forEach((group) => { + expect(group).to.include.all.keys(expectedProps); + }); + expect(res.body.message).to.equal("Roles fetched successfully!"); + return done(); + }); + }); + }); }); diff --git a/test/integration/members.test.js b/test/integration/members.test.js index 7788b8f0e..ee74b0689 100644 --- a/test/integration/members.test.js +++ b/test/integration/members.test.js @@ -12,6 +12,9 @@ const userData = require("../fixtures/user/user")(); const config = require("config"); const cookieName = config.get("userToken.cookieName"); +const Sinon = require("sinon"); +const { INTERNAL_SERVER_ERROR } = require("../../constants/errorMessages"); +const members = require("../../models/members"); chai.use(chaiHttp); @@ -258,16 +261,42 @@ describe("Members", function () { }); describe("PATCH /members/archiveMembers/:username", function () { + let archiveRoleToMemberStub; beforeEach(async function () { const superUserId = await addUser(superUser); jwt = authService.generateAuthToken({ userId: superUserId }); }); + afterEach(async function () { + Sinon.restore(); + await cleanDb(); + }); + it("Should return an object with status 500 and an error message", function (done) { + archiveRoleToMemberStub = Sinon.stub(members, "addArchiveRoleToMembers"); + archiveRoleToMemberStub.throws(new Error(INTERNAL_SERVER_ERROR)); + addUser(userToBeArchived).then(() => { + chai + .request(app) + .patch(`/members/archiveMembers/${userToBeArchived.username}`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ reason: "some reason" }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(500); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal(INTERNAL_SERVER_ERROR); + return done(); + }); + }); + }); it("Should return 404 if user doesn't exist", function (done) { chai .request(app) .patch(`/members/archiveMembers/${userDoesNotExists.username}`) .set("cookie", `${cookieName}=${jwt}`) + .send({ reason: "some reason" }) .end((err, res) => { if (err) { return done(err); @@ -278,13 +307,31 @@ describe("Members", function () { return done(); }); }); + it("Should return 400 if body is empty", function (done) { + chai + .request(app) + .patch(`/members/archiveMembers/${userToBeArchived.username}`) + .set("cookie", `${cookieName}=${jwt}`) + .send({}) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("Reason is required"); + return done(); + }); + }); it("Should archive the user", function (done) { addUser(userToBeArchived).then(() => { chai .request(app) .patch(`/members/archiveMembers/${userToBeArchived.username}`) .set("cookie", `${cookieName}=${jwt}`) + .send({ reason: "some reason" }) .end((err, res) => { if (err) { return done(err); @@ -305,6 +352,7 @@ describe("Members", function () { .request(app) .patch(`/members/archiveMembers/${userAlreadyArchived.username}`) .set("cookie", `${cookieName}=${jwt}`) + .send({ reason: "some reason" }) .end((err, res) => { if (err) { return done(err); @@ -318,26 +366,5 @@ describe("Members", function () { }); }); }); - - it("Should return 401 if user is not a super user", function (done) { - addUser(nonSuperUser).then((nonSuperUserId) => { - const nonSuperUserJwt = authService.generateAuthToken({ userId: nonSuperUserId }); - chai - .request(app) - .patch(`/members/moveToMembers/${nonSuperUser.username}`) - .set("cookie", `${cookieName}=${nonSuperUserJwt}`) - .end((err, res) => { - if (err) { - return done(err); - } - - expect(res).to.have.status(401); - expect(res.body).to.be.a("object"); - expect(res.body.message).to.equal("You are not authorized for this action."); - - return done(); - }); - }); - }); }); }); diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 668411976..906f108de 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -767,7 +767,7 @@ describe("Tasks", function () { }); it("Should return Forbidden error if task is not assigned to self", async function () { - const { userId } = await addUser(userData[0]); + const userId = await addUser(userData[0]); const jwt = authService.generateAuthToken({ userId }); const res = await chai.request(app).patch(`/tasks/self/${taskId1}`).set("cookie", `${cookieName}=${jwt}`); diff --git a/test/integration/users.test.js b/test/integration/users.test.js index 7f3f58272..d41b5e154 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -290,7 +290,6 @@ describe("Users", function () { expect(res.body.users).to.be.a("array"); expect(res.body.users[0]).to.not.have.property("phone"); expect(res.body.users[0]).to.not.have.property("email"); - expect(res.body.users[0]).to.not.have.property("tokens"); expect(res.body.users[0]).to.not.have.property("chaincode"); return done(); @@ -315,7 +314,6 @@ describe("Users", function () { }); expect(res.body.users[0]).to.not.have.property("phone"); expect(res.body.users[0]).to.not.have.property("email"); - expect(res.body.users[0]).to.not.have.property("tokens"); expect(res.body.users[0]).to.not.have.property("chaincode"); return done(); }); @@ -341,7 +339,6 @@ describe("Users", function () { expect(res.body.users.length).to.equal(1); expect(res.body.users[0]).to.not.have.property("phone"); expect(res.body.users[0]).to.not.have.property("email"); - expect(res.body.users[0]).to.not.have.property("tokens"); expect(res.body.users[0]).to.not.have.property("chaincode"); return done(); }); @@ -552,31 +549,11 @@ describe("Users", function () { expect(res.body).to.be.a("object"); expect(res.body).to.not.have.property("phone"); expect(res.body).to.not.have.property("email"); - expect(res.body).to.not.have.property("tokens"); expect(res.body).to.not.have.property("chaincode"); return done(); }); }); - it("Should return details with phone and email when query 'private' is true", function (done) { - chai - .request(app) - .get("/users/self") - .query({ private: true }) - .set("cookie", `${cookieName}=${jwt}`) - .end((err, res) => { - if (err) { - return done(); - } - - expect(res).to.have.status(200); - expect(res.body).to.be.a("object"); - expect(res.body).to.have.property("phone"); - expect(res.body).to.have.property("email"); - return done(); - }); - }); - it("Should return 401 if not logged in", function (done) { chai .request(app) @@ -616,7 +593,6 @@ describe("Users", function () { expect(res.body.user).to.be.a("object"); expect(res.body.user).to.not.have.property("phone"); expect(res.body.user).to.not.have.property("email"); - expect(res.body.user).to.not.have.property("tokens"); expect(res.body.user).to.not.have.property("chaincode"); return done(); }); @@ -658,7 +634,6 @@ describe("Users", function () { expect(res.body.user).to.be.a("object"); expect(res.body.user).to.not.have.property("phone"); expect(res.body.user).to.not.have.property("email"); - expect(res.body.user).to.not.have.property("tokens"); expect(res.body.user).to.not.have.property("chaincode"); return done(); }); diff --git a/test/integration/usersFilter.test.js b/test/integration/usersFilter.test.js index 9e0f5ed97..1263b2f77 100644 --- a/test/integration/usersFilter.test.js +++ b/test/integration/usersFilter.test.js @@ -365,7 +365,6 @@ describe("Filter Users", function () { res.body.users.forEach((user) => { expect(user).to.not.have.property("phone"); expect(user).to.not.have.property("email"); - expect(user).to.not.have.property("tokens"); }); return done(); }); diff --git a/test/unit/models/discordactions.test.js b/test/unit/models/discordactions.test.js index 6c3b2fa66..2405c0a70 100644 --- a/test/unit/models/discordactions.test.js +++ b/test/unit/models/discordactions.test.js @@ -12,6 +12,7 @@ const { isGroupRoleExists, addGroupRoleToMember, updateDiscordImageForVerification, + getNumberOfMemberForGroups, } = require("../../../models/discordactions"); const { groupData, roleData, existingRole } = require("../../fixtures/discordactions/discordactions"); const cleanDb = require("../../utils/cleanDb"); @@ -243,4 +244,40 @@ describe("discordactions", function () { } }); }); + + describe("getNumberOfMemberForGroups", function () { + before(async function () { + await Promise.all([ + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: 1 }), + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: 2 }), + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: 3 }), + addGroupRoleToMember({ roleid: groupData[1].roleid, userid: 1 }), + ]); + }); + + after(async function () { + await cleanDb(); + }); + + it("should return an empty array if the parameter is an empty array", async function () { + const result = await getNumberOfMemberForGroups([]); + expect(result).to.be.an("array"); + expect(result.length).to.equal(0); + }); + + it("should return an empty array if the parameter no parameter is passed", async function () { + const result = await getNumberOfMemberForGroups(); + expect(result).to.be.an("array"); + expect(result.length).to.equal(0); + }); + + it("should return group details with memberCount details ", async function () { + const result = await getNumberOfMemberForGroups(groupData); + expect(result).to.deep.equal([ + { rolename: groupData[0].rolename, roleid: 1, memberCount: 3 }, + { rolename: groupData[1].rolename, roleid: 2, memberCount: 1 }, + { rolename: groupData[2].rolename, roleid: 3, memberCount: 0 }, + ]); + }); + }); }); diff --git a/test/unit/models/events.test.js b/test/unit/models/events.test.js index 4dd937923..49902f414 100644 --- a/test/unit/models/events.test.js +++ b/test/unit/models/events.test.js @@ -6,6 +6,7 @@ const firestore = require("../../../utils/firestore"); const eventQuery = require("../../../models/events"); const eventModel = firestore.collection("events"); +const peerModel = firestore.collection("peers"); const eventDataArray = require("../../fixtures/events/events")(); const eventData = eventDataArray[0]; @@ -17,61 +18,100 @@ describe("Events", function () { describe("createEvent", function () { it("should create a new event in firestore", async function () { - // Call the function with sample data const result = await eventQuery.createEvent(eventData); - // Add sample data to Firestore const data = (await eventModel.doc(eventData.room_id).get()).data(); - // Verify that the event was created expect(result).to.deep.equal(data); }); }); describe("updateEvent", function () { it("should update the enabled property of a event", async function () { - // Add sample data to Firestore const docRef = eventModel.doc(eventData.room_id); await docRef.set(eventData); - // Call the function with sample data await eventQuery.updateEvent({ id: "641e3b43a42edf3910cbc8bf", enabled: true }, eventModel); - // Get updated data from Firestore const docSnapshot = await eventModel.doc(docRef.id).get(); const data = docSnapshot.data(); - // Verify that the enabled property was updated expect(data.enabled).to.equal(true); }); }); describe("endActiveEvent", function () { it("should update the lock, reason, and status of a event", async function () { - // Add sample data to Firestore const docRef = await eventModel.add(eventData); try { - // Call the function with sample data await eventQuery.endActiveEvent({ id: docRef.id, reason: "test reason", lock: true, }); - // Get updated data from Firestore const docSnapshot = await eventModel.doc(docRef.id).get(); const data = docSnapshot.data(); - // Verify that the lock, reason, and status properties were updated expect(data.lock).to.equal(true); expect(data.reason).to.equal("test reason"); expect(data.status).to.equal("inactive"); } catch (error) { - // Check that the function threw an error expect(error).to.exist(); expect(error.message).to.equal("Error in enabling event."); } }); }); + + describe("addPeerToEvent", function () { + it("should create a new peer document if it doesn't exist", async function () { + const docRef = await eventModel.add(eventData); + + const peerData = { + peerId: "someid", + name: "NonExistingPeer", + eventId: docRef.id, + role: "participant", + joinedAt: new Date(), + }; + + const result = await eventQuery.addPeerToEvent(peerData); + + const docSnapshot = await peerModel.doc(result.peerId).get(); + const data = docSnapshot.data(); + + expect(data.name).to.equal(peerData.name); + expect(data.joinedEvents).to.have.lengthOf(1); + expect(data.joinedEvents[0].event_id).to.equal(peerData.eventId); + expect(data.joinedEvents[0].role).to.equal(peerData.role); + }); + + it("should update the joinedEvents array if the peer document exists", async function () { + const docRef = await eventModel.add(eventData); + + const peerData = { + peerId: "someid", + name: "ExistingPeer", + eventId: docRef.id, + role: "participant", + joinedAt: new Date(), + }; + + await peerModel.add({ + peerId: peerData.peerId, + name: peerData.name, + joinedEvents: [], + }); + + await eventQuery.addPeerToEvent(peerData); + + const docSnapshot = await peerModel.doc(peerData.peerId).get(); + const data = docSnapshot.data(); + + expect(data.joinedEvents).to.have.lengthOf(1); + expect(data.joinedEvents[0].event_id).to.equal(peerData.eventId); + expect(data.joinedEvents[0].role).to.equal(peerData.role); + }); + }); }); diff --git a/test/unit/models/logs.test.js b/test/unit/models/logs.test.js index 9d3df2ccd..7526ba1ed 100644 --- a/test/unit/models/logs.test.js +++ b/test/unit/models/logs.test.js @@ -4,6 +4,17 @@ const { expect } = chai; const cleanDb = require("../../utils/cleanDb"); const logsQuery = require("../../../models/logs"); const cacheData = require("../../fixtures/cloudflareCache/data"); +const logsData = require("../../fixtures/logs/archievedUsers"); +const app = require("../../../server"); +const Sinon = require("sinon"); +const { INTERNAL_SERVER_ERROR } = require("../../../constants/errorMessages"); +const userData = require("../../fixtures/user/user")(); +const addUser = require("../../utils/addUser"); +const cookieName = config.get("userToken.cookieName"); +const authService = require("../../../services/authService"); + +const superUser = userData[4]; +const userToBeMadeMember = userData[1]; describe("Logs", function () { after(async function () { @@ -37,4 +48,77 @@ describe("Logs", function () { expect(data[0].timestamp._nanoseconds).to.be.a("number"); }); }); + + describe("GET /logs/archived-details", function () { + let addLogsStub; + let jwt; + beforeEach(async function () { + const superUserId = await addUser(superUser); + jwt = authService.generateAuthToken({ userId: superUserId }); + await cleanDb(); + }); + afterEach(function () { + Sinon.restore(); + }); + + it("Should return an Internal server error message", async function () { + addLogsStub = Sinon.stub(logsQuery, "fetchLogs"); + addLogsStub.throws(new Error(INTERNAL_SERVER_ERROR)); + + addUser(userToBeMadeMember).then(() => { + const res = chai.request(app).get("/logs/archived-details").set("cookie", `${cookieName}=${jwt}`).send(); + + expect(res.body.message).to.equal(INTERNAL_SERVER_ERROR); + }); + }); + it("Should return empty array if no logs found", async function () { + const { type } = logsData.archivedUserDetailsModal[0]; + const query = {}; + + const data = await logsQuery.fetchLogs(query, type); + + expect(data).to.be.an("array").with.lengthOf(0); + }); + it("Should fetch all archived logs", async function () { + const { type, meta, body } = logsData.archivedUserDetailsModal[0]; + const query = {}; + + await logsQuery.addLog(type, meta, body); + const data = await logsQuery.fetchLogs(query, type); + + expect(data).to.be.an("array").with.lengthOf.greaterThan(0); + expect(data[0]).to.have.property("timestamp").that.is.an("object"); + expect(data[0].timestamp).to.have.property("_seconds").that.is.a("number"); + expect(data[0].timestamp).to.have.property("_nanoseconds").that.is.a("number"); + expect(data[0].body.archived_user).to.have.property("username").that.is.a("string"); + expect(data[0].body).to.have.property("reason").that.is.a("string"); + }); + it("Should fetch all archived logs for given user_id", async function () { + const { type, meta, body } = logsData.archivedUserDetailsModal[0]; + const query = { + userId: body.archived_user.user_id, + }; + await logsQuery.addLog(type, meta, body); + const data = await logsQuery.fetchLogs(query, type); + + expect(data).to.be.an("array").with.lengthOf.greaterThan(0); + expect(data[0]).to.have.property("timestamp").that.is.an("object"); + expect(data[0].timestamp).to.have.property("_seconds").that.is.a("number"); + expect(data[0].timestamp).to.have.property("_nanoseconds").that.is.a("number"); + expect(data[0].body).to.have.property("reason").that.is.a("string"); + }); + it("Should throw response status 404, if username is incorrect in the query", async function () { + const { type, meta, body } = logsData.archivedUserDetailsModal[0]; + const query = { + userId: "1234_test", // incorrect username + }; + await logsQuery.addLog(type, meta, body); + const data = await logsQuery.fetchLogs(query, type); + const response = await chai.request(app).get(`/logs/${type}/${query}`); + + expect(data).to.be.an("array").with.lengthOf(0); + expect(response).to.have.status(404); + expect(response.body.message).to.be.equal("Not Found"); + }); + }); }); diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 42d7cad61..474b28f11 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -94,7 +94,7 @@ describe("users", function () { }); it("It should have created_At and updated_At fields", async function () { - const userData = userDataArray[14]; + const userData = userDataArray[15]; await users.addOrUpdate(userData); const githubUsername = "sahsisunny"; const { user, userExists } = await users.fetchUser({ githubUsername }); @@ -279,7 +279,7 @@ describe("users", function () { }); it("returns users with member role", async function () { const members = await users.getUsersByRole("member"); - expect(members.length).to.be.equal(6); + expect(members.length).to.be.equal(7); members.forEach((member) => { expect(member.roles.member).to.be.equal(true); }); @@ -290,4 +290,32 @@ describe("users", function () { }); }); }); + describe("fetch users by id", function () { + let allIds = []; + before(async function () { + const addUsersPromises = []; + userDataArray.forEach((user, index) => { + addUsersPromises.push(userModel.add({ ...user })); + }); + const responses = await Promise.all(addUsersPromises); + allIds = responses.map((response) => response.id); + }); + + after(async function () { + await cleanDb(); + }); + + it("should fetch the details of users whose ids are present in the array", async function () { + const randomIds = allIds.sort(() => 0.5 - Math.random()).slice(0, 3); // Select random ids from allIds + const result = await users.fetchUserByIds(randomIds); + const fetchedUserIds = Object.keys(result); + expect(fetchedUserIds).to.deep.equal(randomIds); + }); + + it("should return empty object if no ids are passed", async function () { + const result = await users.fetchUserByIds(); + const fetchedUserIds = Object.keys(result); + expect(fetchedUserIds).to.deep.equal([]); + }); + }); }); diff --git a/test/unit/services/dataAccessLayer.test.js b/test/unit/services/dataAccessLayer.test.js index c12e8ad11..0a15dff9b 100644 --- a/test/unit/services/dataAccessLayer.test.js +++ b/test/unit/services/dataAccessLayer.test.js @@ -12,14 +12,17 @@ const { retrieveUsersWithRole, retrieveMembers, retreiveFilteredUsers, + levelSpecificAccess, } = require("../../../services/dataAccessLayer"); +const { KEYS_NOT_ALLOWED, ACCESS_LEVEL } = require("../../../constants/userDataLevels"); + const userData = require("../../fixtures/user/user")(); -const { USER_SENSITIVE_DATA } = require("../../../constants/users"); chai.use(chaiHttp); const expect = chai.expect; let fetchUserStub; + describe("Data Access Layer", function () { describe("retrieveUsers", function () { it("should fetch a single user by ID and remove sensitive info", async function () { @@ -28,7 +31,7 @@ describe("Data Access Layer", function () { const result = await retrieveUsers({ id: userData[12].id }); removeSensitiveInfo(userData[12]); expect(result.user).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { expect(result.user).to.not.have.property(key); }); }); @@ -38,7 +41,7 @@ describe("Data Access Layer", function () { const result = await retrieveUsers({ username: userData[12].username }); removeSensitiveInfo(userData[12]); expect(result.user).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { expect(result.user).to.not.have.property(key); }); }); @@ -48,10 +51,10 @@ describe("Data Access Layer", function () { fetchUserStub.returns(Promise.resolve({ users: [userData[12]] })); const result = await retrieveUsers({ usernames: [userData[12].username] }); removeSensitiveInfo(userData[12]); - result.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); }); }); }); @@ -62,10 +65,10 @@ describe("Data Access Layer", function () { const query = { page: 1 }; const result = await retrieveUsers({ query }); removeSensitiveInfo(userData[12]); - result.allUsers.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.users.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); }); }); }); @@ -74,7 +77,7 @@ describe("Data Access Layer", function () { const userdata = userData[12]; await retrieveUsers({ userdata }); removeSensitiveInfo(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { expect(userdata).to.not.have.property(key); }); }); @@ -85,10 +88,10 @@ describe("Data Access Layer", function () { const fetchUserStub = sinon.stub(userQuery, "getDiscordUsers"); fetchUserStub.returns(Promise.resolve([userData[12]])); const result = await retrieveDiscordUsers(); - result.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); }); }); }); @@ -100,10 +103,10 @@ describe("Data Access Layer", function () { fetchUserStub.returns(Promise.resolve([userData[12]])); const query = { showArchived: true }; const result = await retrieveUsersWithRole(query); - result.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); }); }); }); @@ -114,10 +117,23 @@ describe("Data Access Layer", function () { const fetchUserStub = sinon.stub(members, "fetchUsers"); fetchUserStub.returns(Promise.resolve([userData[12]])); const result = await retrieveMembers(); - result.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); + }); + }); + }); + + it("should fetch multiple users details based on ids and remove sensitive data", async function () { + const fetchUserStub = sinon.stub(userQuery, "fetchUserByIds"); + fetchUserStub.returns(Promise.resolve({ [userData[12].id]: userData[12] })); + const result = await retrieveUsers({ userIds: [userData[12].id] }); + removeSensitiveInfo(userData[12]); + Object.keys(result).forEach((id) => { + expect(result[id]).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(result[id]).to.not.have.property(key); }); }); }); @@ -129,10 +145,10 @@ describe("Data Access Layer", function () { fetchUserStub.returns(Promise.resolve([userData[12]])); const query = { state: "ACTIVE" }; const result = await retreiveFilteredUsers(query); - result.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); }); }); }); @@ -140,10 +156,37 @@ describe("Data Access Layer", function () { describe("removeSensitiveInfo", function () { it("should remove sensitive information from the users object", function () { - removeSensitiveInfo(userData); - USER_SENSITIVE_DATA.forEach((key) => { + removeSensitiveInfo(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { expect(userData[12]).to.not.have.property(key); }); }); }); + + describe("levelSpecificAccess", function () { + it("should return the user object for PUBLIC level after removing all sensitive info", function () { + const result = levelSpecificAccess({ ...userData[12] }, ACCESS_LEVEL.PUBLIC); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(result).to.not.have.property(key); + }); + }); + + it('should return "unauthorized" for non-superuser role', function () { + const unauthorizedRole = "member"; + const result = levelSpecificAccess({ ...userData[12] }, ACCESS_LEVEL.PRIVATE, unauthorizedRole); + expect(result).to.equal("unauthorized"); + }); + + it("should keep sensitive info for valid role and level", function () { + const user = { ...userData[12], email: "a@b.com", phone: "7890654329", chaincode: "78906" }; + const role = "super_user"; + const level = ACCESS_LEVEL.PRIVATE; + const result = levelSpecificAccess(user, level, role); + KEYS_NOT_ALLOWED[level].forEach((key) => { + expect(result).to.not.have.property(key); + }); + expect(result).to.have.property("phone"); + expect(result).to.have.property("email"); + }); + }); }); From b31632752704cab343c8214fd687913b4e8484f6 Mon Sep 17 00:00:00 2001 From: Sunny Sahsi Date: Tue, 22 Aug 2023 17:27:59 +0530 Subject: [PATCH 103/105] FEATURE: Filter task based assignee and task title (#1392) * FEATURE: add functionality to fetch task base on filter like assignee and task title * FIX : fix assignee filter * FIX : add order by title in status filter * FIX: task size related issue * FIX: order all task at initial state * TEMP * FIX: change variable name * FEATURE: add functionality to fetch task based on filter like assignee and task title * FIX(TEST) : fix failing tests * REFACTOR:change term to title query param * TEST: add test for tasks validator chages * TEST: add test for task utils * TEST: add test for task models changes * TEST: add test for task controller(integration test) * Update test/integration/tasks.test.js --------- Co-authored-by: Pratiyush Kumar <82165483+Pratiyushkumar@users.noreply.github.com> --- controllers/tasks.js | 4 +-- middlewares/validators/tasks.js | 2 ++ models/tasks.js | 32 ++++++++++++++++--- test/integration/tasks.test.js | 26 +++++++++++++++ test/unit/middlewares/tasks-validator.test.js | 15 +++++++++ test/unit/models/tasks.test.js | 26 +++++++++++++++ test/unit/utils/transformQuery.test.js | 6 ++++ utils/tasks.js | 12 +++++-- 8 files changed, 115 insertions(+), 8 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index b3918308b..9770af903 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -126,8 +126,8 @@ const fetchPaginatedTasks = async (query) => { const fetchTasks = async (req, res) => { try { - const { dev, status, page, size, prev, next, q: queryString } = req.query; - const transformedQuery = transformQuery(dev, status, size, page); + const { dev, status, page, size, prev, next, q: queryString, assignee, title } = req.query; + const transformedQuery = transformQuery(dev, status, size, page, assignee, title); if (dev) { const paginatedTasks = await fetchPaginatedTasks({ ...transformedQuery, prev, next }); diff --git a/middlewares/validators/tasks.js b/middlewares/validators/tasks.js index 3b0ab3e73..d25799888 100644 --- a/middlewares/validators/tasks.js +++ b/middlewares/validators/tasks.js @@ -142,6 +142,8 @@ const getTasksValidator = async (req, res, next) => { .insensitive() .valid(...MAPPED_TASK_STATUS_ENUM) .optional(), + assignee: joi.string().insensitive().optional(), + title: joi.string().insensitive().optional(), page: joi.number().integer().min(0), next: joi .string() diff --git a/models/tasks.js b/models/tasks.js index 1a5f5cf53..b237f840f 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -119,15 +119,39 @@ const getBuiltTasks = async (tasksSnapshot, searchTerm) => { return taskList; }; -const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, prev, dev = false }) => { +const fetchPaginatedTasks = async ({ + status = "", + size = TASK_SIZE, + page, + next, + prev, + dev = false, + assignee, + title, +}) => { try { - let initialQuery; + let initialQuery = tasksModel.orderBy("title"); + if (status === TASK_STATUS.OVERDUE && dev) { const currentTime = Math.floor(Date.now() / 1000); - initialQuery = tasksModel.where("endsOn", "<", currentTime); + initialQuery = initialQuery.where("endsOn", "<", currentTime); } else { - initialQuery = status ? tasksModel.where("status", "==", status).orderBy("title") : tasksModel.orderBy("title"); + if (status) { + initialQuery = initialQuery.where("status", "==", status); + } + + if (assignee) { + const user = await userUtils.getUserId(assignee); + if (user) { + initialQuery = initialQuery.where("assignee", "==", user); + } + } + + if (title) { + initialQuery = initialQuery.where("title", ">=", title).where("title", "<=", title + "\uf8ff"); + } } + let queryDoc = initialQuery; if (prev) { diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 67a211cd4..870dff919 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -211,6 +211,32 @@ describe("Tasks", function () { }); }); + it("Should get all tasks filtered with status ,assignee, title when passed to GET /tasks", function (done) { + chai + .request(app) + .get(`/tasks?status=${TASK_STATUS.AVAILABLE}&dev=true&assignee=sagar&title=Test`) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("Tasks returned successfully!"); + expect(res.body.tasks).to.be.a("array"); + expect(res.body).to.have.property("next"); + expect(res.body).to.have.property("prev"); + + const tasksData = res.body.tasks ?? []; + tasksData.forEach((task) => { + expect(task.status).to.equal(TASK_STATUS.AVAILABLE); + expect(task.assignee).to.equal("sagar"); + expect(task.title).to.include("Test"); + }); + return done(); + }); + }); + it("Should get all overdue tasks GET /tasks", function (done) { chai .request(app) diff --git a/test/unit/middlewares/tasks-validator.test.js b/test/unit/middlewares/tasks-validator.test.js index 362c4ac4e..a00eb48db 100644 --- a/test/unit/middlewares/tasks-validator.test.js +++ b/test/unit/middlewares/tasks-validator.test.js @@ -383,4 +383,19 @@ describe("getTasks validator", function () { await getTasksValidator(req, res, nextMiddlewareSpy); expect(nextMiddlewareSpy.callCount).to.be.equal(0); }); + + it("should pass the request when correct parameters are passed: assignee, dev, status and title", async function () { + const req = { + query: { + dev: "true", + assignee: "assignee", + title: "title", + status: TASK_STATUS.ASSIGNED, + }, + }; + const res = {}; + const nextMiddlewareSpy = Sinon.spy(); + await getTasksValidator(req, res, nextMiddlewareSpy); + expect(nextMiddlewareSpy.callCount).to.be.equal(1); + }); }); diff --git a/test/unit/models/tasks.test.js b/test/unit/models/tasks.test.js index 6ac225ae8..4ba5a7a05 100644 --- a/test/unit/models/tasks.test.js +++ b/test/unit/models/tasks.test.js @@ -156,6 +156,32 @@ describe("tasks", function () { expect(result.allTasks).to.have.length(tasksLength); result.allTasks.forEach((task) => expect(task.status).to.be.equal(status)); }); + + it("should fetch all tasks filtered by the assignee and title", async function () { + const assignee = "ankur"; + const title = "Overdue"; + const result = await tasks.fetchPaginatedTasks({ assignee, title }); + + const filteredTasks = tasksData.filter((task) => task.assignee === assignee && task.title.includes(title)); + + expect(result).to.have.property("allTasks"); + filteredTasks.forEach((task) => { + expect(task.assignee).to.be.equal(assignee); + expect(task.title).to.include(title); + }); + }); + + it("should fetch all tasks filtered by the assignee passed", async function () { + const assignee = "ankur"; + const result = await tasks.fetchPaginatedTasks({ assignee }); + + const filteredTasks = tasksData.filter((task) => task.assignee === assignee); + + expect(result).to.have.property("allTasks"); + filteredTasks.forEach((task) => { + expect(task.assignee).to.be.equal(assignee); + }); + }); }); describe("update Dependency", function () { diff --git a/test/unit/utils/transformQuery.test.js b/test/unit/utils/transformQuery.test.js index 959bb1ae2..1fee26299 100644 --- a/test/unit/utils/transformQuery.test.js +++ b/test/unit/utils/transformQuery.test.js @@ -34,4 +34,10 @@ describe("transformQuery", function () { expect(transformedQuery.page).to.be.equal(1); expect(typeof transformedQuery.page).to.equal("number"); }); + + it("should transfrom and parse assignee to lowercase when passed as param", function () { + const transformedQuery = transformQuery(false, TASK_STATUS.ASSIGNED, 5, 1, "Test"); + expect(transformedQuery.assignee).to.be.equal("test"); + expect(typeof transformedQuery.assignee).to.equal("string"); + }); }); diff --git a/utils/tasks.js b/utils/tasks.js index 0e6a46038..a2c14615b 100644 --- a/utils/tasks.js +++ b/utils/tasks.js @@ -64,10 +64,12 @@ const buildTasks = (tasks, initialTaskArray = []) => { return initialTaskArray; }; -const transformQuery = (dev = false, status = "", size, page) => { +const transformQuery = (dev = false, status = "", size, page, assignee = "", title = "") => { const query = {}; const transformedDev = JSON.parse(dev); const transformedStatus = MAPPED_TASK_STATUS[status.toUpperCase()]; + const transformedAssignee = assignee.toLowerCase(); + const transformedTitle = title; if (page) { query.page = parseInt(page); @@ -77,7 +79,13 @@ const transformQuery = (dev = false, status = "", size, page) => { query.size = parseInt(size); } - return { status: transformedStatus, dev: transformedDev, ...query }; + return { + status: transformedStatus, + dev: transformedDev, + assignee: transformedAssignee, + title: transformedTitle, + ...query, + }; }; const parseSearchQuery = (queryString) => { From eb5e9a55b8dc2f2c1875bd5752b682b307710518 Mon Sep 17 00:00:00 2001 From: Sunny Sahsi Date: Tue, 22 Aug 2023 22:26:11 +0530 Subject: [PATCH 104/105] FIX: Overdue tasks query issue (#1441) * FIX: overdue tasks query issue * FIX: overdue tasks query issue --- models/tasks.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/models/tasks.js b/models/tasks.js index b237f840f..0ade96760 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -130,12 +130,13 @@ const fetchPaginatedTasks = async ({ title, }) => { try { - let initialQuery = tasksModel.orderBy("title"); + let initialQuery = tasksModel; if (status === TASK_STATUS.OVERDUE && dev) { const currentTime = Math.floor(Date.now() / 1000); - initialQuery = initialQuery.where("endsOn", "<", currentTime); + initialQuery = tasksModel.where("endsOn", "<", currentTime); } else { + initialQuery = tasksModel.orderBy("title"); if (status) { initialQuery = initialQuery.where("status", "==", status); } From 3477da6c32edf8c7645a9446ca95a2c3fcea3ab1 Mon Sep 17 00:00:00 2001 From: Bikash Singh Date: Wed, 23 Aug 2023 23:34:20 +0530 Subject: [PATCH 105/105] Add isMember Field to API Response for Group Membership (#1413) * Adding isMember param to show membership of the user * reverting script * populating membership details only when flag is set * Adding tests * fixing PR comments: more specific checks for flags * Handling firebase limitation of 30 comparision * PR comments: semantical fix --- controllers/discordactions.js | 29 ++---- models/discordactions.js | 87 +++++++++++++--- services/dataAccessLayer.js | 7 +- test/fixtures/user/user.js | 1 + test/integration/discordactions.test.js | 47 +++++++-- test/unit/models/discordactions.test.js | 116 ++++++++++++++++++--- test/unit/services/dataAccessLayer.test.js | 5 + test/unit/utils/helper.test.js | 17 ++- utils/helper.js | 22 ++++ 9 files changed, 268 insertions(+), 63 deletions(-) diff --git a/controllers/discordactions.js b/controllers/discordactions.js index f91da7504..0d9995288 100644 --- a/controllers/discordactions.js +++ b/controllers/discordactions.js @@ -3,7 +3,6 @@ const admin = require("firebase-admin"); const config = require("config"); const jwt = require("jsonwebtoken"); const discordRolesModel = require("../models/discordactions"); -const { retrieveUsers } = require("../services/dataAccessLayer"); /** * Creates a role @@ -67,25 +66,19 @@ const createGroupRole = async (req, res) => { const getAllGroupRoles = async (req, res) => { try { const { groups } = await discordRolesModel.getAllGroupRoles(); - const groupsWithMemberCount = await discordRolesModel.getNumberOfMemberForGroups(groups); - const groupCreatorIds = groupsWithMemberCount.reduce((ids, group) => { - ids.add(group.createdBy); - return ids; - }, new Set()); - const groupCreatorsDetails = await retrieveUsers({ userIds: Array.from(groupCreatorIds) }); - const groupsWithUserDetails = groupsWithMemberCount.map((group) => { - const groupCreator = groupCreatorsDetails[group.createdBy]; - return { - ...group, - firstName: groupCreator.first_name, - lastName: groupCreator.last_name, - image: groupCreator.picture?.url, - }; - }); - + const dev = req.query.dev === "true"; + if (dev) { + // Placing the new changes under the feature flag. + const discordId = req.userData?.discordId; + const groupsWithMembershipInfo = await discordRolesModel.enrichGroupDataWithMembershipInfo(discordId, groups); + return res.json({ + message: "Roles fetched successfully!", + groups: groupsWithMembershipInfo, + }); + } return res.json({ message: "Roles fetched successfully!", - groups: groupsWithUserDetails, + groups, }); } catch (err) { logger.error(`Error while getting roles: ${err}`); diff --git a/models/discordactions.js b/models/discordactions.js index c8e30230d..468e83523 100644 --- a/models/discordactions.js +++ b/models/discordactions.js @@ -3,6 +3,9 @@ const firestore = require("../utils/firestore"); const discordRoleModel = firestore.collection("discord-roles"); const memberRoleModel = firestore.collection("member-group-roles"); const admin = require("firebase-admin"); +const { findSubscribedGroupIds } = require("../utils/helper"); +const { retrieveUsers } = require("../services/dataAccessLayer"); +const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); const photoVerificationModel = firestore.collection("photo-verification"); /** @@ -114,32 +117,83 @@ const updateDiscordImageForVerification = async (userDiscordId) => { } }; -const getNumberOfMemberForGroups = async (groups = []) => { +/** + * Enriches group data with membership information for a given Discord ID. + * + * @param {string} discordId - The Discord ID of the user. + * @param {Array} groups - Array of group objects to process. + * @returns {Promise>} - An array of group objects with enriched information. + */ +const enrichGroupDataWithMembershipInfo = async (discordId, groups = []) => { try { if (!groups.length) { return []; } + + const groupCreatorIds = groups.reduce((ids, group) => { + if (group.createdBy) { + ids.add(group.createdBy); + } + return ids; + }, new Set()); + + const groupCreatorsDetails = await retrieveUsers({ userIds: Array.from(groupCreatorIds) }); const roleIds = groups.map((group) => group.roleid); + const groupsToUserMappings = await fetchGroupToUserMapping(roleIds); + const roleIdToCountMap = {}; - const snapshots = await memberRoleModel.where("roleid", "in", roleIds).get(); - const roleCount = {}; + groupsToUserMappings.forEach((groupToUserMapping) => { + // Count how many times roleId comes up in the array. + // This says how many users we have for a given roleId + roleIdToCountMap[groupToUserMapping.roleid] = (roleIdToCountMap[groupToUserMapping.roleid] ?? 0) + 1; + }); - snapshots.forEach((doc) => { - const roleToMemberMapping = doc.data(); + const subscribedGroupIds = findSubscribedGroupIds(discordId, groupsToUserMappings); - if (roleCount[roleToMemberMapping.roleid]) { - roleCount[roleToMemberMapping.roleid] += 1; - } else { - roleCount[roleToMemberMapping.roleid] = 1; - } + return groups.map((group) => { + const groupCreator = groupCreatorsDetails[group.createdBy]; + return { + ...group, + firstName: groupCreator?.first_name, + lastName: groupCreator?.last_name, + image: groupCreator?.picture?.url, + memberCount: roleIdToCountMap[group.roleid] || 0, // Number of users joined this group + isMember: subscribedGroupIds.has(group.roleid), // Is current loggedIn user is a member of this group + }; }); + } catch (err) { + logger.error("Error while enriching group data with membership info", err); + throw err; + } +}; + +/** + * + * @param {Array} roleIds Array of roleIds whose user mapping needs to fetched + * @returns Array of roleId to userId mapping + * + * Breaking the roleIds array into chunks of 30 or less due to firebase limitation + */ +const fetchGroupToUserMapping = async (roleIds) => { + try { + const roleIdChunks = []; + + for (let i = 0; i < roleIds.length; i += BATCH_SIZE_IN_CLAUSE) { + roleIdChunks.push(roleIds.slice(i, i + BATCH_SIZE_IN_CLAUSE)); + } + + const promises = roleIdChunks.map(async (roleIdChunk) => { + const querySnapshot = await memberRoleModel.where("roleid", "in", roleIdChunk).get(); + return querySnapshot.docs.map((doc) => doc.data()); + }); + + const snapshots = await Promise.all(promises); + + const groupToUserMappingArray = snapshots.flat(); - return groups.map((group) => ({ - ...group, - memberCount: roleCount[group.roleid] || 0, - })); + return groupToUserMappingArray; } catch (err) { - logger.error("Error while counting members for each group", err); + logger.error("Error while fetching group to user mapping", err); throw err; } }; @@ -150,5 +204,6 @@ module.exports = { addGroupRoleToMember, isGroupRoleExists, updateDiscordImageForVerification, - getNumberOfMemberForGroups, + enrichGroupDataWithMembershipInfo, + fetchGroupToUserMapping, }; diff --git a/services/dataAccessLayer.js b/services/dataAccessLayer.js index 4810e1bbf..ed2f32488 100644 --- a/services/dataAccessLayer.js +++ b/services/dataAccessLayer.js @@ -10,7 +10,7 @@ const retrieveUsers = async ({ userdata, level = ACCESS_LEVEL.PUBLIC, role = null, - userIds = [], + userIds = null, }) => { if (id || username) { let result; @@ -30,7 +30,10 @@ const retrieveUsers = async ({ result.push(user); }); return result; - } else if (userIds.length > 0) { + } else if (userIds) { + if (userIds.length === 0) { + return {}; + } const userDetails = await userQuery.fetchUserByIds(userIds); Object.keys(userDetails).forEach((userId) => { removeSensitiveInfo(userDetails[userId]); diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index 7ca3dd0a0..63ebd591f 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -43,6 +43,7 @@ module.exports = () => { { username: "nikhil", first_name: "Nikhil", + discordId: "1234567890", last_name: "Bhandarkar", yoe: 0, img: "./img.png", diff --git a/test/integration/discordactions.test.js b/test/integration/discordactions.test.js index 928091bc5..dc8062710 100644 --- a/test/integration/discordactions.test.js +++ b/test/integration/discordactions.test.js @@ -92,24 +92,30 @@ describe("Discord actions", function () { }); describe("GET /discord-actions/groups", function () { + let newGroupData; + let allIds = []; before(async function () { - let allIds = []; - const addUsersPromises = userData.map((user) => userModel.add({ ...user })); const responses = await Promise.all(addUsersPromises); allIds = responses.map((response) => response.id); + newGroupData = groupData.map((group, index) => { + return { + ...group, + createdBy: allIds[Math.min(index, allIds.length - 1)], + }; + }); const addRolesPromises = [ - discordRoleModel.add({ roleid: groupData[0].roleid, rolename: groupData[0].rolename, createdBy: allIds[1] }), - discordRoleModel.add({ roleid: groupData[1].roleid, rolename: groupData[1].rolename, createdBy: allIds[0] }), + discordRoleModel.add(newGroupData[0]), + discordRoleModel.add(newGroupData[1]), + discordRoleModel.add(newGroupData[2]), ]; await Promise.all(addRolesPromises); const addGroupRolesPromises = [ - addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[0] }), - addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[1] }), - addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[1] }), - addGroupRoleToMember({ roleid: groupData[1].roleid, userid: allIds[0] }), + addGroupRoleToMember({ roleid: newGroupData[0].roleid, userid: userData[0].discordId }), + addGroupRoleToMember({ roleid: newGroupData[0].roleid, userid: userData[1].discordId }), + addGroupRoleToMember({ roleid: newGroupData[1].roleid, userid: userData[0].discordId }), ]; await Promise.all(addGroupRolesPromises); }); @@ -118,7 +124,7 @@ describe("Discord actions", function () { await cleanDb(); }); - it("should successfully return all groups detail", function (done) { + it("should successfully return old groups detail", function (done) { chai .request(app) .get(`/discord-actions/groups`) @@ -131,7 +137,28 @@ describe("Discord actions", function () { expect(res).to.have.status(200); expect(res.body).to.be.an("object"); // Verify presence of specific properties in each group - const expectedProps = ["roleid", "rolename", "memberCount", "firstName", "lastName", "image"]; + const expectedProps = ["roleid", "rolename", "memberCount", "firstName", "lastName", "image", "isMember"]; + res.body.groups.forEach((group) => { + expect(group).not.to.include.all.keys(expectedProps); + }); + expect(res.body.message).to.equal("Roles fetched successfully!"); + return done(); + }); + }); + it("should successfully return new groups detail when flag is set", function (done) { + chai + .request(app) + .get(`/discord-actions/groups?dev=true`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body).to.be.an("object"); + // Verify presence of specific properties in each group + const expectedProps = ["roleid", "rolename", "memberCount", "firstName", "lastName", "image", "isMember"]; res.body.groups.forEach((group) => { expect(group).to.include.all.keys(expectedProps); }); diff --git a/test/unit/models/discordactions.test.js b/test/unit/models/discordactions.test.js index 2405c0a70..67092705d 100644 --- a/test/unit/models/discordactions.test.js +++ b/test/unit/models/discordactions.test.js @@ -5,6 +5,8 @@ const firestore = require("../../../utils/firestore"); const photoVerificationModel = firestore.collection("photo-verification"); const discordRoleModel = firestore.collection("discord-roles"); const memberRoleModel = firestore.collection("member-group-roles"); +const userModel = firestore.collection("users"); +const admin = require("firebase-admin"); const { createNewRole, @@ -12,11 +14,13 @@ const { isGroupRoleExists, addGroupRoleToMember, updateDiscordImageForVerification, - getNumberOfMemberForGroups, + enrichGroupDataWithMembershipInfo, + fetchGroupToUserMapping, } = require("../../../models/discordactions"); const { groupData, roleData, existingRole } = require("../../fixtures/discordactions/discordactions"); const cleanDb = require("../../utils/cleanDb"); const { userPhotoVerificationData } = require("../../fixtures/user/photo-verification"); +const userData = require("../../fixtures/user/user")(); chai.should(); @@ -245,14 +249,34 @@ describe("discordactions", function () { }); }); - describe("getNumberOfMemberForGroups", function () { + describe("enrichGroupDataWithMembershipInfo", function () { + let newGroupData; + let allIds = []; + before(async function () { - await Promise.all([ - addGroupRoleToMember({ roleid: groupData[0].roleid, userid: 1 }), - addGroupRoleToMember({ roleid: groupData[0].roleid, userid: 2 }), - addGroupRoleToMember({ roleid: groupData[0].roleid, userid: 3 }), - addGroupRoleToMember({ roleid: groupData[1].roleid, userid: 1 }), - ]); + const addUsersPromises = userData.map((user) => userModel.add({ ...user })); + const responses = await Promise.all(addUsersPromises); + allIds = responses.map((response) => response.id); + newGroupData = groupData.map((group, index) => { + return { + ...group, + createdBy: allIds[Math.min(index, allIds.length - 1)], + }; + }); + + const addRolesPromises = [ + discordRoleModel.add(newGroupData[0]), + discordRoleModel.add(newGroupData[1]), + discordRoleModel.add(newGroupData[2]), + ]; + await Promise.all(addRolesPromises); + + const addGroupRolesPromises = [ + addGroupRoleToMember({ roleid: newGroupData[0].roleid, userid: userData[0].discordId }), + addGroupRoleToMember({ roleid: newGroupData[0].roleid, userid: userData[1].discordId }), + addGroupRoleToMember({ roleid: newGroupData[1].roleid, userid: userData[0].discordId }), + ]; + await Promise.all(addGroupRolesPromises); }); after(async function () { @@ -260,24 +284,84 @@ describe("discordactions", function () { }); it("should return an empty array if the parameter is an empty array", async function () { - const result = await getNumberOfMemberForGroups([]); + const result = await enrichGroupDataWithMembershipInfo(userData[0].discordId, []); expect(result).to.be.an("array"); expect(result.length).to.equal(0); }); it("should return an empty array if the parameter no parameter is passed", async function () { - const result = await getNumberOfMemberForGroups(); + const result = await enrichGroupDataWithMembershipInfo(); expect(result).to.be.an("array"); expect(result.length).to.equal(0); }); it("should return group details with memberCount details ", async function () { - const result = await getNumberOfMemberForGroups(groupData); - expect(result).to.deep.equal([ - { rolename: groupData[0].rolename, roleid: 1, memberCount: 3 }, - { rolename: groupData[1].rolename, roleid: 2, memberCount: 1 }, - { rolename: groupData[2].rolename, roleid: 3, memberCount: 0 }, - ]); + const result = await enrichGroupDataWithMembershipInfo(userData[0].discordId, newGroupData); + expect(result[0]).to.deep.equal({ + ...newGroupData[0], + memberCount: 2, + firstName: userData[0].first_name, + lastName: userData[0].last_name, + image: userData[0].picture.url, + isMember: true, + }); + + expect(result[1]).to.deep.equal({ + ...newGroupData[1], + memberCount: 1, + firstName: userData[1].first_name, + lastName: userData[1].last_name, + image: userData[1].picture.url, + isMember: true, + }); + + expect(result[2]).to.deep.equal({ + ...newGroupData[2], + memberCount: 0, + firstName: userData[2].first_name, + lastName: userData[2].last_name, + image: userData[2].picture.url, + isMember: false, + }); + }); + }); + + describe("fetchGroupToMemberMapping", function () { + const roleIds = []; + before(async function () { + // Add 50 different roles and user mapping + const addGroupRolesPromises = Array.from({ length: 65 }).map((_, index) => { + const roleId = `role-id-${index}`; + roleIds.push(roleId); + return addGroupRoleToMember({ + roleid: roleId, + userid: index, + date: admin.firestore.Timestamp.fromDate(new Date()), + }); + }); + await Promise.all(addGroupRolesPromises); + }); + + after(async function () { + await cleanDb(); + }); + + it("should return empty array for empty roleId", async function () { + const groupToMemberMappings = await fetchGroupToUserMapping([]); + expect(groupToMemberMappings).to.be.an("array"); + expect(groupToMemberMappings).to.have.lengthOf(0); + }); + + it("should be able to fetch mapping for less than 30 roleIds", async function () { + const groupToMemberMappings = await fetchGroupToUserMapping(roleIds.slice(0, 25)); + expect(groupToMemberMappings).to.be.an("array"); + expect(groupToMemberMappings).to.have.lengthOf(25); + }); + + it("should be able to fetch mapping for more than 30 roleIds", async function () { + const groupToMemberMappings = await fetchGroupToUserMapping(roleIds); + expect(groupToMemberMappings).to.be.an("array"); + expect(groupToMemberMappings).to.have.lengthOf(65); }); }); }); diff --git a/test/unit/services/dataAccessLayer.test.js b/test/unit/services/dataAccessLayer.test.js index 0a15dff9b..c25283aad 100644 --- a/test/unit/services/dataAccessLayer.test.js +++ b/test/unit/services/dataAccessLayer.test.js @@ -137,6 +137,11 @@ describe("Data Access Layer", function () { }); }); }); + + it("should return empty object if array with no userIds are provided", async function () { + const result = await retrieveUsers({ userIds: [] }); + expect(result).to.deep.equal({}); + }); }); describe("retrieveFilteredUsers", function () { diff --git a/test/unit/utils/helper.test.js b/test/unit/utils/helper.test.js index 630010160..83ee8f054 100644 --- a/test/unit/utils/helper.test.js +++ b/test/unit/utils/helper.test.js @@ -1,5 +1,10 @@ const chai = require("chai"); -const { getDateTimeRangeForPRs, getQualifiers, getPaginatedLink } = require("../../../utils/helper"); +const { + getDateTimeRangeForPRs, + getQualifiers, + getPaginatedLink, + findSubscribedGroupIds, +} = require("../../../utils/helper"); const { TASK_STATUS, TASK_SIZE } = require("../../../constants/tasks"); const { expect } = chai; @@ -93,4 +98,14 @@ describe("helper", function () { expect(result).to.not.contain(`next=${nextId}`); }); }); + + describe("findSubscribedGroupIds", function () { + it("should return set of member groupIds", function () { + const memberGroupIds = findSubscribedGroupIds("1234", [ + { userid: "1234", roleid: "1" }, + { userid: "12345", roleid: "3" }, + ]); + expect(memberGroupIds).to.deep.equal(new Set(["1"])); + }); + }); }); diff --git a/utils/helper.js b/utils/helper.js index 5ee6636f9..b0ab267d0 100644 --- a/utils/helper.js +++ b/utils/helper.js @@ -67,8 +67,30 @@ const getPaginatedLink = ({ return paginatedLink; }; +/** + * Finds and returns the set of subscribed group IDs for a given Discord user ID based on group-to-user mappings. + * + * @param {string} discordId - The Discord user ID for which to find subscribed group IDs. + * @param {Array} groupToUserMappings - An array of group-to-user mappings containing user and role information. + * @returns {Set} - A Set containing the group IDs to which the user is subscribed. + */ +function findSubscribedGroupIds(discordId, groupToUserMappings = []) { + // Initialize a Set to store the subscribed group IDs + const subscribedGroupIds = new Set(); + + // Iterate through groupToUserMappings to find subscribed group IDs + groupToUserMappings.forEach((group) => { + if (group.userid === discordId) { + subscribedGroupIds.add(group.roleid); + } + }); + + return subscribedGroupIds; +} + module.exports = { getQualifiers, getDateTimeRangeForPRs, getPaginatedLink, + findSubscribedGroupIds, };