From e0d7dbba462c890f9d3bfbb3158c8726c7bf0c0b Mon Sep 17 00:00:00 2001 From: VinuB-Dev Date: Fri, 15 Nov 2024 15:50:32 +0530 Subject: [PATCH 1/8] Created tasks/orphaned-tasks api - tasks/orphaned-tasks retrieves tasks assigned to departed users Added test cases for the API and fixed test case issues arising from mock data additions used in current API testing. --- controllers/tasks.js | 12 +++++ models/tasks.js | 30 ++++++++++++ routes/tasks.js | 1 + test/fixtures/tasks/tasks.js | 84 ++++++++++++++++++++++++++++++++ test/fixtures/user/user.js | 63 ++++++++++++++++++++++++ test/integration/tasks.test.js | 58 ++++++++++++++++++++++ test/unit/models/tasks.test.js | 8 +-- test/unit/services/tasks.test.js | 4 +- test/unit/services/users.test.js | 4 +- 9 files changed, 256 insertions(+), 8 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index 9e36c639c..41af73f4f 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -532,6 +532,17 @@ const getUsersHandler = async (req, res) => { } }; +const getOrphanedTasks = async (req, res) => { + try { + const data = await tasks.fetchOrphanedTasks(); + + return res.status(200).json({ message: "Orphan tasks fetched successfully", data }); + } catch (error) { + logger.error("Error in getting tasks which were abandoned", error); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + module.exports = { addNewTask, fetchTasks, @@ -545,4 +556,5 @@ module.exports = { updateStatus, getUsersHandler, orphanTasks, + getOrphanedTasks, }; diff --git a/models/tasks.js b/models/tasks.js index 8b0754b1b..f30606247 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -701,6 +701,35 @@ const markUnDoneTasksOfArchivedUsersBacklog = async (users) => { } }; +const fetchOrphanedTasks = async () => { + try { + const COMPLETED_STATUSES = [DONE, COMPLETED]; + const abandonedTasks = []; + + const userSnapshot = await userModel + .where("roles.archived", "==", true) + .where("roles.in_discord", "==", false) + .get(); + + for (const userDoc of userSnapshot.docs) { + const user = userDoc.data(); + const abandonedTasksQuerySnapshot = await tasksModel + .where("assigneeId", "==", user.id || "") + .where("status", "not-in", COMPLETED_STATUSES) + .get(); + + // Check if the user has any tasks with status not in [Done, Complete] + if (!abandonedTasksQuerySnapshot.empty) { + abandonedTasks.push(...abandonedTasksQuerySnapshot.docs.map((doc) => doc.data())); + } + } + return abandonedTasks; + } catch (error) { + logger.error(`Error in getting tasks abandoned by users: ${error}`); + throw error; + } +}; + module.exports = { updateTask, fetchTasks, @@ -720,4 +749,5 @@ module.exports = { updateTaskStatus, updateOrphanTasksStatus, markUnDoneTasksOfArchivedUsersBacklog, + fetchOrphanedTasks, }; diff --git a/routes/tasks.js b/routes/tasks.js index 5596f982c..57095e82c 100644 --- a/routes/tasks.js +++ b/routes/tasks.js @@ -33,6 +33,7 @@ const enableDevModeMiddleware = (req, res, next) => { } }; +router.get("/orphaned-tasks", tasks.getOrphanedTasks); 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); diff --git a/test/fixtures/tasks/tasks.js b/test/fixtures/tasks/tasks.js index 1d5efa978..1768c6cd6 100644 --- a/test/fixtures/tasks/tasks.js +++ b/test/fixtures/tasks/tasks.js @@ -140,5 +140,89 @@ module.exports = () => { createdAt: 1644753600, updatedAt: 1644753600, }, + { + id: "task1_id", + title: "Abandoned Task 1", + type: "feature", + status: "IN_PROGRESS", + priority: "HIGH", + percentCompleted: 50, + createdAt: 1727027666, + updatedAt: 1727027999, + startedOn: 1727027777, + endsOn: 1731542400, + assignee: "archived_user1", + assigneeId: "user1_id", + github: { + issue: { + html_url: "https://github.com/org/repo/issues/1", + url: "https://api.github.com/repos/org/repo/issues/1", + }, + }, + dependsOn: [], + }, + { + id: "task2_id", + title: "Abandoned Task 2", + type: "bug", + status: "BLOCKED", + priority: "MEDIUM", + percentCompleted: 30, + createdAt: 1727027666, + updatedAt: 1727027999, + startedOn: 1727027777, + endsOn: 1731542400, + assignee: "archived_user2", + assigneeId: "user2_id", + github: { + issue: { + html_url: "https://github.com/org/repo/issues/2", + url: "https://api.github.com/repos/org/repo/issues/2", + }, + }, + dependsOn: [], + }, + { + id: "task3_id", + title: "Completed Archived Task", + type: "feature", + status: "DONE", + priority: "LOW", + percentCompleted: 100, + createdAt: 1727027666, + updatedAt: 1727027999, + startedOn: 1727027777, + endsOn: 1731542400, + assignee: "archived_user1", + assigneeId: "user1_id", + github: { + issue: { + html_url: "https://github.com/org/repo/issues/3", + url: "https://api.github.com/repos/org/repo/issues/3", + }, + }, + dependsOn: [], + }, + { + id: "task4_id", + title: "Active User Task", + type: "feature", + status: "IN_PROGRESS", + priority: "HIGH", + percentCompleted: 75, + createdAt: 1727027666, + updatedAt: 1727027999, + startedOn: 1727027777, + endsOn: 1731542400, + assignee: "active_user", + assigneeId: "user3_id", + github: { + issue: { + html_url: "https://github.com/org/repo/issues/4", + url: "https://api.github.com/repos/org/repo/issues/4", + }, + }, + dependsOn: [], + }, ]; }; diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index 485255efc..674718d61 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -442,5 +442,68 @@ module.exports = () => { url: "https://res.cloudinary.com/realdevsquad/image/upload/v1667685133/profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar.jpg", }, }, + { + id: "user1_id", + discordId: "123456789", + github_id: "github_user1", + username: "archived_user1", + first_name: "Archived", + last_name: "User One", + linkedin_id: "archived_user1", + github_display_name: "archived-user-1", + phone: "1234567890", + email: "archived1@test.com", + roles: { + archived: true, + in_discord: false, + }, + discordJoinedAt: "2024-01-01T00:00:00.000Z", + picture: { + publicId: "profile/user1", + url: "https://example.com/user1.jpg", + }, + }, + { + id: "user2_id", + discordId: "987654321", + github_id: "github_user2", + username: "archived_user2", + first_name: "Archived", + last_name: "User Two", + linkedin_id: "archived_user2", + github_display_name: "archived-user-2", + phone: "0987654321", + email: "archived2@test.com", + roles: { + archived: true, + in_discord: false, + }, + discordJoinedAt: "2024-01-02T00:00:00.000Z", + picture: { + publicId: "profile/user2", + url: "https://example.com/user2.jpg", + }, + }, + { + id: "user3_id", + discordId: "555555555", + github_id: "github_user3", + username: "active_user", + first_name: "Active", + last_name: "User", + linkedin_id: "active_user", + github_display_name: "active-user", + phone: "5555555555", + email: "active@test.com", + roles: { + archived: false, + in_discord: true, + }, + discordJoinedAt: "2024-01-03T00:00:00.000Z", + picture: { + publicId: "profile/user3", + url: "https://example.com/user3.jpg", + }, + }, ]; }; diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 6a8442875..e142958bb 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -1633,4 +1633,62 @@ describe("Tasks", function () { }); }); }); + + describe("fetchOrphanedTasks", function () { + beforeEach(async function () { + // Clean the database + await cleanDb(); + + // Add test users to the database + const userPromises = userData.map((user) => userDBModel.add(user)); + await Promise.all(userPromises); + + // Add test tasks to the database + const taskPromises = tasksData.map((task) => tasksModel.add(task)); + await Promise.all(taskPromises); + }); + + afterEach(async function () { + await cleanDb(); + }); + + it("should fetch tasks assigned to archived and non-discord users", async function () { + const abandonedTasks = await tasks.fetchOrphanedTasks(); + + expect(abandonedTasks).to.be.an("array"); + expect(abandonedTasks).to.have.lengthOf(2); // Two tasks abandoned by users + }); + + it("should not include completed or done tasks", async function () { + const abandonedTasks = await tasks.fetchOrphanedTasks(); + + abandonedTasks.forEach((task) => { + expect(task.status).to.not.be.oneOf(["DONE", "COMPLETED"]); + }); + }); + + it("should not include tasks from active users", async function () { + const abandonedTasks = await tasks.fetchOrphanedTasks(); + + abandonedTasks.forEach((task) => { + expect(task.assignee).to.not.equal("active_user"); + }); + }); + + it("should handle case when no users are archived", async function () { + await cleanDb(); + + // Add only active users + const activeUser = userData[11]; // Using the active user from our test data + await userDBModel.add(activeUser); + + // Add a task assigned to the active user + const activeTask = tasksData[11]; // Using the active user's task + await tasksModel.add(activeTask); + + const abandonedTasks = await tasks.fetchOrphanedTasks(); + expect(abandonedTasks).to.be.an("array"); + expect(abandonedTasks).to.have.lengthOf(0); + }); + }); }); diff --git a/test/unit/models/tasks.test.js b/test/unit/models/tasks.test.js index 44a560fd5..db9ccd15e 100644 --- a/test/unit/models/tasks.test.js +++ b/test/unit/models/tasks.test.js @@ -303,12 +303,12 @@ describe("tasks", function () { overdueTask.endsOn = Date.now() / 1000 + 24 * 60 * 60 * 7; await tasks.updateTask(overdueTask); const usersWithOverdueTasks = await tasks.getOverdueTasks(days); - expect(usersWithOverdueTasks.length).to.be.equal(5); + expect(usersWithOverdueTasks.length).to.be.equal(8); }); it("should return all users which have overdue tasks if days is not passed", async function () { const usersWithOverdueTasks = await tasks.getOverdueTasks(); - expect(usersWithOverdueTasks.length).to.be.equal(4); + expect(usersWithOverdueTasks.length).to.be.equal(7); }); }); @@ -332,8 +332,8 @@ describe("tasks", function () { it("Should update task status COMPLETED to DONE", async function () { const res = await tasks.updateTaskStatus(); - expect(res.totalTasks).to.be.equal(8); - expect(res.totalUpdatedStatus).to.be.equal(8); + expect(res.totalTasks).to.be.equal(12); + expect(res.totalUpdatedStatus).to.be.equal(12); }); it("should throw an error if firebase batch operation fails", async function () { diff --git a/test/unit/services/tasks.test.js b/test/unit/services/tasks.test.js index 02d424134..8e675958c 100644 --- a/test/unit/services/tasks.test.js +++ b/test/unit/services/tasks.test.js @@ -46,7 +46,7 @@ describe("Tasks services", function () { const res = await updateTaskStatusToDone(tasks); expect(res).to.deep.equal({ - totalUpdatedStatus: 8, + totalUpdatedStatus: 12, totalOperationsFailed: 0, updatedTaskDetails: taskDetails, failedTaskDetails: [], @@ -66,7 +66,7 @@ describe("Tasks services", function () { expect(res).to.deep.equal({ totalUpdatedStatus: 0, - totalOperationsFailed: 8, + totalOperationsFailed: 12, updatedTaskDetails: [], failedTaskDetails: taskDetails, }); diff --git a/test/unit/services/users.test.js b/test/unit/services/users.test.js index 60cf04c9d..611b1e64c 100644 --- a/test/unit/services/users.test.js +++ b/test/unit/services/users.test.js @@ -55,7 +55,7 @@ describe("Users services", function () { expect(res).to.deep.equal({ message: "Successfully completed batch updates", - totalUsersArchived: 20, + totalUsersArchived: 23, totalOperationsFailed: 0, updatedUserDetails: userDetails, failedUserDetails: [], @@ -76,7 +76,7 @@ describe("Users services", function () { expect(res).to.deep.equal({ message: "Firebase batch operation failed", totalUsersArchived: 0, - totalOperationsFailed: 20, + totalOperationsFailed: 23, updatedUserDetails: [], failedUserDetails: userDetails, }); From 3a1473a56bab848c465f874a9a89f28ad02fce2e Mon Sep 17 00:00:00 2001 From: VinuB-Dev Date: Fri, 15 Nov 2024 22:59:37 +0530 Subject: [PATCH 2/8] - moved the function which returns the required data to service layer and the corresponding queries to model. - removed test cases from integration testing and moved it to unit testing. - moved updateTaskStatusToDone to tasks model as it was causing circular dependency. - created a separate fixture file for mock data required to test the feature. - reverted the test case changes done for other test cases due to mock data changes. --- controllers/tasks.js | 4 +- models/tasks.js | 60 ++++--- models/users.js | 63 ++++++- services/tasks.js | 53 +++--- .../abandoned-tasks/departed-users.js | 154 ++++++++++++++++++ test/fixtures/tasks/tasks.js | 84 ---------- test/fixtures/user/user.js | 63 ------- test/integration/tasks.test.js | 58 ------- test/unit/models/tasks.test.js | 8 +- test/unit/models/users.test.js | 21 +++ test/unit/services/tasks.test.js | 70 +++++++- test/unit/services/users.test.js | 8 +- 12 files changed, 374 insertions(+), 272 deletions(-) create mode 100644 test/fixtures/abandoned-tasks/departed-users.js diff --git a/controllers/tasks.js b/controllers/tasks.js index 41af73f4f..63808a2ab 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -12,7 +12,7 @@ const { getPaginatedLink } = require("../utils/helper"); const { updateUserStatusOnTaskUpdate, updateStatusOnTaskCompletion } = require("../models/userStatus"); const dataAccess = require("../services/dataAccessLayer"); const { parseSearchQuery } = require("../utils/tasks"); -const { addTaskCreatedAtAndUpdatedAtFields } = require("../services/tasks"); +const { addTaskCreatedAtAndUpdatedAtFields, fetchOrphanedTasks } = require("../services/tasks"); const { RQLQueryParser } = require("../utils/RQLParser"); const { getMissedProgressUpdatesUsers } = require("../models/discordactions"); const { logType } = require("../constants/logs"); @@ -534,7 +534,7 @@ const getUsersHandler = async (req, res) => { const getOrphanedTasks = async (req, res) => { try { - const data = await tasks.fetchOrphanedTasks(); + const data = await fetchOrphanedTasks(); return res.status(200).json({ message: "Orphan tasks fetched successfully", data }); } catch (error) { diff --git a/models/tasks.js b/models/tasks.js index f30606247..3e98b7a55 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -4,7 +4,6 @@ const userModel = firestore.collection("users"); const ItemModel = firestore.collection("itemTags"); const dependencyModel = firestore.collection("taskDependencies"); const userUtils = require("../utils/users"); -const { updateTaskStatusToDone } = require("../services/tasks"); const { chunks } = require("../utils/array"); const { DOCUMENT_WRITE_SIZE } = require("../constants/constants"); const { fromFirestoreData, toFirestoreData, buildTasks } = require("../utils/tasks"); @@ -24,6 +23,33 @@ const { const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING, OLD_COMPLETED } = TASK_STATUS_OLD; const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); +const updateTaskStatusToDone = async (tasksData) => { + const batch = firestore.batch(); + const tasksBatch = []; + const summary = { + totalUpdatedStatus: 0, + totalOperationsFailed: 0, + updatedTaskDetails: [], + failedTaskDetails: [], + }; + tasksData.forEach((task) => { + const updateTaskData = { ...task, status: "DONE" }; + batch.update(tasksModel.doc(task.id), updateTaskData); + tasksBatch.push(task.id); + }); + try { + await batch.commit(); + summary.totalUpdatedStatus += tasksData.length; + summary.updatedTaskDetails = [...tasksBatch]; + return { ...summary }; + } catch (err) { + logger.error("Firebase batch Operation Failed!"); + summary.totalOperationsFailed += tasksData.length; + summary.failedTaskDetails = [...tasksBatch]; + return { ...summary }; + } +}; + /** * Adds and Updates tasks * @@ -701,31 +727,16 @@ const markUnDoneTasksOfArchivedUsersBacklog = async (users) => { } }; -const fetchOrphanedTasks = async () => { +const fetchIncompleteTaskForUser = async (user) => { + const COMPLETED_STATUSES = [DONE, COMPLETED]; try { - const COMPLETED_STATUSES = [DONE, COMPLETED]; - const abandonedTasks = []; - - const userSnapshot = await userModel - .where("roles.archived", "==", true) - .where("roles.in_discord", "==", false) + const incompleteTaskForUser = await tasksModel + .where("assigneeId", "==", user.id) + .where("status", "not-in", COMPLETED_STATUSES) .get(); - - for (const userDoc of userSnapshot.docs) { - const user = userDoc.data(); - const abandonedTasksQuerySnapshot = await tasksModel - .where("assigneeId", "==", user.id || "") - .where("status", "not-in", COMPLETED_STATUSES) - .get(); - - // Check if the user has any tasks with status not in [Done, Complete] - if (!abandonedTasksQuerySnapshot.empty) { - abandonedTasks.push(...abandonedTasksQuerySnapshot.docs.map((doc) => doc.data())); - } - } - return abandonedTasks; + return incompleteTaskForUser; } catch (error) { - logger.error(`Error in getting tasks abandoned by users: ${error}`); + logger.error("Error when fetching incomplete tasks:", error); throw error; } }; @@ -749,5 +760,6 @@ module.exports = { updateTaskStatus, updateOrphanTasksStatus, markUnDoneTasksOfArchivedUsersBacklog, - fetchOrphanedTasks, + fetchIncompleteTaskForUser, + updateTaskStatusToDone, }; diff --git a/models/users.js b/models/users.js index e0746de92..1656389dd 100644 --- a/models/users.js +++ b/models/users.js @@ -8,8 +8,12 @@ const firestore = require("../utils/firestore"); const { fetchWallet, createWallet } = require("../models/wallets"); const { updateUserStatus } = require("../models/userStatus"); const { arraysHaveCommonItem, chunks } = require("../utils/array"); -const { archiveUsers } = require("../services/users"); -const { ALLOWED_FILTER_PARAMS, FIRESTORE_IN_CLAUSE_SIZE } = require("../constants/users"); +const { + ALLOWED_FILTER_PARAMS, + FIRESTORE_IN_CLAUSE_SIZE, + USERS_PATCH_HANDLER_SUCCESS_MESSAGES, + USERS_PATCH_HANDLER_ERROR_MESSAGES, +} = require("../constants/users"); const { DOCUMENT_WRITE_SIZE } = require("../constants/constants"); const { userState } = require("../constants/userStatus"); const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); @@ -27,6 +31,46 @@ const { formatUsername } = require("../utils/username"); const { logType } = require("../constants/logs"); const { addLog } = require("../services/logService"); +const archiveUsers = async (usersData) => { + const batch = firestore.batch(); + const usersBatch = []; + const summary = { + totalUsersArchived: 0, + totalOperationsFailed: 0, + updatedUserDetails: [], + failedUserDetails: [], + }; + + usersData.forEach((user) => { + const { id, first_name: firstName, last_name: lastName } = user; + const updatedUserData = { + ...user, + roles: { + ...user.roles, + archived: true, + }, + updated_at: Date.now(), + }; + batch.update(userModel.doc(id), updatedUserData); + usersBatch.push({ id, firstName, lastName }); + }); + + try { + await batch.commit(); + summary.totalUsersArchived += usersData.length; + 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; + summary.failedUserDetails = [...usersBatch]; + return { message: USERS_PATCH_HANDLER_ERROR_MESSAGES.ARCHIVE_USERS.BATCH_DATA_UPDATED_FAILED, ...summary }; + } +}; + /** * Adds or updates the user data * @@ -1030,7 +1074,21 @@ const updateUsersWithNewUsernames = async () => { } }; +const fetchUsersNotInDiscordServer = async () => { + try { + const usersNotInDiscordServer = await userModel + .where("roles.archived", "==", true) + .where("roles.in_discord", "==", false) + .get(); + return usersNotInDiscordServer; + } catch (error) { + logger.error(`Error in getting users who are not in discord server: ${error}`); + throw error; + } +}; + module.exports = { + archiveUsers, addOrUpdate, fetchPaginatedUsers, fetchUser, @@ -1059,4 +1117,5 @@ module.exports = { fetchUserForKeyValue, getNonNickNameSyncedUsers, updateUsersWithNewUsernames, + fetchUsersNotInDiscordServer, }; diff --git a/services/tasks.js b/services/tasks.js index 6e1a5dfe7..fe5085100 100644 --- a/services/tasks.js +++ b/services/tasks.js @@ -2,33 +2,8 @@ const firestore = require("../utils/firestore"); const tasksModel = firestore.collection("tasks"); const { chunks } = require("../utils/array"); const { DOCUMENT_WRITE_SIZE: FIRESTORE_BATCH_OPERATIONS_LIMIT } = require("../constants/constants"); - -const updateTaskStatusToDone = async (tasksData) => { - const batch = firestore.batch(); - const tasksBatch = []; - const summary = { - totalUpdatedStatus: 0, - totalOperationsFailed: 0, - updatedTaskDetails: [], - failedTaskDetails: [], - }; - tasksData.forEach((task) => { - const updateTaskData = { ...task, status: "DONE" }; - batch.update(tasksModel.doc(task.id), updateTaskData); - tasksBatch.push(task.id); - }); - try { - await batch.commit(); - summary.totalUpdatedStatus += tasksData.length; - summary.updatedTaskDetails = [...tasksBatch]; - return { ...summary }; - } catch (err) { - logger.error("Firebase batch Operation Failed!"); - summary.totalOperationsFailed += tasksData.length; - summary.failedTaskDetails = [...tasksBatch]; - return { ...summary }; - } -}; +const { fetchUsersNotInDiscordServer } = require("../models/users"); +const { fetchIncompleteTaskForUser } = require("../models/tasks"); const addTaskCreatedAtAndUpdatedAtFields = async () => { const operationStats = { @@ -83,7 +58,29 @@ const addTaskCreatedAtAndUpdatedAtFields = async () => { return operationStats; }; +const fetchOrphanedTasks = async () => { + try { + const abandonedTasks = []; + + const userSnapshot = await fetchUsersNotInDiscordServer(); + + for (const userDoc of userSnapshot.docs) { + const user = userDoc.data(); + const abandonedTasksQuerySnapshot = await fetchIncompleteTaskForUser(user); + + // Check if the user has any tasks with status not in [Done, Complete] + if (!abandonedTasksQuerySnapshot.empty) { + abandonedTasks.push(...abandonedTasksQuerySnapshot.docs.map((doc) => doc.data())); + } + } + return abandonedTasks; + } catch (error) { + logger.error(`Error in getting tasks abandoned by users: ${error}`); + throw error; + } +}; + module.exports = { - updateTaskStatusToDone, addTaskCreatedAtAndUpdatedAtFields, + fetchOrphanedTasks, }; diff --git a/test/fixtures/abandoned-tasks/departed-users.js b/test/fixtures/abandoned-tasks/departed-users.js new file mode 100644 index 000000000..d16ca551b --- /dev/null +++ b/test/fixtures/abandoned-tasks/departed-users.js @@ -0,0 +1,154 @@ +const usersData = [ + { + id: "user1_id", + discordId: "123456789", + github_id: "github_user1", + username: "archived_user1", + first_name: "Archived", + last_name: "User One", + linkedin_id: "archived_user1", + github_display_name: "archived-user-1", + phone: "1234567890", + email: "archived1@test.com", + roles: { + archived: true, + in_discord: false, + }, + discordJoinedAt: "2024-01-01T00:00:00.000Z", + picture: { + publicId: "profile/user1", + url: "https://example.com/user1.jpg", + }, + }, + { + id: "user2_id", + discordId: "987654321", + github_id: "github_user2", + username: "archived_user2", + first_name: "Archived", + last_name: "User Two", + linkedin_id: "archived_user2", + github_display_name: "archived-user-2", + phone: "0987654321", + email: "archived2@test.com", + roles: { + archived: true, + in_discord: false, + }, + discordJoinedAt: "2024-01-02T00:00:00.000Z", + picture: { + publicId: "profile/user2", + url: "https://example.com/user2.jpg", + }, + }, + { + id: "user3_id", + discordId: "555555555", + github_id: "github_user3", + username: "active_user", + first_name: "Active", + last_name: "User", + linkedin_id: "active_user", + github_display_name: "active-user", + phone: "5555555555", + email: "active@test.com", + roles: { + archived: false, + in_discord: true, + }, + discordJoinedAt: "2024-01-03T00:00:00.000Z", + picture: { + publicId: "profile/user3", + url: "https://example.com/user3.jpg", + }, + }, +]; + +const tasksData = [ + { + id: "task1_id", + title: "Abandoned Task 1", + type: "feature", + status: "IN_PROGRESS", + priority: "HIGH", + percentCompleted: 50, + createdAt: 1727027666, + updatedAt: 1727027999, + startedOn: 1727027777, + endsOn: 1731542400, + assignee: "archived_user1", + assigneeId: "user1_id", + github: { + issue: { + html_url: "https://github.com/org/repo/issues/1", + url: "https://api.github.com/repos/org/repo/issues/1", + }, + }, + dependsOn: [], + }, + { + id: "task2_id", + title: "Abandoned Task 2", + type: "bug", + status: "BLOCKED", + priority: "MEDIUM", + percentCompleted: 30, + createdAt: 1727027666, + updatedAt: 1727027999, + startedOn: 1727027777, + endsOn: 1731542400, + assignee: "archived_user2", + assigneeId: "user2_id", + github: { + issue: { + html_url: "https://github.com/org/repo/issues/2", + url: "https://api.github.com/repos/org/repo/issues/2", + }, + }, + dependsOn: [], + }, + { + id: "task3_id", + title: "Completed Archived Task", + type: "feature", + status: "DONE", + priority: "LOW", + percentCompleted: 100, + createdAt: 1727027666, + updatedAt: 1727027999, + startedOn: 1727027777, + endsOn: 1731542400, + assignee: "archived_user1", + assigneeId: "user1_id", + github: { + issue: { + html_url: "https://github.com/org/repo/issues/3", + url: "https://api.github.com/repos/org/repo/issues/3", + }, + }, + dependsOn: [], + }, + { + id: "task4_id", + title: "Active User Task", + type: "feature", + status: "IN_PROGRESS", + priority: "HIGH", + percentCompleted: 75, + createdAt: 1727027666, + updatedAt: 1727027999, + startedOn: 1727027777, + endsOn: 1731542400, + assignee: "active_user", + assigneeId: "user3_id", + github: { + issue: { + html_url: "https://github.com/org/repo/issues/4", + url: "https://api.github.com/repos/org/repo/issues/4", + }, + }, + dependsOn: [], + }, +]; + +module.exports = { usersData, tasksData }; diff --git a/test/fixtures/tasks/tasks.js b/test/fixtures/tasks/tasks.js index 1768c6cd6..1d5efa978 100644 --- a/test/fixtures/tasks/tasks.js +++ b/test/fixtures/tasks/tasks.js @@ -140,89 +140,5 @@ module.exports = () => { createdAt: 1644753600, updatedAt: 1644753600, }, - { - id: "task1_id", - title: "Abandoned Task 1", - type: "feature", - status: "IN_PROGRESS", - priority: "HIGH", - percentCompleted: 50, - createdAt: 1727027666, - updatedAt: 1727027999, - startedOn: 1727027777, - endsOn: 1731542400, - assignee: "archived_user1", - assigneeId: "user1_id", - github: { - issue: { - html_url: "https://github.com/org/repo/issues/1", - url: "https://api.github.com/repos/org/repo/issues/1", - }, - }, - dependsOn: [], - }, - { - id: "task2_id", - title: "Abandoned Task 2", - type: "bug", - status: "BLOCKED", - priority: "MEDIUM", - percentCompleted: 30, - createdAt: 1727027666, - updatedAt: 1727027999, - startedOn: 1727027777, - endsOn: 1731542400, - assignee: "archived_user2", - assigneeId: "user2_id", - github: { - issue: { - html_url: "https://github.com/org/repo/issues/2", - url: "https://api.github.com/repos/org/repo/issues/2", - }, - }, - dependsOn: [], - }, - { - id: "task3_id", - title: "Completed Archived Task", - type: "feature", - status: "DONE", - priority: "LOW", - percentCompleted: 100, - createdAt: 1727027666, - updatedAt: 1727027999, - startedOn: 1727027777, - endsOn: 1731542400, - assignee: "archived_user1", - assigneeId: "user1_id", - github: { - issue: { - html_url: "https://github.com/org/repo/issues/3", - url: "https://api.github.com/repos/org/repo/issues/3", - }, - }, - dependsOn: [], - }, - { - id: "task4_id", - title: "Active User Task", - type: "feature", - status: "IN_PROGRESS", - priority: "HIGH", - percentCompleted: 75, - createdAt: 1727027666, - updatedAt: 1727027999, - startedOn: 1727027777, - endsOn: 1731542400, - assignee: "active_user", - assigneeId: "user3_id", - github: { - issue: { - html_url: "https://github.com/org/repo/issues/4", - url: "https://api.github.com/repos/org/repo/issues/4", - }, - }, - dependsOn: [], - }, ]; }; diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index 674718d61..485255efc 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -442,68 +442,5 @@ module.exports = () => { url: "https://res.cloudinary.com/realdevsquad/image/upload/v1667685133/profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar.jpg", }, }, - { - id: "user1_id", - discordId: "123456789", - github_id: "github_user1", - username: "archived_user1", - first_name: "Archived", - last_name: "User One", - linkedin_id: "archived_user1", - github_display_name: "archived-user-1", - phone: "1234567890", - email: "archived1@test.com", - roles: { - archived: true, - in_discord: false, - }, - discordJoinedAt: "2024-01-01T00:00:00.000Z", - picture: { - publicId: "profile/user1", - url: "https://example.com/user1.jpg", - }, - }, - { - id: "user2_id", - discordId: "987654321", - github_id: "github_user2", - username: "archived_user2", - first_name: "Archived", - last_name: "User Two", - linkedin_id: "archived_user2", - github_display_name: "archived-user-2", - phone: "0987654321", - email: "archived2@test.com", - roles: { - archived: true, - in_discord: false, - }, - discordJoinedAt: "2024-01-02T00:00:00.000Z", - picture: { - publicId: "profile/user2", - url: "https://example.com/user2.jpg", - }, - }, - { - id: "user3_id", - discordId: "555555555", - github_id: "github_user3", - username: "active_user", - first_name: "Active", - last_name: "User", - linkedin_id: "active_user", - github_display_name: "active-user", - phone: "5555555555", - email: "active@test.com", - roles: { - archived: false, - in_discord: true, - }, - discordJoinedAt: "2024-01-03T00:00:00.000Z", - picture: { - publicId: "profile/user3", - url: "https://example.com/user3.jpg", - }, - }, ]; }; diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index e142958bb..6a8442875 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -1633,62 +1633,4 @@ describe("Tasks", function () { }); }); }); - - describe("fetchOrphanedTasks", function () { - beforeEach(async function () { - // Clean the database - await cleanDb(); - - // Add test users to the database - const userPromises = userData.map((user) => userDBModel.add(user)); - await Promise.all(userPromises); - - // Add test tasks to the database - const taskPromises = tasksData.map((task) => tasksModel.add(task)); - await Promise.all(taskPromises); - }); - - afterEach(async function () { - await cleanDb(); - }); - - it("should fetch tasks assigned to archived and non-discord users", async function () { - const abandonedTasks = await tasks.fetchOrphanedTasks(); - - expect(abandonedTasks).to.be.an("array"); - expect(abandonedTasks).to.have.lengthOf(2); // Two tasks abandoned by users - }); - - it("should not include completed or done tasks", async function () { - const abandonedTasks = await tasks.fetchOrphanedTasks(); - - abandonedTasks.forEach((task) => { - expect(task.status).to.not.be.oneOf(["DONE", "COMPLETED"]); - }); - }); - - it("should not include tasks from active users", async function () { - const abandonedTasks = await tasks.fetchOrphanedTasks(); - - abandonedTasks.forEach((task) => { - expect(task.assignee).to.not.equal("active_user"); - }); - }); - - it("should handle case when no users are archived", async function () { - await cleanDb(); - - // Add only active users - const activeUser = userData[11]; // Using the active user from our test data - await userDBModel.add(activeUser); - - // Add a task assigned to the active user - const activeTask = tasksData[11]; // Using the active user's task - await tasksModel.add(activeTask); - - const abandonedTasks = await tasks.fetchOrphanedTasks(); - expect(abandonedTasks).to.be.an("array"); - expect(abandonedTasks).to.have.lengthOf(0); - }); - }); }); diff --git a/test/unit/models/tasks.test.js b/test/unit/models/tasks.test.js index db9ccd15e..44a560fd5 100644 --- a/test/unit/models/tasks.test.js +++ b/test/unit/models/tasks.test.js @@ -303,12 +303,12 @@ describe("tasks", function () { overdueTask.endsOn = Date.now() / 1000 + 24 * 60 * 60 * 7; await tasks.updateTask(overdueTask); const usersWithOverdueTasks = await tasks.getOverdueTasks(days); - expect(usersWithOverdueTasks.length).to.be.equal(8); + expect(usersWithOverdueTasks.length).to.be.equal(5); }); it("should return all users which have overdue tasks if days is not passed", async function () { const usersWithOverdueTasks = await tasks.getOverdueTasks(); - expect(usersWithOverdueTasks.length).to.be.equal(7); + expect(usersWithOverdueTasks.length).to.be.equal(4); }); }); @@ -332,8 +332,8 @@ describe("tasks", function () { it("Should update task status COMPLETED to DONE", async function () { const res = await tasks.updateTaskStatus(); - expect(res.totalTasks).to.be.equal(12); - expect(res.totalUpdatedStatus).to.be.equal(12); + expect(res.totalTasks).to.be.equal(8); + expect(res.totalUpdatedStatus).to.be.equal(8); }); it("should throw an error if firebase batch operation fails", async function () { diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 42165094f..32165ecea 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -21,6 +21,7 @@ const photoVerificationModel = firestore.collection("photo-verification"); const userData = require("../../fixtures/user/user"); const addUser = require("../../utils/addUser"); const { userState } = require("../../../constants/userStatus"); +const { usersData: abandonedUsersData } = require("../../fixtures/abandoned-tasks/departed-users"); /** * Test the model functions and validate the data stored */ @@ -527,4 +528,24 @@ describe("users", function () { expect(userDoc.user.roles.super_user).to.be.equal(false); }); }); + + describe("fetchUsersNotInDiscordServer", function () { + beforeEach(async function () { + // Clean the database + await cleanDb(); + + // Add test users to the database + const taskPromises = abandonedUsersData.map((task) => userModel.add(task)); + await Promise.all(taskPromises); + }); + + afterEach(async function () { + await cleanDb(); + }); + + it("should fetch users not in discord server", async function () { + const usersNotInDiscordServer = await users.fetchUsersNotInDiscordServer(); + expect(usersNotInDiscordServer.docs.length).to.be.equal(2); + }); + }); }); diff --git a/test/unit/services/tasks.test.js b/test/unit/services/tasks.test.js index 8e675958c..06506e031 100644 --- a/test/unit/services/tasks.test.js +++ b/test/unit/services/tasks.test.js @@ -3,9 +3,15 @@ const { expect } = require("chai"); const firestore = require("../../../utils/firestore"); const tasksModel = firestore.collection("tasks"); +const userModel = firestore.collection("users"); const cleanDb = require("../../utils/cleanDb"); const taskDataArray = require("../../fixtures/tasks/tasks")(); -const { updateTaskStatusToDone } = require("../../../services/tasks"); +const { fetchOrphanedTasks } = require("../../../services/tasks"); +const { + usersData: abandonedUsersData, + tasksData: abandonedTasksData, +} = require("../../fixtures/abandoned-tasks/departed-users"); +const { updateTaskStatusToDone } = require("../../../models/tasks"); describe("Tasks services", function () { describe("task status COMPLETED to DONE in bulk", function () { @@ -46,7 +52,7 @@ describe("Tasks services", function () { const res = await updateTaskStatusToDone(tasks); expect(res).to.deep.equal({ - totalUpdatedStatus: 12, + totalUpdatedStatus: 8, totalOperationsFailed: 0, updatedTaskDetails: taskDetails, failedTaskDetails: [], @@ -66,10 +72,68 @@ describe("Tasks services", function () { expect(res).to.deep.equal({ totalUpdatedStatus: 0, - totalOperationsFailed: 12, + totalOperationsFailed: 8, updatedTaskDetails: [], failedTaskDetails: taskDetails, }); }); }); + + describe("fetchOrphanedTasks", function () { + beforeEach(async function () { + // Clean the database + await cleanDb(); + + // Add test users to the database + const userPromises = abandonedUsersData.map((user) => userModel.add(user)); + await Promise.all(userPromises); + + // Add test tasks to the database + const taskPromises = abandonedTasksData.map((task) => tasksModel.add(task)); + await Promise.all(taskPromises); + }); + + afterEach(async function () { + await cleanDb(); + }); + + it("should fetch tasks assigned to archived and non-discord users", async function () { + const abandonedTasks = await fetchOrphanedTasks(); + + expect(abandonedTasks).to.be.an("array"); + expect(abandonedTasks).to.have.lengthOf(2); // Two tasks abandoned by users + }); + + it("should not include completed or done tasks", async function () { + const abandonedTasks = await fetchOrphanedTasks(); + + abandonedTasks.forEach((task) => { + expect(task.status).to.not.be.oneOf(["DONE", "COMPLETED"]); + }); + }); + + it("should not include tasks from active users", async function () { + const abandonedTasks = await fetchOrphanedTasks(); + + abandonedTasks.forEach((task) => { + expect(task.assignee).to.not.equal("active_user"); + }); + }); + + it("should handle case when no users are archived", async function () { + await cleanDb(); + + // Add only active users + const activeUser = abandonedUsersData[2]; // Using the active user from our test data + await userModel.add(activeUser); + + // Add a task assigned to the active user + const activeTask = abandonedTasksData[3]; // Using the active user's task + await tasksModel.add(activeTask); + + const abandonedTasks = await fetchOrphanedTasks(); + expect(abandonedTasks).to.be.an("array"); + expect(abandonedTasks).to.have.lengthOf(0); + }); + }); }); diff --git a/test/unit/services/users.test.js b/test/unit/services/users.test.js index 611b1e64c..58095a116 100644 --- a/test/unit/services/users.test.js +++ b/test/unit/services/users.test.js @@ -5,8 +5,8 @@ const firestore = require("../../../utils/firestore"); const userModel = firestore.collection("users"); const cleanDb = require("../../utils/cleanDb"); const userDataArray = require("../../fixtures/user/user")(); -const { archiveUsers, generateUniqueUsername } = require("../../../services/users"); -const { addOrUpdate } = require("../../../models/users"); +const { generateUniqueUsername } = require("../../../services/users"); +const { addOrUpdate, archiveUsers } = require("../../../models/users"); describe("Users services", function () { describe("archive inactive discord users in bulk", function () { @@ -55,7 +55,7 @@ describe("Users services", function () { expect(res).to.deep.equal({ message: "Successfully completed batch updates", - totalUsersArchived: 23, + totalUsersArchived: 20, totalOperationsFailed: 0, updatedUserDetails: userDetails, failedUserDetails: [], @@ -76,7 +76,7 @@ describe("Users services", function () { expect(res).to.deep.equal({ message: "Firebase batch operation failed", totalUsersArchived: 0, - totalOperationsFailed: 23, + totalOperationsFailed: 20, updatedUserDetails: [], failedUserDetails: userDetails, }); From b4415d964a7c74620f111f6ef85c6f3df494ea07 Mon Sep 17 00:00:00 2001 From: VinuB-Dev Date: Fri, 15 Nov 2024 23:06:58 +0530 Subject: [PATCH 3/8] Fix: Reverted changes which occured due to departed-users api. --- models/users.js | 49 ++------------------------------ test/unit/services/users.test.js | 4 +-- 2 files changed, 4 insertions(+), 49 deletions(-) diff --git a/models/users.js b/models/users.js index 1656389dd..0d1f9af32 100644 --- a/models/users.js +++ b/models/users.js @@ -8,12 +8,8 @@ const firestore = require("../utils/firestore"); const { fetchWallet, createWallet } = require("../models/wallets"); const { updateUserStatus } = require("../models/userStatus"); const { arraysHaveCommonItem, chunks } = require("../utils/array"); -const { - ALLOWED_FILTER_PARAMS, - FIRESTORE_IN_CLAUSE_SIZE, - USERS_PATCH_HANDLER_SUCCESS_MESSAGES, - USERS_PATCH_HANDLER_ERROR_MESSAGES, -} = require("../constants/users"); +const { archiveUsers } = require("../services/users"); +const { ALLOWED_FILTER_PARAMS, FIRESTORE_IN_CLAUSE_SIZE } = require("../constants/users"); const { DOCUMENT_WRITE_SIZE } = require("../constants/constants"); const { userState } = require("../constants/userStatus"); const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); @@ -31,46 +27,6 @@ const { formatUsername } = require("../utils/username"); const { logType } = require("../constants/logs"); const { addLog } = require("../services/logService"); -const archiveUsers = async (usersData) => { - const batch = firestore.batch(); - const usersBatch = []; - const summary = { - totalUsersArchived: 0, - totalOperationsFailed: 0, - updatedUserDetails: [], - failedUserDetails: [], - }; - - usersData.forEach((user) => { - const { id, first_name: firstName, last_name: lastName } = user; - const updatedUserData = { - ...user, - roles: { - ...user.roles, - archived: true, - }, - updated_at: Date.now(), - }; - batch.update(userModel.doc(id), updatedUserData); - usersBatch.push({ id, firstName, lastName }); - }); - - try { - await batch.commit(); - summary.totalUsersArchived += usersData.length; - 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; - summary.failedUserDetails = [...usersBatch]; - return { message: USERS_PATCH_HANDLER_ERROR_MESSAGES.ARCHIVE_USERS.BATCH_DATA_UPDATED_FAILED, ...summary }; - } -}; - /** * Adds or updates the user data * @@ -1088,7 +1044,6 @@ const fetchUsersNotInDiscordServer = async () => { }; module.exports = { - archiveUsers, addOrUpdate, fetchPaginatedUsers, fetchUser, diff --git a/test/unit/services/users.test.js b/test/unit/services/users.test.js index 58095a116..60cf04c9d 100644 --- a/test/unit/services/users.test.js +++ b/test/unit/services/users.test.js @@ -5,8 +5,8 @@ const firestore = require("../../../utils/firestore"); const userModel = firestore.collection("users"); const cleanDb = require("../../utils/cleanDb"); const userDataArray = require("../../fixtures/user/user")(); -const { generateUniqueUsername } = require("../../../services/users"); -const { addOrUpdate, archiveUsers } = require("../../../models/users"); +const { archiveUsers, generateUniqueUsername } = require("../../../services/users"); +const { addOrUpdate } = require("../../../models/users"); describe("Users services", function () { describe("archive inactive discord users in bulk", function () { From 556cac89f9c6d2c6482e6e64106d2001a5398fe2 Mon Sep 17 00:00:00 2001 From: VinuB-Dev Date: Sat, 16 Nov 2024 10:12:26 +0530 Subject: [PATCH 4/8] Chore: Feedback changes - Added function documentation for all the modified and new model functions. - Implemented a negative unit test case for the `fetchUsersNotInDiscordServer` function. - Updated `fetchUsersNotInDiscordServer` to use `userId` as a parameter instead of the entire `user` object. - Added test cases for recent model changes. --- models/tasks.js | 19 ++++++++++++-- models/users.js | 5 ++++ services/tasks.js | 2 +- test/unit/models/tasks.test.js | 43 ++++++++++++++++++++++++++++++++ test/unit/models/users.test.js | 24 ++++++++++++++++-- test/unit/services/tasks.test.js | 11 +++----- 6 files changed, 91 insertions(+), 13 deletions(-) diff --git a/models/tasks.js b/models/tasks.js index 3e98b7a55..8a21bf630 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -23,6 +23,15 @@ const { const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING, OLD_COMPLETED } = TASK_STATUS_OLD; const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); +/** + * Update multiple tasks' status to DONE in one batch operation. + * @param {Object[]} tasksData - Tasks data to update, must contain 'id' and 'status' fields. + * @returns {Object} - Summary of the batch operation. + * @property {number} totalUpdatedStatus - Number of tasks that has their status updated to DONE. + * @property {number} totalOperationsFailed - Number of tasks that failed to update. + * @property {string[]} updatedTaskDetails - IDs of tasks that has their status updated to DONE. + * @property {string[]} failedTaskDetails - IDs of tasks that failed to update. + */ const updateTaskStatusToDone = async (tasksData) => { const batch = firestore.batch(); const tasksBatch = []; @@ -727,11 +736,17 @@ const markUnDoneTasksOfArchivedUsersBacklog = async (users) => { } }; -const fetchIncompleteTaskForUser = async (user) => { +/** + * Fetch incomplete tasks assigned to a specific user + * @param {string} userId - The unique identifier for the user. + * @returns {Promise} - A promise that resolves to an array of incomplete tasks for the given user. + * @throws {Error} - Throws an error if the database query fails. + */ +const fetchIncompleteTaskForUser = async (userId) => { const COMPLETED_STATUSES = [DONE, COMPLETED]; try { const incompleteTaskForUser = await tasksModel - .where("assigneeId", "==", user.id) + .where("assigneeId", "==", userId) .where("status", "not-in", COMPLETED_STATUSES) .get(); return incompleteTaskForUser; diff --git a/models/users.js b/models/users.js index 0d1f9af32..baa11144a 100644 --- a/models/users.js +++ b/models/users.js @@ -1030,6 +1030,11 @@ const updateUsersWithNewUsernames = async () => { } }; +/** + * Fetches users who are not in the Discord server. + * @returns {Promise} - A promise that resolves to a Firestore QuerySnapshot containing the users matching the criteria. + * @throws {Error} - Throws an error if the database query fails. + */ const fetchUsersNotInDiscordServer = async () => { try { const usersNotInDiscordServer = await userModel diff --git a/services/tasks.js b/services/tasks.js index fe5085100..b2d5047f5 100644 --- a/services/tasks.js +++ b/services/tasks.js @@ -66,7 +66,7 @@ const fetchOrphanedTasks = async () => { for (const userDoc of userSnapshot.docs) { const user = userDoc.data(); - const abandonedTasksQuerySnapshot = await fetchIncompleteTaskForUser(user); + const abandonedTasksQuerySnapshot = await fetchIncompleteTaskForUser(user.id); // Check if the user has any tasks with status not in [Done, Complete] if (!abandonedTasksQuerySnapshot.empty) { diff --git a/test/unit/models/tasks.test.js b/test/unit/models/tasks.test.js index 44a560fd5..77eaa179e 100644 --- a/test/unit/models/tasks.test.js +++ b/test/unit/models/tasks.test.js @@ -17,6 +17,10 @@ const dependencyModel = firestore.collection("TaskDependencies"); const tasksModel = firestore.collection("tasks"); const userData = require("../../fixtures/user/user"); const addUser = require("../../utils/addUser"); +const { + usersData: abandonedUsersData, + tasksData: abandonedTasksData, +} = require("../../fixtures/abandoned-tasks/departed-users"); describe("tasks", function () { afterEach(async function () { @@ -352,4 +356,43 @@ describe("tasks", function () { } }); }); + + describe("fetchIncompleteTaskForUser", function () { + beforeEach(async function () { + await cleanDb(); + + const taskPromises = abandonedTasksData.map((task) => tasksModel.add(task)); + await Promise.all(taskPromises); + }); + + afterEach(async function () { + await cleanDb(); + sinon.restore(); + }); + + it("should fetch tasks which are incomplete for the given user", async function () { + const inactiveUser = abandonedUsersData[0]; + const incompleteTasks = await tasks.fetchIncompleteTaskForUser(inactiveUser.id); + expect(incompleteTasks.docs.length).to.be.equal(1); + }); + + it("should return an empty array if there are no tasks incomplete for the user", async function () { + await cleanDb(); + + const activeUser = abandonedUsersData[2]; + const incompleteTasks = await tasks.fetchIncompleteTaskForUser(activeUser.id); + expect(incompleteTasks.docs.length).to.be.equal(0); + }); + + it("should handle errors gracefully if the database query fails", async function () { + sinon.stub(tasks, "fetchIncompleteTaskForUser").throws(new Error("Database query failed")); + + try { + await tasks.fetchIncompleteTaskForUser(); + expect.fail("Expected function to throw an error"); + } catch (error) { + expect(error.message).to.equal("Database query failed"); + } + }); + }); }); diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 32165ecea..c3dd691ac 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -531,21 +531,41 @@ describe("users", function () { describe("fetchUsersNotInDiscordServer", function () { beforeEach(async function () { - // Clean the database await cleanDb(); - // Add test users to the database const taskPromises = abandonedUsersData.map((task) => userModel.add(task)); await Promise.all(taskPromises); }); afterEach(async function () { await cleanDb(); + sinon.restore(); }); it("should fetch users not in discord server", async function () { const usersNotInDiscordServer = await users.fetchUsersNotInDiscordServer(); expect(usersNotInDiscordServer.docs.length).to.be.equal(2); }); + + it("should return an empty array if there are no users in the database", async function () { + await cleanDb(); + + const activeUser = abandonedUsersData[2]; + await userModel.add(activeUser); + + const usersNotInDiscordServer = await users.fetchUsersNotInDiscordServer(); + expect(usersNotInDiscordServer.docs.length).to.be.equal(0); + }); + + it("should handle errors gracefully if the database query fails", async function () { + sinon.stub(users, "fetchUsersNotInDiscordServer").throws(new Error("Database query failed")); + + try { + await users.fetchUsersNotInDiscordServer(); + expect.fail("Expected function to throw an error"); + } catch (error) { + expect(error.message).to.equal("Database query failed"); + } + }); }); }); diff --git a/test/unit/services/tasks.test.js b/test/unit/services/tasks.test.js index 06506e031..e3d0e14f3 100644 --- a/test/unit/services/tasks.test.js +++ b/test/unit/services/tasks.test.js @@ -81,14 +81,11 @@ describe("Tasks services", function () { describe("fetchOrphanedTasks", function () { beforeEach(async function () { - // Clean the database await cleanDb(); - // Add test users to the database const userPromises = abandonedUsersData.map((user) => userModel.add(user)); await Promise.all(userPromises); - // Add test tasks to the database const taskPromises = abandonedTasksData.map((task) => tasksModel.add(task)); await Promise.all(taskPromises); }); @@ -101,7 +98,7 @@ describe("Tasks services", function () { const abandonedTasks = await fetchOrphanedTasks(); expect(abandonedTasks).to.be.an("array"); - expect(abandonedTasks).to.have.lengthOf(2); // Two tasks abandoned by users + expect(abandonedTasks).to.have.lengthOf(2); }); it("should not include completed or done tasks", async function () { @@ -123,12 +120,10 @@ describe("Tasks services", function () { it("should handle case when no users are archived", async function () { await cleanDb(); - // Add only active users - const activeUser = abandonedUsersData[2]; // Using the active user from our test data + const activeUser = abandonedUsersData[2]; await userModel.add(activeUser); - // Add a task assigned to the active user - const activeTask = abandonedTasksData[3]; // Using the active user's task + const activeTask = abandonedTasksData[3]; await tasksModel.add(activeTask); const abandonedTasks = await fetchOrphanedTasks(); From 47de0b137eee73554c20b10d6a9fbeb461b0e5e0 Mon Sep 17 00:00:00 2001 From: VinuB-Dev Date: Sat, 16 Nov 2024 19:39:34 +0530 Subject: [PATCH 5/8] Chore: Added test cases for controller. --- services/tasks.js | 1 - test/integration/tasks.test.js | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/services/tasks.js b/services/tasks.js index b2d5047f5..4e090ce70 100644 --- a/services/tasks.js +++ b/services/tasks.js @@ -68,7 +68,6 @@ const fetchOrphanedTasks = async () => { const user = userDoc.data(); const abandonedTasksQuerySnapshot = await fetchIncompleteTaskForUser(user.id); - // Check if the user has any tasks with status not in [Done, Complete] if (!abandonedTasksQuerySnapshot.empty) { abandonedTasks.push(...abandonedTasksQuerySnapshot.docs.map((doc) => doc.data())); } diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 6a8442875..56472f2bf 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -37,6 +37,10 @@ const { stubbedModelTaskProgressData } = require("../fixtures/progress/progresse const { convertDaysToMilliseconds } = require("../../utils/time"); const { getDiscordMembers } = require("../fixtures/discordResponse/discord-response"); const { generateCronJobToken } = require("../utils/generateBotToken"); +const { + usersData: abandonedUsersData, + tasksData: abandonedTasksData, +} = require("../fixtures/abandoned-tasks/departed-users"); const taskData = [ { @@ -1633,4 +1637,42 @@ describe("Tasks", function () { }); }); }); + + describe("GET /tasks/orphaned-tasks", function () { + beforeEach(async function () { + await cleanDb(); + const userPromises = abandonedUsersData.map((user) => userDBModel.add(user)); + await Promise.all(userPromises); + + const taskPromises = abandonedTasksData.map((task) => tasksModel.add(task)); + await Promise.all(taskPromises); + }); + + afterEach(async function () { + sinon.restore(); + await cleanDb(); + }); + + it("should fetch tasks assigned to archived and non-discord users", async function () { + const res = await chai.request(app).get("/tasks/orphaned-tasks"); + + expect(res).to.have.status(200); + expect(res.body).to.have.property("message").that.equals("Orphan tasks fetched successfully"); + expect(res.body.data).to.be.an("array").with.lengthOf(2); + }); + + it("should return an empty array when no users are archived", async function () { + await cleanDb(); + const user = abandonedUsersData[2]; + await userDBModel.add(user); + + const task = abandonedTasksData[3]; + await tasksModel.add(task); + const res = await chai.request(app).get("/tasks/orphaned-tasks"); + + expect(res).to.have.status(200); + expect(res.body).to.have.property("message").that.equals("Orphan tasks fetched successfully"); + expect(res.body.data).to.be.an("array").with.lengthOf(0); + }); + }); }); From 7ffecf2a77cab456c9d3effc8ca8cb75db769c5c Mon Sep 17 00:00:00 2001 From: VinuB-Dev Date: Tue, 19 Nov 2024 10:09:34 +0530 Subject: [PATCH 6/8] - Added dev feature flag through the middleware to control access to the /tasks/orphaned-tasks API. - Implemented negative test cases for the /tasks/orphaned-tasks endpoint: - Tested the scenario where the dev flag is not provided, expecting a 404 status code with a "Route not found" message. - Tested the scenario where the database query fails, ensuring the API responds with a 500 status code and an appropriate error message. --- controllers/tasks.js | 7 ++++--- routes/tasks.js | 3 ++- test/integration/tasks.test.js | 24 +++++++++++++++++++----- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index 63808a2ab..2ea362256 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -12,7 +12,8 @@ const { getPaginatedLink } = require("../utils/helper"); const { updateUserStatusOnTaskUpdate, updateStatusOnTaskCompletion } = require("../models/userStatus"); const dataAccess = require("../services/dataAccessLayer"); const { parseSearchQuery } = require("../utils/tasks"); -const { addTaskCreatedAtAndUpdatedAtFields, fetchOrphanedTasks } = require("../services/tasks"); +const { addTaskCreatedAtAndUpdatedAtFields } = require("../services/tasks"); +const tasksService = require("../services/tasks"); const { RQLQueryParser } = require("../utils/RQLParser"); const { getMissedProgressUpdatesUsers } = require("../models/discordactions"); const { logType } = require("../constants/logs"); @@ -534,8 +535,8 @@ const getUsersHandler = async (req, res) => { const getOrphanedTasks = async (req, res) => { try { - const data = await fetchOrphanedTasks(); - + const data = await tasksService.fetchOrphanedTasks(); + if (data.length === 0) return res.status(204).send(); return res.status(200).json({ message: "Orphan tasks fetched successfully", data }); } catch (error) { logger.error("Error in getting tasks which were abandoned", error); diff --git a/routes/tasks.js b/routes/tasks.js index 57095e82c..b1b9c0d5a 100644 --- a/routes/tasks.js +++ b/routes/tasks.js @@ -17,6 +17,7 @@ const { cacheResponse, invalidateCache } = require("../utils/cache"); const { ALL_TASKS } = require("../constants/cacheKeys"); const { verifyCronJob } = require("../middlewares/authorizeBot"); const { CLOUDFLARE_WORKER, CRON_JOB_HANDLER } = require("../constants/bot"); +const { devFlagMiddleware } = require("../middlewares/devFlag"); const oldAuthorizationMiddleware = authorizeRoles([APPOWNER, SUPERUSER]); const newAuthorizationMiddleware = authorizeAndAuthenticate( @@ -33,7 +34,7 @@ const enableDevModeMiddleware = (req, res, next) => { } }; -router.get("/orphaned-tasks", tasks.getOrphanedTasks); +router.get("/orphaned-tasks", devFlagMiddleware, tasks.getOrphanedTasks); 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); diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 56472f2bf..82b0aaa46 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -25,7 +25,8 @@ const userDBModel = firestore.collection("users"); const discordService = require("../../services/discordService"); const { CRON_JOB_HANDLER } = require("../../constants/bot"); const { logType } = require("../../constants/logs"); - +const { INTERNAL_SERVER_ERROR } = require("../../constants/errorMessages"); +const tasksService = require("../../services/tasks"); chai.use(chaiHttp); const appOwner = userData[3]; @@ -1654,7 +1655,7 @@ describe("Tasks", function () { }); it("should fetch tasks assigned to archived and non-discord users", async function () { - const res = await chai.request(app).get("/tasks/orphaned-tasks"); + const res = await chai.request(app).get("/tasks/orphaned-tasks?dev=true"); expect(res).to.have.status(200); expect(res.body).to.have.property("message").that.equals("Orphan tasks fetched successfully"); @@ -1668,11 +1669,24 @@ describe("Tasks", function () { const task = abandonedTasksData[3]; await tasksModel.add(task); + const res = await chai.request(app).get("/tasks/orphaned-tasks?dev=true"); + + expect(res).to.have.status(204); + }); + + it("should fail if dev flag is not passed", async function () { const res = await chai.request(app).get("/tasks/orphaned-tasks"); + expect(res).to.have.status(404); + expect(res.body.message).to.be.equal("Route not found"); + }); - expect(res).to.have.status(200); - expect(res.body).to.have.property("message").that.equals("Orphan tasks fetched successfully"); - expect(res.body.data).to.be.an("array").with.lengthOf(0); + it("should handle errors gracefully if the database query fails", async function () { + sinon.stub(tasksService, "fetchOrphanedTasks").rejects(new Error(INTERNAL_SERVER_ERROR)); + + const res = await chai.request(app).get("/tasks/orphaned-tasks?dev=true"); + + expect(res).to.have.status(500); + expect(res.body.message).to.be.equal(INTERNAL_SERVER_ERROR); }); }); }); From 511a22b6c70541b9dc9d559fdf0a10a3c3afd141 Mon Sep 17 00:00:00 2001 From: VinuB-Dev Date: Sun, 24 Nov 2024 15:50:49 +0530 Subject: [PATCH 7/8] feat: Removed separate endpoint for orphaned tasks and added it under /tasks?orphaned=true route. Modifications in code and tests related to the same. --- controllers/tasks.js | 50 +++++++++++++++++++++++--------- middlewares/validators/tasks.js | 1 + routes/tasks.js | 2 -- services/tasks.js | 17 ++++++----- test/integration/tasks.test.js | 30 ++++++++++--------- test/unit/services/tasks.test.js | 36 +++++++++++++++-------- 6 files changed, 88 insertions(+), 48 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index 2ea362256..77063c87b 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -135,7 +135,19 @@ const fetchPaginatedTasks = async (query) => { const fetchTasks = async (req, res) => { try { - const { status, page, size, prev, next, q: queryString, assignee, title, userFeatureFlag } = req.query; + const { + status, + page, + size, + prev, + next, + q: queryString, + assignee, + title, + userFeatureFlag, + orphaned, + dev, + } = req.query; const transformedQuery = transformQuery(status, size, page, assignee, title); if (queryString !== undefined) { @@ -160,6 +172,30 @@ const fetchTasks = async (req, res) => { }); } + const isOrphaned = orphaned === "true"; + const isDev = dev === "true"; + if (isOrphaned) { + if (isDev) { + try { + const orphanedTasks = await tasksService.fetchOrphanedTasks(); + + if (!orphanedTasks || orphanedTasks.length === 0) { + return res.sendStatus(204); + } + const tasksWithRdsAssigneeInfo = await fetchTasksWithRdsAssigneeInfo(orphanedTasks); + return res.status(200).json({ + message: "Orphan tasks fetched successfully", + data: tasksWithRdsAssigneeInfo, + }); + } catch (error) { + logger.error("Error in getting tasks which were abandoned", error); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } + } else { + return res.boom.notFound("Route not found"); + } + } + const paginatedTasks = await fetchPaginatedTasks({ ...transformedQuery, prev, next, userFeatureFlag }); return res.json({ message: "Tasks returned successfully!", @@ -533,17 +569,6 @@ const getUsersHandler = async (req, res) => { } }; -const getOrphanedTasks = async (req, res) => { - try { - const data = await tasksService.fetchOrphanedTasks(); - if (data.length === 0) return res.status(204).send(); - return res.status(200).json({ message: "Orphan tasks fetched successfully", data }); - } catch (error) { - logger.error("Error in getting tasks which were abandoned", error); - return res.boom.badImplementation(INTERNAL_SERVER_ERROR); - } -}; - module.exports = { addNewTask, fetchTasks, @@ -557,5 +582,4 @@ module.exports = { updateStatus, getUsersHandler, orphanTasks, - getOrphanedTasks, }; diff --git a/middlewares/validators/tasks.js b/middlewares/validators/tasks.js index 5467137c8..4b7a3234e 100644 --- a/middlewares/validators/tasks.js +++ b/middlewares/validators/tasks.js @@ -193,6 +193,7 @@ const getTasksValidator = async (req, res, next) => { return value; }, "Invalid query format"), userFeatureFlag: joi.string().optional(), + orphaned: joi.string().optional(), }); try { diff --git a/routes/tasks.js b/routes/tasks.js index b1b9c0d5a..5596f982c 100644 --- a/routes/tasks.js +++ b/routes/tasks.js @@ -17,7 +17,6 @@ const { cacheResponse, invalidateCache } = require("../utils/cache"); const { ALL_TASKS } = require("../constants/cacheKeys"); const { verifyCronJob } = require("../middlewares/authorizeBot"); const { CLOUDFLARE_WORKER, CRON_JOB_HANDLER } = require("../constants/bot"); -const { devFlagMiddleware } = require("../middlewares/devFlag"); const oldAuthorizationMiddleware = authorizeRoles([APPOWNER, SUPERUSER]); const newAuthorizationMiddleware = authorizeAndAuthenticate( @@ -34,7 +33,6 @@ const enableDevModeMiddleware = (req, res, next) => { } }; -router.get("/orphaned-tasks", devFlagMiddleware, tasks.getOrphanedTasks); 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); diff --git a/services/tasks.js b/services/tasks.js index 4e090ce70..60ed4816a 100644 --- a/services/tasks.js +++ b/services/tasks.js @@ -2,8 +2,8 @@ const firestore = require("../utils/firestore"); const tasksModel = firestore.collection("tasks"); const { chunks } = require("../utils/array"); const { DOCUMENT_WRITE_SIZE: FIRESTORE_BATCH_OPERATIONS_LIMIT } = require("../constants/constants"); -const { fetchUsersNotInDiscordServer } = require("../models/users"); -const { fetchIncompleteTaskForUser } = require("../models/tasks"); +const usersQuery = require("../models/users"); +const tasksQuery = require("../models/tasks"); const addTaskCreatedAtAndUpdatedAtFields = async () => { const operationStats = { @@ -60,13 +60,16 @@ const addTaskCreatedAtAndUpdatedAtFields = async () => { const fetchOrphanedTasks = async () => { try { - const abandonedTasks = []; + const userSnapshot = await usersQuery.fetchUsersNotInDiscordServer(); + if (userSnapshot.empty) { + return []; + } + const userIds = userSnapshot.docs.map((doc) => doc.id); - const userSnapshot = await fetchUsersNotInDiscordServer(); + const abandonedTasks = []; - for (const userDoc of userSnapshot.docs) { - const user = userDoc.data(); - const abandonedTasksQuerySnapshot = await fetchIncompleteTaskForUser(user.id); + for (const userId of userIds) { + const abandonedTasksQuerySnapshot = await tasksQuery.fetchIncompleteTaskForUser(userId); if (!abandonedTasksQuerySnapshot.empty) { abandonedTasks.push(...abandonedTasksQuerySnapshot.docs.map((doc) => doc.data())); diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 82b0aaa46..74d691e4d 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -1639,10 +1639,10 @@ describe("Tasks", function () { }); }); - describe("GET /tasks/orphaned-tasks", function () { + describe("GET /tasks?orphaned", function () { beforeEach(async function () { await cleanDb(); - const userPromises = abandonedUsersData.map((user) => userDBModel.add(user)); + const userPromises = abandonedUsersData.map((user) => userDBModel.doc(user.id).set(user)); await Promise.all(userPromises); const taskPromises = abandonedTasksData.map((task) => tasksModel.add(task)); @@ -1654,28 +1654,30 @@ describe("Tasks", function () { await cleanDb(); }); - it("should fetch tasks assigned to archived and non-discord users", async function () { - const res = await chai.request(app).get("/tasks/orphaned-tasks?dev=true"); - - expect(res).to.have.status(200); - expect(res.body).to.have.property("message").that.equals("Orphan tasks fetched successfully"); - expect(res.body.data).to.be.an("array").with.lengthOf(2); - }); - - it("should return an empty array when no users are archived", async function () { + it("should return 204 status when no users are archived", async function () { await cleanDb(); + const user = abandonedUsersData[2]; await userDBModel.add(user); const task = abandonedTasksData[3]; await tasksModel.add(task); - const res = await chai.request(app).get("/tasks/orphaned-tasks?dev=true"); + + const res = await chai.request(app).get("/tasks?dev=true&orphaned=true").set("Accept", "application/json"); expect(res).to.have.status(204); }); + it("should fetch tasks assigned to archived and non-discord users", async function () { + const res = await chai.request(app).get("/tasks?dev=true&orphaned=true"); + + expect(res).to.have.status(200); + expect(res.body).to.have.property("message").that.equals("Orphan tasks fetched successfully"); + expect(res.body.data).to.be.an("array").with.lengthOf(2); + }); + it("should fail if dev flag is not passed", async function () { - const res = await chai.request(app).get("/tasks/orphaned-tasks"); + const res = await chai.request(app).get("/tasks?orphaned=true"); expect(res).to.have.status(404); expect(res.body.message).to.be.equal("Route not found"); }); @@ -1683,7 +1685,7 @@ describe("Tasks", function () { it("should handle errors gracefully if the database query fails", async function () { sinon.stub(tasksService, "fetchOrphanedTasks").rejects(new Error(INTERNAL_SERVER_ERROR)); - const res = await chai.request(app).get("/tasks/orphaned-tasks?dev=true"); + const res = await chai.request(app).get("/tasks?orphaned=true&dev=true"); expect(res).to.have.status(500); expect(res.body.message).to.be.equal(INTERNAL_SERVER_ERROR); diff --git a/test/unit/services/tasks.test.js b/test/unit/services/tasks.test.js index e3d0e14f3..d7f232a6b 100644 --- a/test/unit/services/tasks.test.js +++ b/test/unit/services/tasks.test.js @@ -12,6 +12,7 @@ const { tasksData: abandonedTasksData, } = require("../../fixtures/abandoned-tasks/departed-users"); const { updateTaskStatusToDone } = require("../../../models/tasks"); +const tasksQuery = require("../../../models/tasks"); describe("Tasks services", function () { describe("task status COMPLETED to DONE in bulk", function () { @@ -83,7 +84,7 @@ describe("Tasks services", function () { beforeEach(async function () { await cleanDb(); - const userPromises = abandonedUsersData.map((user) => userModel.add(user)); + const userPromises = abandonedUsersData.map((user) => userModel.doc(user.id).set(user)); await Promise.all(userPromises); const taskPromises = abandonedTasksData.map((task) => tasksModel.add(task)); @@ -95,24 +96,23 @@ describe("Tasks services", function () { }); it("should fetch tasks assigned to archived and non-discord users", async function () { - const abandonedTasks = await fetchOrphanedTasks(); - - expect(abandonedTasks).to.be.an("array"); - expect(abandonedTasks).to.have.lengthOf(2); + const orphanedTasks = await fetchOrphanedTasks(); + expect(orphanedTasks).to.be.an("array"); + expect(orphanedTasks).to.have.lengthOf(2); }); it("should not include completed or done tasks", async function () { - const abandonedTasks = await fetchOrphanedTasks(); + const orphanedTasks = await fetchOrphanedTasks(); - abandonedTasks.forEach((task) => { + orphanedTasks.forEach((task) => { expect(task.status).to.not.be.oneOf(["DONE", "COMPLETED"]); }); }); it("should not include tasks from active users", async function () { - const abandonedTasks = await fetchOrphanedTasks(); + const orphanedTasks = await fetchOrphanedTasks(); - abandonedTasks.forEach((task) => { + orphanedTasks.forEach((task) => { expect(task.assignee).to.not.equal("active_user"); }); }); @@ -126,9 +126,21 @@ describe("Tasks services", function () { const activeTask = abandonedTasksData[3]; await tasksModel.add(activeTask); - const abandonedTasks = await fetchOrphanedTasks(); - expect(abandonedTasks).to.be.an("array"); - expect(abandonedTasks).to.have.lengthOf(0); + const orphanedTasks = await fetchOrphanedTasks(); + expect(orphanedTasks).to.be.an("array"); + expect(orphanedTasks).to.have.lengthOf(0); + }); + + it("should handle errors gracefully if getUsersWithIncompleteTasks fails", async function () { + Sinon.stub(tasksQuery, "fetchIncompleteTaskForUser").throws(new Error("Database query failed")); + + try { + await fetchOrphanedTasks(); + expect.fail("Expected function to throw an error"); + } catch (error) { + expect(error.message).to.equal("Database query failed"); + } + Sinon.restore(); }); }); }); From e186cb3961f451c65aca5e7a08a56b8a352f8065 Mon Sep 17 00:00:00 2001 From: VinuB-Dev Date: Sun, 24 Nov 2024 18:47:42 +0530 Subject: [PATCH 8/8] chore: Added early return for orphaned tasks api without dev flag. --- controllers/tasks.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index 77063c87b..17fa75819 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -175,25 +175,23 @@ const fetchTasks = async (req, res) => { const isOrphaned = orphaned === "true"; const isDev = dev === "true"; if (isOrphaned) { - if (isDev) { - try { - const orphanedTasks = await tasksService.fetchOrphanedTasks(); - - if (!orphanedTasks || orphanedTasks.length === 0) { - return res.sendStatus(204); - } - const tasksWithRdsAssigneeInfo = await fetchTasksWithRdsAssigneeInfo(orphanedTasks); - return res.status(200).json({ - message: "Orphan tasks fetched successfully", - data: tasksWithRdsAssigneeInfo, - }); - } catch (error) { - logger.error("Error in getting tasks which were abandoned", error); - return res.boom.badImplementation(INTERNAL_SERVER_ERROR); - } - } else { + if (!isDev) { return res.boom.notFound("Route not found"); } + try { + const orphanedTasks = await tasksService.fetchOrphanedTasks(); + if (!orphanedTasks || orphanedTasks.length === 0) { + return res.sendStatus(204); + } + const tasksWithRdsAssigneeInfo = await fetchTasksWithRdsAssigneeInfo(orphanedTasks); + return res.status(200).json({ + message: "Orphan tasks fetched successfully", + data: tasksWithRdsAssigneeInfo, + }); + } catch (error) { + logger.error("Error in getting tasks which were abandoned", error); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } } const paginatedTasks = await fetchPaginatedTasks({ ...transformedQuery, prev, next, userFeatureFlag });