Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,32 @@ const getUsers = async (req, res) => {
}
}

const isDeparted = req.query.departed === "true";

if (isDeparted) {
let result;
if (dev) {
try {
result = await dataAccess.retrieveUsers({ query: req.query });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what will be the query here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be { departed: "true" }

const departedUsers = await userService.getUsersWithIncompleteTasks(result.users);
if (departedUsers.length === 0) res.status(204).send();
return res.json({
message: "Users with abandoned tasks fetched successfully",
users: departedUsers,
links: {
next: result.nextId ? getPaginationLink(req.query, "next", result.nextId) : "",
prev: result.prevId ? getPaginationLink(req.query, "prev", result.prevId) : "",
},
});
} catch (error) {
logger.error("Error when fetching users who abandoned tasks:", error);
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will be the status code?

Copy link
Contributor Author

@VinuB-Dev VinuB-Dev Nov 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be 500
You can see ti at the end here - https://www.npmjs.com/package/boom/v/2.5.0?activeTab=readme

}
} else {
return res.boom.notFound("Route not found");
}
}

if (transformedQuery?.filterBy === OVERDUE_TASKS) {
try {
const tasksData = await getOverdueTasks(days);
Expand Down Expand Up @@ -1030,17 +1056,6 @@ const updateUsernames = async (req, res) => {
}
};

const getUsersWithAbandonedTasks = async (req, res) => {
try {
const data = await userService.getUsersWithIncompleteTasks();
if (data.length === 0) res.status(204).send();
return res.status(200).json({ message: "Users with abandoned tasks fetched successfully", data });
} catch (error) {
logger.error("Error in getting user who abandoned tasks:", error);
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
}
};

module.exports = {
verifyUser,
generateChaincode,
Expand Down Expand Up @@ -1073,5 +1088,4 @@ module.exports = {
isDeveloper,
getIdentityStats,
updateUsernames,
getUsersWithAbandonedTasks,
};
1 change: 1 addition & 0 deletions middlewares/validators/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ async function getUsers(req, res, next) {
filterBy: joi.string().optional(),
days: joi.string().optional(),
dev: joi.string().optional(),
departed: joi.string().optional(),
roles: joi.optional().custom((value, helpers) => {
if (value !== "member") {
return helpers.message("only member role is supported");
Expand Down
29 changes: 7 additions & 22 deletions models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,11 @@ const getSuggestedUsers = async (skill) => {
*/
const fetchPaginatedUsers = async (query) => {
const isDevMode = query.dev === "true";

try {
const size = parseInt(query.size) || 100;
const doc = (query.next || query.prev) && (await userModel.doc(query.next || query.prev).get());

const isArchived = query.departed === "true";
let dbQuery;
/**
* !!NOTE : At the time of writing we only support member in the role query
Expand All @@ -245,9 +245,9 @@ const fetchPaginatedUsers = async (query) => {
* if you're making changes to this code remove the archived check in the role query, example: role=archived,member
*/
if (query.roles === "member") {
dbQuery = userModel.where("roles.archived", "==", false).where("roles.member", "==", true);
dbQuery = userModel.where("roles.archived", "==", isArchived).where("roles.member", "==", true);
} else {
dbQuery = userModel.where("roles.archived", "==", false).orderBy("username");
dbQuery = userModel.where("roles.archived", "==", isArchived).orderBy("username");
}

let compositeQuery = [dbQuery];
Expand All @@ -267,6 +267,10 @@ const fetchPaginatedUsers = async (query) => {
}

if (Object.keys(query).length) {
if (query.departed) {
compositeQuery = compositeQuery.map((query) => query.where("roles.in_discord", "==", false));
dbQuery = dbQuery.where("roles.in_discord", "==", false);
}
if (query.search) {
const searchValue = query.search.toLowerCase().trim();
dbQuery = dbQuery.startAt(searchValue).endAt(searchValue + "\uf8ff");
Expand Down Expand Up @@ -1080,24 +1084,6 @@ const updateUsersWithNewUsernames = async () => {
}
};

/**
* Fetches users who are not in the Discord server.
* @returns {Promise<FirebaseFirestore.QuerySnapshot>} - A promise resolving to a Firestore QuerySnapshot of matching users.
* @throws {Error} - Throws an error if the database query fails.
*/
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,
Expand Down Expand Up @@ -1128,5 +1114,4 @@ module.exports = {
fetchUserForKeyValue,
getNonNickNameSyncedUsers,
updateUsersWithNewUsernames,
fetchUsersNotInDiscordServer,
};
2 changes: 0 additions & 2 deletions routes/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ const { authorizeAndAuthenticate } = require("../middlewares/authorizeUsersAndSe
const ROLES = require("../constants/roles");
const { Services } = require("../constants/bot");
const authenticateProfile = require("../middlewares/authenticateProfile");
const { devFlagMiddleware } = require("../middlewares/devFlag");

router.get("/departed-users", devFlagMiddleware, users.getUsersWithAbandonedTasks);
router.post("/", authorizeAndAuthenticate([ROLES.SUPERUSER], [Services.CRON_JOB_HANDLER]), users.markUnverified);
router.post("/update-in-discord", authenticate, authorizeRoles([SUPERUSER]), users.setInDiscordScript);
router.post("/verify", authenticate, users.verifyUser);
Expand Down
16 changes: 5 additions & 11 deletions services/users.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
const firestore = require("../utils/firestore");
const { formatUsername } = require("../utils/username");
const userModel = firestore.collection("users");
const { fetchUsersNotInDiscordServer } = require("../models/users");
const { fetchIncompleteTaskForUser } = require("../models/tasks");
const tasksQuery = require("../models/tasks");

const getUsersWithIncompleteTasks = async () => {
const getUsersWithIncompleteTasks = async (users) => {
if (users.length === 0) return [];
try {
const eligibleUsersWithTasks = [];

const userSnapshot = await fetchUsersNotInDiscordServer();

for (const userDoc of userSnapshot.docs) {
const user = userDoc.data();

const abandonedTasksQuerySnapshot = await fetchIncompleteTaskForUser(user.id);

for (const user of users) {
const abandonedTasksQuerySnapshot = await tasksQuery.fetchIncompleteTaskForUser(user.id);
if (!abandonedTasksQuerySnapshot.empty) {
eligibleUsersWithTasks.push(user);
}
Expand Down
101 changes: 50 additions & 51 deletions test/integration/users.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1447,6 +1447,56 @@ describe("Users", function () {
});
});

describe("GET /users?departed", function () {
beforeEach(async function () {
await cleanDb();
const userPromises = abandonedUsersData.map((user) => userModel.doc(user.id).set(user));
await Promise.all(userPromises);

const taskPromises = abandonedTasksData.map((task) => taskModel.add(task));
await Promise.all(taskPromises);
});

afterEach(async function () {
Sinon.restore();
await cleanDb();
});

it("should return a list of users with abandoned tasks", async function () {
const res = await chai.request(app).get("/users?dev=true&departed=true");
expect(res).to.have.status(200);
expect(res.body).to.have.property("message").that.equals("Users with abandoned tasks fetched successfully");
expect(res.body).to.have.property("users").to.be.an("array").with.lengthOf(2);
});

it("should return an empty array when no users have abandoned tasks", async function () {
await cleanDb();
const user = abandonedUsersData[2];
await userModel.add(user);

const task = abandonedTasksData[3];
await taskModel.add(task);
const res = await chai.request(app).get("/users?dev=true&departed=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("/users?departed=true");
expect(res).to.have.status(404);
expect(res.body.message).to.be.equal("Route not found");
});

it("should handle errors gracefully if getUsersWithIncompleteTasks fails", async function () {
Sinon.stub(userService, "getUsersWithIncompleteTasks").rejects(new Error(INTERNAL_SERVER_ERROR));

const res = await chai.request(app).get("/users?departed=true&dev=true");

expect(res).to.have.status(500);
expect(res.body.message).to.be.equal(INTERNAL_SERVER_ERROR);
});
});

describe("PUT /users/self/intro", function () {
let userStatusData;

Expand Down Expand Up @@ -2652,55 +2702,4 @@ describe("Users", function () {
expect(res).to.have.status(401);
});
});

describe("GET /users/departed-users", function () {
beforeEach(async function () {
await cleanDb();
const userPromises = abandonedUsersData.map((user) => userModel.add(user));
await Promise.all(userPromises);

const taskPromises = abandonedTasksData.map((task) => taskModel.add(task));
await Promise.all(taskPromises);
});

afterEach(async function () {
Sinon.restore();
await cleanDb();
});

it("should return a list of users with abandoned tasks", async function () {
const res = await chai.request(app).get("/users/departed-users?dev=true");

expect(res).to.have.status(200);
expect(res.body).to.have.property("message").that.equals("Users with abandoned tasks fetched successfully");
expect(res.body.data).to.be.an("array").with.lengthOf(2);
});

it("should return an empty array when no users have abandoned tasks", async function () {
await cleanDb();
const user = abandonedUsersData[2];
await userModel.add(user);

const task = abandonedTasksData[3];
await taskModel.add(task);
const res = await chai.request(app).get("/users/departed-users?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("/users/departed-users");
expect(res).to.have.status(404);
expect(res.body.message).to.be.equal("Route not found");
});

it("should handle errors gracefully if the database query fails", async function () {
Sinon.stub(userService, "getUsersWithIncompleteTasks").rejects(new Error(INTERNAL_SERVER_ERROR));

const res = await chai.request(app).get("/users/departed-users?dev=true");

expect(res).to.have.status(500);
expect(res.body.message).to.be.equal(INTERNAL_SERVER_ERROR);
});
});
});
21 changes: 13 additions & 8 deletions test/unit/models/users.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ describe("users", function () {
});
});

describe("fetchUsersNotInDiscordServer", function () {
describe("fetchPaginatedUsers - Departed Users", function () {
beforeEach(async function () {
await cleanDb();

Expand All @@ -543,24 +543,29 @@ describe("users", function () {
});

it("should fetch users not in discord server", async function () {
const usersNotInDiscordServer = await users.fetchUsersNotInDiscordServer();
expect(usersNotInDiscordServer.docs.length).to.be.equal(2);
const result = await users.fetchPaginatedUsers({ departed: "true" });
expect(result.allUsers.length).to.be.equal(2);
});

it("should return an empty array if there are no users in the database", async function () {
it("should return no users if departed flag is false", async function () {
const result = await users.fetchPaginatedUsers({ departed: "false" });
expect(result.allUsers.length).to.be.equal(0);
});

it("should return an empty array if there are no departed 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);
const result = await users.fetchPaginatedUsers({ departed: "true" });
expect(result.allUsers.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"));
sinon.stub(users, "fetchPaginatedUsers").throws(new Error("Database query failed"));

try {
await users.fetchUsersNotInDiscordServer();
await users.fetchPaginatedUsers();
expect.fail("Expected function to throw an error");
} catch (error) {
expect(error.message).to.equal("Database query failed");
Expand Down
Loading