diff --git a/constants/errorMessages.js b/constants/errorMessages.js index c7c7c4959..66a2477d0 100644 --- a/constants/errorMessages.js +++ b/constants/errorMessages.js @@ -8,4 +8,5 @@ module.exports = { BAD_REQUEST: "BAD_REQUEST", INVALID_QUERY_PARAM: "Invalid Query Parameters Passed", FILE_TOO_LARGE: (size) => `File too large, max accepted size is ${size} MB`, + USER_DOES_NOT_EXIST_ERROR: "User does not exist!", }; diff --git a/constants/tasks.js b/constants/tasks.js index 06dc326e0..37e22ca37 100644 --- a/constants/tasks.js +++ b/constants/tasks.js @@ -20,6 +20,7 @@ const TASK_STATUS = { RELEASED: "RELEASED", VERIFIED: "VERIFIED", DONE: "DONE", + OVERDUE: "OVERDUE", }; // TODO: convert this to new task status diff --git a/constants/userDataLevels.js b/constants/userDataLevels.js new file mode 100644 index 000000000..a907e265a --- /dev/null +++ b/constants/userDataLevels.js @@ -0,0 +1,21 @@ +const ACCESS_LEVEL = { + PUBLIC: "public", + INTERNAL: "internal", + PRIVATE: "private", + CONFIDENTIAL: "confidential", +}; + +const ROLE_LEVEL = { + private: ["super_user"], + internal: ["super_user"], + confidential: ["super_user"], +}; + +const KEYS_NOT_ALLOWED = { + public: ["email", "phone", "chaincode"], + internal: ["phone", "chaincode"], + private: ["chaincode"], + confidential: [], +}; + +module.exports = { ACCESS_LEVEL, KEYS_NOT_ALLOWED, ROLE_LEVEL }; diff --git a/constants/users.js b/constants/users.js index 3a11cf768..f96dbd19e 100644 --- a/constants/users.js +++ b/constants/users.js @@ -4,8 +4,6 @@ const profileStatus = { NOT_APPROVED: "NOT APPROVED", }; -const USER_SENSITIVE_DATA = ["phone", "email", "chaincode", "tokens"]; - const USER_STATUS = { OOO: "ooo", IDLE: "idle", @@ -19,9 +17,34 @@ const ALLOWED_FILTER_PARAMS = { ROLE: ["role"], }; +const DOCUMENT_WRITE_SIZE = 500; + +const USERS_PATCH_HANDLER_ACTIONS = { + ARCHIVE_USERS: "archiveUsers", + NON_VERFIED_DISCORD_USERS: "nonVerifiedDiscordUsers", +}; + +const USERS_PATCH_HANDLER_ERROR_MESSAGES = { + VALIDATE_PAYLOAD: "Invalid Payload", + ARCHIVE_USERS: { + NO_USERS_DATA_TO_UPDATE: "Couldn't find any users currently inactive in Discord but not archived.", + BATCH_DATA_UPDATED_FAILED: "Firebase batch operation failed", + }, +}; + +const USERS_PATCH_HANDLER_SUCCESS_MESSAGES = { + ARCHIVE_USERS: { + SUCCESSFULLY_UPDATED_DATA: "Successfully updated users archived role to true if in_discord role is false", + SUCCESSFULLY_COMPLETED_BATCH_UPDATES: "Successfully completed batch updates", + }, +}; + module.exports = { profileStatus, USER_STATUS, ALLOWED_FILTER_PARAMS, - USER_SENSITIVE_DATA, + DOCUMENT_WRITE_SIZE, + USERS_PATCH_HANDLER_ACTIONS, + USERS_PATCH_HANDLER_ERROR_MESSAGES, + USERS_PATCH_HANDLER_SUCCESS_MESSAGES, }; diff --git a/controllers/auth.js b/controllers/auth.js index 183f4a394..38bdbd135 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -2,7 +2,13 @@ const passport = require("passport"); const users = require("../models/users"); const QrCodeAuthModel = require("../models/qrCodeAuth"); const authService = require("../services/authService"); -const { SOMETHING_WENT_WRONG, DATA_ADDED_SUCCESSFULLY, BAD_REQUEST } = require("../constants/errorMessages"); +const dataAccess = require("../services/dataAccessLayer"); +const { + SOMETHING_WENT_WRONG, + DATA_ADDED_SUCCESSFULLY, + USER_DOES_NOT_EXIST_ERROR, +} = require("../constants/errorMessages"); +const { generateUniqueToken } = require("../utils/generateUniqueToken"); /** * Makes authentication call to GitHub statergy @@ -49,11 +55,10 @@ const githubAuthCallback = (req, res, next) => { logger.error(err); return res.boom.unauthorized("User cannot be authenticated"); } - userData = { github_id: user.username, github_display_name: user.displayName, - // github_account_created_at: user.created_at, + github_created_at: Number(new Date(user._json.created_at).getTime()), created_at: Date.now(), updated_at: Date.now(), }; @@ -111,13 +116,12 @@ const storeUserDeviceInfo = async (req, res) => { authorization_status: "NOT_INIT", }; - const userInfo = await QrCodeAuthModel.storeUserDeviceInfo(userJson); + const userInfoData = await dataAccess.retrieveUsers({ id: userJson.user_id }); - if (!userInfo) { - return res.status(404).json({ - message: BAD_REQUEST, - }); + if (!userInfoData.userExists) { + return res.boom.notFound(USER_DOES_NOT_EXIST_ERROR); } + const userInfo = await QrCodeAuthModel.storeUserDeviceInfo(userJson); return res.status(201).json({ ...userInfo, @@ -133,7 +137,11 @@ const updateAuthStatus = async (req, res) => { try { const userId = req.userData.id; const authStatus = req.params.authorization_status; - const result = await QrCodeAuthModel.updateStatus(userId, authStatus); + let token; + if (authStatus === "AUTHORIZED") { + token = await generateUniqueToken(); + } + const result = await QrCodeAuthModel.updateStatus(userId, authStatus, token); if (!result.userExists) { return res.boom.notFound("Document not found!"); @@ -151,8 +159,8 @@ const updateAuthStatus = async (req, res) => { const fetchUserDeviceInfo = async (req, res) => { try { - const deviceId = req.query.device_id; - const userDeviceInfoData = await QrCodeAuthModel.retrieveUserDeviceInfo(deviceId); + const { device_id: deviceId } = req.query; + const userDeviceInfoData = await QrCodeAuthModel.retrieveUserDeviceInfo({ deviceId }); if (!userDeviceInfoData.userExists) { return res.boom.notFound(`User with id ${deviceId} does not exist.`); } @@ -166,6 +174,23 @@ const fetchUserDeviceInfo = async (req, res) => { } }; +const fetchDeviceDetails = async (req, res) => { + try { + const userId = req.userData.id; + const userDeviceInfoData = await QrCodeAuthModel.retrieveUserDeviceInfo({ userId }); + if (!userDeviceInfoData.userExists) { + return res.boom.notFound(`User with id ${userId} does not exist.`); + } + return res.json({ + message: "Authentication document Exists", + data: { device_info: userDeviceInfoData.data?.device_info }, + }); + } catch (error) { + logger.error(`Error while fetching user device info: ${error}`); + return res.boom.badImplementation(SOMETHING_WENT_WRONG); + } +}; + module.exports = { githubAuthLogin, githubAuthCallback, @@ -173,4 +198,5 @@ module.exports = { storeUserDeviceInfo, updateAuthStatus, fetchUserDeviceInfo, + fetchDeviceDetails, }; diff --git a/controllers/discordactions.js b/controllers/discordactions.js index a24bf665b..0d9995288 100644 --- a/controllers/discordactions.js +++ b/controllers/discordactions.js @@ -66,6 +66,16 @@ const createGroupRole = async (req, res) => { const getAllGroupRoles = async (req, res) => { try { const { groups } = await discordRolesModel.getAllGroupRoles(); + const dev = req.query.dev === "true"; + if (dev) { + // Placing the new changes under the feature flag. + const discordId = req.userData?.discordId; + const groupsWithMembershipInfo = await discordRolesModel.enrichGroupDataWithMembershipInfo(discordId, groups); + return res.json({ + message: "Roles fetched successfully!", + groups: groupsWithMembershipInfo, + }); + } return res.json({ message: "Roles fetched successfully!", groups, diff --git a/controllers/events.js b/controllers/events.js index b0b77bbdc..1429d65d7 100644 --- a/controllers/events.js +++ b/controllers/events.js @@ -1,9 +1,11 @@ -/* eslint-disable camelcase */ const { GET_ALL_EVENTS_LIMIT_MIN, UNWANTED_PROPERTIES_FROM_100MS } = require("../constants/events"); +const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); + const { EventTokenService, EventAPIService } = require("../services"); -const { removeUnwantedProperties } = require("../utils/events"); const eventQuery = require("../models/events"); + const logger = require("../utils/logger"); +const { removeUnwantedProperties } = require("../utils/events"); const tokenService = new EventTokenService(); const apiService = new EventAPIService(tokenService); @@ -100,7 +102,7 @@ const joinEvent = async (req, res) => { }); } catch (error) { logger.error({ error }); - return res.status(500).send("Internal Server Error"); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); } }; @@ -193,6 +195,70 @@ const endActiveEvent = async (req, res) => { } }; +/** + * Adds a peer to an event. + * + * @async + * @function + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @returns {Promise} The JSON response with the added peer data and a success message. + * @throws {Object} The JSON response with an error message if an error occurred while adding the peer. + */ +const addPeerToEvent = async (req, res) => { + try { + const data = await eventQuery.addPeerToEvent({ + peerId: req.body.peerId, + name: req.body.name, + role: req.body.role, + joinedAt: req.body.joinedAt, + eventId: req.params.id, + }); + return res.status(200).json({ + data, + message: `Selected Participant is added to the event.`, + }); + } catch (error) { + logger.error({ error }); + return res.status(500).json({ + error: error.code, + message: "You can't add selected Participant. Please ask Admin or Host for help.", + }); + } +}; + +/** + * Kicks out a peer from an event. + * + * @async + * @function + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @returns {Promise} The JSON response with a success message if the peer is successfully kicked out. + * @throws {Object} The JSON response with an error message if an error occurred while kicking out the peer. + */ +const kickoutPeer = async (req, res) => { + const { id } = req.params; + const payload = { + peer_id: req.body.peerId, + reason: req.body.reason, + }; + + try { + await apiService.post(`/active-rooms/${id}/remove-peers`, payload); + await eventQuery.kickoutPeer({ eventId: id, peerId: payload.peer_id, reason: req.body.reason }); + return res.status(200).json({ + message: `Selected Participant is removed from event.`, + }); + } catch (error) { + logger.error({ error }); + return res.status(500).json({ + error: error.code, + message: "You can't remove selected Participant from Remove, Please ask Admin or Host for help.", + }); + } +}; + module.exports = { createEvent, getAllEvents, @@ -200,4 +266,6 @@ module.exports = { getEventById, updateEvent, endActiveEvent, + addPeerToEvent, + kickoutPeer, }; diff --git a/controllers/extensionRequests.js b/controllers/extensionRequests.js index f3ed89d69..15b2e1a19 100644 --- a/controllers/extensionRequests.js +++ b/controllers/extensionRequests.js @@ -1,7 +1,7 @@ const extensionRequestsQuery = require("../models/extensionRequests"); const { addLog } = require("../models/logs"); const tasks = require("../models/tasks"); -const { getUsername } = require("../utils/users"); +const { getUsername, getUsernameElseUndefined, getUserIdElseUndefined } = require("../utils/users"); const { EXTENSION_REQUEST_STATUS } = require("../constants/extensionRequests"); const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** @@ -14,11 +14,22 @@ const createTaskExtensionRequest = async (req, res) => { try { const extensionBody = req.body; + let assigneeUsername = await getUsernameElseUndefined(extensionBody.assignee); + let assigneeId = extensionBody.assignee; + if (!assigneeUsername) { + assigneeId = await getUserIdElseUndefined(extensionBody.assignee); + assigneeUsername = extensionBody.assignee; + extensionBody.assignee = assigneeId; + } + + if (!assigneeId) { + return res.boom.badRequest("User with this id or username doesn't exist."); + } + if (req.userData.id !== extensionBody.assignee && !req.userData.roles?.super_user) { - return res.boom.forbidden("Only Super User can create an extension request for this task."); + return res.boom.forbidden("Only assigned user and super user can create an extension request for this task."); } - const assigneeUsername = await getUsername(extensionBody.assignee); const { taskData: task } = await tasks.fetchTask(extensionBody.taskId); if (!task) { return res.boom.badRequest("Task with this id or taskid doesn't exist."); diff --git a/controllers/members.js b/controllers/members.js index abb6af3f5..1ece7d3bc 100644 --- a/controllers/members.js +++ b/controllers/members.js @@ -1,8 +1,9 @@ const ROLES = require("../constants/roles"); const members = require("../models/members"); const tasks = require("../models/tasks"); -const { SOMETHING_WENT_WRONG } = require("../constants/errorMessages"); +const { SOMETHING_WENT_WRONG, INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); const dataAccess = require("../services/dataAccessLayer"); +const { addLog } = require("../models/logs"); /** * Fetches the data about our members * @@ -83,17 +84,33 @@ const archiveMembers = async (req, res) => { try { const { username } = req.params; const user = await dataAccess.retrieveUsers({ username }); + const superUserId = req.userData.id; + const { reason } = req.body; + const roles = req?.userData?.roles; if (user?.userExists) { const successObject = await members.addArchiveRoleToMembers(user.user.id); if (successObject.isArchived) { return res.boom.badRequest("User is already archived"); } + const body = { + reason: reason || "", + archived_user: { + user_id: user.user.id, + username: user.user.username, + }, + archived_by: { + user_id: superUserId, + roles: roles, + }, + }; + + addLog("archived-details", {}, body); return res.status(204).send(); } return res.boom.notFound("User doesn't exist"); } catch (err) { logger.error(`Error while retriving contributions ${err}`); - return res.boom.badImplementation(SOMETHING_WENT_WRONG); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); } }; diff --git a/controllers/tasks.js b/controllers/tasks.js index b3918308b..9770af903 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -126,8 +126,8 @@ const fetchPaginatedTasks = async (query) => { const fetchTasks = async (req, res) => { try { - const { dev, status, page, size, prev, next, q: queryString } = req.query; - const transformedQuery = transformQuery(dev, status, size, page); + const { dev, status, page, size, prev, next, q: queryString, assignee, title } = req.query; + const transformedQuery = transformQuery(dev, status, size, page, assignee, title); if (dev) { const paginatedTasks = await fetchPaginatedTasks({ ...transformedQuery, prev, next }); diff --git a/controllers/users.js b/controllers/users.js index 1a990dd9d..453a8a3e0 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -9,12 +9,17 @@ const dataAccess = require("../services/dataAccessLayer"); const logger = require("../utils/logger"); const { SOMETHING_WENT_WRONG, INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); const { getPaginationLink, getUsernamesFromPRs, getRoleToUpdate } = require("../utils/users"); -const { setInDiscordFalseScript } = require("../services/discordService"); +const { setInDiscordFalseScript, setUserDiscordNickname } = require("../services/discordService"); const { generateDiscordProfileImageUrl } = require("../utils/discord-actions"); const { addRoleToUser, getDiscordMembers } = require("../services/discordService"); const { fetchAllUsers } = require("../models/users"); const { getQualifiers } = require("../utils/helper"); const { getFilteredPRsOrIssues } = require("../utils/pullRequests"); +const { + USERS_PATCH_HANDLER_ACTIONS, + USERS_PATCH_HANDLER_ERROR_MESSAGES, + USERS_PATCH_HANDLER_SUCCESS_MESSAGES, +} = require("../constants/users"); const verifyUser = async (req, res) => { const userId = req.userData.id; @@ -101,9 +106,10 @@ const getUsers = async (req, res) => { } const data = await dataAccess.retrieveUsers({ query: req.query }); + return res.json({ message: "Users returned successfully!", - users: data.allUsers, + users: data.users, links: { next: data.nextId ? getPaginationLink(req.query, "next", data.nextId) : "", prev: data.prevId ? getPaginationLink(req.query, "prev", data.prevId) : "", @@ -205,10 +211,9 @@ const getUsernameAvailabilty = async (req, res) => { const getSelfDetails = async (req, res) => { try { if (req.userData) { - if (req.query.private) { - return res.send(req.userData); - } - const user = await dataAccess.retrieveUsers({ userdata: req.userData }); + const user = await dataAccess.retrieveUsers({ + userdata: req.userData, + }); return res.send(user); } return res.boom.notFound("User doesn't exist"); @@ -310,6 +315,36 @@ const verifyUserImage = async (req, res) => { } }; +/** + * Patch Update user nickname + * + * @param req {Object} - Express request object + * @param res {Object} - Express response object + */ + +const updateDiscordUserNickname = async (req, res) => { + const { userId } = req.params; + try { + const userToBeUpdated = await dataAccess.retrieveUsers({ id: userId }); + const { discordId, username } = userToBeUpdated.user; + if (!discordId) { + throw new Error("user not verified"); + } + const response = await setUserDiscordNickname(username, discordId); + + return res.json({ + userAffected: { + userId, + username, + discordId, + }, + message: response, + }); + } catch (err) { + logger.error(`Error while updating nickname: ${err}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; const markUnverified = async (req, res) => { try { const [usersInRdsDiscordServer, allRdsLoggedInUsers] = await Promise.all([getDiscordMembers(), fetchAllUsers()]); @@ -407,6 +442,7 @@ const updateUser = async (req, res) => { const generateChaincode = async (req, res) => { try { const { id } = req.userData; + const chaincode = await chaincodeQuery.storeChaincode(id); await userQuery.addOrUpdate({ chaincode }, id); return res.json({ @@ -569,9 +605,9 @@ const filterUsers = async (req, res) => { } }; -const nonVerifiedDiscordUsers = async (req, res) => { +const nonVerifiedDiscordUsers = async () => { const data = await dataAccess.retrieveDiscordUsers(); - return res.json(data); + return data; }; const setInDiscordScript = async (req, res) => { @@ -583,25 +619,6 @@ const setInDiscordScript = async (req, res) => { } }; -const removeTokens = async (req, res) => { - try { - const users = await userQuery.fetchUsersWithToken(); - - if (!users.length) { - return res.status(404).json({ message: "No users found with github Token!" }); - } - - await userQuery.removeGitHubToken(users); - - return res.status(200).json({ - message: "Github Token removed from all users!", - usersFound: users.length, - }); - } catch (err) { - return res.boom.badImplementation({ message: INTERNAL_SERVER_ERROR }); - } -}; - const updateRoles = async (req, res) => { try { const result = await dataAccess.retrieveUsers({ id: req.params.id }); @@ -625,6 +642,58 @@ const updateRoles = async (req, res) => { } }; +const archiveUserIfNotInDiscord = async () => { + try { + const data = await userQuery.archiveUserIfNotInDiscord(); + + if (data.totalUsers === 0) { + return { + message: USERS_PATCH_HANDLER_ERROR_MESSAGES.ARCHIVE_USERS.NO_USERS_DATA_TO_UPDATE, + summary: data, + }; + } + + return { + message: USERS_PATCH_HANDLER_SUCCESS_MESSAGES.ARCHIVE_USERS.SUCCESSFULLY_UPDATED_DATA, + summary: data, + }; + } catch (error) { + logger.error(`Error while updating the archived role: ${error}`); + throw Error(INTERNAL_SERVER_ERROR); + } +}; + +async function usersPatchHandler(req, res) { + try { + const { action } = req.body; + let response; + + if (action === USERS_PATCH_HANDLER_ACTIONS.NON_VERFIED_DISCORD_USERS) { + const data = await nonVerifiedDiscordUsers(); + response = data; + } + + if (action === USERS_PATCH_HANDLER_ACTIONS.ARCHIVE_USERS) { + const debugQuery = req.query.debug?.toLowerCase(); + const data = await archiveUserIfNotInDiscord(); + + if (debugQuery === "true") { + data.summary.updatedUserDetails = data.summary.updatedUserDetails.slice(-3); + response = data; + } else { + delete data.summary.updatedUserDetails; + delete data.summary.failedUserDetails; + response = data; + } + } + + return res.status(200).json(response); + } catch (error) { + logger.error("Error while handling the users common patch route:", error); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +} + module.exports = { verifyUser, generateChaincode, @@ -649,6 +718,8 @@ module.exports = { nonVerifiedDiscordUsers, setInDiscordScript, markUnverified, - removeTokens, updateRoles, + updateDiscordUserNickname, + archiveUserIfNotInDiscord, + usersPatchHandler, }; diff --git a/middlewares/authenticate.js b/middlewares/authenticate.js index d72b2c71f..667d41b32 100644 --- a/middlewares/authenticate.js +++ b/middlewares/authenticate.js @@ -1,5 +1,5 @@ const authService = require("../services/authService"); -const users = require("../models/users"); +const dataAccess = require("../services/dataAccessLayer"); /** * Middleware to check if the user has been restricted. If user is restricted, @@ -54,7 +54,7 @@ module.exports = async (req, res, next) => { const { userId } = authService.verifyAuthToken(token); // add user data to `req.userData` for further use - const userData = await users.fetchUser({ userId }); + const userData = await dataAccess.retrieveUsers({ id: userId }); req.userData = userData.user; return checkRestricted(req, res, next); @@ -79,8 +79,7 @@ module.exports = async (req, res, next) => { }); // add user data to `req.userData` for further use - req.userData = await users.fetchUser({ userId }); - + req.userData = await dataAccess.retrieveUsers({ id: userId }); return checkRestricted(req, res, next); } else { return res.boom.unauthorized("Unauthenticated User"); diff --git a/middlewares/validators/events.js b/middlewares/validators/events.js index c5196f2c7..10e479fc3 100644 --- a/middlewares/validators/events.js +++ b/middlewares/validators/events.js @@ -100,6 +100,50 @@ const endActiveEvent = async (req, res, next) => { } }; +const addPeerToEvent = async (req, res, next) => { + const { id } = req.params; + const { peerId, name, role, joinedAt } = req.body; + + const schema = joi.object({ + peerId: joi.string().required(), + name: joi.string().required(), + id: joi.string().required(), + role: joi.string().required(), + joinedAt: joi.date().required(), + }); + + const validationOptions = { abortEarly: false }; + + try { + await schema.validateAsync({ peerId, name, id, role, joinedAt }, validationOptions); + next(); + } catch (error) { + logger.error(`Error while adding a peer to the event: ${error}`); + res.boom.badRequest(error.details[0].message); + } +}; + +const kickoutPeer = async (req, res, next) => { + const { id } = req.params; + const { peerId, reason } = req.body; + + const schema = joi.object({ + id: joi.string().required(), + peerId: joi.string().required(), + reason: joi.string().required(), + }); + + const validationOptions = { abortEarly: false }; + + try { + await schema.validateAsync({ id, peerId, reason }, validationOptions); + next(); + } catch (error) { + logger.error(`We encountered some error while removing selected Participant from event: ${error}`); + res.boom.badRequest(error.details[0].message); + } +}; + module.exports = { createEvent, getAllEvents, @@ -107,4 +151,6 @@ module.exports = { getEventById, updateEvent, endActiveEvent, + addPeerToEvent, + kickoutPeer, }; diff --git a/middlewares/validators/qrCodeAuth.js b/middlewares/validators/qrCodeAuth.js index 8aea8b3e7..ee9ae21f4 100644 --- a/middlewares/validators/qrCodeAuth.js +++ b/middlewares/validators/qrCodeAuth.js @@ -12,6 +12,7 @@ const storeUserDeviceInfo = async (req, res, next) => { next(); } catch (error) { logger.error(`Error validating newDeviceInfo payload : ${error}`); + res.boom.badRequest(error.details[0].message); } }; @@ -41,8 +42,8 @@ const validateFetchingUserDocument = async (req, res, next) => { await schema.validateAsync(req.query); next(); } catch (error) { - logger.error(`Invalid Query Parameters Passed`); - res.boom.badRequest(`Invalid Query Parameters Passed`); + logger.error("Invalid Query Parameters Passed"); + res.boom.badRequest("Invalid Query Parameters Passed"); } }; diff --git a/middlewares/validators/staging.js b/middlewares/validators/staging.js index e9180826e..66f43addc 100644 --- a/middlewares/validators/staging.js +++ b/middlewares/validators/staging.js @@ -4,7 +4,7 @@ const validateUserRoles = async (req, res, next) => { const schema = joi.object().strict().keys({ super_user: joi.boolean().optional(), member: joi.boolean().optional(), - archive: joi.boolean().optional(), + archived: joi.boolean().optional(), in_discord: joi.boolean().optional(), }); diff --git a/middlewares/validators/tasks.js b/middlewares/validators/tasks.js index c83b52fdd..d25799888 100644 --- a/middlewares/validators/tasks.js +++ b/middlewares/validators/tasks.js @@ -84,7 +84,7 @@ const updateTask = async (req, res, next) => { .string() .valid(...TASK_STATUS_ENUM, ...Object.values(TASK_STATUS_OLD)) .optional(), - assignee: joi.string().optional(), + assignee: joi.alternatives().try(joi.string().optional(), joi.valid(null)), percentCompleted: joi.number().integer().min(0).max(100).optional(), dependsOn: joi.array().items(joi.string()).optional(), participants: joi.array().items(joi.string()).optional(), @@ -142,6 +142,8 @@ const getTasksValidator = async (req, res, next) => { .insensitive() .valid(...MAPPED_TASK_STATUS_ENUM) .optional(), + assignee: joi.string().insensitive().optional(), + title: joi.string().insensitive().optional(), page: joi.number().integer().min(0), next: joi .string() diff --git a/middlewares/validators/user.js b/middlewares/validators/user.js index a781ac3dc..a00ce6b54 100644 --- a/middlewares/validators/user.js +++ b/middlewares/validators/user.js @@ -1,7 +1,11 @@ const { customWordCountValidator } = require("../../utils/customWordCountValidator"); const joi = require("joi"); -const { USER_STATUS } = require("../../constants/users"); +const { + USER_STATUS, + USERS_PATCH_HANDLER_ACTIONS, + USERS_PATCH_HANDLER_ERROR_MESSAGES, +} = require("../../constants/users"); const ROLES = require("../../constants/roles"); const { IMAGE_VERIFICATION_TYPES } = require("../../constants/imageVerificationTypes"); const { userState } = require("../../constants/userStatus"); @@ -207,6 +211,10 @@ async function validateUserQueryParams(req, res, next) { .optional(), role: joi.string().valid(ROLES.MEMBER, ROLES.INDISCORD, ROLES.ARCHIVED).optional(), verified: joi.string().optional(), + time: joi + .string() + .regex(/^[1-9]\d*d$/) + .optional(), }) .messages({ "object.min": "Please provide at least one filter criteria", @@ -256,6 +264,23 @@ async function validateUpdateRoles(req, res, next) { } } +async function validateUsersPatchHandler(req, res, next) { + const requestBodySchema = joi.object({ + action: joi + .string() + .valid(USERS_PATCH_HANDLER_ACTIONS.ARCHIVE_USERS, USERS_PATCH_HANDLER_ACTIONS.NON_VERFIED_DISCORD_USERS) + .required(), + }); + + try { + await requestBodySchema.validateAsync(req.body); + next(); + } catch (error) { + logger.error("Error in validating action payload", error); + res.boom.badRequest(`${USERS_PATCH_HANDLER_ERROR_MESSAGES.VALIDATE_PAYLOAD}: ${error.message}`); + } +} + module.exports = { updateUser, updateProfileURL, @@ -264,4 +289,5 @@ module.exports = { validateUserQueryParams, validateImageVerificationQuery, validateUpdateRoles, + validateUsersPatchHandler, }; diff --git a/models/discordactions.js b/models/discordactions.js index cb93d3336..468e83523 100644 --- a/models/discordactions.js +++ b/models/discordactions.js @@ -3,6 +3,9 @@ const firestore = require("../utils/firestore"); const discordRoleModel = firestore.collection("discord-roles"); const memberRoleModel = firestore.collection("member-group-roles"); const admin = require("firebase-admin"); +const { findSubscribedGroupIds } = require("../utils/helper"); +const { retrieveUsers } = require("../services/dataAccessLayer"); +const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); const photoVerificationModel = firestore.collection("photo-verification"); /** @@ -114,10 +117,93 @@ const updateDiscordImageForVerification = async (userDiscordId) => { } }; +/** + * Enriches group data with membership information for a given Discord ID. + * + * @param {string} discordId - The Discord ID of the user. + * @param {Array} groups - Array of group objects to process. + * @returns {Promise>} - An array of group objects with enriched information. + */ +const enrichGroupDataWithMembershipInfo = async (discordId, groups = []) => { + try { + if (!groups.length) { + return []; + } + + const groupCreatorIds = groups.reduce((ids, group) => { + if (group.createdBy) { + ids.add(group.createdBy); + } + return ids; + }, new Set()); + + const groupCreatorsDetails = await retrieveUsers({ userIds: Array.from(groupCreatorIds) }); + const roleIds = groups.map((group) => group.roleid); + const groupsToUserMappings = await fetchGroupToUserMapping(roleIds); + const roleIdToCountMap = {}; + + groupsToUserMappings.forEach((groupToUserMapping) => { + // Count how many times roleId comes up in the array. + // This says how many users we have for a given roleId + roleIdToCountMap[groupToUserMapping.roleid] = (roleIdToCountMap[groupToUserMapping.roleid] ?? 0) + 1; + }); + + const subscribedGroupIds = findSubscribedGroupIds(discordId, groupsToUserMappings); + + return groups.map((group) => { + const groupCreator = groupCreatorsDetails[group.createdBy]; + return { + ...group, + firstName: groupCreator?.first_name, + lastName: groupCreator?.last_name, + image: groupCreator?.picture?.url, + memberCount: roleIdToCountMap[group.roleid] || 0, // Number of users joined this group + isMember: subscribedGroupIds.has(group.roleid), // Is current loggedIn user is a member of this group + }; + }); + } catch (err) { + logger.error("Error while enriching group data with membership info", err); + throw err; + } +}; + +/** + * + * @param {Array} roleIds Array of roleIds whose user mapping needs to fetched + * @returns Array of roleId to userId mapping + * + * Breaking the roleIds array into chunks of 30 or less due to firebase limitation + */ +const fetchGroupToUserMapping = async (roleIds) => { + try { + const roleIdChunks = []; + + for (let i = 0; i < roleIds.length; i += BATCH_SIZE_IN_CLAUSE) { + roleIdChunks.push(roleIds.slice(i, i + BATCH_SIZE_IN_CLAUSE)); + } + + const promises = roleIdChunks.map(async (roleIdChunk) => { + const querySnapshot = await memberRoleModel.where("roleid", "in", roleIdChunk).get(); + return querySnapshot.docs.map((doc) => doc.data()); + }); + + const snapshots = await Promise.all(promises); + + const groupToUserMappingArray = snapshots.flat(); + + return groupToUserMappingArray; + } catch (err) { + logger.error("Error while fetching group to user mapping", err); + throw err; + } +}; + module.exports = { createNewRole, getAllGroupRoles, addGroupRoleToMember, isGroupRoleExists, updateDiscordImageForVerification, + enrichGroupDataWithMembershipInfo, + fetchGroupToUserMapping, }; diff --git a/models/events.js b/models/events.js index b78f2fbfe..46077ced2 100644 --- a/models/events.js +++ b/models/events.js @@ -1,7 +1,9 @@ +const Firestore = require("@google-cloud/firestore"); const firestore = require("../utils/firestore"); const logger = require("../utils/logger"); const eventModel = firestore.collection("events"); +const peerModel = firestore.collection("peers"); /** * Creates a new event document in Firestore and returns the data for the created document. @@ -66,8 +68,112 @@ const endActiveEvent = async ({ id, reason, lock }) => { } }; +/** + * Adds a peer to an event in the Firestore database. + * @async + * @function + * @param {Object} peerData - The data of the peer to be added. + * @param {string} peerData.name - The name of the peer. + * @param {string} peerData.eventId - The unique identifier of the event the peer is being added to. + * @param {string} peerData.role - The role of the peer in the event. + * @param {Date} peerData.joinedAt - The timestamp indicating when the peer joined the event. + * @returns {Promise} The data of the added peer. + * @throws {Error} If an error occurs while adding the peer to the event. + */ + +const addPeerToEvent = async (peerData) => { + try { + const batch = firestore.batch(); + + const peerRef = peerModel.doc(peerData.peerId); + const peerDocSnapshot = await peerRef.get(); + + if (!peerDocSnapshot.exists) { + // If the peer document doesn't exist, create a new one + const peerDocData = { + peerId: peerData.peerId, + name: peerData.name, + joinedEvents: [ + { + event_id: peerData.eventId, + role: peerData.role, + joined_at: peerData.joinedAt, + }, + ], + }; + batch.set(peerRef, peerDocData); + } else { + // If the peer document exists, update the joinedEvents array + batch.update(peerRef, { + joinedEvents: Firestore.FieldValue.arrayUnion({ + event_id: peerData.eventId, + role: peerData.role, + joined_at: peerData.joinedAt, + }), + }); + } + + const eventRef = eventModel.doc(peerData.eventId); + batch.update(eventRef, { + peers: Firestore.FieldValue.arrayUnion(peerRef.id), + }); + + await batch.commit(); + + const updatedPeerSnapshot = await peerRef.get(); + return updatedPeerSnapshot.data(); + } catch (error) { + logger.error("Error in adding peer to the event", error); + throw error; + } +}; + +/** + * Removes a peer from an event and marks them as kicked out in the Firestore database. + * @async + * @function + * @param {Object} params - The parameters for kicking out the peer. + * @param {string} params.eventId - The unique identifier of the event from which the peer is being kicked out. + * @param {string} params.peerId - The unique identifier of the peer being kicked out. + * @param {string} params.reason - The reason for kicking out the peer from the event. + * @returns {Promise} The updated data of the kicked-out peer. + * @throws {Error} If the peer is not found or is not part of the specified event. + */ +const kickoutPeer = async ({ eventId, peerId, reason }) => { + try { + const peerRef = peerModel.doc(peerId); + const peerSnapshot = await peerRef.get(); + + if (!peerSnapshot.exists) { + throw new Error("Participant not found"); + } + + const peerData = peerSnapshot.data(); + const joinedEvents = peerData.joinedEvents; + + const eventIndex = joinedEvents.findIndex((event) => event.event_id === eventId); + if (eventIndex === -1) { + throw new Error("Participant is not part of the specified event"); + } + + const updatedJoinedEvents = joinedEvents.map((event, index) => + index === eventIndex ? { ...event, left_at: new Date(), reason: reason, isKickedout: true } : event + ); + + await peerRef.update({ joinedEvents: updatedJoinedEvents }); + + const updatedPeerSnapshot = await peerRef.get(); + return updatedPeerSnapshot.data(); + } catch (error) { + logger.error("Error in removing peer from the event.", error); + throw error; + } +}; + module.exports = { createEvent, updateEvent, endActiveEvent, + addPeerToEvent, + kickoutPeer, }; diff --git a/models/logs.js b/models/logs.js index 80daf676f..8779d0601 100644 --- a/models/logs.js +++ b/models/logs.js @@ -3,6 +3,7 @@ const { getBeforeHourTime } = require("../utils/time"); const logsModel = firestore.collection("logs"); const admin = require("firebase-admin"); const { logType } = require("../constants/logs"); +const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** * Adds log @@ -22,7 +23,7 @@ const addLog = async (type, meta, body) => { return await logsModel.add(log); } catch (err) { logger.error("Error in adding log", err); - throw err; + throw new Error(INTERNAL_SERVER_ERROR); } }; @@ -42,14 +43,27 @@ const fetchLogs = async (query, param) => { } }); - const { limit, lastDocId } = query; + const { limit, lastDocId, userId } = query; let lastDoc; const limitDocuments = Number(limit); if (lastDocId) { lastDoc = await logsModel.doc(lastDocId).get(); } - + if (userId) { + const logsSnapshot = await logsModel + .where("type", "==", param) + .where("body.archived_user.user_id", "==", userId) + .orderBy("timestamp", "desc") + .get(); + const logs = []; + logsSnapshot.forEach((doc) => { + logs.push({ + ...doc.data(), + }); + }); + return logs; + } const logsSnapshotQuery = call.orderBy("timestamp", "desc").startAfter(lastDoc ?? ""); const snapshot = limit ? await logsSnapshotQuery.limit(limitDocuments).get() @@ -64,7 +78,7 @@ const fetchLogs = async (query, param) => { return logs; } catch (err) { logger.error("Error in adding log", err); - throw err; + throw new Error(INTERNAL_SERVER_ERROR); } }; diff --git a/models/qrCodeAuth.js b/models/qrCodeAuth.js index 0bbdadbac..e071282e8 100644 --- a/models/qrCodeAuth.js +++ b/models/qrCodeAuth.js @@ -1,7 +1,6 @@ const firestore = require("../utils/firestore"); const QrCodeAuthModel = firestore.collection("QrCodeAuth"); -const userModel = firestore.collection("users"); -const USER_DOES_NOT_EXIST_ERROR = "User does not exist."; + /** * Stores the user device info * @@ -9,7 +8,7 @@ const USER_DOES_NOT_EXIST_ERROR = "User does not exist."; * @return {Promise<{userDeviceInfoData|Object}>} */ -const updateStatus = async (userId, authStatus = "NOT_INIT") => { +const updateStatus = async (userId, authStatus = "NOT_INIT", token) => { try { const authData = await QrCodeAuthModel.doc(userId).get(); @@ -22,8 +21,8 @@ const updateStatus = async (userId, authStatus = "NOT_INIT") => { await QrCodeAuthModel.doc(userId).set({ ...authData.data(), authorization_status: authStatus, + token: `${token}`, }); - return { userExists: true, data: { @@ -40,24 +39,26 @@ const updateStatus = async (userId, authStatus = "NOT_INIT") => { const storeUserDeviceInfo = async (userDeviceInfoData) => { try { const { user_id: userId } = userDeviceInfoData; - const user = await userModel.doc(userId).get(); - if (user.data()) { - await QrCodeAuthModel.doc(userId).set(userDeviceInfoData); + await QrCodeAuthModel.doc(userId).set(userDeviceInfoData); - return { userDeviceInfoData }; - } else { - throw new Error(USER_DOES_NOT_EXIST_ERROR); - } + return { userDeviceInfoData }; } catch (err) { logger.error("Error in storing user device info.", err); throw err; } }; -const retrieveUserDeviceInfo = async (deviceId) => { +const retrieveUserDeviceInfo = async ({ deviceId, userId }) => { + let queryDocument; try { - const queryDocument = await QrCodeAuthModel.where("device_id", "==", deviceId).get(); - const userData = queryDocument.docs[0]; + if (deviceId) { + queryDocument = await QrCodeAuthModel.where("device_id", "==", deviceId).get(); + } else if (userId) { + queryDocument = await QrCodeAuthModel.where("user_id", "==", userId) + .where("authorization_status", "==", "NOT_INIT") + .get(); + } + const userData = queryDocument?.docs[0]; if (!userData) { return { diff --git a/models/tasks.js b/models/tasks.js index 88bd98cd6..0ade96760 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -5,7 +5,7 @@ const dependencyModel = firestore.collection("taskDependencies"); const userUtils = require("../utils/users"); const { fromFirestoreData, toFirestoreData, buildTasks } = require("../utils/tasks"); const { TASK_TYPE, TASK_STATUS, TASK_STATUS_OLD, TASK_SIZE } = require("../constants/tasks"); -const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, COMPLETED } = TASK_STATUS; +const { IN_PROGRESS, BLOCKED, SMOKE_TESTING, COMPLETED, MERGED, RELEASED, VERIFIED, AVAILABLE } = TASK_STATUS; const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING, OLD_COMPLETED } = TASK_STATUS_OLD; /** @@ -119,11 +119,40 @@ const getBuiltTasks = async (tasksSnapshot, searchTerm) => { return taskList; }; -const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, prev }) => { +const fetchPaginatedTasks = async ({ + status = "", + size = TASK_SIZE, + page, + next, + prev, + dev = false, + assignee, + title, +}) => { try { - const initialQuery = status - ? tasksModel.where("status", "==", status).orderBy("title") - : tasksModel.orderBy("title"); + let initialQuery = tasksModel; + + if (status === TASK_STATUS.OVERDUE && dev) { + const currentTime = Math.floor(Date.now() / 1000); + initialQuery = tasksModel.where("endsOn", "<", currentTime); + } else { + initialQuery = tasksModel.orderBy("title"); + if (status) { + initialQuery = initialQuery.where("status", "==", status); + } + + if (assignee) { + const user = await userUtils.getUserId(assignee); + if (user) { + initialQuery = initialQuery.where("assignee", "==", user); + } + } + + if (title) { + initialQuery = initialQuery.where("title", ">=", title).where("title", "<=", title + "\uf8ff"); + } + } + let queryDoc = initialQuery; if (prev) { @@ -152,6 +181,17 @@ const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next, const nextDoc = await initialQuery.startAfter(last).limit(1).get(); const allTasks = await getBuiltTasks(snapshot); + + if (status === TASK_STATUS.OVERDUE && dev) { + const nonOverdueTasksStatus = [MERGED, COMPLETED, RELEASED, VERIFIED, AVAILABLE]; + const overdueTasks = allTasks.filter((task) => !nonOverdueTasksStatus.includes(task.status) && task.assignee); + return { + allTasks: overdueTasks, + next: nextDoc.docs[0]?.id ?? "", + prev: prevDoc.docs[0]?.id ?? "", + }; + } + return { allTasks, next: nextDoc.docs[0]?.id ?? "", @@ -459,6 +499,7 @@ const overdueTasks = async (overDueTasks) => { throw err; } }; + module.exports = { updateTask, fetchTasks, diff --git a/models/users.js b/models/users.js index 5aeac0924..1ab987b7b 100644 --- a/models/users.js +++ b/models/users.js @@ -7,8 +7,9 @@ const walletConstants = require("../constants/wallets"); const firestore = require("../utils/firestore"); const { fetchWallet, createWallet } = require("../models/wallets"); const { updateUserStatus } = require("../models/userStatus"); -const { arraysHaveCommonItem } = require("../utils/array"); -const { ALLOWED_FILTER_PARAMS } = require("../constants/users"); +const { arraysHaveCommonItem, chunks } = require("../utils/array"); +const { archiveUsers } = require("../services/users"); +const { ALLOWED_FILTER_PARAMS, DOCUMENT_WRITE_SIZE } = require("../constants/users"); const { userState } = require("../constants/userStatus"); const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); const ROLES = require("../constants/roles"); @@ -19,6 +20,7 @@ const userStatusModel = firestore.collection("usersStatus"); const photoVerificationModel = firestore.collection("photo-verification"); const { ITEM_TAG, USER_STATE } = ALLOWED_FILTER_PARAMS; const admin = require("firebase-admin"); +const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** * Adds or updates the user data @@ -506,6 +508,14 @@ const getUsersBasedOnFilter = async (query) => { const userRefs = finalItems.map((itemId) => userModel.doc(itemId)); const userDocs = (await firestore.getAll(...userRefs)).map((doc) => ({ id: doc.id, ...doc.data() })); const filteredUserDocs = userDocs.filter((doc) => !doc.roles?.archived); + if (query.time && query.state === "ONBOARDING") { + const fetchUsersWithOnBoardingState = await getUsersWithOnboardingStateInRange( + filteredUserDocs, + stateItems, + query.time + ); + return fetchUsersWithOnBoardingState; + } return filteredUserDocs; } @@ -539,9 +549,29 @@ const getUsersBasedOnFilter = async (query) => { return filteredUsers.filter((user) => !user.roles?.archived); } + return []; }; +const getUsersWithOnboardingStateInRange = async (filteredUserDocs, stateItems, time) => { + const usersInRange = []; + const range = Number(time.split("d")[0]); + const filteredUsers = filteredUserDocs.filter((userDoc) => { + return stateItems.some((stateItem) => stateItem.userId === userDoc.id); + }); + filteredUsers.forEach((user) => { + if (user.discordJoinedAt) { + const userDiscordJoinedDate = new Date(user.discordJoinedAt); + const currentTimeStamp = new Date().getTime(); + const timeDifferenceInMilliseconds = currentTimeStamp - userDiscordJoinedDate.getTime(); + const currentAndUserJoinedDateDifference = Math.floor(timeDifferenceInMilliseconds / (1000 * 60 * 60 * 24)); + if (currentAndUserJoinedDateDifference > range) { + usersInRange.push(user); + } + } + }); + return usersInRange; +}; /** * Fetch all users * @@ -574,16 +604,72 @@ const fetchAllUsers = async () => { return users; }; -const fetchUsersWithToken = async () => { +const archiveUserIfNotInDiscord = async () => { try { - const users = []; - const usersRef = await userModel.where("tokens", "!=", false).get(); - usersRef.forEach((user) => { - users.push(userModel.doc(user.id)); + const snapshot = await userModel.where("roles.in_discord", "==", false).where("roles.archived", "==", false).get(); + const usersNotInDiscord = []; + let summary = { + totalUsers: snapshot.size, + totalUsersArchived: 0, + totalOperationsFailed: 0, + updatedUserDetails: [], + failedUserDetails: [], + }; + + if (snapshot.size === 0) { + return summary; + } + + snapshot.forEach((user) => { + const id = user.id; + const userData = user.data(); + usersNotInDiscord.push({ ...userData, id }); }); + + const userNotInDiscordChunks = chunks(usersNotInDiscord, DOCUMENT_WRITE_SIZE); + for (const users of userNotInDiscordChunks) { + const res = await archiveUsers(users); + summary = { + ...summary, + totalUsersArchived: (summary.totalUsersArchived += res.totalUsersArchived), + totalOperationsFailed: (summary.totalOperationsFailed += res.totalOperationsFailed), + updatedUserDetails: [...summary.updatedUserDetails, ...res.updatedUserDetails], + failedUserDetails: [...summary.failedUserDetails, ...res.failedUserDetails], + }; + } + + if (summary.totalOperationsFailed === summary.totalUsers) { + throw Error(INTERNAL_SERVER_ERROR); + } + + return summary; + } catch (error) { + logger.error(`Error in updating Users archived role: ${error}`); + throw error; + } +}; +/** + * + * @param {[string]} userIds Array id's of user + * @returns Object containing the details of the users whose userId was provided. + */ +const fetchUserByIds = async (userIds = []) => { + if (userIds.length === 0) { + return {}; + } + try { + const users = {}; + const usersRefs = userIds.map((docId) => userModel.doc(docId)); + const documents = await firestore.getAll(...usersRefs); + documents.forEach((snapshot) => { + if (snapshot.exists) { + users[snapshot.id] = snapshot.data(); + } + }); + return users; } catch (err) { - logger.error(`Error while fetching all users with tokens field: ${err}`); + logger.error("Error retrieving user data", err); throw err; } }; @@ -663,7 +749,8 @@ module.exports = { getUserImageForVerification, getDiscordUsers, fetchAllUsers, - fetchUsersWithToken, + archiveUserIfNotInDiscord, removeGitHubToken, getUsersByRole, + fetchUserByIds, }; diff --git a/routes/auth.js b/routes/auth.js index 110d6c34c..6e2a91931 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -13,6 +13,8 @@ router.get("/signout", auth.signout); router.get("/qr-code-auth", userDeviceInfoValidator.validateFetchingUserDocument, auth.fetchUserDeviceInfo); +router.get("/device", authenticate, auth.fetchDeviceDetails); + router.post("/qr-code-auth", userDeviceInfoValidator.storeUserDeviceInfo, auth.storeUserDeviceInfo); router.patch( diff --git a/routes/events.js b/routes/events.js index 9c5df3116..5a2b2ce8c 100644 --- a/routes/events.js +++ b/routes/events.js @@ -10,5 +10,7 @@ router.post("/join", eventsValidator.joinEvent, events.joinEvent); router.get("/:id", eventsValidator.getEventById, events.getEventById); router.patch("/", authenticate, eventsValidator.updateEvent, events.updateEvent); router.patch("/end", authenticate, eventsValidator.endActiveEvent, events.endActiveEvent); +router.post("/:id/peers", authenticate, eventsValidator.addPeerToEvent, events.addPeerToEvent); +router.patch("/:id/peers/kickout", authenticate, eventsValidator.kickoutPeer, events.kickoutPeer); module.exports = router; diff --git a/routes/users.js b/routes/users.js index 5f4612b48..2d4d379c5 100644 --- a/routes/users.js +++ b/routes/users.js @@ -19,14 +19,26 @@ router.get("/self", authenticate, users.getSelfDetails); router.get("/isUsernameAvailable/:username", authenticate, users.getUsernameAvailabilty); router.get("/chaincode", authenticate, users.generateChaincode); router.get("/search", userValidator.validateUserQueryParams, users.filterUsers); -router.post("/tokens", authenticate, authorizeRoles([SUPERUSER]), users.removeTokens); +router.patch( + "/:userId/update-nickname", + authenticate, + authorizeRoles([SUPERUSER]), + checkIsVerifiedDiscord, + users.updateDiscordUserNickname +); router.get("/:username", users.getUser); router.get("/:userId/intro", authenticate, authorizeRoles([SUPERUSER]), users.getUserIntro); router.put("/self/intro", authenticate, userValidator.validateJoinData, users.addUserIntro); router.get("/:id/skills", users.getUserSkills); router.get("/:id/badges", getUserBadges); -router.patch("/", authenticate, authorizeRoles([SUPERUSER]), users.nonVerifiedDiscordUsers); +router.patch( + "/", + authenticate, + authorizeRoles([SUPERUSER]), + userValidator.validateUsersPatchHandler, + users.usersPatchHandler +); router.patch( "/:id/temporary/data", authenticate, diff --git a/services/dataAccessLayer.js b/services/dataAccessLayer.js index 83c4ef3bd..ed2f32488 100644 --- a/services/dataAccessLayer.js +++ b/services/dataAccessLayer.js @@ -1,8 +1,17 @@ const userQuery = require("../models/users"); const members = require("../models/members"); -const { USER_SENSITIVE_DATA } = require("../constants/users"); +const { ROLE_LEVEL, KEYS_NOT_ALLOWED, ACCESS_LEVEL } = require("../constants/userDataLevels"); -const retrieveUsers = async ({ id = null, username = null, usernames = null, query = null, userdata }) => { +const retrieveUsers = async ({ + id = null, + username = null, + usernames = null, + query = null, + userdata, + level = ACCESS_LEVEL.PUBLIC, + role = null, + userIds = null, +}) => { if (id || username) { let result; if (id != null) { @@ -10,66 +19,90 @@ const retrieveUsers = async ({ id = null, username = null, usernames = null, que } else { result = await userQuery.fetchUser({ username: username }); } - removeSensitiveInfo(result.user); + const user = levelSpecificAccess(result.user, level, role); + result.user = user; return result; } else if (usernames) { const { users } = await userQuery.fetchUsers(usernames); - users.forEach((element) => { - removeSensitiveInfo(element); + const result = []; + users.forEach((userdata) => { + const user = levelSpecificAccess(userdata, level, role); + result.push(user); }); - return users; + return result; + } else if (userIds) { + if (userIds.length === 0) { + return {}; + } + const userDetails = await userQuery.fetchUserByIds(userIds); + Object.keys(userDetails).forEach((userId) => { + removeSensitiveInfo(userDetails[userId]); + }); + return userDetails; } else if (query) { const { allUsers, nextId, prevId } = await userQuery.fetchPaginatedUsers(query); - allUsers.forEach((element) => { - removeSensitiveInfo(element); + const users = []; + allUsers.forEach((userdata) => { + const user = levelSpecificAccess(userdata, level, role); + users.push(user); }); - return { allUsers, nextId, prevId }; + return { users, nextId, prevId }; } else { - removeSensitiveInfo(userdata); - return userdata; + const result = await userQuery.fetchUser({ userId: userdata.id }); + return levelSpecificAccess(result.user, level, role); } }; -const retrieveDiscordUsers = async () => { +const retrieveDiscordUsers = async (level = ACCESS_LEVEL.PUBLIC, role = null) => { const users = await userQuery.getDiscordUsers(); - users.forEach((element) => { - removeSensitiveInfo(element); + const usersData = []; + users.forEach((userdata) => { + const user = levelSpecificAccess(userdata, level, role); + usersData.push(user); }); - return users; + return usersData; }; const retreiveFilteredUsers = async (query) => { const users = await userQuery.getUsersBasedOnFilter(query); - users.forEach((element) => { - removeSensitiveInfo(element); + users.forEach((userdata) => { + removeSensitiveInfo(userdata); }); return users; }; const retrieveMembers = async (query) => { const allUsers = await members.fetchUsers(query); - allUsers.forEach((element) => { - removeSensitiveInfo(element); + allUsers.forEach((userdata) => { + removeSensitiveInfo(userdata); }); return allUsers; }; const retrieveUsersWithRole = async (role) => { const users = await members.fetchUsersWithRole(role); - users.forEach((element) => { - removeSensitiveInfo(element); + users.forEach((userdata) => { + removeSensitiveInfo(userdata); }); return users; }; -const removeSensitiveInfo = function (obj) { - for (let i = 0; i < USER_SENSITIVE_DATA.length; i++) { - if (Object.prototype.hasOwnProperty.call(obj, USER_SENSITIVE_DATA[i])) { - delete obj[USER_SENSITIVE_DATA[i]]; +const removeSensitiveInfo = function (obj, level = ACCESS_LEVEL.PUBLIC) { + for (let i = 0; i < KEYS_NOT_ALLOWED[level].length; i++) { + if (Object.prototype.hasOwnProperty.call(obj, KEYS_NOT_ALLOWED[level][i])) { + delete obj[KEYS_NOT_ALLOWED[level][i]]; } } }; +const levelSpecificAccess = (user, level = ACCESS_LEVEL.PUBLIC, role = null) => { + if (level === ACCESS_LEVEL.PUBLIC || ROLE_LEVEL[level].includes(role)) { + removeSensitiveInfo(user, level); + return user; + } + return "unauthorized"; +}; + module.exports = { retrieveUsers, removeSensitiveInfo, @@ -77,4 +110,5 @@ module.exports = { retrieveMembers, retrieveUsersWithRole, retreiveFilteredUsers, + levelSpecificAccess, }; diff --git a/services/discordService.js b/services/discordService.js index bc28be382..b7eb2aa2f 100644 --- a/services/discordService.js +++ b/services/discordService.js @@ -68,9 +68,28 @@ const removeRoleFromUser = async (roleId, discordId) => { } }; +const setUserDiscordNickname = async (userName, discordId) => { + try { + const authToken = await generateAuthTokenForCloudflare(); + + const response = await ( + await fetch(`${DISCORD_BASE_URL}/guild/member`, { + method: "PATCH", + body: JSON.stringify({ userName, discordId }), + headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` }, + }) + ).json(); + return response; + } catch (err) { + logger.error("Error in updating discord Nickname", err); + throw err; + } +}; + module.exports = { getDiscordMembers, setInDiscordFalseScript, addRoleToUser, removeRoleFromUser, + setUserDiscordNickname, }; diff --git a/services/users.js b/services/users.js new file mode 100644 index 000000000..1da2d6f63 --- /dev/null +++ b/services/users.js @@ -0,0 +1,46 @@ +const { USERS_PATCH_HANDLER_SUCCESS_MESSAGES, USERS_PATCH_HANDLER_ERROR_MESSAGES } = require("../constants/users"); +const firestore = require("../utils/firestore"); +const userModel = firestore.collection("users"); + +const 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, + }, + }; + 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 }; + } +}; + +module.exports = { + archiveUsers, +}; diff --git a/test/fixtures/discordResponse/discord-response.js b/test/fixtures/discordResponse/discord-response.js index ad9e4699f..2d34e03dd 100644 --- a/test/fixtures/discordResponse/discord-response.js +++ b/test/fixtures/discordResponse/discord-response.js @@ -89,7 +89,17 @@ const usersFromRds = [ }, ]; +const updatedNicknameResponse = { + userAffected: { + userId: "X0H3paYveEWh7Q2fPhor", + username: "test-name-007", + discordId: "1123566257019568232", + }, + message: "User nickname changed successfully", +}; + module.exports = { getDiscordMembers, usersFromRds, + updatedNicknameResponse, }; diff --git a/test/fixtures/discordactions/discordactions.js b/test/fixtures/discordactions/discordactions.js index f14c57b52..0a84b617d 100644 --- a/test/fixtures/discordactions/discordactions.js +++ b/test/fixtures/discordactions/discordactions.js @@ -1,7 +1,7 @@ const groupData = [ - { id: "1", name: "Group 1" }, - { id: "2", name: "Group 2" }, - { id: "3", name: "Group 3" }, + { rolename: "Group 1", roleid: 1 }, + { rolename: "Group 2", roleid: 2 }, + { rolename: "Group 3", roleid: 3 }, ]; const roleData = { diff --git a/test/fixtures/logs/archievedUsers.js b/test/fixtures/logs/archievedUsers.js new file mode 100644 index 000000000..66f9b3497 --- /dev/null +++ b/test/fixtures/logs/archievedUsers.js @@ -0,0 +1,68 @@ +const archivedUserDetailsModal = [ + { + type: "archived-details", + meta: {}, + body: { + reason: "test reason", + archived_user: { user_id: "R5kljdsleH4Gr2t7tvr0Z", username: "testUser1" }, + archived_by: { + user_id: "ReMyuklislajwooncVL", + roles: { + in_discord: true, + super_user: false, + member: true, + archived: false, + }, + }, + }, + timestamp: { + _seconds: 1657193216, + _nanoseconds: 912000000, + }, + }, + { + type: "archived-details", + meta: {}, + body: { + reason: "test reason", + archived_user: { user_id: "R5kljdsleH4Gr2t7tvr0Z", username: "testUser1" }, + archived_by: { + user_id: "ReMyuklislajwooncVL", + roles: { + in_discord: true, + super_user: false, + member: true, + archived: false, + }, + }, + }, + timestamp: { + _seconds: 1657193216, + _nanoseconds: 912000000, + }, + }, + { + type: "archived-details", + meta: {}, + body: { + reason: "test reason", + archived_user: { user_id: "Efskee4Gr2t7tvr0Z", username: "testUser2" }, + archived_by: { + user_id: "ReMyuklislajwooncVL", + roles: { + in_discord: true, + super_user: false, + member: true, + archived: false, + }, + }, + }, + timestamp: { + _seconds: 1657193216, + _nanoseconds: 912000000, + }, + }, +]; +module.exports = { + archivedUserDetailsModal, +}; diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index caf2735c5..63ebd591f 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -19,9 +19,11 @@ module.exports = () => { linkedin_id: "ankurnarkhede", github_id: githubUserInfo[0].username, github_display_name: githubUserInfo[0].displayName, + github_created_at: Number(new Date(githubUserInfo[0]._json.created_at).getTime()), isMember: true, phone: "1234567890", email: "abc@gmail.com", + discordJoinedAt: "2023-04-06T01:47:34.488000+00:00", joined_discord: "2023-01-13T18:21:09.278000+00:00", roles: { member: true, @@ -41,12 +43,14 @@ module.exports = () => { { username: "nikhil", first_name: "Nikhil", + discordId: "1234567890", last_name: "Bhandarkar", yoe: 0, img: "./img.png", github_id: "whydonti", linkedin_id: "nikhil-bhandarkar", twitter_id: "whatifi", + discordJoinedAt: "2023-04-06T01:47:34.488000+00:00", phone: "1234567891", email: "abc1@gmail.com", picture: { @@ -63,6 +67,7 @@ module.exports = () => { github_id: "cartmanishere", linkedin_id: "pranav-gajjewar", twitter_id: "PGajjewar", + discordJoinedAt: "2023-04-06T01:47:34.488000+00:00", phone: "1234567891", email: "pgajjewar@gmail.com", roles: { @@ -82,6 +87,7 @@ module.exports = () => { linkedin_id: "sagarbajpai", github_id: "sagarbajpai", github_display_name: "Sagar Bajpai", + discordJoinedAt: "2023-04-06T01:47:34.488000+00:00", phone: "1234567890", email: "abc@gmail.com", status: "active", @@ -267,12 +273,6 @@ module.exports = () => { linkedin_id: "testuser1", github_id: "testuser1", github_display_name: "Test User", - phone: "1234567890", - email: "tu@gmail.com", - chaincode: "1234", - tokens: { - githubAccessToken: "githubAccessToken", - }, roles: { member: true, }, @@ -329,6 +329,29 @@ module.exports = () => { twitter_id: "ramsingh123", linkedin_id: "ramsingh123", }, + { + username: "testuser3", + first_name: "test3", + last_name: "user3", + yoe: 1, + img: "./img.png", + linkedin_id: "testuser1", + github_id: "testuser", + github_display_name: "Test User 3", + phone: "1234567890", + email: "abcd@gmail.com", + chaincode: "12345", + tokens: { + githubAccessToken: "githubAccessToken", + }, + roles: { + member: true, + }, + picture: { + publicId: "profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar", + url: "https://res.cloudinary.com/realdevsquad/image/upload/v1667685133/profile/mtS4DhUvNYsKqI7oCWVB/aenklfhtjldc5ytei3ar.jpg", + }, + }, { username: "sahsisunny", first_name: "sunny", diff --git a/test/integration/discord.test.js b/test/integration/discord.test.js index 5147efee3..9a801157c 100644 --- a/test/integration/discord.test.js +++ b/test/integration/discord.test.js @@ -8,6 +8,14 @@ const authService = require("../../services/authService"); const userData = require("../fixtures/user/user")(); const { requestRoleData } = require("../fixtures/discordactions/discordactions"); +const firestore = require("../../utils/firestore"); +const discordRoleModel = firestore.collection("discord-roles"); +const userModel = firestore.collection("users"); + +const { addGroupRoleToMember } = require("../../models/discordactions"); + +const { groupData } = require("../fixtures/discordactions/discordactions"); + const cookieName = config.get("userToken.cookieName"); let userId; @@ -59,6 +67,29 @@ describe("test discord actions", function () { const user = { ...userData[4], discordId: "123456789" }; userId = await addUser(user); jwt = authService.generateAuthToken({ userId }); + + let allIds = []; + + const addUsersPromises = userData.map((user) => userModel.add({ ...user })); + const responses = await Promise.all(addUsersPromises); + allIds = responses.map((response) => response.id); + + const addRolesPromises = [ + discordRoleModel.add({ roleid: groupData[0].roleid, rolename: groupData[0].rolename, createdBy: allIds[1] }), + discordRoleModel.add({ roleid: groupData[1].roleid, rolename: groupData[1].rolename, createdBy: allIds[0] }), + ]; + await Promise.all(addRolesPromises); + + const addGroupRolesPromises = [ + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[0] }), + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[1] }), + addGroupRoleToMember({ roleid: groupData[0].roleid, userid: allIds[1] }), + addGroupRoleToMember({ roleid: groupData[1].roleid, userid: allIds[0] }), + ]; + await Promise.all(addGroupRolesPromises); + }); + afterEach(async function () { + await cleanDb(); }); it("returns 200 for active users get method", function (done) { diff --git a/test/integration/discordactions.test.js b/test/integration/discordactions.test.js index 74ec16085..dc8062710 100644 --- a/test/integration/discordactions.test.js +++ b/test/integration/discordactions.test.js @@ -16,6 +16,11 @@ const cookieName = config.get("userToken.cookieName"); const firestore = require("../../utils/firestore"); const { userPhotoVerificationData } = require("../fixtures/user/photo-verification"); const photoVerificationModel = firestore.collection("photo-verification"); +const discordRoleModel = firestore.collection("discord-roles"); +const userModel = firestore.collection("users"); + +const { groupData } = require("../fixtures/discordactions/discordactions"); +const { addGroupRoleToMember } = require("../../models/discordactions"); chai.use(chaiHttp); describe("Discord actions", function () { @@ -85,4 +90,81 @@ describe("Discord actions", function () { }); }); }); + + describe("GET /discord-actions/groups", function () { + let newGroupData; + let allIds = []; + before(async function () { + const addUsersPromises = userData.map((user) => userModel.add({ ...user })); + const responses = await Promise.all(addUsersPromises); + allIds = responses.map((response) => response.id); + newGroupData = groupData.map((group, index) => { + return { + ...group, + createdBy: allIds[Math.min(index, allIds.length - 1)], + }; + }); + + const addRolesPromises = [ + discordRoleModel.add(newGroupData[0]), + discordRoleModel.add(newGroupData[1]), + discordRoleModel.add(newGroupData[2]), + ]; + await Promise.all(addRolesPromises); + + const addGroupRolesPromises = [ + addGroupRoleToMember({ roleid: newGroupData[0].roleid, userid: userData[0].discordId }), + addGroupRoleToMember({ roleid: newGroupData[0].roleid, userid: userData[1].discordId }), + addGroupRoleToMember({ roleid: newGroupData[1].roleid, userid: userData[0].discordId }), + ]; + await Promise.all(addGroupRolesPromises); + }); + + after(async function () { + await cleanDb(); + }); + + it("should successfully return old groups detail", function (done) { + chai + .request(app) + .get(`/discord-actions/groups`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body).to.be.an("object"); + // Verify presence of specific properties in each group + const expectedProps = ["roleid", "rolename", "memberCount", "firstName", "lastName", "image", "isMember"]; + res.body.groups.forEach((group) => { + expect(group).not.to.include.all.keys(expectedProps); + }); + expect(res.body.message).to.equal("Roles fetched successfully!"); + return done(); + }); + }); + it("should successfully return new groups detail when flag is set", function (done) { + chai + .request(app) + .get(`/discord-actions/groups?dev=true`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body).to.be.an("object"); + // Verify presence of specific properties in each group + const expectedProps = ["roleid", "rolename", "memberCount", "firstName", "lastName", "image", "isMember"]; + res.body.groups.forEach((group) => { + expect(group).to.include.all.keys(expectedProps); + }); + expect(res.body.message).to.equal("Roles fetched successfully!"); + return done(); + }); + }); + }); }); diff --git a/test/integration/extensionRequests.test.js b/test/integration/extensionRequests.test.js index d1ab13dc3..6ea9c66f1 100644 --- a/test/integration/extensionRequests.test.js +++ b/test/integration/extensionRequests.test.js @@ -24,7 +24,7 @@ const superUser = userData[4]; let appOwnerjwt, superUserJwt, jwt; describe("Extension Requests", function () { - let taskId1, taskId2, taskId3, extensionRequestId1, extensionRequestId2; + let taskId0, taskId1, taskId2, taskId3, extensionRequestId1, extensionRequestId2; before(async function () { const userId = await addUser(user); @@ -37,6 +37,19 @@ describe("Extension Requests", function () { jwt = authService.generateAuthToken({ userId: userId }); const taskData = [ + { + title: "Test task 1", + type: "feature", + endsOn: 1234, + startedOn: 4567, + status: "active", + percentCompleted: 10, + participants: [], + assignee: appOwner.username, + isNoteworthy: true, + completionAward: { [DINERO]: 3, [NEELAM]: 300 }, + lossRate: { [DINERO]: 1 }, + }, { title: "Test task", type: "feature", @@ -83,13 +96,14 @@ describe("Extension Requests", function () { ]; // Add the active task - taskId1 = (await tasks.updateTask(taskData[0])).taskId; + taskId0 = (await tasks.updateTask(taskData[0])).taskId; + taskId1 = (await tasks.updateTask(taskData[1])).taskId; // Add the completed task - taskId2 = (await tasks.updateTask(taskData[1])).taskId; + taskId2 = (await tasks.updateTask(taskData[2])).taskId; // Add the completed task - taskId3 = (await tasks.updateTask(taskData[2])).taskId; + taskId3 = (await tasks.updateTask(taskData[3])).taskId; const extensionRequest = { taskId: taskId3, @@ -214,6 +228,56 @@ describe("Extension Requests", function () { return done(); }); }); + it("Should return success response after adding the extension request (sending assignee username)", function (done) { + chai + .request(app) + .post("/extension-requests") + .set("cookie", `${cookieName}=${appOwnerjwt}`) + .send({ + taskId: taskId0, + title: "change ETA", + assignee: appOwner.username, + oldEndsOn: 1234, + newEndsOn: 1235, + reason: "family event", + status: "PENDING", + }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("Extension Request created successfully!"); + expect(res.body.extensionRequest).to.be.a("object"); + expect(res.body.extensionRequest.assignee).to.equal(appOwner.id); + expect(res.body.extensionRequest.status).to.equal(EXTENSION_REQUEST_STATUS.PENDING); + return done(); + }); + }); + it("Should return failure response after adding the extension request (sending wrong assignee info)", function (done) { + chai + .request(app) + .post("/extension-requests") + .set("cookie", `${cookieName}=${appOwnerjwt}`) + .send({ + taskId: taskId0, + title: "change ETA", + assignee: "hello", + oldEndsOn: 1234, + newEndsOn: 1235, + reason: "family event", + status: "PENDING", + }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(400); + expect(res.body.message).to.equal("User with this id or username doesn't exist."); + return done(); + }); + }); it("Should return fail response if someone try to create a extension request for someone else and is not a super user", function (done) { chai .request(app) @@ -235,7 +299,9 @@ describe("Extension Requests", function () { expect(res).to.have.status(403); expect(res.body).to.be.a("object"); - expect(res.body.message).to.equal("Only Super User can create an extension request for this task."); + expect(res.body.message).to.equal( + "Only assigned user and super user can create an extension request for this task." + ); return done(); }); }); diff --git a/test/integration/members.test.js b/test/integration/members.test.js index 7788b8f0e..5da4a06aa 100644 --- a/test/integration/members.test.js +++ b/test/integration/members.test.js @@ -12,6 +12,9 @@ const userData = require("../fixtures/user/user")(); const config = require("config"); const cookieName = config.get("userToken.cookieName"); +const Sinon = require("sinon"); +const { INTERNAL_SERVER_ERROR } = require("../../constants/errorMessages"); +const members = require("../../models/members"); chai.use(chaiHttp); @@ -258,16 +261,42 @@ describe("Members", function () { }); describe("PATCH /members/archiveMembers/:username", function () { + let archiveRoleToMemberStub; beforeEach(async function () { const superUserId = await addUser(superUser); jwt = authService.generateAuthToken({ userId: superUserId }); }); + afterEach(async function () { + Sinon.restore(); + await cleanDb(); + }); + it("Should return an object with status 500 and an error message", function (done) { + archiveRoleToMemberStub = Sinon.stub(members, "addArchiveRoleToMembers"); + archiveRoleToMemberStub.throws(new Error(INTERNAL_SERVER_ERROR)); + addUser(userToBeArchived).then(() => { + chai + .request(app) + .patch(`/members/archiveMembers/${userToBeArchived.username}`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ reason: "some reason" }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(500); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal(INTERNAL_SERVER_ERROR); + return done(); + }); + }); + }); it("Should return 404 if user doesn't exist", function (done) { chai .request(app) .patch(`/members/archiveMembers/${userDoesNotExists.username}`) .set("cookie", `${cookieName}=${jwt}`) + .send({ reason: "some reason" }) .end((err, res) => { if (err) { return done(err); @@ -278,13 +307,13 @@ describe("Members", function () { return done(); }); }); - it("Should archive the user", function (done) { addUser(userToBeArchived).then(() => { chai .request(app) .patch(`/members/archiveMembers/${userToBeArchived.username}`) .set("cookie", `${cookieName}=${jwt}`) + .send({ reason: "some reason" }) .end((err, res) => { if (err) { return done(err); @@ -305,6 +334,7 @@ describe("Members", function () { .request(app) .patch(`/members/archiveMembers/${userAlreadyArchived.username}`) .set("cookie", `${cookieName}=${jwt}`) + .send({ reason: "some reason" }) .end((err, res) => { if (err) { return done(err); @@ -318,26 +348,5 @@ describe("Members", function () { }); }); }); - - it("Should return 401 if user is not a super user", function (done) { - addUser(nonSuperUser).then((nonSuperUserId) => { - const nonSuperUserJwt = authService.generateAuthToken({ userId: nonSuperUserId }); - chai - .request(app) - .patch(`/members/moveToMembers/${nonSuperUser.username}`) - .set("cookie", `${cookieName}=${nonSuperUserJwt}`) - .end((err, res) => { - if (err) { - return done(err); - } - - expect(res).to.have.status(401); - expect(res.body).to.be.a("object"); - expect(res.body.message).to.equal("You are not authorized for this action."); - - return done(); - }); - }); - }); }); }); diff --git a/test/integration/qrCodeAuth.test.js b/test/integration/qrCodeAuth.test.js index 47d04d3b0..ac788d747 100644 --- a/test/integration/qrCodeAuth.test.js +++ b/test/integration/qrCodeAuth.test.js @@ -10,10 +10,12 @@ const qrCodeAuthModel = require("../../models/qrCodeAuth"); const authService = require("../../services/authService"); const config = require("config"); const cookieName = config.get("userToken.cookieName"); +const USER_DOES_NOT_EXIST_ERROR = "User does not exist!"; // Import fixtures let userDeviceInfoData; let wrongUserDeviceInfoData; +let wrongUserIdDeviceInfo; let userId; const user = userData[0]; @@ -21,8 +23,10 @@ describe("QrCodeAuth", function () { describe("POST call for adding user", function () { beforeEach(async function () { userId = await addUser(user); + userDeviceInfoData = { ...userDeviceInfoDataArray[0], user_id: userId }; wrongUserDeviceInfoData = userDeviceInfoDataArray[0]; + wrongUserIdDeviceInfo = { ...userDeviceInfoDataArray[0], user_id: userId, device_info: 2 }; }); afterEach(async function () { await cleanDb(); @@ -48,21 +52,39 @@ describe("QrCodeAuth", function () { }); }); - it("Should return a 500 status code and the correct error message when an error occurs while storing user device info", function (done) { + it("should fail with 404, when the user is not found", function (done) { chai .request(app) .post("/auth/qr-code-auth") .send(wrongUserDeviceInfoData) .end((err, res) => { if (err) { - return done(); + return done(err); } - expect(res).to.have.status(500); - expect(res.body).to.eql({ - statusCode: 500, - error: "Internal Server Error", - message: "An internal server error occurred", - }); + + expect(res).to.have.status(404); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal(USER_DOES_NOT_EXIST_ERROR); + expect(res.body.error).to.equal("Not Found"); + + return done(); + }); + }); + + it("should throw 400, if the validation of the values passed in the body does not pass", function (done) { + chai + .request(app) + .post("/auth/qr-code-auth") + .send(wrongUserIdDeviceInfo) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal('"device_info" must be a string'); + expect(res.body.error).to.equal("Bad Request"); return done(); }); diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 668411976..870dff919 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -61,6 +61,7 @@ describe("Tasks", function () { completionAward: { [DINERO]: 3, [NEELAM]: 300 }, lossRate: { [DINERO]: 1 }, isNoteworthy: false, + assignee: appOwner.username, }, ]; @@ -210,6 +211,47 @@ describe("Tasks", function () { }); }); + it("Should get all tasks filtered with status ,assignee, title when passed to GET /tasks", function (done) { + chai + .request(app) + .get(`/tasks?status=${TASK_STATUS.AVAILABLE}&dev=true&assignee=sagar&title=Test`) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("Tasks returned successfully!"); + expect(res.body.tasks).to.be.a("array"); + expect(res.body).to.have.property("next"); + expect(res.body).to.have.property("prev"); + + const tasksData = res.body.tasks ?? []; + tasksData.forEach((task) => { + expect(task.status).to.equal(TASK_STATUS.AVAILABLE); + expect(task.assignee).to.equal("sagar"); + expect(task.title).to.include("Test"); + }); + return done(); + }); + }); + + it("Should get all overdue tasks GET /tasks", function (done) { + chai + .request(app) + .get(`/tasks?dev=true&status=overdue`) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.tasks[0].id).to.be.oneOf([taskId, taskId1]); + return done(); + }); + }); + it("Should get tasks when correct query parameters are passed", function (done) { chai .request(app) @@ -767,7 +809,7 @@ describe("Tasks", function () { }); it("Should return Forbidden error if task is not assigned to self", async function () { - const { userId } = await addUser(userData[0]); + const userId = await addUser(userData[0]); const jwt = authService.generateAuthToken({ userId }); const res = await chai.request(app).patch(`/tasks/self/${taskId1}`).set("cookie", `${cookieName}=${jwt}`); diff --git a/test/integration/users.test.js b/test/integration/users.test.js index 7f3f58272..5d90c29bf 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -15,7 +15,7 @@ const superUser = userData[4]; const searchParamValues = require("../fixtures/user/search")(); const config = require("config"); -const { getDiscordMembers } = require("../fixtures/discordResponse/discord-response"); +const { getDiscordMembers, updatedNicknameResponse } = require("../fixtures/discordResponse/discord-response"); const joinData = require("../fixtures/user/join"); const { userStatusDataAfterSignup, @@ -45,6 +45,7 @@ describe("Users", function () { let superUserId; let superUserAuthToken; let userId = ""; + let fetchStub; beforeEach(async function () { userId = await addUser(); @@ -290,7 +291,6 @@ describe("Users", function () { expect(res.body.users).to.be.a("array"); expect(res.body.users[0]).to.not.have.property("phone"); expect(res.body.users[0]).to.not.have.property("email"); - expect(res.body.users[0]).to.not.have.property("tokens"); expect(res.body.users[0]).to.not.have.property("chaincode"); return done(); @@ -315,7 +315,6 @@ describe("Users", function () { }); expect(res.body.users[0]).to.not.have.property("phone"); expect(res.body.users[0]).to.not.have.property("email"); - expect(res.body.users[0]).to.not.have.property("tokens"); expect(res.body.users[0]).to.not.have.property("chaincode"); return done(); }); @@ -341,7 +340,6 @@ describe("Users", function () { expect(res.body.users.length).to.equal(1); expect(res.body.users[0]).to.not.have.property("phone"); expect(res.body.users[0]).to.not.have.property("email"); - expect(res.body.users[0]).to.not.have.property("tokens"); expect(res.body.users[0]).to.not.have.property("chaincode"); return done(); }); @@ -552,31 +550,11 @@ describe("Users", function () { expect(res.body).to.be.a("object"); expect(res.body).to.not.have.property("phone"); expect(res.body).to.not.have.property("email"); - expect(res.body).to.not.have.property("tokens"); expect(res.body).to.not.have.property("chaincode"); return done(); }); }); - it("Should return details with phone and email when query 'private' is true", function (done) { - chai - .request(app) - .get("/users/self") - .query({ private: true }) - .set("cookie", `${cookieName}=${jwt}`) - .end((err, res) => { - if (err) { - return done(); - } - - expect(res).to.have.status(200); - expect(res.body).to.be.a("object"); - expect(res.body).to.have.property("phone"); - expect(res.body).to.have.property("email"); - return done(); - }); - }); - it("Should return 401 if not logged in", function (done) { chai .request(app) @@ -616,7 +594,6 @@ describe("Users", function () { expect(res.body.user).to.be.a("object"); expect(res.body.user).to.not.have.property("phone"); expect(res.body.user).to.not.have.property("email"); - expect(res.body.user).to.not.have.property("tokens"); expect(res.body.user).to.not.have.property("chaincode"); return done(); }); @@ -658,7 +635,6 @@ describe("Users", function () { expect(res.body.user).to.be.a("object"); expect(res.body.user).to.not.have.property("phone"); expect(res.body.user).to.not.have.property("email"); - expect(res.body.user).to.not.have.property("tokens"); expect(res.body.user).to.not.have.property("chaincode"); return done(); }); @@ -1640,28 +1616,203 @@ describe("Users", function () { }); }); - describe("POST /users/tokens", function () { - before(async function () { - await addOrUpdate(userData[0]); - await addOrUpdate(userData[1]); - await addOrUpdate(userData[2]); - await addOrUpdate(userData[3]); + describe("PATCH /users", function () { + let userId1; + let userId2; + let userId3; + + beforeEach(async function () { + const rolesToBeAdded = { + archived: false, + in_discord: false, + }; + userId1 = await addUser({ ...userData[0], roles: rolesToBeAdded }); + userId2 = await addUser({ ...userData[1], roles: rolesToBeAdded }); + userId3 = await addUser({ ...userData[2], roles: rolesToBeAdded }); }); - after(async function () { + + afterEach(async function () { await cleanDb(); + Sinon.restore(); }); - it("should remove all the users with token field", function (done) { + + it("should return 400 if payload is not passed correctly", function (done) { chai .request(app) - .post("/users/tokens") + .patch("/users") + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .send() + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(400); + expect(res.body.message).to.equal('Invalid Payload: "action" is required'); + return done(); + }); + }); + + it("should returns successful response for api archiveUsersIfNotInDiscord", function (done) { + chai + .request(app) + .patch("/users") + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .send({ action: "archiveUsers" }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.summary).to.have.all.keys(["totalUsersArchived", "totalOperationsFailed", "totalUsers"]); + expect(res.body.summary).to.not.have.property("updatedUserIds"); + expect(res.body.summary.totalUsersArchived).to.be.equal(3); + expect(res.body.summary.totalUsers).to.be.equal(3); + expect(res.body.summary.totalOperationsFailed).to.be.equal(0); + expect(res.body.message).to.equal( + "Successfully updated users archived role to true if in_discord role is false" + ); + return done(); + }); + }); + + it("should return proper response if no documents are found to update for api archiveUsersIfNotInDiscord", async function () { + const roles = { + archived: true, + in_discord: false, + }; + await addOrUpdate({ ...userData[0], roles }, userId1); + await addOrUpdate({ ...userData[1], roles }, userId2); + await addOrUpdate({ ...userData[2], roles }, userId3); + + const res = await chai + .request(app) + .patch("/users") + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .send({ action: "archiveUsers" }); + + expect(res).to.have.status(200); + expect(res.body.summary).to.have.all.keys(["totalUsersArchived", "totalOperationsFailed", "totalUsers"]); + expect(res.body.summary).to.not.have.property("updatedUserIds"); + expect(res.body.summary.totalUsers).to.be.equal(0); + expect(res.body.summary.totalUsersArchived).to.be.equal(0); + expect(res.body.summary.totalOperationsFailed).to.be.equal(0); + expect(res.body.message).to.equal("Couldn't find any users currently inactive in Discord but not archived."); + }); + + it("should throw an error if firestore batch operations fail for api archiveUsersIfNotInDiscord", async function () { + const stub = Sinon.stub(firestore, "batch"); + stub.returns({ + update: function () {}, + commit: function () { + throw new Error("Firestore batch commit failed!"); + }, + }); + + const res = await chai + .request(app) + .patch(`/users`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .send({ action: "archiveUsers" }); + + expect(res.status).to.equal(500); + const response = res.body; + expect(response.message).to.be.equal("An internal server error occurred"); + }); + + it("should return correct response if debug query is passed for api archiveUsersIfNotInDiscord", function (done) { + chai + .request(app) + .patch("/users?debug=true") + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .send({ action: "archiveUsers" }) + .end((err, res) => { + if (err) { + return done(err); + } + + expect(res).to.have.status(200); + expect(res.body.summary).to.have.all.keys([ + "totalUsersArchived", + "totalOperationsFailed", + "totalUsers", + "updatedUserDetails", + "failedUserDetails", + ]); + expect(res.body.summary.totalUsersArchived).to.be.equal(3); + expect(res.body.summary.totalUsers).to.be.equal(3); + expect(res.body.summary.totalOperationsFailed).to.be.equal(0); + expect(res.body.summary.updatedUserDetails.length).to.equal(3); + expect(res.body.summary.failedUserDetails.length).to.equal(0); + expect(res.body.message).to.equal( + "Successfully updated users archived role to true if in_discord role is false" + ); + return done(); + }); + }); + }); + describe("PATCH /:userId/update-nickname", function () { + beforeEach(async function () { + fetchStub = Sinon.stub(global, "fetch"); + userId = await addUser(userData[0]); + }); + afterEach(async function () { + await cleanDb(); + Sinon.restore(); + }); + it("returns 200 for successfully updating nickname with patch method", function (done) { + fetchStub.returns( + Promise.resolve({ + status: 200, + json: () => Promise.resolve(updatedNicknameResponse), + }) + ); + chai + .request(app) + .patch(`/users/${userId}/update-nickname`) .set("Cookie", `${cookieName}=${superUserAuthToken}`) .end((err, res) => { if (err) { return done(err); } expect(res).to.have.status(200); - expect(res.body.message).to.be.equal("Github Token removed from all users!"); - expect(res.body.usersFound).to.be.equal(3); + expect(res.body.message.message).to.be.equal("User nickname changed successfully"); + return done(); + }); + }); + }); + + describe("test discord actions of nickname for unverified user", function () { + beforeEach(async function () { + fetchStub = Sinon.stub(global, "fetch"); + const superUser = userData[4]; + userId = await addUser(userData[2]); + superUserId = await addUser(superUser); + superUserAuthToken = authService.generateAuthToken({ userId: superUserId }); + }); + afterEach(async function () { + await cleanDb(); + Sinon.restore(); + }); + it("throw error if discordId is not present and user is not verified", function (done) { + fetchStub.returns({ + update: function () {}, + commit: function () { + throw new Error("User not verified"); + }, + }); + chai + .request(app) + .patch(`/users/${userId}/update-nickname`) + .set("Cookie", `${cookieName}=${superUserAuthToken}`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(500); + const response = res.body; + expect(response.message).to.be.equal("An internal server error occurred"); return done(); }); }); diff --git a/test/integration/usersFilter.test.js b/test/integration/usersFilter.test.js index 9e0f5ed97..e619b24a9 100644 --- a/test/integration/usersFilter.test.js +++ b/test/integration/usersFilter.test.js @@ -177,6 +177,29 @@ describe("Filter Users", function () { }); }); + it("Should search users based on Onboarding state and discord join more then 31 days", function (done) { + chai + .request(app) + .get("/users/search") + .query({ state: "ONBOARDING", time: "31d" }) + .set("cookie", `${cookieName}=${jwt}`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body).to.be.a("object"); + expect(res.body.count).to.be.a("number"); + expect(res.body.message).to.equal("Users found successfully!"); + expect(res.body.users).to.be.a("array"); + expect(res.body.users.length).to.equal(1); + res.body.users.forEach((user) => { + expect(user).to.have.property("discordJoinedAt"); + }); + return done(); + }); + }); + it("Should search users based on Tag", function (done) { chai .request(app) @@ -365,7 +388,6 @@ describe("Filter Users", function () { res.body.users.forEach((user) => { expect(user).to.not.have.property("phone"); expect(user).to.not.have.property("email"); - expect(user).to.not.have.property("tokens"); }); return done(); }); diff --git a/test/unit/middlewares/qrCodeAuthValidator.test.js b/test/unit/middlewares/qrCodeAuthValidator.test.js index 63606b5c9..0dfd69905 100644 --- a/test/unit/middlewares/qrCodeAuthValidator.test.js +++ b/test/unit/middlewares/qrCodeAuthValidator.test.js @@ -29,7 +29,11 @@ describe("qrCodeAuth", function () { }, }; - const res = {}; + const res = { + boom: { + badRequest: () => {}, + }, + }; const nextSpy = Sinon.spy(); await storeUserDeviceInfo(req, res, nextSpy); diff --git a/test/unit/middlewares/tasks-validator.test.js b/test/unit/middlewares/tasks-validator.test.js index 362c4ac4e..a00eb48db 100644 --- a/test/unit/middlewares/tasks-validator.test.js +++ b/test/unit/middlewares/tasks-validator.test.js @@ -383,4 +383,19 @@ describe("getTasks validator", function () { await getTasksValidator(req, res, nextMiddlewareSpy); expect(nextMiddlewareSpy.callCount).to.be.equal(0); }); + + it("should pass the request when correct parameters are passed: assignee, dev, status and title", async function () { + const req = { + query: { + dev: "true", + assignee: "assignee", + title: "title", + status: TASK_STATUS.ASSIGNED, + }, + }; + const res = {}; + const nextMiddlewareSpy = Sinon.spy(); + await getTasksValidator(req, res, nextMiddlewareSpy); + expect(nextMiddlewareSpy.callCount).to.be.equal(1); + }); }); diff --git a/test/unit/middlewares/tasks.test.js b/test/unit/middlewares/tasks.test.js new file mode 100644 index 000000000..fe738904e --- /dev/null +++ b/test/unit/middlewares/tasks.test.js @@ -0,0 +1,64 @@ +const { updateTask } = require("../../../middlewares/validators/tasks"); // Replace with the actual path to your updateTask module +const { expect } = require("chai"); +const sinon = require("sinon"); + +describe("updateTask function", function () { + // Helper function to create a request object with a specific body + const createRequest = (body) => ({ body }); + + // Helper function to create a response object with a mocked boom function + const createResponse = () => ({ + boom: { + badRequest: sinon.stub().returns({ error: true, message: "Bad Request" }), + }, + }); + + // Helper function to create a next function that simply calls done + const createNext = () => sinon.stub(); + + afterEach(function () { + sinon.restore(); + }); + + it("validates a valid request body", async function () { + const validRequestBody = { + title: "Sample Task", + purpose: "Test purposes", + type: "Sample Type", + status: "active", + isNoteworthy: true, + isCollapsed: false, + }; + + const req = createRequest(validRequestBody); + const res = createResponse(); + const next = createNext(); + + await updateTask(req, res, next); + + expect(res.boom.badRequest.calledOnce).to.be.equal(false); + expect(next.calledOnce).to.be.equal(true); + }); + + it("handles invalid request body", async function () { + const invalidRequestBody = { + // Missing required fields, or incorrect data types + title: 123, + purpose: 456, + type: true, + status: "invalid_status", + isNoteworthy: "yes", + isCollapsed: "no", + assignee: "", + }; + + const req = createRequest(invalidRequestBody); + const res = createResponse(); + const next = createNext(); + + await updateTask(req, res, next); + + expect(res.boom.badRequest.calledOnce).to.be.equal(true); + expect(next.calledOnce).to.be.equal(false); + }); +}); diff --git a/test/unit/middlewares/user-validator.test.js b/test/unit/middlewares/user-validator.test.js index aa16bd9bf..fc38abdf7 100644 --- a/test/unit/middlewares/user-validator.test.js +++ b/test/unit/middlewares/user-validator.test.js @@ -1,5 +1,5 @@ const sinon = require("sinon"); -const { validateJoinData } = require("./../../../middlewares/validators/user"); +const { validateJoinData, validateUsersPatchHandler } = require("./../../../middlewares/validators/user"); const joinData = require("./../../fixtures/user/join"); const userData = require("./../../fixtures/user/user"); const { expect } = require("chai"); @@ -37,6 +37,56 @@ describe("Middleware | Validators | User", function () { }); }); + describe("User validator for usersPatchHandler", function () { + it("should call the next for api archiveUsers", async function () { + const req = {}; + + const res = { + boom: { + badRequest: () => {}, + }, + }; + + const next = sinon.spy(); + await validateUsersPatchHandler(req, res, next); + expect(next.calledOnce).to.be.equal(true); + }); + + it("should call the next for api nonVerifiedDiscordUsers", async function () { + const req = { + body: { + action: "nonVerifiedDiscordUsers", + }, + }; + + const res = {}; + + const next = sinon.spy(); + await validateUsersPatchHandler(req, res, next); + expect(next.calledOnce).to.be.equal(true); + }); + + it("should stop the propagation of next", async function () { + const req = { + body: { + action: "", + }, + }; + + const res = { + boom: { + badRequest: () => {}, + }, + }; + + const next = sinon.spy(); + await validateUsersPatchHandler(req, res, next).catch((error) => { + expect(error).to.be.an.instanceOf(Error); + }); + expect(next.calledOnce).to.be.equal(false); + }); + }); + describe("Create user validator for updateUser", function () { it("lets the request pass to next", async function () { const req = { diff --git a/test/unit/middlewares/users-validator.test.js b/test/unit/middlewares/users-validator.test.js index 615ee4cec..d260d58ed 100644 --- a/test/unit/middlewares/users-validator.test.js +++ b/test/unit/middlewares/users-validator.test.js @@ -1,5 +1,5 @@ const Sinon = require("sinon"); -const { validateUpdateRoles } = require("../../../middlewares/validators/user"); +const { validateUpdateRoles, validateUserQueryParams } = require("../../../middlewares/validators/user"); const { expect } = require("chai"); describe("Test the roles update validator", function () { @@ -15,6 +15,19 @@ describe("Test the roles update validator", function () { expect(nextSpy.callCount).to.be.equal(1); }); + it("Allows the request for time as 31d", async function () { + const req = { + query: { + state: "ONBOARDING", + time: "31d", + }, + }; + const res = {}; + const nextSpy = Sinon.spy(); + await validateUserQueryParams(req, res, nextSpy); + expect(nextSpy.callCount).to.be.equal(1); + }); + it("Allows the request to pass with archived property", async function () { const req = { body: { diff --git a/test/unit/models/discordactions.test.js b/test/unit/models/discordactions.test.js index 6c3b2fa66..67092705d 100644 --- a/test/unit/models/discordactions.test.js +++ b/test/unit/models/discordactions.test.js @@ -5,6 +5,8 @@ const firestore = require("../../../utils/firestore"); const photoVerificationModel = firestore.collection("photo-verification"); const discordRoleModel = firestore.collection("discord-roles"); const memberRoleModel = firestore.collection("member-group-roles"); +const userModel = firestore.collection("users"); +const admin = require("firebase-admin"); const { createNewRole, @@ -12,10 +14,13 @@ const { isGroupRoleExists, addGroupRoleToMember, updateDiscordImageForVerification, + enrichGroupDataWithMembershipInfo, + fetchGroupToUserMapping, } = require("../../../models/discordactions"); const { groupData, roleData, existingRole } = require("../../fixtures/discordactions/discordactions"); const cleanDb = require("../../utils/cleanDb"); const { userPhotoVerificationData } = require("../../fixtures/user/photo-verification"); +const userData = require("../../fixtures/user/user")(); chai.should(); @@ -243,4 +248,120 @@ describe("discordactions", function () { } }); }); + + describe("enrichGroupDataWithMembershipInfo", function () { + let newGroupData; + let allIds = []; + + before(async function () { + const addUsersPromises = userData.map((user) => userModel.add({ ...user })); + const responses = await Promise.all(addUsersPromises); + allIds = responses.map((response) => response.id); + newGroupData = groupData.map((group, index) => { + return { + ...group, + createdBy: allIds[Math.min(index, allIds.length - 1)], + }; + }); + + const addRolesPromises = [ + discordRoleModel.add(newGroupData[0]), + discordRoleModel.add(newGroupData[1]), + discordRoleModel.add(newGroupData[2]), + ]; + await Promise.all(addRolesPromises); + + const addGroupRolesPromises = [ + addGroupRoleToMember({ roleid: newGroupData[0].roleid, userid: userData[0].discordId }), + addGroupRoleToMember({ roleid: newGroupData[0].roleid, userid: userData[1].discordId }), + addGroupRoleToMember({ roleid: newGroupData[1].roleid, userid: userData[0].discordId }), + ]; + await Promise.all(addGroupRolesPromises); + }); + + after(async function () { + await cleanDb(); + }); + + it("should return an empty array if the parameter is an empty array", async function () { + const result = await enrichGroupDataWithMembershipInfo(userData[0].discordId, []); + expect(result).to.be.an("array"); + expect(result.length).to.equal(0); + }); + + it("should return an empty array if the parameter no parameter is passed", async function () { + const result = await enrichGroupDataWithMembershipInfo(); + expect(result).to.be.an("array"); + expect(result.length).to.equal(0); + }); + + it("should return group details with memberCount details ", async function () { + const result = await enrichGroupDataWithMembershipInfo(userData[0].discordId, newGroupData); + expect(result[0]).to.deep.equal({ + ...newGroupData[0], + memberCount: 2, + firstName: userData[0].first_name, + lastName: userData[0].last_name, + image: userData[0].picture.url, + isMember: true, + }); + + expect(result[1]).to.deep.equal({ + ...newGroupData[1], + memberCount: 1, + firstName: userData[1].first_name, + lastName: userData[1].last_name, + image: userData[1].picture.url, + isMember: true, + }); + + expect(result[2]).to.deep.equal({ + ...newGroupData[2], + memberCount: 0, + firstName: userData[2].first_name, + lastName: userData[2].last_name, + image: userData[2].picture.url, + isMember: false, + }); + }); + }); + + describe("fetchGroupToMemberMapping", function () { + const roleIds = []; + before(async function () { + // Add 50 different roles and user mapping + const addGroupRolesPromises = Array.from({ length: 65 }).map((_, index) => { + const roleId = `role-id-${index}`; + roleIds.push(roleId); + return addGroupRoleToMember({ + roleid: roleId, + userid: index, + date: admin.firestore.Timestamp.fromDate(new Date()), + }); + }); + await Promise.all(addGroupRolesPromises); + }); + + after(async function () { + await cleanDb(); + }); + + it("should return empty array for empty roleId", async function () { + const groupToMemberMappings = await fetchGroupToUserMapping([]); + expect(groupToMemberMappings).to.be.an("array"); + expect(groupToMemberMappings).to.have.lengthOf(0); + }); + + it("should be able to fetch mapping for less than 30 roleIds", async function () { + const groupToMemberMappings = await fetchGroupToUserMapping(roleIds.slice(0, 25)); + expect(groupToMemberMappings).to.be.an("array"); + expect(groupToMemberMappings).to.have.lengthOf(25); + }); + + it("should be able to fetch mapping for more than 30 roleIds", async function () { + const groupToMemberMappings = await fetchGroupToUserMapping(roleIds); + expect(groupToMemberMappings).to.be.an("array"); + expect(groupToMemberMappings).to.have.lengthOf(65); + }); + }); }); diff --git a/test/unit/models/events.test.js b/test/unit/models/events.test.js index 4dd937923..49902f414 100644 --- a/test/unit/models/events.test.js +++ b/test/unit/models/events.test.js @@ -6,6 +6,7 @@ const firestore = require("../../../utils/firestore"); const eventQuery = require("../../../models/events"); const eventModel = firestore.collection("events"); +const peerModel = firestore.collection("peers"); const eventDataArray = require("../../fixtures/events/events")(); const eventData = eventDataArray[0]; @@ -17,61 +18,100 @@ describe("Events", function () { describe("createEvent", function () { it("should create a new event in firestore", async function () { - // Call the function with sample data const result = await eventQuery.createEvent(eventData); - // Add sample data to Firestore const data = (await eventModel.doc(eventData.room_id).get()).data(); - // Verify that the event was created expect(result).to.deep.equal(data); }); }); describe("updateEvent", function () { it("should update the enabled property of a event", async function () { - // Add sample data to Firestore const docRef = eventModel.doc(eventData.room_id); await docRef.set(eventData); - // Call the function with sample data await eventQuery.updateEvent({ id: "641e3b43a42edf3910cbc8bf", enabled: true }, eventModel); - // Get updated data from Firestore const docSnapshot = await eventModel.doc(docRef.id).get(); const data = docSnapshot.data(); - // Verify that the enabled property was updated expect(data.enabled).to.equal(true); }); }); describe("endActiveEvent", function () { it("should update the lock, reason, and status of a event", async function () { - // Add sample data to Firestore const docRef = await eventModel.add(eventData); try { - // Call the function with sample data await eventQuery.endActiveEvent({ id: docRef.id, reason: "test reason", lock: true, }); - // Get updated data from Firestore const docSnapshot = await eventModel.doc(docRef.id).get(); const data = docSnapshot.data(); - // Verify that the lock, reason, and status properties were updated expect(data.lock).to.equal(true); expect(data.reason).to.equal("test reason"); expect(data.status).to.equal("inactive"); } catch (error) { - // Check that the function threw an error expect(error).to.exist(); expect(error.message).to.equal("Error in enabling event."); } }); }); + + describe("addPeerToEvent", function () { + it("should create a new peer document if it doesn't exist", async function () { + const docRef = await eventModel.add(eventData); + + const peerData = { + peerId: "someid", + name: "NonExistingPeer", + eventId: docRef.id, + role: "participant", + joinedAt: new Date(), + }; + + const result = await eventQuery.addPeerToEvent(peerData); + + const docSnapshot = await peerModel.doc(result.peerId).get(); + const data = docSnapshot.data(); + + expect(data.name).to.equal(peerData.name); + expect(data.joinedEvents).to.have.lengthOf(1); + expect(data.joinedEvents[0].event_id).to.equal(peerData.eventId); + expect(data.joinedEvents[0].role).to.equal(peerData.role); + }); + + it("should update the joinedEvents array if the peer document exists", async function () { + const docRef = await eventModel.add(eventData); + + const peerData = { + peerId: "someid", + name: "ExistingPeer", + eventId: docRef.id, + role: "participant", + joinedAt: new Date(), + }; + + await peerModel.add({ + peerId: peerData.peerId, + name: peerData.name, + joinedEvents: [], + }); + + await eventQuery.addPeerToEvent(peerData); + + const docSnapshot = await peerModel.doc(peerData.peerId).get(); + const data = docSnapshot.data(); + + expect(data.joinedEvents).to.have.lengthOf(1); + expect(data.joinedEvents[0].event_id).to.equal(peerData.eventId); + expect(data.joinedEvents[0].role).to.equal(peerData.role); + }); + }); }); diff --git a/test/unit/models/logs.test.js b/test/unit/models/logs.test.js index 9d3df2ccd..7526ba1ed 100644 --- a/test/unit/models/logs.test.js +++ b/test/unit/models/logs.test.js @@ -4,6 +4,17 @@ const { expect } = chai; const cleanDb = require("../../utils/cleanDb"); const logsQuery = require("../../../models/logs"); const cacheData = require("../../fixtures/cloudflareCache/data"); +const logsData = require("../../fixtures/logs/archievedUsers"); +const app = require("../../../server"); +const Sinon = require("sinon"); +const { INTERNAL_SERVER_ERROR } = require("../../../constants/errorMessages"); +const userData = require("../../fixtures/user/user")(); +const addUser = require("../../utils/addUser"); +const cookieName = config.get("userToken.cookieName"); +const authService = require("../../../services/authService"); + +const superUser = userData[4]; +const userToBeMadeMember = userData[1]; describe("Logs", function () { after(async function () { @@ -37,4 +48,77 @@ describe("Logs", function () { expect(data[0].timestamp._nanoseconds).to.be.a("number"); }); }); + + describe("GET /logs/archived-details", function () { + let addLogsStub; + let jwt; + beforeEach(async function () { + const superUserId = await addUser(superUser); + jwt = authService.generateAuthToken({ userId: superUserId }); + await cleanDb(); + }); + afterEach(function () { + Sinon.restore(); + }); + + it("Should return an Internal server error message", async function () { + addLogsStub = Sinon.stub(logsQuery, "fetchLogs"); + addLogsStub.throws(new Error(INTERNAL_SERVER_ERROR)); + + addUser(userToBeMadeMember).then(() => { + const res = chai.request(app).get("/logs/archived-details").set("cookie", `${cookieName}=${jwt}`).send(); + + expect(res.body.message).to.equal(INTERNAL_SERVER_ERROR); + }); + }); + it("Should return empty array if no logs found", async function () { + const { type } = logsData.archivedUserDetailsModal[0]; + const query = {}; + + const data = await logsQuery.fetchLogs(query, type); + + expect(data).to.be.an("array").with.lengthOf(0); + }); + it("Should fetch all archived logs", async function () { + const { type, meta, body } = logsData.archivedUserDetailsModal[0]; + const query = {}; + + await logsQuery.addLog(type, meta, body); + const data = await logsQuery.fetchLogs(query, type); + + expect(data).to.be.an("array").with.lengthOf.greaterThan(0); + expect(data[0]).to.have.property("timestamp").that.is.an("object"); + expect(data[0].timestamp).to.have.property("_seconds").that.is.a("number"); + expect(data[0].timestamp).to.have.property("_nanoseconds").that.is.a("number"); + expect(data[0].body.archived_user).to.have.property("username").that.is.a("string"); + expect(data[0].body).to.have.property("reason").that.is.a("string"); + }); + it("Should fetch all archived logs for given user_id", async function () { + const { type, meta, body } = logsData.archivedUserDetailsModal[0]; + const query = { + userId: body.archived_user.user_id, + }; + await logsQuery.addLog(type, meta, body); + const data = await logsQuery.fetchLogs(query, type); + + expect(data).to.be.an("array").with.lengthOf.greaterThan(0); + expect(data[0]).to.have.property("timestamp").that.is.an("object"); + expect(data[0].timestamp).to.have.property("_seconds").that.is.a("number"); + expect(data[0].timestamp).to.have.property("_nanoseconds").that.is.a("number"); + expect(data[0].body).to.have.property("reason").that.is.a("string"); + }); + it("Should throw response status 404, if username is incorrect in the query", async function () { + const { type, meta, body } = logsData.archivedUserDetailsModal[0]; + const query = { + userId: "1234_test", // incorrect username + }; + await logsQuery.addLog(type, meta, body); + const data = await logsQuery.fetchLogs(query, type); + const response = await chai.request(app).get(`/logs/${type}/${query}`); + + expect(data).to.be.an("array").with.lengthOf(0); + expect(response).to.have.status(404); + expect(response.body.message).to.be.equal("Not Found"); + }); + }); }); diff --git a/test/unit/models/qrCodeAuth.test.js b/test/unit/models/qrCodeAuth.test.js index 8a3e98b54..add397171 100644 --- a/test/unit/models/qrCodeAuth.test.js +++ b/test/unit/models/qrCodeAuth.test.js @@ -92,7 +92,45 @@ describe("mobile auth", function () { }; await qrCodeAuth.storeUserDeviceInfo(userDeviceInfoData); - const response = await qrCodeAuth.retrieveUserDeviceInfo(userDeviceInfoData.device_id); + const response = await qrCodeAuth.retrieveUserDeviceInfo({ deviceId: userDeviceInfoData.device_id }); + const userDeviceInfo = response.data; + const { + user_id: userID, + device_info: deviceInfo, + device_id: deviceId, + authorization_status: authorizationStatus, + access_token: accessToken, + } = userDeviceInfo; + + const data = (await qrCodeAuthModel.doc(userId).get()).data(); + + Object.keys(userDeviceInfo).forEach((key) => { + expect(userDeviceInfo[key]).to.deep.equal(data[key]); + }); + + expect(response).to.be.an("object"); + expect(userID).to.be.a("string"); + expect(deviceInfo).to.be.a("string"); + expect(deviceId).to.be.a("string"); + expect(authorizationStatus).to.be.a("string"); + expect(accessToken).to.be.a("string"); + }); + }); + + describe("retrieveUserDeviceInfo with userId", function () { + it("should fetch the user device info for mobile auth", async function () { + const userData = userDataArray[0]; + const { userId } = await users.addOrUpdate(userData); + + const userDeviceInfoData = { + ...userDeviceInfoDataArray[0], + user_id: userId, + authorization_status: "NOT_INIT", + access_token: "ACCESS_TOKEN", + }; + + await qrCodeAuth.storeUserDeviceInfo(userDeviceInfoData); + const response = await qrCodeAuth.retrieveUserDeviceInfo({ userId: userDeviceInfoData.user_id }); const userDeviceInfo = response.data; const { user_id: userID, diff --git a/test/unit/models/tasks.test.js b/test/unit/models/tasks.test.js index 6ac225ae8..4ba5a7a05 100644 --- a/test/unit/models/tasks.test.js +++ b/test/unit/models/tasks.test.js @@ -156,6 +156,32 @@ describe("tasks", function () { expect(result.allTasks).to.have.length(tasksLength); result.allTasks.forEach((task) => expect(task.status).to.be.equal(status)); }); + + it("should fetch all tasks filtered by the assignee and title", async function () { + const assignee = "ankur"; + const title = "Overdue"; + const result = await tasks.fetchPaginatedTasks({ assignee, title }); + + const filteredTasks = tasksData.filter((task) => task.assignee === assignee && task.title.includes(title)); + + expect(result).to.have.property("allTasks"); + filteredTasks.forEach((task) => { + expect(task.assignee).to.be.equal(assignee); + expect(task.title).to.include(title); + }); + }); + + it("should fetch all tasks filtered by the assignee passed", async function () { + const assignee = "ankur"; + const result = await tasks.fetchPaginatedTasks({ assignee }); + + const filteredTasks = tasksData.filter((task) => task.assignee === assignee); + + expect(result).to.have.property("allTasks"); + filteredTasks.forEach((task) => { + expect(task.assignee).to.be.equal(assignee); + }); + }); }); describe("update Dependency", function () { diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 42d7cad61..afce2af58 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -5,18 +5,23 @@ /* eslint-disable security/detect-object-injection */ const chai = require("chai"); +const sinon = require("sinon"); const { expect } = chai; const cleanDb = require("../../utils/cleanDb"); const users = require("../../../models/users"); const firestore = require("../../../utils/firestore"); const { userPhotoVerificationData, newUserPhotoVerificationData } = require("../../fixtures/user/photo-verification"); +const { generateStatusDataForState } = require("../../fixtures/userStatus/userStatus"); const userModel = firestore.collection("users"); +const userStatusModel = firestore.collection("usersStatus"); const joinModel = firestore.collection("applicants"); const userDataArray = require("../../fixtures/user/user")(); const joinData = require("../../fixtures/user/join")(); const photoVerificationModel = firestore.collection("photo-verification"); - +const userData = require("../../fixtures/user/user"); +const addUser = require("../../utils/addUser"); +const { userState } = require("../../../constants/userStatus"); /** * Test the model functions and validate the data stored */ @@ -94,7 +99,7 @@ describe("users", function () { }); it("It should have created_At and updated_At fields", async function () { - const userData = userDataArray[14]; + const userData = userDataArray[15]; await users.addOrUpdate(userData); const githubUsername = "sahsisunny"; const { user, userExists } = await users.fetchUser({ githubUsername }); @@ -102,6 +107,14 @@ describe("users", function () { expect(user).to.haveOwnProperty("updated_at"); expect(userExists).to.equal(true); }); + + it("It should have github_created_at fields", async function () { + const userData = userDataArray[0]; + await users.addOrUpdate(userData); + const githubUsername = "ankur"; + const { user } = await users.fetchUser({ githubUsername }); + expect(user).to.haveOwnProperty("github_created_at"); + }); }); describe("user image verification", function () { @@ -232,43 +245,71 @@ describe("users", function () { }); }); - describe("remove github token from users", function () { + describe("archive user if not in discord", function () { beforeEach(async function () { const addUsersPromises = []; userDataArray.forEach((user) => { - addUsersPromises.push(userModel.add(user)); + const userData = { + ...user, + roles: { + ...user.roles, + in_discord: false, + archived: false, + }, + }; + addUsersPromises.push(userModel.add(userData)); }); + await Promise.all(addUsersPromises); }); - afterEach(async function () { - await cleanDb(); + afterEach(function () { + sinon.restore(); }); - it("return array of users", async function () { - const data = await users.fetchUsersWithToken(); - expect(data).to.be.not.equal(null); - }); - it('removes token field from user"s data', async function () { - const userRef = await users.fetchUsersWithToken(); - const dataBefore = await userRef[1].get(); - const beforeRemoval = Object.keys(dataBefore.data()).includes("tokens"); - expect(beforeRemoval).to.be.equal(true); - await users.removeGitHubToken(userRef); - const dataAfter = await userRef[1].get(); - const afterRemoval = Object.keys(dataAfter.data()).includes("tokens"); - expect(afterRemoval).to.be.equal(false); + it("should update archived role to true if in_discord is false", async function () { + await users.archiveUserIfNotInDiscord(); + + const updatedUsers = await userModel + .where("roles.in_discord", "==", false) + .where("roles.archived", "==", false) + .get(); + + updatedUsers.forEach((user) => { + const userData = user.data(); + expect(userData.roles.in_discord).to.be.equal(false); + expect(userData.roles.archived).to.be.equal(true); + }); }); - it("throws error if id is not found in db", async function () { + it("should throw an error if firebase batch operation fails", async function () { + const stub = sinon.stub(firestore, "batch"); + stub.returns({ + update: function () {}, + commit: function () { + throw new Error("Firestore batch update failed"); + }, + }); + try { - await users.removeGitHubToken("1223"); + await users.archiveUserIfNotInDiscord(); } catch (error) { - expect(error).to.be.instanceOf(Error); + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal("An internal server error occurred"); } + + const updatedUsers = await userModel + .where("roles.in_discord", "==", false) + .where("roles.archived", "==", false) + .get(); + + updatedUsers.forEach((user) => { + const userData = user.data(); + expect(userData.roles.in_discord).to.be.equal(false); + expect(userData.roles.archived).to.be.not.equal(true); + }); }); }); - describe("get users by roles", function () { beforeEach(async function () { const addUsersPromises = []; @@ -279,7 +320,7 @@ describe("users", function () { }); it("returns users with member role", async function () { const members = await users.getUsersByRole("member"); - expect(members.length).to.be.equal(6); + expect(members.length).to.be.equal(7); members.forEach((member) => { expect(member.roles.member).to.be.equal(true); }); @@ -290,4 +331,58 @@ describe("users", function () { }); }); }); + + describe("getUsersBasedOnFilter", function () { + let [userId0, userId1, userId2] = []; + + beforeEach(async function () { + const userArr = userData(); + userId0 = await addUser(userArr[0]); + userId1 = await addUser(userArr[1]); + userId2 = await addUser(userArr[2]); + await userStatusModel.doc("userStatus000").set(generateStatusDataForState(userId0, userState.ONBOARDING)); + await userStatusModel.doc("userStatus001").set(generateStatusDataForState(userId1, userState.ONBOARDING)); + await userStatusModel.doc("userStatus002").set(generateStatusDataForState(userId2, userState.IDLE)); + }); + + afterEach(async function () { + await cleanDb(); + }); + it("should render users with onboarding state and time as 31days", async function () { + const query = { + state: "ONBOARDING", + time: "31d", + }; + const result = await users.getUsersBasedOnFilter(query); + expect(result.length).to.equal(2); + }); + }); + describe("fetch users by id", function () { + let allIds = []; + before(async function () { + const addUsersPromises = []; + userDataArray.forEach((user, index) => { + addUsersPromises.push(userModel.add({ ...user })); + }); + const responses = await Promise.all(addUsersPromises); + allIds = responses.map((response) => response.id); + }); + + after(async function () { + await cleanDb(); + }); + + it("should fetch the details of users whose ids are present in the array", async function () { + const randomIds = allIds.sort(() => 0.5 - Math.random()).slice(0, 3); // Select random ids from allIds + const result = await users.fetchUserByIds(randomIds); + const fetchedUserIds = Object.keys(result); + expect(fetchedUserIds).to.deep.equal(randomIds); + }); + + it("should return empty object if no ids are passed", async function () { + const result = await users.fetchUserByIds(); + const fetchedUserIds = Object.keys(result); + expect(fetchedUserIds).to.deep.equal([]); + }); + }); }); diff --git a/test/unit/services/dataAccessLayer.test.js b/test/unit/services/dataAccessLayer.test.js index c12e8ad11..c25283aad 100644 --- a/test/unit/services/dataAccessLayer.test.js +++ b/test/unit/services/dataAccessLayer.test.js @@ -12,14 +12,17 @@ const { retrieveUsersWithRole, retrieveMembers, retreiveFilteredUsers, + levelSpecificAccess, } = require("../../../services/dataAccessLayer"); +const { KEYS_NOT_ALLOWED, ACCESS_LEVEL } = require("../../../constants/userDataLevels"); + const userData = require("../../fixtures/user/user")(); -const { USER_SENSITIVE_DATA } = require("../../../constants/users"); chai.use(chaiHttp); const expect = chai.expect; let fetchUserStub; + describe("Data Access Layer", function () { describe("retrieveUsers", function () { it("should fetch a single user by ID and remove sensitive info", async function () { @@ -28,7 +31,7 @@ describe("Data Access Layer", function () { const result = await retrieveUsers({ id: userData[12].id }); removeSensitiveInfo(userData[12]); expect(result.user).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { expect(result.user).to.not.have.property(key); }); }); @@ -38,7 +41,7 @@ describe("Data Access Layer", function () { const result = await retrieveUsers({ username: userData[12].username }); removeSensitiveInfo(userData[12]); expect(result.user).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { expect(result.user).to.not.have.property(key); }); }); @@ -48,10 +51,10 @@ describe("Data Access Layer", function () { fetchUserStub.returns(Promise.resolve({ users: [userData[12]] })); const result = await retrieveUsers({ usernames: [userData[12].username] }); removeSensitiveInfo(userData[12]); - result.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); }); }); }); @@ -62,10 +65,10 @@ describe("Data Access Layer", function () { const query = { page: 1 }; const result = await retrieveUsers({ query }); removeSensitiveInfo(userData[12]); - result.allUsers.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.users.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); }); }); }); @@ -74,7 +77,7 @@ describe("Data Access Layer", function () { const userdata = userData[12]; await retrieveUsers({ userdata }); removeSensitiveInfo(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { expect(userdata).to.not.have.property(key); }); }); @@ -85,10 +88,10 @@ describe("Data Access Layer", function () { const fetchUserStub = sinon.stub(userQuery, "getDiscordUsers"); fetchUserStub.returns(Promise.resolve([userData[12]])); const result = await retrieveDiscordUsers(); - result.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); }); }); }); @@ -100,10 +103,10 @@ describe("Data Access Layer", function () { fetchUserStub.returns(Promise.resolve([userData[12]])); const query = { showArchived: true }; const result = await retrieveUsersWithRole(query); - result.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); }); }); }); @@ -114,13 +117,31 @@ describe("Data Access Layer", function () { const fetchUserStub = sinon.stub(members, "fetchUsers"); fetchUserStub.returns(Promise.resolve([userData[12]])); const result = await retrieveMembers(); - result.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); + }); + }); + }); + + it("should fetch multiple users details based on ids and remove sensitive data", async function () { + const fetchUserStub = sinon.stub(userQuery, "fetchUserByIds"); + fetchUserStub.returns(Promise.resolve({ [userData[12].id]: userData[12] })); + const result = await retrieveUsers({ userIds: [userData[12].id] }); + removeSensitiveInfo(userData[12]); + Object.keys(result).forEach((id) => { + expect(result[id]).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(result[id]).to.not.have.property(key); }); }); }); + + it("should return empty object if array with no userIds are provided", async function () { + const result = await retrieveUsers({ userIds: [] }); + expect(result).to.deep.equal({}); + }); }); describe("retrieveFilteredUsers", function () { @@ -129,10 +150,10 @@ describe("Data Access Layer", function () { fetchUserStub.returns(Promise.resolve([userData[12]])); const query = { state: "ACTIVE" }; const result = await retreiveFilteredUsers(query); - result.forEach((element) => { - expect(element).to.deep.equal(userData[12]); - USER_SENSITIVE_DATA.forEach((key) => { - expect(element).to.not.have.property(key); + result.forEach((user) => { + expect(user).to.deep.equal(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(user).to.not.have.property(key); }); }); }); @@ -140,10 +161,37 @@ describe("Data Access Layer", function () { describe("removeSensitiveInfo", function () { it("should remove sensitive information from the users object", function () { - removeSensitiveInfo(userData); - USER_SENSITIVE_DATA.forEach((key) => { + removeSensitiveInfo(userData[12]); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { expect(userData[12]).to.not.have.property(key); }); }); }); + + describe("levelSpecificAccess", function () { + it("should return the user object for PUBLIC level after removing all sensitive info", function () { + const result = levelSpecificAccess({ ...userData[12] }, ACCESS_LEVEL.PUBLIC); + KEYS_NOT_ALLOWED[ACCESS_LEVEL.PUBLIC].forEach((key) => { + expect(result).to.not.have.property(key); + }); + }); + + it('should return "unauthorized" for non-superuser role', function () { + const unauthorizedRole = "member"; + const result = levelSpecificAccess({ ...userData[12] }, ACCESS_LEVEL.PRIVATE, unauthorizedRole); + expect(result).to.equal("unauthorized"); + }); + + it("should keep sensitive info for valid role and level", function () { + const user = { ...userData[12], email: "a@b.com", phone: "7890654329", chaincode: "78906" }; + const role = "super_user"; + const level = ACCESS_LEVEL.PRIVATE; + const result = levelSpecificAccess(user, level, role); + KEYS_NOT_ALLOWED[level].forEach((key) => { + expect(result).to.not.have.property(key); + }); + expect(result).to.have.property("phone"); + expect(result).to.have.property("email"); + }); + }); }); diff --git a/test/unit/services/discordService.test.js b/test/unit/services/discordService.test.js index 18ca58d5d..8b4708db7 100644 --- a/test/unit/services/discordService.test.js +++ b/test/unit/services/discordService.test.js @@ -5,6 +5,7 @@ const { addRoleToUser, getDiscordMembers, removeRoleFromUser, + setUserDiscordNickname, } = require("../../../services/discordService"); const { fetchAllUsers } = require("../../../models/users"); const Sinon = require("sinon"); @@ -114,4 +115,27 @@ describe("Discord services", function () { }); }); }); + + describe("change user nickname on discord", function () { + beforeEach(function () { + fetchStub = Sinon.stub(global, "fetch"); + }); + + afterEach(function () { + fetchStub.restore(); + }); + + it("makes a API call to update the user's discord nickname ", async function () { + fetchStub.returns( + Promise.resolve({ + status: 200, + json: () => Promise.resolve({ message: "done" }), + }) + ); + + const response = await setUserDiscordNickname("aMYlI7sxQ4JMPwiqLQlp", "username"); + + expect(response.message).to.be.equal("done"); + }); + }); }); diff --git a/test/unit/services/users.test.js b/test/unit/services/users.test.js new file mode 100644 index 000000000..c3c060b47 --- /dev/null +++ b/test/unit/services/users.test.js @@ -0,0 +1,83 @@ +const Sinon = require("sinon"); +const { expect } = require("chai"); + +const firestore = require("../../../utils/firestore"); +const userModel = firestore.collection("users"); +const cleanDb = require("../../utils/cleanDb"); +const userDataArray = require("../../fixtures/user/user")(); +const { archiveUsers } = require("../../../services/users"); + +describe("Users services", function () { + describe("archive inactive discord users in bulk", function () { + const users = []; + const userDetails = []; + beforeEach(async function () { + const addUsersPromises = []; + userDataArray.forEach((user) => { + const userData = { + ...user, + roles: { + ...user.roles, + in_discord: false, + archived: false, + }, + }; + addUsersPromises.push(userModel.add(userData)); + }); + await Promise.all(addUsersPromises); + + users.length = 0; + userDetails.length = 0; + + const snapshot = await userModel + .where("roles.in_discord", "==", false) + .where("roles.archived", "==", false) + .get(); + + snapshot.forEach((user) => { + const id = user.id; + const userData = user.data(); + const { first_name: firstName, last_name: lastName } = userData; + users.push({ ...userData, id }); + userDetails.push({ id, firstName, lastName }); + }); + }); + + afterEach(async function () { + await cleanDb(); + Sinon.restore(); + }); + + it("Should return successful response", async function () { + const res = await archiveUsers(users); + + expect(res).to.deep.equal({ + message: "Successfully completed batch updates", + totalUsersArchived: 16, + totalOperationsFailed: 0, + updatedUserDetails: userDetails, + failedUserDetails: [], + }); + }); + + it("should return failed response", async function () { + const batchStub = Sinon.stub(firestore, "batch"); + batchStub.returns({ + update: function () {}, + commit: function () { + throw new Error("Firebase batch operation failed"); + }, + }); + + const res = await archiveUsers(users); + + expect(res).to.deep.equal({ + message: "Firebase batch operation failed", + totalUsersArchived: 0, + totalOperationsFailed: 16, + updatedUserDetails: [], + failedUserDetails: userDetails, + }); + }); + }); +}); diff --git a/test/unit/utils/helper.test.js b/test/unit/utils/helper.test.js index 630010160..83ee8f054 100644 --- a/test/unit/utils/helper.test.js +++ b/test/unit/utils/helper.test.js @@ -1,5 +1,10 @@ const chai = require("chai"); -const { getDateTimeRangeForPRs, getQualifiers, getPaginatedLink } = require("../../../utils/helper"); +const { + getDateTimeRangeForPRs, + getQualifiers, + getPaginatedLink, + findSubscribedGroupIds, +} = require("../../../utils/helper"); const { TASK_STATUS, TASK_SIZE } = require("../../../constants/tasks"); const { expect } = chai; @@ -93,4 +98,14 @@ describe("helper", function () { expect(result).to.not.contain(`next=${nextId}`); }); }); + + describe("findSubscribedGroupIds", function () { + it("should return set of member groupIds", function () { + const memberGroupIds = findSubscribedGroupIds("1234", [ + { userid: "1234", roleid: "1" }, + { userid: "12345", roleid: "3" }, + ]); + expect(memberGroupIds).to.deep.equal(new Set(["1"])); + }); + }); }); diff --git a/test/unit/utils/transformQuery.test.js b/test/unit/utils/transformQuery.test.js index 959bb1ae2..1fee26299 100644 --- a/test/unit/utils/transformQuery.test.js +++ b/test/unit/utils/transformQuery.test.js @@ -34,4 +34,10 @@ describe("transformQuery", function () { expect(transformedQuery.page).to.be.equal(1); expect(typeof transformedQuery.page).to.equal("number"); }); + + it("should transfrom and parse assignee to lowercase when passed as param", function () { + const transformedQuery = transformQuery(false, TASK_STATUS.ASSIGNED, 5, 1, "Test"); + expect(transformedQuery.assignee).to.be.equal("test"); + expect(typeof transformedQuery.assignee).to.equal("string"); + }); }); diff --git a/utils/generateUniqueToken.js b/utils/generateUniqueToken.js new file mode 100644 index 000000000..3d98a0c98 --- /dev/null +++ b/utils/generateUniqueToken.js @@ -0,0 +1,12 @@ +const crypto = require("crypto"); + +export const generateUniqueToken = async () => { + const uuidToken = crypto.randomUUID(); + const randomNumber = Math.floor(Math.random() * 1000000); + const generationTime = Date.now(); + const encoder = new TextEncoder(); + const encodedString = encoder.encode(uuidToken + randomNumber + generationTime); + const hash = await crypto.subtle.digest("SHA-256", encodedString); + const token = [...new Uint8Array(hash)].map((x) => x.toString(16).padStart(2, "0")).join(""); + return token; +}; diff --git a/utils/helper.js b/utils/helper.js index 5ee6636f9..b0ab267d0 100644 --- a/utils/helper.js +++ b/utils/helper.js @@ -67,8 +67,30 @@ const getPaginatedLink = ({ return paginatedLink; }; +/** + * Finds and returns the set of subscribed group IDs for a given Discord user ID based on group-to-user mappings. + * + * @param {string} discordId - The Discord user ID for which to find subscribed group IDs. + * @param {Array} groupToUserMappings - An array of group-to-user mappings containing user and role information. + * @returns {Set} - A Set containing the group IDs to which the user is subscribed. + */ +function findSubscribedGroupIds(discordId, groupToUserMappings = []) { + // Initialize a Set to store the subscribed group IDs + const subscribedGroupIds = new Set(); + + // Iterate through groupToUserMappings to find subscribed group IDs + groupToUserMappings.forEach((group) => { + if (group.userid === discordId) { + subscribedGroupIds.add(group.roleid); + } + }); + + return subscribedGroupIds; +} + module.exports = { getQualifiers, getDateTimeRangeForPRs, getPaginatedLink, + findSubscribedGroupIds, }; diff --git a/utils/tasks.js b/utils/tasks.js index 0e6a46038..a2c14615b 100644 --- a/utils/tasks.js +++ b/utils/tasks.js @@ -64,10 +64,12 @@ const buildTasks = (tasks, initialTaskArray = []) => { return initialTaskArray; }; -const transformQuery = (dev = false, status = "", size, page) => { +const transformQuery = (dev = false, status = "", size, page, assignee = "", title = "") => { const query = {}; const transformedDev = JSON.parse(dev); const transformedStatus = MAPPED_TASK_STATUS[status.toUpperCase()]; + const transformedAssignee = assignee.toLowerCase(); + const transformedTitle = title; if (page) { query.page = parseInt(page); @@ -77,7 +79,13 @@ const transformQuery = (dev = false, status = "", size, page) => { query.size = parseInt(size); } - return { status: transformedStatus, dev: transformedDev, ...query }; + return { + status: transformedStatus, + dev: transformedDev, + assignee: transformedAssignee, + title: transformedTitle, + ...query, + }; }; const parseSearchQuery = (queryString) => { diff --git a/utils/users.js b/utils/users.js index e3145c775..5beabda9e 100644 --- a/utils/users.js +++ b/utils/users.js @@ -43,6 +43,46 @@ const getUsername = async (userId) => { throw error; } }; + +/** + * Used for receiving username when providing userId, if not found then returns undefined + * + * @param userId {String} - userId of the User. + * @returns username {String} - username of the same user + */ +const getUsernameElseUndefined = async (userId) => { + try { + const { + user: { username }, + } = await fetchUser({ userId }); + return username; + } catch (error) { + logger.error("Something went wrong", error); + return undefined; + } +}; + +/** + * Used for receiving userId when providing username, if not found then returns undefined + * + * @param username {String} - username of the User. + * @returns id {String} - userId of the same user + */ + +const getUserIdElseUndefined = async (username) => { + try { + const { + userExists, + user: { id }, + } = await fetchUser({ username }); + + return userExists ? id : false; + } catch (error) { + logger.error("Something went wrong", error); + return undefined; + } +}; + /** * Converts the userIds entered in the array to corresponding usernames * @param participantArray {array} : participants array to be updated @@ -165,5 +205,7 @@ module.exports = { getLowestLevelSkill, getPaginationLink, getUsernamesFromPRs, + getUsernameElseUndefined, + getUserIdElseUndefined, getRoleToUpdate, };