From 2fc51c6c0d70212d6f5640a521454634d3fee807 Mon Sep 17 00:00:00 2001 From: Joy <56365512+ardourApeX@users.noreply.github.com> Date: Thu, 4 Apr 2024 23:41:48 +0530 Subject: [PATCH] Feat/updating verification flow (#1990) * Feat : Added API to verify external-account * Tests : Updated fixures & external-account response & payload for tests * Test : Added tests for external-account/link API * Test : Added more tests to inc code coverage --------- Co-authored-by: Achintya Chatterjee <55826451+Achintya-Chatterjee@users.noreply.github.com> --- controllers/external-accounts.js | 30 +++++ middlewares/validators/external-accounts.js | 41 ++++++- routes/external-accounts.js | 1 + .../external-accounts/external-accounts.js | 16 +++ test/fixtures/user/user.js | 1 - test/integration/external-accounts.test.js | 110 ++++++++++++++++++ .../external-accounts-validator.test.js | 33 +++++- test/unit/models/external-accounts.test.js | 4 + 8 files changed, 227 insertions(+), 9 deletions(-) diff --git a/controllers/external-accounts.js b/controllers/external-accounts.js index 751c7ccdd..f37675bb8 100644 --- a/controllers/external-accounts.js +++ b/controllers/external-accounts.js @@ -45,6 +45,35 @@ const getExternalAccountData = async (req, res) => { return res.boom.serverUnavailable(SOMETHING_WENT_WRONG); } }; +const linkExternalAccount = async (req, res) => { + try { + const { id: userId, roles } = req.userData; + + const externalAccountData = await externalAccountsModel.fetchExternalAccountData(req.query, req.params.token); + if (!externalAccountData.id) { + return res.boom.notFound("No data found"); + } + + const attributes = externalAccountData.attributes; + if (attributes.expiry && attributes.expiry < Date.now()) { + return res.boom.unauthorized("Token Expired. Please generate it again"); + } + + await addOrUpdate( + { + roles: { ...roles, in_discord: true }, + discordId: attributes.discordId, + discordJoinedAt: attributes.discordJoinedAt, + }, + userId + ); + + return res.status(204).json({ message: "Your discord profile has been linked successfully" }); + } catch (error) { + logger.error(`Error getting external account data: ${error}`); + return res.boom.serverUnavailable(SOMETHING_WENT_WRONG); + } +}; /** * @deprecated @@ -222,6 +251,7 @@ const newSyncExternalAccountData = async (req, res) => { module.exports = { addExternalAccountData, getExternalAccountData, + linkExternalAccount, syncExternalAccountData, newSyncExternalAccountData, externalAccountsUsersPostHandler, diff --git a/middlewares/validators/external-accounts.js b/middlewares/validators/external-accounts.js index aae09611e..1b083d3ab 100644 --- a/middlewares/validators/external-accounts.js +++ b/middlewares/validators/external-accounts.js @@ -2,11 +2,21 @@ const joi = require("joi"); const { EXTERNAL_ACCOUNTS_POST_ACTIONS } = require("../../constants/external-accounts"); const externalAccountData = async (req, res, next) => { - const schema = joi.object().strict().keys({ - type: joi.string().required(), - token: joi.string().required(), - attributes: joi.object().strict().required(), - }); + const schema = joi + .object() + .strict() + .keys({ + type: joi.string().required(), + token: joi.string().required(), + attributes: { + userName: joi.string().required(), + discriminator: joi.string().required(), + userAvatar: joi.string().required(), + discordId: joi.string().required(), + discordJoinedAt: joi.string().required(), + expiry: joi.number().required(), + }, + }); try { await schema.validateAsync(req.body); @@ -35,4 +45,23 @@ const postExternalAccountsUsers = async (req, res, next) => { res.boom.badRequest(error.details[0].message); } }; -module.exports = { externalAccountData, postExternalAccountsUsers }; + +const linkDiscord = async (req, res, next) => { + const { token } = req.params; + + const schema = joi.object({ + token: joi.string().required(), + }); + + const validationOptions = { abortEarly: false }; + + try { + await schema.validateAsync({ token }, validationOptions); + next(); + } catch (error) { + logger.error(`Error retrieving event: ${error}`); + res.boom.badRequest(error.details.map((detail) => detail.message)); + } +}; + +module.exports = { externalAccountData, postExternalAccountsUsers, linkDiscord }; diff --git a/routes/external-accounts.js b/routes/external-accounts.js index 76a10c62d..2cf24c484 100644 --- a/routes/external-accounts.js +++ b/routes/external-accounts.js @@ -12,6 +12,7 @@ const { authorizeAndAuthenticate } = require("../middlewares/authorizeUsersAndSe router.post("/", validator.externalAccountData, authorizeBot.verifyDiscordBot, externalAccount.addExternalAccountData); router.get("/:token", authenticate, externalAccount.getExternalAccountData); +router.patch("/link/:token", authenticate, validator.linkDiscord, externalAccount.linkExternalAccount); router.patch("/discord-sync", authenticate, authorizeRoles([SUPERUSER]), externalAccount.syncExternalAccountData); router.post( "/users", diff --git a/test/fixtures/external-accounts/external-accounts.js b/test/fixtures/external-accounts/external-accounts.js index 58fe54f7a..86a061e10 100644 --- a/test/fixtures/external-accounts/external-accounts.js +++ b/test/fixtures/external-accounts/external-accounts.js @@ -4,7 +4,11 @@ module.exports = () => { type: "discord", token: "", attributes: { + userName: "", + discriminator: "", + userAvatar: "", discordId: "", + discordJoinedAt: "", expiry: 1674041460211, }, }, @@ -13,7 +17,11 @@ module.exports = () => { type: "discord", token: 123, attributes: { + userName: "", + discriminator: "", + userAvatar: "", discordId: "", + discordJoinedAt: "", expiry: 1674041460211, }, }, @@ -21,7 +29,11 @@ module.exports = () => { type: "discord", token: "", attributes: { + userName: "", + discriminator: "", + userAvatar: "", discordId: "", + discordJoinedAt: "", expiry: Date.now() + 600000, }, }, @@ -29,7 +41,11 @@ module.exports = () => { type: "discord", token: "", attributes: { + userName: "", + discriminator: "", + userAvatar: "", discordId: "", + discordJoinedAt: "", expiry: Date.now() - 600000, }, }, diff --git a/test/fixtures/user/user.js b/test/fixtures/user/user.js index ebf0977a1..9903a64af 100644 --- a/test/fixtures/user/user.js +++ b/test/fixtures/user/user.js @@ -91,7 +91,6 @@ 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", diff --git a/test/integration/external-accounts.test.js b/test/integration/external-accounts.test.js index f58525812..0572eaf36 100644 --- a/test/integration/external-accounts.test.js +++ b/test/integration/external-accounts.test.js @@ -425,4 +425,114 @@ describe("External Accounts", function () { }); }); }); + + describe("PATCH /external-accounts/link/:token", function () { + let newUserJWT; + + beforeEach(async function () { + const userId = await addUser(userData[3]); + newUserJWT = authService.generateAuthToken({ userId }); + await externalAccountsModel.addExternalAccountData(externalAccountData[2]); + await externalAccountsModel.addExternalAccountData(externalAccountData[3]); + }); + + afterEach(async function () { + Sinon.restore(); + await cleanDb(); + }); + + it("Should return 404 when token is not provided in path variable", async function () { + const res = await chai.request(app).patch("/external-accounts/link").set("Cookie", `${cookieName}=${newUserJWT}`); + expect(res).to.have.status(404); + expect(res.body.message).to.equal("Not Found"); + }); + + it("Should return 404 when no data found", function (done) { + chai + .request(app) + .get("/external-accounts/") + .set("Authorization", `Bearer ${newUserJWT}`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(404); + expect(res.body).to.have.property("message"); + expect(res.body.message).to.equal("No data found"); + + return done(); + }); + }); + + it("Should return 401 when token is expired", function (done) { + chai + .request(app) + .get("/external-accounts/") + .set("Authorization", `Bearer ${newUserJWT}`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(401); + expect(res.body).to.be.an("object"); + expect(res.body).to.eql({ + statusCode: 401, + error: "Unauthorized", + message: "Token Expired. Please generate it again", + }); + + return done(); + }); + }); + + it("Should return 401 when user is not authenticated", function (done) { + chai + .request(app) + .get("/external-accounts/") + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(401); + expect(res.body).to.be.an("object"); + expect(res.body).to.eql({ + statusCode: 401, + error: "Unauthorized", + message: "Unauthenticated User", + }); + + return done(); + }); + }); + + it("Should return 204 when valid action is provided", async function () { + await externalAccountsModel.addExternalAccountData(externalAccountData[2]); + const getUserResponseBeforeUpdate = await chai + .request(app) + .get("/users/self") + .set("cookie", `${cookieName}=${newUserJWT}`); + + expect(getUserResponseBeforeUpdate).to.have.status(200); + expect(getUserResponseBeforeUpdate.body.roles.in_discord).to.equal(false); + expect(getUserResponseBeforeUpdate.body).to.not.have.property("discordId"); + expect(getUserResponseBeforeUpdate.body).to.not.have.property("discordJoinedAt"); + + const response = await chai + .request(app) + .patch(`/external-accounts/link/${externalAccountData[2].token}`) + .query({ action: EXTERNAL_ACCOUNTS_POST_ACTIONS.DISCORD_USERS_SYNC }) + .set("Cookie", `${cookieName}=${newUserJWT}`); + + expect(response).to.have.status(204); + + const updatedUserDetails = await chai + .request(app) + .get("/users/self") + .set("cookie", `${cookieName}=${newUserJWT}`); + + expect(updatedUserDetails.body.roles.in_discord).to.equal(true); + expect(updatedUserDetails.body).to.have.property("discordId"); + expect(updatedUserDetails.body).to.have.property("discordJoinedAt"); + }); + }); }); diff --git a/test/unit/middlewares/external-accounts-validator.test.js b/test/unit/middlewares/external-accounts-validator.test.js index b10f97557..a06f291cb 100644 --- a/test/unit/middlewares/external-accounts-validator.test.js +++ b/test/unit/middlewares/external-accounts-validator.test.js @@ -1,5 +1,9 @@ const Sinon = require("sinon"); -const { externalAccountData, postExternalAccountsUsers } = require("../../../middlewares/validators/external-accounts"); +const { + externalAccountData, + postExternalAccountsUsers, + linkDiscord, +} = require("../../../middlewares/validators/external-accounts"); const { EXTERNAL_ACCOUNTS_POST_ACTIONS } = require("../../../constants/external-accounts"); const { expect } = require("chai"); @@ -10,7 +14,14 @@ describe("Middleware | Validators | external accounts", function () { body: { type: "some type", token: "some token", - attributes: {}, + attributes: { + userName: "some name", + discriminator: "some discriminator", + userAvatar: "some avatar", + discordId: "some id", + discordJoinedAt: "some date", + expiry: Date.now(), + }, }, }; const res = {}; @@ -62,4 +73,22 @@ describe("Middleware | Validators | external accounts", function () { expect(res.boom.badRequest.callCount).to.be.equal(1); }); }); + + describe("linkDiscord", function () { + it("should call next with a valid token", async function () { + const req = { params: { token: "validToken" } }; + const res = {}; + const nextSpy = Sinon.spy(); + await linkDiscord(req, res, nextSpy); + expect(nextSpy.calledOnce).to.be.equal(true); + }); + + it("should throw an error when token is empty", async function () { + const req = { params: { token: "" } }; + const res = { boom: { badRequest: Sinon.spy() } }; + const nextSpy = Sinon.spy(); + await linkDiscord(req, res, nextSpy); + expect(res.boom.badRequest.calledOnce).to.be.equal(true); + }); + }); }); diff --git a/test/unit/models/external-accounts.test.js b/test/unit/models/external-accounts.test.js index 59f3dbd04..93b9a79b0 100644 --- a/test/unit/models/external-accounts.test.js +++ b/test/unit/models/external-accounts.test.js @@ -33,7 +33,11 @@ describe("External Accounts", function () { expect(response.token).to.equal(externalAccountData[2].token); expect(response.attributes).to.be.eql({ discordId: externalAccountData[2].attributes.discordId, + discordJoinedAt: externalAccountData[2].attributes.discordJoinedAt, expiry: externalAccountData[2].attributes.expiry, + userName: externalAccountData[2].attributes.userName, + discriminator: externalAccountData[2].attributes.discriminator, + userAvatar: externalAccountData[2].attributes.userAvatar, }); });