diff --git a/controllers/users.js b/controllers/users.js index 6cbebe7b4..f873a7e3a 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -11,7 +11,6 @@ const { profileDiffStatus } = require("../constants/profileDiff"); const { logType } = require("../constants/logs"); const ROLES = require("../constants/roles"); const dataAccess = require("../services/dataAccessLayer"); -const { isLastPRMergedWithinDays } = require("../services/githubService"); const logger = require("../utils/logger"); const { SOMETHING_WENT_WRONG, INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); const { OVERDUE_TASKS } = require("../constants/users"); @@ -20,7 +19,6 @@ const { setInDiscordFalseScript, setUserDiscordNickname } = require("../services const { generateDiscordProfileImageUrl } = require("../utils/discord-actions"); const { addRoleToUser, getDiscordMembers } = require("../services/discordService"); const { fetchAllUsers } = require("../models/users"); -const { getOverdueTasks } = require("../models/tasks"); const { getQualifiers } = require("../utils/helper"); const { parseSearchQuery } = require("../utils/users"); const { getFilteredPRsOrIssues } = require("../utils/pullRequests"); @@ -31,12 +29,12 @@ const { USERS_PATCH_HANDLER_SUCCESS_MESSAGES, } = require("../constants/users"); const { addLog } = require("../models/logs"); -const { getUserStatus } = require("../models/userStatus"); const config = require("config"); const { generateUniqueUsername } = require("../services/users"); -const userService = require("../services/users"); +const { NotFound, BadRequest, InternalServerError } = require("http-errors"); const discordDeveloperRoleId = config.get("discordDeveloperRoleId"); const usersCollection = firestore.collection("users"); +const userService = require("../services/users"); const verifyUser = async (req, res) => { const userId = req.userData.id; @@ -89,208 +87,122 @@ const getUserById = async (req, res) => { const getUsers = async (req, res) => { try { - // getting user details by id if present. - const { q, dev: devParam, query } = req.query; + const reqQueryObject = req.query; + const { q, dev: devParam, query, departed, id, profile: profileParam, discordId } = reqQueryObject; + const userData = req.userData || {}; + + const profile = profileParam === "true"; + const isDeparted = departed === "true"; const dev = devParam === "true"; const queryString = (dev ? q : query) || ""; const transformedQuery = parseSearchQuery(queryString); + const { filterBy, days } = transformedQuery; const qualifiers = getQualifiers(queryString); // Should throw an error if the new query parameter is without feature flag if (q && !dev) { return res.boom.notFound("Route not found"); } - // getting user details by id if present. - - if (req.query.id) { - const id = req.query.id; - let result, user; - try { - result = await dataAccess.retrieveUsers({ id: id }); - user = result.user; - } catch (error) { - logger.error(`Error while fetching user: ${error}`); - return res.boom.serverUnavailable(SOMETHING_WENT_WRONG); - } - if (!result.userExists) { - return res.boom.notFound("User doesn't exist"); - } - return res.json({ + + if (id) { + const user = await userService.findUserById(id); + return res.status(200).json({ message: "User returned successfully!", - user, + user: user, }); } - const profile = req.query.profile === "true"; - if (profile) { - if (!req.userData.id) { - return res.boom.badRequest("User ID not provided."); - } - - try { - const result = await dataAccess.retrieveUsers({ id: req.userData.id }); - return res.send(result.user); - } catch (error) { - logger.error(`Error while fetching user: ${error}`); - return res.boom.serverUnavailable(INTERNAL_SERVER_ERROR); - } + const user = await userService.getUserByProfileData(userData); + return res.status(200).send(user); } if (!transformedQuery?.days && transformedQuery?.filterBy === "unmerged_prs") { return res.boom.badRequest(`Days is required for filterBy ${transformedQuery?.filterBy}`); } - const { filterBy, days } = transformedQuery; if (filterBy === "unmerged_prs" && days) { - try { - const inDiscordUser = await dataAccess.retrieveUsersWithRole(ROLES.INDISCORD); - const users = []; - - for (const user of inDiscordUser) { - const username = user.github_id; - const isMerged = await isLastPRMergedWithinDays(username, days); - if (!isMerged) { - users.push(user.id); - } - } - - return res.json({ - message: "Inactive users returned successfully!", - count: users.length, - users: users, - }); - } catch (error) { - logger.error(`Error while fetching all users: ${error}`); - return res.boom.serverUnavailable("Something went wrong please contact admin"); - } + const users = await userService.getUsersByUnmergedPrs(days); + return res.status(200).json({ + message: "Inactive users returned successfully!", + count: users.length, + users: users, + }); } - // getting user details by discord id if present. - const discordId = req.query.discordId; - - if (req.query.discordId) { + if (discordId) { if (dev) { - let result, user; - try { - result = await dataAccess.retrieveUsers({ discordId }); - user = result.user; - if (!result.userExists) { - return res.json({ - message: "User not found", - user: null, - }); - } - - const userStatusResult = await getUserStatus(user.id); - if (userStatusResult.userStatusExists) { - user.state = userStatusResult.data.currentStatus.state; - } - } catch (error) { - logger.error(`Error while fetching user: ${error}`); - return res.boom.serverUnavailable(INTERNAL_SERVER_ERROR); - } - return res.json({ - message: "User returned successfully!", - user, + const user = await userService.getUserByDiscordId(discordId); + return res.status(200).json({ + message: user ? "User returned successfully!" : "User not found", + user: user, }); } else { return res.boom.notFound("Route not found"); } } - const isDeparted = req.query.departed === "true"; - if (isDeparted) { if (!dev) { return res.boom.notFound("Route not found"); } - try { - const result = await dataAccess.retrieveUsers({ query: req.query }); - const departedUsers = await userService.getUsersWithIncompleteTasks(result.users); - if (departedUsers.length === 0) return 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); - } + const { result, departedUsers } = await userService.getDepartedUsers(reqQueryObject); + if (departedUsers.length === 0) return res.status(204).send(); + return res.status(200).json({ + message: "Users with abandoned tasks fetched successfully", + users: departedUsers, + links: { + next: result.nextId ? getPaginationLink(reqQueryObject, "next", result.nextId) : "", + prev: result.prevId ? getPaginationLink(reqQueryObject, "prev", result.prevId) : "", + }, + }); } - if (transformedQuery?.filterBy === OVERDUE_TASKS) { - try { - const tasksData = await getOverdueTasks(days); - if (!tasksData.length) { - return res.json({ - message: "No users found", - users: [], - }); - } - const userIds = new Set(); - const usersData = []; - - tasksData.forEach((task) => { - if (task.assignee) { - userIds.add(task.assignee); - } - }); - - const userInfo = await dataAccess.retrieveUsers({ userIds: Array.from(userIds) }); - userInfo.forEach((user) => { - if (!user.roles.archived) { - const userTasks = tasksData.filter((task) => task.assignee === user.id); - const userData = { - id: user.id, - discordId: user.discordId, - username: user.username, - }; - if (dev) { - userData.tasks = userTasks; - } - usersData.push(userData); - } - }); - - return res.json({ - message: "Users returned successfully!", - count: usersData.length, - users: usersData, + if (filterBy === OVERDUE_TASKS) { + const users = await userService.getUsersByOverDueTasks(days, dev); + if (!users || users.length === 0) { + return res.status(200).json({ + message: "No users found", + users: [], }); - } catch (error) { - const errorMessage = `Error while fetching users and tasks: ${error}`; - logger.error(errorMessage); - return res.boom.serverUnavailable("Something went wrong, please contact admin"); } + return res.status(200).json({ + message: "Users returned successfully!", + count: users.length, + users: users, + }); } if (qualifiers?.filterBy) { const allPRs = await getFilteredPRsOrIssues(qualifiers); const usernames = getUsernamesFromPRs(allPRs); const users = await dataAccess.retrieveUsers({ usernames: usernames }); - return res.json({ + return res.status(200).json({ message: "Users returned successfully!", users, }); } - const data = await dataAccess.retrieveUsers({ query: req.query }); + const data = await dataAccess.retrieveUsers({ query: reqQueryObject }); - return res.json({ + return res.status(200).json({ message: "Users returned successfully!", users: data.users, links: { - next: data.nextId ? getPaginationLink(req.query, "next", data.nextId) : "", - prev: data.prevId ? getPaginationLink(req.query, "prev", data.prevId) : "", + next: data.nextId ? getPaginationLink(reqQueryObject, "next", data.nextId) : "", + prev: data.prevId ? getPaginationLink(reqQueryObject, "prev", data.prevId) : "", }, }); - } catch (error) { - logger.error(`Error while fetching all users: ${error}`); - return res.boom.serverUnavailable(SOMETHING_WENT_WRONG); + } catch (e) { + switch (true) { + case e instanceof NotFound: + return res.boom.notFound(e.message); + case e instanceof BadRequest: + return res.boom.badRequest(e.message); + case e instanceof InternalServerError: + return res.boom.internal(e.message); + default: + return res.boom.serverUnavailable(SOMETHING_WENT_WRONG); + } } }; diff --git a/services/users.js b/services/users.js index 94646d896..51e5053ca 100644 --- a/services/users.js +++ b/services/users.js @@ -2,6 +2,14 @@ const firestore = require("../utils/firestore"); const { formatUsername } = require("../utils/username"); const userModel = firestore.collection("users"); const tasksModel = require("../models/tasks"); +const dataAccess = require("./dataAccessLayer"); +const { NotFound, BadRequest, InternalServerError } = require("http-errors"); +const logger = require("../utils/logger"); +const ROLES = require("../constants/roles"); +const { isLastPRMergedWithinDays } = require("./githubService"); +const { getUserStatus } = require("../models/userStatus"); +const { getOverdueTasks } = require("../models/tasks"); +const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); const getUsersWithIncompleteTasks = async (users) => { if (users.length === 0) return []; @@ -46,7 +54,154 @@ const generateUniqueUsername = async (firstName, lastName) => { } }; +/** + * @param userId { string }: Id of the User + * @returns Promise + */ +const findUserById = async (userId) => { + try { + const result = await dataAccess.retrieveUsers({ id: userId }); + if (!result.userExists) { + throw NotFound("User doesn't exist"); + } + return result.user; + } catch (error) { + logger.error(`Error while fetching user: ${error}`); + throw error; + } +}; + +/** + * @param userData { Object }: req.userData + * @returns Promise + */ +const getUserByProfileData = async (userData) => { + if (!userData.id) { + throw BadRequest("User ID not provided."); + } + + try { + const result = await dataAccess.retrieveUsers({ id: userData.id }); + return result.user; + } catch (error) { + logger.error(`Error while fetching user: ${error}`); + throw error; + } +}; + +/** + * @param days {number}: days since last unmerged pr. + * @returns Promise + */ +const getUsersByUnmergedPrs = async (days) => { + try { + const inDiscordUser = await dataAccess.retrieveUsersWithRole(ROLES.INDISCORD); + const users = []; + + for (const user of inDiscordUser) { + const username = user.github_id; + const isMerged = await isLastPRMergedWithinDays(username, days); + if (!isMerged) { + users.push(user.id); + } + } + + return users; + } catch (error) { + logger.error(`Error while fetching all users: ${error}`); + throw error; + } +}; + +/** + * @param discordId { string }: discordId of the user + * @returns Promise + */ +const getUserByDiscordId = async (discordId) => { + try { + const result = await dataAccess.retrieveUsers({ discordId }); + const user = result.user; + if (!result.userExists) { + return null; + } + + const userStatusResult = await getUserStatus(user.id); + if (userStatusResult.userStatusExists) { + user.state = userStatusResult.data.currentStatus.state; + } + return user; + } catch (error) { + logger.error(`Error while fetching user: ${error}`); + throw error; + } +}; + +/** + * @param queryObject { Object }: request query object + * @returns Promise + */ +const getDepartedUsers = async (queryObject) => { + try { + const result = await dataAccess.retrieveUsers({ query: queryObject }); + const departedUsers = await getUsersWithIncompleteTasks(result.users); + if (!departedUsers || departedUsers.length === 0) return { departedUsers: [] }; + return { result, departedUsers }; + } catch (error) { + logger.error("Error when fetching users who abandoned tasks:", error); + throw new InternalServerError(INTERNAL_SERVER_ERROR); + } +}; + +/** + * @param days { number }: overdue days + * @param dev {boolean}: dev feature flag + * @returns Promise + */ +const getUsersByOverDueTasks = async (days, dev) => { + try { + const tasksData = await getOverdueTasks(days); + if (!tasksData.length) { + return []; + } + const userIds = new Set(); + const usersData = []; + + tasksData.forEach((task) => { + if (task.assignee) { + userIds.add(task.assignee); + } + }); + + const userInfo = await dataAccess.retrieveUsers({ userIds: Array.from(userIds) }); + userInfo.forEach((user) => { + if (!user.roles.archived) { + const userTasks = tasksData.filter((task) => task.assignee === user.id); + const userData = { + id: user.id, + discordId: user.discordId, + username: user.username, + }; + if (dev) { + userData.tasks = userTasks; + } + usersData.push(userData); + } + }); + + return usersData; + } catch (error) { + logger.error(`Error while fetching users and tasks: ${error}`); + throw error; + } +}; + module.exports = { generateUniqueUsername, getUsersWithIncompleteTasks, + getUsersByOverDueTasks, + getDepartedUsers, + getUserByProfileData, + getUsersByUnmergedPrs, + getUserByDiscordId, + findUserById, }; diff --git a/test/integration/users.test.js b/test/integration/users.test.js index 8ba547d3f..f7d7f7a36 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -47,7 +47,7 @@ const { usersData: abandonedUsersData, tasksData: abandonedTasksData, } = require("../fixtures/abandoned-tasks/departed-users"); -const userService = require("../../services/users"); +const dataAccess = require("../../services/dataAccessLayer"); chai.use(chaiHttp); describe("Users", function () { @@ -1488,7 +1488,7 @@ describe("Users", function () { }); it("should handle errors gracefully if getUsersWithIncompleteTasks fails", async function () { - Sinon.stub(userService, "getUsersWithIncompleteTasks").rejects(new Error(INTERNAL_SERVER_ERROR)); + Sinon.stub(dataAccess, "retrieveUsers").throws(new Error(INTERNAL_SERVER_ERROR)); const res = await chai.request(app).get("/users?departed=true&dev=true"); diff --git a/utils/users.js b/utils/users.js index 387aadc58..2a183eab1 100644 --- a/utils/users.js +++ b/utils/users.js @@ -4,10 +4,10 @@ const { months, discordNicknameLength } = require("../constants/users"); const dataAccessLayer = require("../services/dataAccessLayer"); const discordService = require("../services/discordService"); const ROLES = require("../constants/roles"); +const logger = require("./logger"); const addUserToDBForTest = async (userData) => { await userModel.add(userData); }; - /** * Used for receiving userId when providing username *