Skip to content

Commit c86e023

Browse files
committed
Resolved conflicts in users and task unit tests and task model.
2 parents e11d4b7 + 41e6a5c commit c86e023

File tree

9 files changed

+291
-33
lines changed

9 files changed

+291
-33
lines changed

controllers/tasks.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { updateUserStatusOnTaskUpdate, updateStatusOnTaskCompletion } = require("
1313
const dataAccess = require("../services/dataAccessLayer");
1414
const { parseSearchQuery } = require("../utils/tasks");
1515
const { addTaskCreatedAtAndUpdatedAtFields } = require("../services/tasks");
16+
const tasksService = require("../services/tasks");
1617
const { RQLQueryParser } = require("../utils/RQLParser");
1718
const { getMissedProgressUpdatesUsers } = require("../models/discordactions");
1819
const { logType } = require("../constants/logs");
@@ -134,7 +135,19 @@ const fetchPaginatedTasks = async (query) => {
134135

135136
const fetchTasks = async (req, res) => {
136137
try {
137-
const { status, page, size, prev, next, q: queryString, assignee, title, userFeatureFlag } = req.query;
138+
const {
139+
status,
140+
page,
141+
size,
142+
prev,
143+
next,
144+
q: queryString,
145+
assignee,
146+
title,
147+
userFeatureFlag,
148+
orphaned,
149+
dev,
150+
} = req.query;
138151
const transformedQuery = transformQuery(status, size, page, assignee, title);
139152

140153
if (queryString !== undefined) {
@@ -159,6 +172,28 @@ const fetchTasks = async (req, res) => {
159172
});
160173
}
161174

175+
const isOrphaned = orphaned === "true";
176+
const isDev = dev === "true";
177+
if (isOrphaned) {
178+
if (!isDev) {
179+
return res.boom.notFound("Route not found");
180+
}
181+
try {
182+
const orphanedTasks = await tasksService.fetchOrphanedTasks();
183+
if (!orphanedTasks || orphanedTasks.length === 0) {
184+
return res.sendStatus(204);
185+
}
186+
const tasksWithRdsAssigneeInfo = await fetchTasksWithRdsAssigneeInfo(orphanedTasks);
187+
return res.status(200).json({
188+
message: "Orphan tasks fetched successfully",
189+
data: tasksWithRdsAssigneeInfo,
190+
});
191+
} catch (error) {
192+
logger.error("Error in getting tasks which were orphaned", error);
193+
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
194+
}
195+
}
196+
162197
const paginatedTasks = await fetchPaginatedTasks({ ...transformedQuery, prev, next, userFeatureFlag });
163198
return res.json({
164199
message: "Tasks returned successfully!",

middlewares/validators/tasks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ const getTasksValidator = async (req, res, next) => {
193193
return value;
194194
}, "Invalid query format"),
195195
userFeatureFlag: joi.string().optional(),
196+
orphaned: joi.boolean().optional(),
196197
});
197198

198199
try {

models/tasks.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const userModel = firestore.collection("users");
44
const ItemModel = firestore.collection("itemTags");
55
const dependencyModel = firestore.collection("taskDependencies");
66
const userUtils = require("../utils/users");
7-
const { updateTaskStatusToDone } = require("../services/tasks");
87
const { chunks } = require("../utils/array");
98
const { DOCUMENT_WRITE_SIZE } = require("../constants/constants");
109
const { fromFirestoreData, toFirestoreData, buildTasks } = require("../utils/tasks");
@@ -24,6 +23,42 @@ const {
2423
const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING, OLD_COMPLETED } = TASK_STATUS_OLD;
2524
const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");
2625

26+
/**
27+
* Update multiple tasks' status to DONE in one batch operation.
28+
* @param {Object[]} tasksData - Tasks data to update, must contain 'id' and 'status' fields.
29+
* @returns {Object} - Summary of the batch operation.
30+
* @property {number} totalUpdatedStatus - Number of tasks that has their status updated to DONE.
31+
* @property {number} totalOperationsFailed - Number of tasks that failed to update.
32+
* @property {string[]} updatedTaskDetails - IDs of tasks that has their status updated to DONE.
33+
* @property {string[]} failedTaskDetails - IDs of tasks that failed to update.
34+
*/
35+
const updateTaskStatusToDone = async (tasksData) => {
36+
const batch = firestore.batch();
37+
const tasksBatch = [];
38+
const summary = {
39+
totalUpdatedStatus: 0,
40+
totalOperationsFailed: 0,
41+
updatedTaskDetails: [],
42+
failedTaskDetails: [],
43+
};
44+
tasksData.forEach((task) => {
45+
const updateTaskData = { ...task, status: "DONE" };
46+
batch.update(tasksModel.doc(task.id), updateTaskData);
47+
tasksBatch.push(task.id);
48+
});
49+
try {
50+
await batch.commit();
51+
summary.totalUpdatedStatus += tasksData.length;
52+
summary.updatedTaskDetails = [...tasksBatch];
53+
return { ...summary };
54+
} catch (err) {
55+
logger.error("Firebase batch Operation Failed!");
56+
summary.totalOperationsFailed += tasksData.length;
57+
summary.failedTaskDetails = [...tasksBatch];
58+
return { ...summary };
59+
}
60+
};
61+
2762
/**
2863
* Adds and Updates tasks
2964
*
@@ -747,5 +782,6 @@ module.exports = {
747782
updateTaskStatus,
748783
updateOrphanTasksStatus,
749784
markUnDoneTasksOfArchivedUsersBacklog,
785+
updateTaskStatusToDone,
750786
fetchIncompleteTasksByUserIds,
751787
};

models/users.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,24 @@ const updateUsersWithNewUsernames = async () => {
10841084
}
10851085
};
10861086

1087+
/**
1088+
* Fetches users who are not in the Discord server.
1089+
* @returns {Promise<FirebaseFirestore.QuerySnapshot>} - A promise that resolves to a Firestore QuerySnapshot containing the users matching the criteria.
1090+
* @throws {Error} - Throws an error if the database query fails.
1091+
*/
1092+
const fetchUsersNotInDiscordServer = async () => {
1093+
try {
1094+
const usersNotInDiscordServer = await userModel
1095+
.where("roles.archived", "==", true)
1096+
.where("roles.in_discord", "==", false)
1097+
.get();
1098+
return usersNotInDiscordServer;
1099+
} catch (error) {
1100+
logger.error(`Error in getting users who are not in discord server: ${error}`);
1101+
throw error;
1102+
}
1103+
};
1104+
10871105
module.exports = {
10881106
archiveUsers,
10891107
addOrUpdate,
@@ -1114,4 +1132,5 @@ module.exports = {
11141132
fetchUserForKeyValue,
11151133
getNonNickNameSyncedUsers,
11161134
updateUsersWithNewUsernames,
1135+
fetchUsersNotInDiscordServer,
11171136
};

services/tasks.js

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,8 @@ const firestore = require("../utils/firestore");
22
const tasksModel = firestore.collection("tasks");
33
const { chunks } = require("../utils/array");
44
const { DOCUMENT_WRITE_SIZE: FIRESTORE_BATCH_OPERATIONS_LIMIT } = require("../constants/constants");
5-
6-
const updateTaskStatusToDone = async (tasksData) => {
7-
const batch = firestore.batch();
8-
const tasksBatch = [];
9-
const summary = {
10-
totalUpdatedStatus: 0,
11-
totalOperationsFailed: 0,
12-
updatedTaskDetails: [],
13-
failedTaskDetails: [],
14-
};
15-
tasksData.forEach((task) => {
16-
const updateTaskData = { ...task, status: "DONE" };
17-
batch.update(tasksModel.doc(task.id), updateTaskData);
18-
tasksBatch.push(task.id);
19-
});
20-
try {
21-
await batch.commit();
22-
summary.totalUpdatedStatus += tasksData.length;
23-
summary.updatedTaskDetails = [...tasksBatch];
24-
return { ...summary };
25-
} catch (err) {
26-
logger.error("Firebase batch Operation Failed!");
27-
summary.totalOperationsFailed += tasksData.length;
28-
summary.failedTaskDetails = [...tasksBatch];
29-
return { ...summary };
30-
}
31-
};
5+
const usersQuery = require("../models/users");
6+
const tasksQuery = require("../models/tasks");
327

338
const addTaskCreatedAtAndUpdatedAtFields = async () => {
349
const operationStats = {
@@ -83,7 +58,30 @@ const addTaskCreatedAtAndUpdatedAtFields = async () => {
8358
return operationStats;
8459
};
8560

61+
const fetchOrphanedTasks = async () => {
62+
try {
63+
const userSnapshot = await usersQuery.fetchUsersNotInDiscordServer();
64+
65+
if (userSnapshot.empty) return [];
66+
67+
const userIds = userSnapshot.docs.map((doc) => doc.id);
68+
69+
const orphanedTasksQuerySnapshot = await tasksQuery.fetchIncompleteTasksByUserIds(userIds);
70+
71+
if (orphanedTasksQuerySnapshot.empty) {
72+
return [];
73+
}
74+
75+
const orphanedTasks = orphanedTasksQuerySnapshot.map((doc) => doc.data());
76+
77+
return orphanedTasks;
78+
} catch (error) {
79+
logger.error(`Error in getting tasks abandoned by users: ${error}`);
80+
throw error;
81+
}
82+
};
83+
8684
module.exports = {
87-
updateTaskStatusToDone,
8885
addTaskCreatedAtAndUpdatedAtFields,
86+
fetchOrphanedTasks,
8987
};

test/integration/tasks.test.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const userDBModel = firestore.collection("users");
2525
const discordService = require("../../services/discordService");
2626
const { CRON_JOB_HANDLER } = require("../../constants/bot");
2727
const { logType } = require("../../constants/logs");
28-
28+
const { INTERNAL_SERVER_ERROR } = require("../../constants/errorMessages");
29+
const tasksService = require("../../services/tasks");
2930
chai.use(chaiHttp);
3031

3132
const appOwner = userData[3];
@@ -37,6 +38,10 @@ const { stubbedModelTaskProgressData } = require("../fixtures/progress/progresse
3738
const { convertDaysToMilliseconds } = require("../../utils/time");
3839
const { getDiscordMembers } = require("../fixtures/discordResponse/discord-response");
3940
const { generateCronJobToken } = require("../utils/generateBotToken");
41+
const {
42+
usersData: abandonedUsersData,
43+
tasksData: abandonedTasksData,
44+
} = require("../fixtures/abandoned-tasks/departed-users");
4045

4146
const taskData = [
4247
{
@@ -1633,4 +1638,57 @@ describe("Tasks", function () {
16331638
});
16341639
});
16351640
});
1641+
1642+
describe("GET /tasks?orphaned", function () {
1643+
beforeEach(async function () {
1644+
await cleanDb();
1645+
const userPromises = abandonedUsersData.map((user) => userDBModel.doc(user.id).set(user));
1646+
await Promise.all(userPromises);
1647+
1648+
const taskPromises = abandonedTasksData.map((task) => tasksModel.add(task));
1649+
await Promise.all(taskPromises);
1650+
});
1651+
1652+
afterEach(async function () {
1653+
sinon.restore();
1654+
await cleanDb();
1655+
});
1656+
1657+
it("should return 204 status when no users are archived", async function () {
1658+
await cleanDb();
1659+
1660+
const user = abandonedUsersData[2];
1661+
await userDBModel.add(user);
1662+
1663+
const task = abandonedTasksData[3];
1664+
await tasksModel.add(task);
1665+
1666+
const res = await chai.request(app).get("/tasks?dev=true&orphaned=true").set("Accept", "application/json");
1667+
1668+
expect(res).to.have.status(204);
1669+
});
1670+
1671+
it("should fetch tasks assigned to archived and non-discord users", async function () {
1672+
const res = await chai.request(app).get("/tasks?dev=true&orphaned=true");
1673+
1674+
expect(res).to.have.status(200);
1675+
expect(res.body).to.have.property("message").that.equals("Orphan tasks fetched successfully");
1676+
expect(res.body.data).to.be.an("array").with.lengthOf(2);
1677+
});
1678+
1679+
it("should fail if dev flag is not passed", async function () {
1680+
const res = await chai.request(app).get("/tasks?orphaned=true");
1681+
expect(res).to.have.status(404);
1682+
expect(res.body.message).to.be.equal("Route not found");
1683+
});
1684+
1685+
it("should handle errors gracefully if the database query fails", async function () {
1686+
sinon.stub(tasksService, "fetchOrphanedTasks").rejects(new Error(INTERNAL_SERVER_ERROR));
1687+
1688+
const res = await chai.request(app).get("/tasks?orphaned=true&dev=true");
1689+
1690+
expect(res).to.have.status(500);
1691+
expect(res.body.message).to.be.equal(INTERNAL_SERVER_ERROR);
1692+
});
1693+
});
16361694
});

test/unit/models/tasks.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ describe("tasks", function () {
357357
});
358358
});
359359

360-
describe("fetchIncompleteTaskForUser", function () {
360+
describe("fetchIncompleteTasksByUserIds", function () {
361361
beforeEach(async function () {
362362
await cleanDb();
363363

test/unit/models/users.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,4 +572,44 @@ describe("users", function () {
572572
}
573573
});
574574
});
575+
576+
describe("fetchUsersNotInDiscordServer", function () {
577+
beforeEach(async function () {
578+
await cleanDb();
579+
580+
const taskPromises = abandonedUsersData.map((task) => userModel.add(task));
581+
await Promise.all(taskPromises);
582+
});
583+
584+
afterEach(async function () {
585+
await cleanDb();
586+
sinon.restore();
587+
});
588+
589+
it("should fetch users not in discord server", async function () {
590+
const usersNotInDiscordServer = await users.fetchUsersNotInDiscordServer();
591+
expect(usersNotInDiscordServer.docs.length).to.be.equal(2);
592+
});
593+
594+
it("should return an empty array if there are no users in the database", async function () {
595+
await cleanDb();
596+
597+
const activeUser = abandonedUsersData[2];
598+
await userModel.add(activeUser);
599+
600+
const usersNotInDiscordServer = await users.fetchUsersNotInDiscordServer();
601+
expect(usersNotInDiscordServer.docs.length).to.be.equal(0);
602+
});
603+
604+
it("should handle errors gracefully if the database query fails", async function () {
605+
sinon.stub(users, "fetchUsersNotInDiscordServer").throws(new Error("Database query failed"));
606+
607+
try {
608+
await users.fetchUsersNotInDiscordServer();
609+
expect.fail("Expected function to throw an error");
610+
} catch (error) {
611+
expect(error.message).to.equal("Database query failed");
612+
}
613+
});
614+
});
575615
});

0 commit comments

Comments
 (0)