diff --git a/controllers/discordactions.js b/controllers/discordactions.js index 1b21180c4..c5b74634d 100644 --- a/controllers/discordactions.js +++ b/controllers/discordactions.js @@ -23,7 +23,8 @@ const DISCORD_BASE_URL = config.get("services.discordBot.baseUrl"); const createGroupRole = async (req, res) => { try { - const rolename = `group-${req.body.rolename}`; + const { role = false } = req.query; + const rolename = role ? req.body.rolename : `group-${req.body.rolename}`; const { roleExists } = await discordRolesModel.isGroupRoleExists({ rolename }); @@ -505,6 +506,7 @@ const generateInviteForUser = async (req, res) => { const inviteOptions = { channelId: channelId, + role: req.approvedApplicationRole, }; const response = await fetch(`${DISCORD_BASE_URL}/invite`, { method: "POST", diff --git a/controllers/external-accounts.js b/controllers/external-accounts.js index d369573ed..a2719ba76 100644 --- a/controllers/external-accounts.js +++ b/controllers/external-accounts.js @@ -5,7 +5,6 @@ const { addOrUpdate, getUsersByRole, updateUsersInBatch } = require("../models/u const { retrieveDiscordUsers, fetchUsersForKeyValues } = require("../services/dataAccessLayer"); const { EXTERNAL_ACCOUNTS_POST_ACTIONS } = require("../constants/external-accounts"); const removeDiscordRoleUtils = require("../utils/removeDiscordRoleFromUser"); -const addDiscordRoleUtils = require("../utils/addDiscordRoleToUser"); const config = require("config"); const logger = require("../utils/logger"); const { markUnDoneTasksOfArchivedUsersBacklog } = require("../models/tasks"); @@ -80,30 +79,7 @@ const linkExternalAccount = async (req, res) => { ); if (!unverifiedRoleRemovalResponse.success) { - // Tolerable errors that should not block role assignment - const tolerableErrors = ["Role doesn't exist", "Role deletion from database failed"]; - const isTolerableError = tolerableErrors.some((err) => unverifiedRoleRemovalResponse.message.includes(err)); - - if (!isTolerableError) { - const message = `User details updated but ${unverifiedRoleRemovalResponse.message}. Please contact admin`; - return res.boom.internal(message, { message }); - } - logger.info( - `Tolerable error during unverified role removal for Discord ID: ${attributes.discordId}: ${unverifiedRoleRemovalResponse.message}` - ); - } - - const developerRoleId = config.get("discordDeveloperRoleId"); - const newRoleId = config.get("discordNewRoleId"); - - try { - await addDiscordRoleUtils.addDiscordRoleToUser(attributes.discordId, developerRoleId, "Developer"); - await addDiscordRoleUtils.addDiscordRoleToUser(attributes.discordId, newRoleId, "New"); - logger.info(`Roles (Developer, New) assigned successfully for Discord ID: ${attributes.discordId}`); - } catch (roleError) { - logger.error(`Error assigning roles after verification: ${roleError}`); - const message = `Your discord profile has been linked but role assignment failed. Please contact admin`; - return res.boom.internal(message, { message }); + return res.boom.internal(null, { message: "Unverified role removal failed. Please contact admin" }); } return res.status(200).json({ message: "Your discord profile has been linked successfully" }); diff --git a/middlewares/checkCanGenerateDiscordLink.ts b/middlewares/checkCanGenerateDiscordLink.ts index 5f1693943..9fd7278a6 100644 --- a/middlewares/checkCanGenerateDiscordLink.ts +++ b/middlewares/checkCanGenerateDiscordLink.ts @@ -23,11 +23,16 @@ const checkCanGenerateDiscordLink = async (req: CustomRequest, res: CustomRespon } const approvedApplication = applications.find((application: { status: string; }) => application.status === 'accepted'); - + if (!approvedApplication) { return res.boom.forbidden("Only users with an accepted application can generate a Discord invite link."); } + if (approvedApplication.isNew !== true || !approvedApplication.role) { + return res.boom.forbidden("You are not eligible to generate a Discord invite link."); + } + + req.approvedApplicationRole = approvedApplication.role; return next(); } catch (error) { return res.boom.badImplementation("An error occurred while checking user applications."); diff --git a/middlewares/validators/discordactions.js b/middlewares/validators/discordactions.js index 42eaf8775..eb57aeb31 100644 --- a/middlewares/validators/discordactions.js +++ b/middlewares/validators/discordactions.js @@ -1,14 +1,20 @@ const Joi = require("joi"); const { validateMillisecondsTimestamp } = require("./utils"); +const logger = require("../../utils/logger"); const validateGroupRoleBody = async (req, res, next) => { - const schema = Joi.object({ + const bodySchema = Joi.object({ rolename: Joi.string().trim().required(), description: Joi.string().trim(), }); + const querySchema = Joi.object({ + role: Joi.boolean().default(false).optional(), + }); + try { - await schema.validateAsync(req.body); + await bodySchema.validateAsync(req.body); + req.query = await querySchema.validateAsync(req.query); next(); } catch (error) { logger.error(`Error validating createGroupRole payload : ${error}`); diff --git a/routes/discordactions.js b/routes/discordactions.js index b0f197b86..0f8a8cdb6 100644 --- a/routes/discordactions.js +++ b/routes/discordactions.js @@ -47,7 +47,7 @@ router.get("/invite", disableRoute, authenticate, getUserDiscordInvite); * Short-circuit this POST method for this endpoint * Refer https://github.com/Real-Dev-Squad/todo-action-items/issues/269 for more details. */ -router.post("/invite", disableRoute, authenticate, checkCanGenerateDiscordLink, generateInviteForUser); +router.post("/invite", authenticate, checkCanGenerateDiscordLink, generateInviteForUser); router.delete("/roles", authenticate, checkIsVerifiedDiscord, deleteRole); router.get("/roles", authenticate, checkIsVerifiedDiscord, getGroupsRoleId); diff --git a/test/integration/discordactions.test.js b/test/integration/discordactions.test.js index ac5c7af1a..69a1d7750 100644 --- a/test/integration/discordactions.test.js +++ b/test/integration/discordactions.test.js @@ -1377,4 +1377,192 @@ describe("Discord actions", function () { }); }); }); + + describe("POST /discord-actions/groups (createGroupRole)", function () { + let testUserId; + let testUserAuthToken; + + beforeEach(async function () { + testUserId = await addUser(userData[0]); + testUserAuthToken = authService.generateAuthToken({ userId: testUserId }); + + fetchStub.returns( + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ id: "discord-role-id-123" }), + }) + ); + + sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({ + roleExists: false, + }); + }); + + afterEach(async function () { + await cleanDb(); + }); + + it("should create a group role with 'group-' prefix when role query param is not provided", function (done) { + const roleData = { + rolename: "developers", + description: "Test group role", + }; + + chai + .request(app) + .post("/discord-actions/groups") + .set("cookie", `${cookieName}=${testUserAuthToken}`) + .send(roleData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(201); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("Role created successfully!"); + expect(res.body).to.have.property("id"); + expect(fetchStub.calledOnce).to.equal(true); + const fetchCall = fetchStub.getCall(0); + const requestBody = JSON.parse(fetchCall.args[1].body); + expect(requestBody.rolename).to.equal("group-developers"); + + return done(); + }); + }); + + it("should create a group role with 'group-' prefix when role=false", function (done) { + const roleData = { + rolename: "developers", + description: "Test group role", + }; + + chai + .request(app) + .post("/discord-actions/groups?role=false") + .set("cookie", `${cookieName}=${testUserAuthToken}`) + .send(roleData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(201); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("Role created successfully!"); + const fetchCall = fetchStub.getCall(0); + const requestBody = JSON.parse(fetchCall.args[1].body); + expect(requestBody.rolename).to.equal("group-developers"); + + return done(); + }); + }); + + it("should create a custom role without prefix when role=true", function (done) { + const roleData = { + rolename: "developers", + description: "Test custom role", + }; + + chai + .request(app) + .post("/discord-actions/groups?role=true") + .set("cookie", `${cookieName}=${testUserAuthToken}`) + .send(roleData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(201); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("Role created successfully!"); + const fetchCall = fetchStub.getCall(0); + const requestBody = JSON.parse(fetchCall.args[1].body); + expect(requestBody.rolename).to.equal("developers"); + + return done(); + }); + }); + + it("should return 400 when role query param is not a boolean", function (done) { + const roleData = { + rolename: "developers", + description: "Test role", + }; + + chai + .request(app) + .post("/discord-actions/groups?role=invalid") + .set("cookie", `${cookieName}=${testUserAuthToken}`) + .send(roleData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(400); + expect(res.body).to.be.an("object"); + expect(res.body.error).to.equal("Bad Request"); + + return done(); + }); + }); + + it("should return 400 when role already exists", function (done) { + sinon.restore(); + fetchStub = sinon.stub(global, "fetch"); + fetchStub.returns( + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ id: "discord-role-id-123" }), + }) + ); + + sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({ + roleExists: true, + }); + + const roleData = { + rolename: "developers", + description: "Test role", + }; + + chai + .request(app) + .post("/discord-actions/groups?role=true") + .set("cookie", `${cookieName}=${testUserAuthToken}`) + .send(roleData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(400); + expect(res.body).to.be.an("object"); + expect(res.body.message).to.equal("Role already exists!"); + + return done(); + }); + }); + + it("should handle string 'true' as true boolean value", function (done) { + const roleData = { + rolename: "testrole", + description: "Test role", + }; + + chai + .request(app) + .post("/discord-actions/groups?role=true") + .set("cookie", `${cookieName}=${testUserAuthToken}`) + .send(roleData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(201); + const fetchCall = fetchStub.getCall(0); + const requestBody = JSON.parse(fetchCall.args[1].body); + expect(requestBody.rolename).to.equal("testrole"); + + return done(); + }); + }); + }); }); diff --git a/test/integration/external-accounts.test.js b/test/integration/external-accounts.test.js index a9753f582..72da8b520 100644 --- a/test/integration/external-accounts.test.js +++ b/test/integration/external-accounts.test.js @@ -13,12 +13,12 @@ const { usersFromRds, getDiscordMembers } = require("../fixtures/discordResponse const Sinon = require("sinon"); const { INTERNAL_SERVER_ERROR } = require("../../constants/errorMessages"); const removeDiscordRoleUtils = require("../../utils/removeDiscordRoleFromUser"); -const addDiscordRoleUtils = require("../../utils/addDiscordRoleToUser"); const firestore = require("../../utils/firestore"); const userData = require("../fixtures/user/user")(); const userModel = firestore.collection("users"); const tasksModel = firestore.collection("tasks"); const { EXTERNAL_ACCOUNTS_POST_ACTIONS } = require("../../constants/external-accounts"); +const config = require("config"); chai.use(chaiHttp); const cookieName = config.get("userToken.cookieName"); @@ -527,7 +527,7 @@ describe("External Accounts", function () { }); }); - it("Should return 204 when valid action is provided and assign Developer and New roles", async function () { + it("Should return 200 when valid token is provided and link discord account successfully", async function () { await externalAccountsModel.addExternalAccountData(externalAccountData[2]); const getUserResponseBeforeUpdate = await chai .request(app) @@ -544,11 +544,6 @@ describe("External Accounts", function () { message: "Role deleted successfully", }); - const addDiscordRoleStub = Sinon.stub(addDiscordRoleUtils, "addDiscordRoleToUser").resolves({ - success: true, - message: "Role added successfully", - }); - const response = await chai .request(app) .patch(`/external-accounts/link/${externalAccountData[2].token}`) @@ -559,12 +554,6 @@ describe("External Accounts", function () { expect(response.body).to.have.property("message"); expect(response.body.message).to.equal("Your discord profile has been linked successfully"); - expect(addDiscordRoleStub.calledTwice).to.equal(true); - expect(addDiscordRoleStub.firstCall.args[0]).to.equal(externalAccountData[2].attributes.discordId); - expect(addDiscordRoleStub.firstCall.args[2]).to.equal("Developer"); - expect(addDiscordRoleStub.secondCall.args[0]).to.equal(externalAccountData[2].attributes.discordId); - expect(addDiscordRoleStub.secondCall.args[2]).to.equal("New"); - const updatedUserDetails = await chai .request(app) .get("/users/self") @@ -575,10 +564,9 @@ describe("External Accounts", function () { expect(updatedUserDetails.body).to.have.property("discordJoinedAt"); removeDiscordRoleStub.restore(); - addDiscordRoleStub.restore(); }); - it("Should return 500 when addDiscordRole fails after successful verification", async function () { + it("Should return 200 when unverified role removal succeeds", async function () { await externalAccountsModel.addExternalAccountData(externalAccountData[2]); const removeDiscordRoleStub = Sinon.stub(removeDiscordRoleUtils, "removeDiscordRoleFromUser").resolves({ @@ -586,36 +574,25 @@ describe("External Accounts", function () { message: "Role deleted successfully", }); - const addDiscordRoleStub = Sinon.stub(addDiscordRoleUtils, "addDiscordRoleToUser").rejects( - new Error("Role assignment failed") - ); - 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(500); + expect(response).to.have.status(200); expect(response.body).to.be.an("object"); expect(response.body).to.have.property("message"); - expect(response.body.message).to.equal( - "Your discord profile has been linked but role assignment failed. Please contact admin" - ); + expect(response.body.message).to.equal("Your discord profile has been linked successfully"); expect(removeDiscordRoleStub.calledOnce).to.equal(true); expect(removeDiscordRoleStub.firstCall.args[1]).to.equal(externalAccountData[2].attributes.discordId); expect(removeDiscordRoleStub.firstCall.args[2]).to.equal(config.get("discordUnverifiedRoleId")); - expect(addDiscordRoleStub.calledTwice).to.not.equal(true); - expect(addDiscordRoleStub.firstCall.args[0]).to.equal(externalAccountData[2].attributes.discordId); - expect(addDiscordRoleStub.firstCall.args[2]).to.equal("Developer"); - expect(addDiscordRoleStub.calledOnce).to.equal(true); // New role was not assigned removeDiscordRoleStub.restore(); - addDiscordRoleStub.restore(); }); - it("Should continue with role assignment when removeDiscordRole fails because role doesn't exist (tolerable error)", async function () { + it("Should return 500 when removeDiscordRole fails", async function () { await externalAccountsModel.addExternalAccountData(externalAccountData[2]); const removeDiscordRoleStub = Sinon.stub(removeDiscordRoleUtils, "removeDiscordRoleFromUser").resolves({ @@ -623,24 +600,18 @@ describe("External Accounts", function () { message: "Role doesn't exist", }); - const addDiscordRoleStub = Sinon.stub(addDiscordRoleUtils, "addDiscordRoleToUser").resolves({ - success: true, - message: "Role added successfully", - }); - 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(200); + expect(response).to.have.status(500); expect(response.body).to.be.an("object"); expect(response.body).to.have.property("message"); - expect(response.body.message).to.equal("Your discord profile has been linked successfully"); + expect(response.body.message).to.equal("Unverified role removal failed. Please contact admin"); removeDiscordRoleStub.restore(); - addDiscordRoleStub.restore(); }); it("Should return 500 when removeDiscordRole fails because role deletion from discord failed", async function () { @@ -657,19 +628,15 @@ describe("External Accounts", function () { .query({ action: EXTERNAL_ACCOUNTS_POST_ACTIONS.DISCORD_USERS_SYNC }) .set("Cookie", `${cookieName}=${newUserJWT}`); - const unverifiedRoleRemovalResponse = await removeDiscordRoleStub(); - expect(response).to.have.status(500); expect(response.body).to.be.an("object"); expect(response.body).to.have.property("message"); - expect(response.body.message).to.equal( - `User details updated but ${unverifiedRoleRemovalResponse.message}. Please contact admin` - ); + expect(response.body.message).to.equal("Unverified role removal failed. Please contact admin"); removeDiscordRoleStub.restore(); }); - it("Should continue with role assignment when removeDiscordRole fails because role deletion from database failed (tolerable error)", async function () { + it("Should return 500 when removeDiscordRole fails because role deletion from database failed", async function () { await externalAccountsModel.addExternalAccountData(externalAccountData[2]); const removeDiscordRoleStub = Sinon.stub(removeDiscordRoleUtils, "removeDiscordRoleFromUser").resolves({ @@ -677,24 +644,18 @@ describe("External Accounts", function () { message: "Role deletion from database failed", }); - const addDiscordRoleStub = Sinon.stub(addDiscordRoleUtils, "addDiscordRoleToUser").resolves({ - success: true, - message: "Role added successfully", - }); - 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(200); + expect(response).to.have.status(500); expect(response.body).to.be.an("object"); expect(response.body).to.have.property("message"); - expect(response.body.message).to.equal("Your discord profile has been linked successfully"); + expect(response.body.message).to.equal("Unverified role removal failed. Please contact admin"); removeDiscordRoleStub.restore(); - addDiscordRoleStub.restore(); }); }); }); diff --git a/types/global.d.ts b/types/global.d.ts index 28e766270..db3c4a19b 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -42,4 +42,4 @@ export type userData = { }; export type CustomResponse = Response & { boom: Boom }; -export type CustomRequest = Request & { userData }; +export type CustomRequest = Request & { userData; approvedApplicationRole?: string };