diff --git a/constants/imageVerificationTypes.ts b/constants/imageVerificationTypes.ts index 6d0c55579..1f5cf5df5 100644 --- a/constants/imageVerificationTypes.ts +++ b/constants/imageVerificationTypes.ts @@ -1,3 +1,7 @@ -const IMAGE_VERIFICATION_TYPES = ["profile", "discord"]; +const IMAGE_VERIFICATION_TYPES = { + PROFILE: 'profile', + DISCORD: 'discord', + PROFILE_DISCORD: 'profile_discord', +}; module.exports = { IMAGE_VERIFICATION_TYPES }; diff --git a/constants/users.ts b/constants/users.ts index 4a3bff687..604f4b09b 100644 --- a/constants/users.ts +++ b/constants/users.ts @@ -1,3 +1,9 @@ +const photoVerificationRequestStatus = { + PENDING: "PENDING", + APPROVED: "APPROVED", + REJECTED: "REJECTED", +}; + const profileStatus = { PENDING: "PENDING", APPROVED: "APPROVED", @@ -61,4 +67,5 @@ module.exports = { months, discordNicknameLength, SIMULTANEOUS_WORKER_CALLS, + photoVerificationRequestStatus }; diff --git a/controllers/discordactions.js b/controllers/discordactions.js index 7a6f64fcf..4e1391fab 100644 --- a/controllers/discordactions.js +++ b/controllers/discordactions.js @@ -193,8 +193,8 @@ const deleteRole = async (req, res) => { */ const updateDiscordImageForVerification = async (req, res) => { try { - const { id: userDiscordId } = req.params; - const discordAvatarUrl = await discordRolesModel.updateDiscordImageForVerification(userDiscordId); + const { id: discordId } = req.params; + const discordAvatarUrl = await discordRolesModel.updateDiscordImageForVerification(discordId); return res.json({ message: "Discord avatar URL updated successfully!", discordAvatarUrl, diff --git a/controllers/users.js b/controllers/users.js index bc4dbfc78..8fb079f59 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -443,9 +443,10 @@ const updateSelf = async (req, res) => { */ const postUserPicture = async (req, res) => { const { file } = req; + const { dev } = req.query; const { id: userId, discordId } = req.userData; const { coordinates } = req.body; - let discordAvatarUrl = ""; + let discordAvatarUrl; let imageData; let verificationResult; try { @@ -456,17 +457,26 @@ const postUserPicture = async (req, res) => { } try { const coordinatesObject = coordinates && JSON.parse(coordinates); - imageData = await imageService.uploadProfilePicture({ file, userId, coordinates: coordinatesObject }); + imageData = await imageService.uploadProfilePicture({ file, userId, coordinates: coordinatesObject }, dev); } catch (error) { logger.error(`Error while adding profile picture of user: ${error}`); return res.boom.badImplementation(INTERNAL_SERVER_ERROR); } - try { - verificationResult = await userQuery.addForVerification(userId, discordId, imageData.url, discordAvatarUrl); - } catch (error) { - logger.error(`Error while adding profile picture of user: ${error}`); - return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + if (dev) { + try { + verificationResult = await userQuery.addForVerification( + userId, + discordId, + imageData.url, + discordAvatarUrl, + imageData.publicId + ); + } catch (error) { + logger.error(`Error while adding profile picture of user: ${error}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } } + return res.status(201).json({ message: `Profile picture uploaded successfully! ${verificationResult.message}`, image: imageData, @@ -482,11 +492,12 @@ const postUserPicture = async (req, res) => { const verifyUserImage = async (req, res) => { try { - const { type: imageType } = req.query; - const { id: userId } = req.params; - await userQuery.markAsVerified(userId, imageType); - return res.json({ - message: `${imageType} image was verified successfully!`, + const { type: imageType, status } = req.query; + const { userId } = req.params; + + const verificationResponse = await userQuery.changePhotoVerificationStatus(userId, imageType, status); + return res.status(200).json({ + message: verificationResponse, }); } catch (error) { logger.error(`Error while verifying image of user: ${error}`); @@ -565,20 +576,45 @@ const markUnverified = async (req, res) => { * @param res {Object} - Express response object */ -const getUserImageForVerification = async (req, res) => { +const getUserPhotoVerificationRequests = async (req, res) => { try { - const { id: userId } = req.params; - const userImageVerificationData = await userQuery.getUserImageForVerification(userId); + const { userId } = req.params; + const userData = req.userData; + if ((userData.id !== userId && !userData.roles[ROLES.SUPERUSER]) || !userId) { + return res.boom.unauthorized("You are not authorized to view this user's image verification data"); + } + const userImageVerificationData = await userQuery.getUserPhotoVerificationRequests(userId); return res.json({ message: "User image verification record fetched successfully!", - data: userImageVerificationData, + data: userImageVerificationData[0], }); } catch (error) { - logger.error(`Error while verifying image of user: ${error}`); + logger.error(`Error while querying image of user: ${error}`); return res.boom.badImplementation(INTERNAL_SERVER_ERROR); } }; +/** + * Updates the user data + * + * @param req {Object} - Express request object + * @param res {Object} - Express response object + */ + +const getAllUsersPhotoVerificationRequests = async (req, res) => { + const { username } = req.query; + try { + const userImageVerificationData = await userQuery.getAllUsersPhotoVerificationRequests(username); + return res.json({ + message: "User image verification record fetched successfully!", + data: userImageVerificationData, + }); + } catch (error) { + logger.error(`Error while querying image of user: ${error}`); + return res.boom.badImplementation(`Error while querying image verification data for ${username}`); + } +}; + /** * Updates the user data * @@ -954,7 +990,8 @@ module.exports = { getUserSkills, filterUsers, verifyUserImage, - getUserImageForVerification, + getAllUsersPhotoVerificationRequests, + getUserPhotoVerificationRequests, nonVerifiedDiscordUsers, setInDiscordScript, markUnverified, diff --git a/middlewares/validators/user.js b/middlewares/validators/user.js index 8be954e2e..c2a27d188 100644 --- a/middlewares/validators/user.js +++ b/middlewares/validators/user.js @@ -5,6 +5,7 @@ const { USER_STATUS, USERS_PATCH_HANDLER_ACTIONS, USERS_PATCH_HANDLER_ERROR_MESSAGES, + photoVerificationRequestStatus, } = require("../../constants/users"); const ROLES = require("../../constants/roles"); const { IMAGE_VERIFICATION_TYPES } = require("../../constants/imageVerificationTypes"); @@ -251,11 +252,15 @@ async function validateUserQueryParams(req, res, next) { * @param next {Object} - Express middleware function */ const validateImageVerificationQuery = async (req, res, next) => { - const { type: imageType } = req.query; + const { type: imageType, status } = req.query; try { - if (!IMAGE_VERIFICATION_TYPES.includes(imageType)) { + if (!Object.values(IMAGE_VERIFICATION_TYPES).includes(imageType)) { throw new Error("Invalid verification type was provided!"); } + + if (![photoVerificationRequestStatus.APPROVED, photoVerificationRequestStatus.REJECTED].includes(status)) { + throw new Error("Invalid verification status was provided!"); + } next(); } catch (error) { logger.error(`Error validating createLevel payload : ${error}`); diff --git a/models/discordactions.js b/models/discordactions.js index 3c71e74f7..d05278769 100644 --- a/models/discordactions.js +++ b/models/discordactions.js @@ -9,7 +9,7 @@ const { retrieveUsers } = require("../services/dataAccessLayer"); const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); const { getAllUserStatus, getGroupRole, getUserStatus } = require("./userStatus"); const { userState } = require("../constants/userStatus"); -const { ONE_DAY_IN_MS, SIMULTANEOUS_WORKER_CALLS } = require("../constants/users"); +const { ONE_DAY_IN_MS, SIMULTANEOUS_WORKER_CALLS, photoVerificationRequestStatus } = require("../constants/users"); const userModel = firestore.collection("users"); const photoVerificationModel = firestore.collection("photo-verification"); const dataAccess = require("../services/dataAccessLayer"); @@ -215,9 +215,14 @@ const addGroupRoleToMember = async (roleData) => { const updateDiscordImageForVerification = async (userDiscordId) => { try { const discordAvatarUrl = await generateDiscordProfileImageUrl(userDiscordId); - const verificationDataSnapshot = await photoVerificationModel.where("discordId", "==", userDiscordId).get(); + const verificationDataSnapshot = await photoVerificationModel + .where("discordId", "==", userDiscordId) + .where("status", "==", photoVerificationRequestStatus.PENDING) + .get(); + const currentTime = new Date(); const unverifiedUserDiscordImage = { - discord: { url: discordAvatarUrl, approved: false, date: admin.firestore.Timestamp.fromDate(new Date()) }, + discord: { url: discordAvatarUrl, approved: false, updatedAt: currentTime.getTime() / 1000 }, + "profile.approved": false, }; if (verificationDataSnapshot.empty) { throw new Error("No user verification record found"); diff --git a/models/users.js b/models/users.js index 235987d63..d6c4a3eac 100644 --- a/models/users.js +++ b/models/users.js @@ -10,6 +10,7 @@ const { updateUserStatus } = require("../models/userStatus"); const { arraysHaveCommonItem, chunks } = require("../utils/array"); const { archiveUsers } = require("../services/users"); const { ALLOWED_FILTER_PARAMS, FIRESTORE_IN_CLAUSE_SIZE } = require("../constants/users"); +const { IMAGE_VERIFICATION_TYPES } = require("../constants/imageVerificationTypes"); const { DOCUMENT_WRITE_SIZE } = require("../constants/constants"); const { userState } = require("../constants/userStatus"); const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); @@ -23,6 +24,7 @@ const { ITEM_TAG, USER_STATE } = ALLOWED_FILTER_PARAMS; const admin = require("firebase-admin"); const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); const { AUTHORITIES } = require("../constants/authorities"); +const { photoVerificationRequestStatus } = require("../constants/users"); /** * Adds or updates the user data @@ -360,19 +362,29 @@ const initializeUser = async (userId) => { * @return {Promise<{message: string}|{message: string}>} * @throws {Error} - If error occurs while creating Verification Entry */ -const addForVerification = async (userId, discordId, profileImageUrl, discordImageUrl) => { +const addForVerification = async (userId, discordId, profileImageUrl, discordImageUrl, profileImageID) => { let isNotVerifiedSnapshot; try { - isNotVerifiedSnapshot = await photoVerificationModel.where("userId", "==", userId).get(); + isNotVerifiedSnapshot = await photoVerificationModel + .where("userId", "==", userId) + .where("status", "==", photoVerificationRequestStatus.PENDING) + .get(); } catch (err) { logger.error("Error in creating Verification Entry", err); throw err; } + const currentTime = new Date(); const unverifiedUserData = { userId, discordId, - discord: { url: discordImageUrl, approved: false, date: admin.firestore.Timestamp.fromDate(new Date()) }, - profile: { url: profileImageUrl, approved: false, date: admin.firestore.Timestamp.fromDate(new Date()) }, + discord: { url: discordImageUrl, approved: false, updatedAt: currentTime.getTime() / 1000 }, + profile: { + url: profileImageUrl, + publicId: profileImageID, + approved: false, + updatedAt: currentTime.getTime() / 1000, + }, + status: photoVerificationRequestStatus.PENDING, }; try { if (!isNotVerifiedSnapshot.empty) { @@ -397,18 +409,46 @@ const addForVerification = async (userId, discordId, profileImageUrl, discordIma * @return {Promise<{message: string}|{message: string}>} * @throws {Error} - If error occurs while verifying user's image */ -const markAsVerified = async (userId, imageType) => { +const changePhotoVerificationStatus = async (userId, imageType, status) => { try { - const verificationUserDataSnapshot = await photoVerificationModel.where("userId", "==", userId).get(); + const verificationUserDataSnapshot = await photoVerificationModel + .where("userId", "==", userId) + .where("status", "==", photoVerificationRequestStatus.PENDING) + .get(); + // THROWS ERROR IF NO DOCUMENT FOUND if (verificationUserDataSnapshot.empty) { throw new Error("No verification document record data for user was found"); } - // VERIFIES BASED ON THE TYPE OF IMAGE - const imageVerificationType = imageType === "discord" ? "discord.approved" : "profile.approved"; + const documentRef = verificationUserDataSnapshot.docs[0].ref; - await documentRef.update({ [imageVerificationType]: true }); - return { message: "User image data verified successfully" }; + + // if status is rejected then remove the verification entry + if (status === photoVerificationRequestStatus.REJECTED) { + await documentRef.update({ status, "discord.approved": false, "profile.approved": false }); + return "User photo verification request rejected successfully"; + } + + // VERIFIES BASED ON THE TYPE OF IMAGE + if (imageType === IMAGE_VERIFICATION_TYPES.PROFILE_DISCORD) { + await documentRef.update({ "discord.approved": true, "profile.approved": true }); + } else { + const imageVerificationType = + imageType === IMAGE_VERIFICATION_TYPES.DISCORD ? "discord.approved" : "profile.approved"; + await documentRef.update({ [imageVerificationType]: true }); + } + + // IF BOTH IMAGES ARE VERIFIED THEN APPROVE THE ENTRY ITSELF + const photoVerificationObject = (await documentRef.get()).data(); + if (photoVerificationObject.discord.approved && photoVerificationObject.profile.approved) { + await documentRef.update({ status: photoVerificationRequestStatus.APPROVED }); + await updateUserPicture( + { url: photoVerificationObject.profile.url, publicId: photoVerificationObject.profile.publicId }, + userId + ); + } + + return "User image data verified successfully"; } catch (err) { logger.error("Error while Removing Verification Entry", err); throw err; @@ -416,18 +456,105 @@ const markAsVerified = async (userId, imageType) => { }; /** - * Removes if user passed a valid image; ignores if no unverified record + * Returns the user's image verification requests (PENDING only) based on the userId * @param userId {String} - RDS user Id * @return {Promise<{Object}|{Object}>} * @throws {Error} - If error occurs while fetching user's image verification entry */ -const getUserImageForVerification = async (userId) => { +const getUserPhotoVerificationRequests = async (userId) => { try { - const verificationImagesSnapshot = await photoVerificationModel.where("userId", "==", userId).get(); + const user = await userModel.doc(userId).get(); + if (!user.exists) { + throw new Error(`No document with userId: ${userId} was found!`); + } + const userData = user.data(); + + const verificationImagesSnapshotQuery = photoVerificationModel.where( + "status", + "==", + photoVerificationRequestStatus.PENDING + ); + + verificationImagesSnapshotQuery.where("userId", "==", userId); + const verificationImagesSnapshot = await verificationImagesSnapshotQuery.get(); if (verificationImagesSnapshot.empty) { throw new Error(`No document with userId: ${userId} was found!`); } - return verificationImagesSnapshot.docs[0].data(); + + const photoVerificationObject = [ + { + ...verificationImagesSnapshot.docs[0].data(), + user: { + username: userData.username, + picture: userData.picture.url, + }, + }, + ]; + return photoVerificationObject; + } catch (err) { + logger.error("Error while Querying Photo Verification Entry", err); + throw err; + } +}; + +/** + * Returns all users image verification requests (PENDING only), optionally filtered by username + * @param username {String} - RDS username + * @return {Promise<{Object}|{Object}>} + * @throws {Error} - If error occurs while fetching user's image verification entry + */ +const getAllUsersPhotoVerificationRequests = async (username = null) => { + try { + const verificationImagesSnapshotQuery = photoVerificationModel.where( + "status", + "==", + photoVerificationRequestStatus.PENDING + ); + + if (username && username !== "") { + const user = await userModel.where("username", "==", username).limit(1).get(); + if (user.empty) { + logger.error(`No document with username: ${username} was found!`); + return []; + } + const userData = { id: user.docs[0].id, ...user.docs[0].data() }; + verificationImagesSnapshotQuery.where("userId", "==", userData.id); + const verificationImagesSnapshot = await verificationImagesSnapshotQuery.get(); + if (verificationImagesSnapshot.empty) { + logger.error(`No document with username: ${username} was found!`); + return []; + } + + const photoVerificationObject = [ + { + ...verificationImagesSnapshot.docs[0].data(), + user: { + username: userData.username, + picture: userData.picture.url, + }, + }, + ]; + return photoVerificationObject; + } + + const verificationImagesSnapshot = await verificationImagesSnapshotQuery.get(); + if (verificationImagesSnapshot.empty) { + return []; + } + + const photoVerificationPromises = verificationImagesSnapshot.docs.map(async (doc) => { + const data = doc.data(); + const user = await userModel.doc(data.userId).get(); + const userData = user.data(); + return { + id: doc.id, + ...data, + user: { username: userData.username, picture: userData.picture.url }, + }; + }); + + const photoVerificationObject = await Promise.all(photoVerificationPromises); + return photoVerificationObject; } catch (err) { logger.error("Error while Removing Verification Entry", err); throw err; @@ -440,7 +567,7 @@ const getUserImageForVerification = async (userId) => { * @param image { Object }: image data ( {publicId, url} ) * @param userId { string }: User id */ -const updateUserPicture = async (image, userId) => { +async function updateUserPicture(image, userId) { try { const userDoc = userModel.doc(userId); await userDoc.update({ @@ -451,7 +578,7 @@ const updateUserPicture = async (image, userId) => { logger.error("Error updating user picture data", err); throw err; } -}; +} /** * fetch the users image by passing array of users @@ -976,9 +1103,10 @@ module.exports = { getRdsUserInfoByGitHubUsername, fetchUsers, getUsersBasedOnFilter, - markAsVerified, + changePhotoVerificationStatus, addForVerification, - getUserImageForVerification, + getUserPhotoVerificationRequests, + getAllUsersPhotoVerificationRequests, getDiscordUsers, fetchAllUsers, archiveUserIfNotInDiscord, diff --git a/routes/discordactions.js b/routes/discordactions.js index 745a306df..09383eb5d 100644 --- a/routes/discordactions.js +++ b/routes/discordactions.js @@ -39,6 +39,7 @@ router.get("/invite", authenticate, getUserDiscordInvite); router.post("/invite", authenticate, checkCanGenerateDiscordLink, generateInviteForUser); router.delete("/roles", authenticate, checkIsVerifiedDiscord, deleteRole); router.get("/roles", authenticate, checkIsVerifiedDiscord, getGroupsRoleId); +// TODO deprecate the below API, for incorrect naming router.patch( "/avatar/verify/:id", authenticate, @@ -46,6 +47,14 @@ router.patch( checkIsVerifiedDiscord, updateDiscordImageForVerification ); +// here "id" is the user's discord id +router.patch( + "/avatar/:id/photo-verification/update", + authenticate, + authorizeRoles([SUPERUSER]), + checkIsVerifiedDiscord, + updateDiscordImageForVerification +); router.put( "/group-idle", authorizeAndAuthenticate([ROLES.SUPERUSER], [Services.CRON_JOB_HANDLER]), diff --git a/routes/users.js b/routes/users.js index 605500305..a05d8a52d 100644 --- a/routes/users.js +++ b/routes/users.js @@ -54,13 +54,14 @@ router.patch( // upload.single('profile') -> multer inmemory storage of file for type multipart/form-data router.post("/picture", authenticate, checkIsVerifiedDiscord, upload.single("profile"), users.postUserPicture); router.patch( - "/picture/verify/:id", + "/picture/verify/:userId", authenticate, authorizeRoles([SUPERUSER]), userValidator.validateImageVerificationQuery, users.verifyUserImage ); -router.get("/picture/:id", authenticate, authorizeRoles([SUPERUSER]), users.getUserImageForVerification); +router.get("/picture/all", authenticate, authorizeRoles([SUPERUSER]), users.getAllUsersPhotoVerificationRequests); +router.get("/picture/:userId", authenticate, users.getUserPhotoVerificationRequests); router.patch("/profileURL", authenticate, userValidator.updateProfileURL, users.profileURL); router.patch("/rejectDiff", authenticate, authorizeRoles([SUPERUSER]), users.rejectProfileDiff); router.patch("/:userId", authenticate, authorizeRoles([SUPERUSER]), users.updateUser); diff --git a/services/imageService.js b/services/imageService.js index 3c5bc739c..3c6ad04b4 100644 --- a/services/imageService.js +++ b/services/imageService.js @@ -9,7 +9,7 @@ const cloudinaryMetaData = require("../constants/cloudinary"); * @param file { Object }: multipart file data * @param userId { string }: User id */ -const uploadProfilePicture = async ({ file, userId, coordinates }) => { +const uploadProfilePicture = async ({ file, userId, coordinates }, dev = false) => { try { const parser = new DatauriParser(); const imageDataUri = parser.format(file.originalname, file.buffer); @@ -24,6 +24,9 @@ const uploadProfilePicture = async ({ file, userId, coordinates }) => { }, }); const { public_id: publicId, secure_url: url } = uploadResponse; + if (dev) { + return { publicId, url }; + } await userModel.updateUserPicture({ publicId, url }, userId); return { publicId, url }; } catch (err) { diff --git a/test/fixtures/user/photo-verification.js b/test/fixtures/user/photo-verification.js index 1950b750e..7f139ca80 100644 --- a/test/fixtures/user/photo-verification.js +++ b/test/fixtures/user/photo-verification.js @@ -1,41 +1,66 @@ -const userPhotoVerificationData = { - discordId: "12345", - userId: "1234567abcd", - discord: { - url: "https://cdn.discordapp.com/avatars/abc/1234abcd.png", - approved: true, - date: { - _seconds: 1686518413, - _nanoseconds: 453000000, +const userPhotoVerificationData = [ + { + discordId: "12345", + userId: "1234567abcd", + discord: { + url: "https://cdn.discordapp.com/avatars/abc/1234abcd.png", + approved: false, + updatedAt: 1712788779, + }, + profile: { + url: "https://res.cloudinary.com/avatars/1234/something.png", + approved: false, + updatedAt: 1712788779, + publicId: "profile/1234567abcd/umgnk8o7ujrzbmy", }, + status: "PENDING", }, - profile: { - url: "https://res.cloudinary.com/avatars/1234/something.png", - approved: false, - date: { - _seconds: 1686518413, - _nanoseconds: 453000000, + { + discordId: "67890", + userId: "abcdefg1234567", + discord: { + url: "https://cdn.discordapp.com/avatars/def/5678efgh.png", + approved: false, + updatedAt: 1712788779, + }, + profile: { + url: "https://res.cloudinary.com/avatars/5678/another.png", + approved: true, + updatedAt: 1712788779, + publicId: "profile/abcdefg1234567/xyzabc123", }, + status: "PENDING", }, -}; + { + discordId: "12345", + userId: "hijklmn8901234", + discord: { + url: "https://cdn.discordapp.com/avatars/abc/1234ijkl.png", + approved: true, + updatedAt: 1712788779, + }, + profile: { + url: "https://res.cloudinary.com/avatars/1234/different.png", + approved: false, + updatedAt: 1712788779, + publicId: "profile/hijklmn8901234/defghi456", + }, + status: "PENDING", + }, +]; + const newUserPhotoVerificationData = { discordId: "1234567", userId: "new-user-id", discord: { url: "https://discord.example.com/demo.png", approved: false, - date: { - _seconds: 1686518413, - _nanoseconds: 453000000, - }, + updatedAt: 1712788779, }, profile: { url: "https://cloudinary.example.com/demo.png", approved: false, - date: { - _seconds: 1686518413, - _nanoseconds: 453000000, - }, + updatedAt: 1712788779, }, }; diff --git a/test/integration/discordactions.test.js b/test/integration/discordactions.test.js index 8d64574fb..b2bbf0258 100644 --- a/test/integration/discordactions.test.js +++ b/test/integration/discordactions.test.js @@ -72,12 +72,7 @@ describe("Discord actions", function () { superUserAuthToken = authService.generateAuthToken({ userId: superUserId }); userAuthToken = authService.generateAuthToken({ userId: userId }); jwt = authService.generateAuthToken({ userId }); - discordId = "12345"; - - const docRefUser0 = photoVerificationModel.doc(); - userPhotoVerificationData.userId = userId; - userPhotoVerificationData.discordId = discordId; - await docRefUser0.set(userPhotoVerificationData); + discordId = userData[0].discordId; }); afterEach(async function () { @@ -85,49 +80,62 @@ describe("Discord actions", function () { await cleanDb(); }); - describe("PATCH /discord-actions/picture/id", function () { - it("Should successfully update a picture", function (done) { + describe("PATCH /discord-actions/avatar/discordId/photo-verification/update", function () { + let photoVerificationData; + + beforeEach(async function () { + photoVerificationData = userPhotoVerificationData[0]; + + const userDocRef = photoVerificationModel.doc(); + photoVerificationData.userId = userId; + photoVerificationData.discordId = discordId; + await userDocRef.set(photoVerificationData); + photoVerificationData.id = userDocRef.id; + }); + + it("Should successfully update a picture", async function () { + const newDiscordAvatarLink = "https://cdn.discordapp.com/avatars/12345/12345.png"; + const newDiscordAvatarName = "12345"; + fetchStub.returns( Promise.resolve({ status: 200, - json: () => Promise.resolve({ user: { avatar: 12345 } }), + json: () => Promise.resolve({ user: { avatar: newDiscordAvatarName } }), }) ); - chai + + const res = await chai .request(app) - .patch(`/discord-actions/avatar/verify/${discordId}`) - .set("cookie", `${cookieName}=${superUserAuthToken}`) - .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("Discord avatar URL updated successfully!"); - return done(); - }); + .patch(`/discord-actions/avatar/${photoVerificationData.discordId}/photo-verification/update`) + .set("cookie", `${cookieName}=${superUserAuthToken}`); + + expect(res).to.have.status(200); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("Discord avatar URL updated successfully!"); + + const updatedPhotoVerificationSnapshot = await photoVerificationModel.doc(photoVerificationData.id).get(); + const updatedPhotoVerificationData = updatedPhotoVerificationSnapshot.data(); + expect(updatedPhotoVerificationData.profile.approved).to.equal(false); + expect(updatedPhotoVerificationData.discord.approved).to.equal(false); + expect(updatedPhotoVerificationData.discord.url).to.equal(newDiscordAvatarLink); }); - it("Should throw error if failed to update a picture", function (done) { + it("Should throw error if failed to update a picture", async function () { fetchStub.returns( Promise.resolve({ status: 200, json: () => Promise.resolve({ user: { avatar: 12345 } }), }) ); - chai + + const res = await chai .request(app) - .patch(`/discord-actions/avatar/verify/${discordId + "random-error-string"}`) - .set("cookie", `${cookieName}=${superUserAuthToken}`) - .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("An internal server error occurred"); - return done(); - }); + .patch(`/discord-actions/avatar/${discordId + "random-error-string"}/photo-verification/update/`) + .set("cookie", `${cookieName}=${superUserAuthToken}`); + + expect(res).to.have.status(500); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("An internal server error occurred"); }); }); diff --git a/test/integration/users.test.js b/test/integration/users.test.js index d613ff2f9..906775ffd 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -25,6 +25,7 @@ const { } = require("../fixtures/userStatus/userStatus"); const { addJoinData, addOrUpdate } = require("../../models/users"); const userStatusModel = require("../../models/userStatus"); +const { IMAGE_VERIFICATION_TYPES } = require("../../constants/imageVerificationTypes"); const userRoleUpdate = userData[4]; const userRoleUnArchived = userData[13]; @@ -39,6 +40,7 @@ const cookieName = config.get("userToken.cookieName"); const { userPhotoVerificationData } = require("../fixtures/user/photo-verification"); const Sinon = require("sinon"); const { INTERNAL_SERVER_ERROR, SOMETHING_WENT_WRONG } = require("../../constants/errorMessages"); +const { photoVerificationRequestStatus } = require("../../constants/users"); const photoVerificationModel = firestore.collection("photo-verification"); chai.use(chaiHttp); @@ -51,14 +53,10 @@ describe("Users", function () { let fetchStub; beforeEach(async function () { - userId = await addUser(); + userId = await addUser(nonSuperUser); jwt = authService.generateAuthToken({ userId }); superUserId = await addUser(superUser); superUserAuthToken = authService.generateAuthToken({ userId: superUserId }); - - const userDocRef = photoVerificationModel.doc(); - userPhotoVerificationData.userId = userId; - await userDocRef.set(userPhotoVerificationData); }); afterEach(async function () { @@ -1657,10 +1655,21 @@ describe("Users", function () { }); describe("PATCH /users/picture/verify/id", function () { + let photoVerificationData; + + beforeEach(async function () { + photoVerificationData = userPhotoVerificationData[0]; + + const userDocRef = photoVerificationModel.doc(); + photoVerificationData.userId = userId; + await userDocRef.set(photoVerificationData); + photoVerificationData.id = userDocRef.id; + }); + it("Should verify the discord image of the user", function (done) { chai .request(app) - .patch(`/users/picture/verify/${userId}?type=discord`) + .patch(`/users/picture/verify/${userId}?status=${photoVerificationRequestStatus.APPROVED}&type=discord`) .set("cookie", `${cookieName}=${superUserAuthToken}`) .end((err, res) => { if (err) { @@ -1668,15 +1677,55 @@ describe("Users", function () { } expect(res).to.have.status(200); expect(res.body).to.be.a("object"); - expect(res.body.message).to.equal("discord image was verified successfully!"); + expect(res.body.message).to.equal("User image data verified successfully"); return done(); }); }); + it("Should verify both the images of the user", async function () { + const res = await chai + .request(app) + .patch( + `/users/picture/verify/${userId}?status=${photoVerificationRequestStatus.APPROVED}&type=${IMAGE_VERIFICATION_TYPES.PROFILE_DISCORD}` + ) + .set("cookie", `${cookieName}=${superUserAuthToken}`); + + expect(res).to.have.status(200); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("User image data verified successfully"); + + const doc = await photoVerificationModel.doc(photoVerificationData.id).get(); + const data = doc.data(); + + expect(data.discord.approved).to.equal(true); + expect(data.profile.approved).to.equal(true); + expect(data.status).to.equal(photoVerificationRequestStatus.APPROVED); + }); + + it("Should reject both the images of the user", async function () { + const res = await chai + .request(app) + .patch( + `/users/picture/verify/${userId}?status=${photoVerificationRequestStatus.REJECTED}&type=${IMAGE_VERIFICATION_TYPES.PROFILE_DISCORD}` + ) + .set("cookie", `${cookieName}=${superUserAuthToken}`); + + expect(res).to.have.status(200); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("User photo verification request rejected successfully"); + + const doc = await photoVerificationModel.doc(photoVerificationData.id).get(); + const data = doc.data(); + + expect(data.discord.approved).to.equal(false); + expect(data.profile.approved).to.equal(false); + expect(data.status).to.equal(photoVerificationRequestStatus.REJECTED); + }); + it("Should throw for wrong query while verifying the discord image of the user", function (done) { chai .request(app) - .patch(`/users/picture/verify/${userId}?type=RANDOM`) + .patch(`/users/picture/verify/${userId}?status=${photoVerificationRequestStatus.APPROVED}&type=RANDOM`) .set("cookie", `${cookieName}=${superUserAuthToken}`) .end((err, res) => { if (err) { @@ -1689,7 +1738,98 @@ describe("Users", function () { }); }); - describe("GET /users/picture/id", function () { + describe("GET /users/picture/all", function () { + let photoVerificationData; + + beforeEach(async function () { + photoVerificationData = userPhotoVerificationData[0]; + + const userDocRef = photoVerificationModel.doc(); + photoVerificationData.userId = userId; + await userDocRef.set(photoVerificationData); + photoVerificationData.id = userDocRef.id; + }); + + it("Should get the all users photo verification record", function (done) { + chai + .request(app) + .get(`/users/picture/all`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .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("User image verification record fetched successfully!"); + + const resultData = res.body.data[0]; + + expect(resultData.userId).to.be.equal(photoVerificationData.userId); + expect(resultData.discordId).to.be.equal(photoVerificationData.discordId); + expect(resultData.profile).to.deep.equal(photoVerificationData.profile); + expect(resultData.discord).to.deep.equal(photoVerificationData.discord); + return done(); + }); + }); + + it("Should get the user's photo verification record, for a given username", function (done) { + chai + .request(app) + .get(`/users/picture/all?username=${nonSuperUser.username}`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .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("User image verification record fetched successfully!"); + + const resultData = res.body.data[0]; + + expect(resultData.user.username).to.be.equal(nonSuperUser.username); + expect(resultData.userId).to.be.equal(photoVerificationData.userId); + expect(resultData.discordId).to.be.equal(photoVerificationData.discordId); + expect(resultData.profile).to.deep.equal(photoVerificationData.profile); + expect(resultData.discord).to.deep.equal(photoVerificationData.discord); + return done(); + }); + }); + + it("Should not get any photo verification object for random username", function (done) { + chai + .request(app) + .get(`/users/picture/all?username=randomUsername`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) + .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("User image verification record fetched successfully!"); + + const resultDataLen = res.body.data.length; + + expect(resultDataLen).to.be.equal(0); + return done(); + }); + }); + }); + + describe("GET /users/picture/userId", function () { + let photoVerificationData; + + beforeEach(async function () { + photoVerificationData = userPhotoVerificationData[0]; + + const userDocRef = photoVerificationModel.doc(); + photoVerificationData.userId = userId; + await userDocRef.set(photoVerificationData); + photoVerificationData.id = userDocRef.id; + }); + it("Should get the user's verification record", function (done) { chai .request(app) @@ -1701,7 +1841,10 @@ describe("Users", function () { } expect(res).to.have.status(200); expect(res.body).to.be.a("object"); - expect(res.body.data).to.deep.equal(userPhotoVerificationData); + expect(res.body.data.userId).to.be.equal(photoVerificationData.userId); + expect(res.body.data.discordId).to.be.equal(photoVerificationData.discordId); + expect(res.body.data.profile).to.deep.equal(photoVerificationData.profile); + expect(res.body.data.discord).to.deep.equal(photoVerificationData.discord); expect(res.body.message).to.equal("User image verification record fetched successfully!"); return done(); }); diff --git a/test/unit/models/discordactions.test.js b/test/unit/models/discordactions.test.js index f8d903872..57fc51b5f 100644 --- a/test/unit/models/discordactions.test.js +++ b/test/unit/models/discordactions.test.js @@ -282,11 +282,13 @@ describe("discordactions", function () { describe("updateDiscordImageForVerification", function () { let fetchStub; + let photoVerificationFixture; beforeEach(async function () { fetchStub = sinon.stub(global, "fetch"); const docRefUser0 = photoVerificationModel.doc(); - await docRefUser0.set(userPhotoVerificationData); + await docRefUser0.set(userPhotoVerificationData[0]); + photoVerificationFixture = userPhotoVerificationData[0]; }); afterEach(async function () { @@ -295,7 +297,7 @@ describe("discordactions", function () { }); it("should update the user's discord image for verification", async function () { - const userDiscordId = "12345"; + const userDiscordId = photoVerificationFixture.discordId; const discordAvatarUrl = "https://cdn.discordapp.com/avatars/12345/12345.png"; fetchStub.returns( Promise.resolve({ @@ -325,7 +327,7 @@ describe("discordactions", function () { }); it("should log and rethrow an error if an error occurs during the process", async function () { - const userDiscordId = "12345"; + const userDiscordId = photoVerificationFixture.discordId; const error = new Error("Test error"); fetchStub.returns( diff --git a/test/unit/models/users.test.js b/test/unit/models/users.test.js index 693c3989b..9e971e6fc 100644 --- a/test/unit/models/users.test.js +++ b/test/unit/models/users.test.js @@ -10,7 +10,7 @@ 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 { userPhotoVerificationData } = require("../../fixtures/user/photo-verification"); const { generateStatusDataForState } = require("../../fixtures/userStatus/userStatus"); const userModel = firestore.collection("users"); const userStatusModel = firestore.collection("usersStatus"); @@ -21,6 +21,8 @@ const photoVerificationModel = firestore.collection("photo-verification"); const userData = require("../../fixtures/user/user"); const addUser = require("../../utils/addUser"); const { userState } = require("../../../constants/userStatus"); +const { photoVerificationRequestStatus } = require("../../../constants/users"); +const { IMAGE_VERIFICATION_TYPES } = require("../../../constants/imageVerificationTypes"); const app = require("../../../server"); const prodUsers = require("../../fixtures/user/prodUsers"); const authService = require("../../../services/authService"); @@ -140,15 +142,18 @@ describe("users", function () { }); describe("user image verification", function () { - let userId, discordId, profileImageUrl, discordImageUrl; + let photoVerificationData = {}; beforeEach(async function () { + const userData = userDataArray[0]; + const { userId } = await users.addOrUpdate(userData); + + const photoVerificationDataFix = userPhotoVerificationData[0]; + photoVerificationDataFix.userId = userId; + const docRefUser0 = photoVerificationModel.doc(); - await docRefUser0.set(userPhotoVerificationData); - userId = newUserPhotoVerificationData.userId; - discordId = newUserPhotoVerificationData.discordId; - profileImageUrl = newUserPhotoVerificationData.profile.url; - discordImageUrl = newUserPhotoVerificationData.discord.url; + await docRefUser0.set(photoVerificationDataFix); + photoVerificationData = userPhotoVerificationData[0]; }); afterEach(async function () { @@ -156,59 +161,136 @@ describe("users", function () { }); it("adds new user images For Verification", async function () { - const result = await users.addForVerification(userId, discordId, profileImageUrl, discordImageUrl); + const userId = photoVerificationData.userId; + const result = await users.addForVerification( + photoVerificationData.userId, + photoVerificationData.discordId, + photoVerificationData.profile.url, + photoVerificationData.discord.url, + photoVerificationData.profile.publicId + ); const verificationSnapshot = await photoVerificationModel.where("userId", "==", userId).limit(1).get(); expect(verificationSnapshot.empty).to.be.equal(false); - newUserPhotoVerificationData.profile.date = verificationSnapshot.docs[0].data().profile.date; - newUserPhotoVerificationData.discord.date = verificationSnapshot.docs[0].data().discord.date; + photoVerificationData.profile.updatedAt = verificationSnapshot.docs[0].data().profile.updatedAt; + photoVerificationData.discord.updatedAt = verificationSnapshot.docs[0].data().discord.updatedAt; const docData = verificationSnapshot.docs[0].data(); - expect(docData).to.deep.equal(newUserPhotoVerificationData); + expect(docData).to.deep.equal(photoVerificationData); expect(result.message).to.be.equal("Profile data added for verification successfully"); }); - it("adds user images For Verification", async function () { - const userId = "1234567abcd"; + it("marks user profile image as verified", async function () { + const userId = photoVerificationData.userId; + const imageType = IMAGE_VERIFICATION_TYPES.PROFILE; const verificationSnapshotBeforeUpdate = await photoVerificationModel .where("userId", "==", userId) + .where("status", "==", photoVerificationRequestStatus.PENDING) .limit(1) .get(); - const docDataPrev = verificationSnapshotBeforeUpdate.docs[0].data(); - const result = await users.addForVerification(userId, discordId, profileImageUrl, discordImageUrl); - const verificationSnapshot = await photoVerificationModel.where("userId", "==", userId).limit(1).get(); - const docData = verificationSnapshot.docs[0].data(); - expect(docData).to.have.all.keys(["userId", "discordId", "discord", "profile"]); + const docRef = verificationSnapshotBeforeUpdate.docs[0].ref; + + const docDataPrev = (await docRef.get()).data(); + const result = await users.changePhotoVerificationStatus( + userId, + imageType, + photoVerificationRequestStatus.APPROVED + ); + + const docData = (await docRef.get()).data(); + expect(docData.profile.approved).to.not.be.equal(docDataPrev.profile.approved); + expect(docData.profile.approved).to.be.equal(true); + + expect(result).to.be.equal("User image data verified successfully"); + }); + + it("adds user images For Verification, updates the pending verification object", async function () { + const userId = photoVerificationData.userId; + const imageType = IMAGE_VERIFICATION_TYPES.PROFILE; + const verificationSnapshotBeforeUpdate = await photoVerificationModel + .where("userId", "==", userId) + .limit(1) + .get(); + + // Update the status of the profile image to approved + await users.changePhotoVerificationStatus(userId, imageType, photoVerificationRequestStatus.APPROVED); + + const docRef = verificationSnapshotBeforeUpdate.docs[0].ref; + + const docDataPrev = (await docRef.get()).data(); + const result = await users.addForVerification( + photoVerificationData.userId, + photoVerificationData.discordId, + photoVerificationData.profile.url, + photoVerificationData.discord.url, + photoVerificationData.profile.publicId + ); + + const docData = (await docRef.get()).data(); + expect(docData).to.have.all.keys(["userId", "discordId", "discord", "profile", "status"]); expect(docData.discord.approved).to.be.equal(docDataPrev.discord.approved); - expect(docData.discord.url).to.be.equal(discordImageUrl); - expect(docData.profile.url).to.be.equal(profileImageUrl); + expect(docData.discord.url).to.be.equal(photoVerificationData.discord.url); + expect(docData.profile.url).to.be.equal(photoVerificationData.profile.url); + expect(docDataPrev.profile.approved).to.be.equal(true); + expect(docData.profile.approved).to.be.equal(false); expect(result.message).to.be.equal("Profile data added for verification successfully"); }); - it("marks user profile image as verified", async function () { - const userId = "1234567abcd"; - const imageType = "profile"; + it("marks photo verification object status as APPROVED", async function () { + const imageType = IMAGE_VERIFICATION_TYPES.PROFILE_DISCORD; + const userId = photoVerificationData.userId; const verificationSnapshotBeforeUpdate = await photoVerificationModel .where("userId", "==", userId) + .where("status", "==", photoVerificationRequestStatus.PENDING) .limit(1) .get(); - const docDataPrev = verificationSnapshotBeforeUpdate.docs[0].data(); - const result = await users.markAsVerified(userId, imageType); - const verificationSnapshot = await photoVerificationModel.where("userId", "==", userId).limit(1).get(); - const docData = verificationSnapshot.docs[0].data(); - expect(docData.profile.approved).to.not.be.equal(docDataPrev.profile.approved); + const docRef = verificationSnapshotBeforeUpdate.docs[0].ref; + const result = await users.changePhotoVerificationStatus( + userId, + imageType, + photoVerificationRequestStatus.APPROVED + ); + + const docData = (await docRef.get()).data(); + expect(docData.profile.approved).to.be.equal(true); + expect(docData.discord.approved).to.be.equal(true); + expect(docData.status).to.be.equal(photoVerificationRequestStatus.APPROVED); + + expect(result).to.be.equal("User image data verified successfully"); + }); + + it("marks photo verification object status as APPROVED, when both images are approved one by one", async function () { + const userId = photoVerificationData.userId; + let imageType = IMAGE_VERIFICATION_TYPES.PROFILE; + const verificationSnapshotBeforeUpdate = await photoVerificationModel + .where("userId", "==", userId) + .limit(1) + .get(); + + const docRef = verificationSnapshotBeforeUpdate.docs[0].ref; + await users.changePhotoVerificationStatus(userId, imageType, photoVerificationRequestStatus.APPROVED); + + let docData = (await docRef.get()).data(); + expect(docData.profile.approved).to.be.equal(true); + expect(docData.discord.approved).to.be.equal(false); + expect(docData.status).to.be.equal(photoVerificationRequestStatus.PENDING); - expect(result.message).to.be.equal("User image data verified successfully"); + imageType = IMAGE_VERIFICATION_TYPES.PROFILE_DISCORD; + await users.changePhotoVerificationStatus(userId, imageType, photoVerificationRequestStatus.APPROVED); + docData = (await docRef.get()).data(); + expect(docData.profile.approved).to.be.equal(true); + expect(docData.discord.approved).to.be.equal(true); + expect(docData.status).to.be.equal(photoVerificationRequestStatus.APPROVED); }); it("throws an error if verification document not found", async function () { const userId = "non-existent-userId"; - const imageType = "profile"; + const imageType = IMAGE_VERIFICATION_TYPES.PROFILE; try { - await users.markAsVerified(userId, imageType); + await users.changePhotoVerificationStatus(userId, imageType, photoVerificationRequestStatus.APPROVED); } catch (error) { expect(error).to.be.instanceOf(Error); expect(error.message).to.be.equal("No verification document record data for user was found"); @@ -216,20 +298,25 @@ describe("users", function () { }); it("gets user image verification data", async function () { - const userId = "1234567abcd"; + const userId = photoVerificationData.userId; - const result = await users.getUserImageForVerification(userId); + const result = (await users.getUserPhotoVerificationRequests(userId))[0]; const verificationSnapshot = await photoVerificationModel.where("userId", "==", userId).limit(1).get(); const docData = verificationSnapshot.docs[0].data(); - expect(result).to.deep.equal(docData); + expect(result.profile.url).to.deep.equal(docData.profile.url); + expect(result.profile.publicId).to.deep.equal(docData.profile.publicId); + expect(result.discord.url).to.deep.equal(docData.discord.url); + expect(result.discordId).to.deep.equal(docData.discordId); + expect(result.userId).to.deep.equal(docData.userId); + expect(result.status).to.deep.equal(docData.status); }); it("throws an error if verification document could not be found due to invalid user Id", async function () { const userId = "non-existent-userId"; try { - await users.getUserImageForVerification(userId); + await users.getUserPhotoVerificationRequests(userId); } catch (error) { expect(error).to.be.instanceOf(Error); expect(error.message).to.be.equal(`No document with userId: ${userId} was found!`);