diff --git a/Server/controllers/inviteController.js b/Server/controllers/inviteController.js index 56d6b08f6..b22f9b2cc 100644 --- a/Server/controllers/inviteController.js +++ b/Server/controllers/inviteController.js @@ -8,17 +8,7 @@ require("dotenv").config(); const jwt = require("jsonwebtoken"); const { handleError, handleValidationError } = require("./controllerUtils"); const SERVICE_NAME = "inviteController"; - -const getTokenFromHeaders = (headers) => { - const authorizationHeader = headers.authorization; - if (!authorizationHeader) throw new Error("No auth headers"); - - const parts = authorizationHeader.split(" "); - if (parts.length !== 2 || parts[0] !== "Bearer") - throw new Error("Invalid auth headers"); - - return parts[1]; -}; +const { getTokenFromHeaders } = require("../utils/utils"); /** * Issues an invitation to a new user. Only admins can invite new users. An invitation token is created and sent via email. @@ -93,6 +83,6 @@ const inviteVerifyController = async (req, res, next) => { }; module.exports = { - inviteController: issueInvitation, + issueInvitation, inviteVerifyController, }; diff --git a/Server/routes/inviteRoute.js b/Server/routes/inviteRoute.js index 17744bf82..93e77e068 100644 --- a/Server/routes/inviteRoute.js +++ b/Server/routes/inviteRoute.js @@ -3,7 +3,7 @@ const { verifyJWT } = require("../middleware/verifyJWT"); const { isAllowed } = require("../middleware/isAllowed"); const { - inviteController, + issueInvitation, inviteVerifyController, } = require("../controllers/inviteController"); @@ -11,8 +11,8 @@ router.post( "/", isAllowed(["admin", "superadmin"]), verifyJWT, - inviteController + issueInvitation ); -router.post("/verify", inviteVerifyController); +router.post("/verify", issueInvitation); module.exports = router; diff --git a/Server/tests/controllers/inviteController.test.js b/Server/tests/controllers/inviteController.test.js new file mode 100644 index 000000000..31085c40f --- /dev/null +++ b/Server/tests/controllers/inviteController.test.js @@ -0,0 +1,202 @@ +const { + issueInvitation, + inviteVerifyController, +} = require("../../controllers/inviteController"); +const jwt = require("jsonwebtoken"); +const { errorMessages, successMessages } = require("../../utils/messages"); +const sinon = require("sinon"); +const joi = require("joi"); +describe("inviteController - issueInvitation", () => { + beforeEach(() => { + req = { + headers: { authorization: "Bearer token" }, + body: { + email: "test@test.com", + role: ["admin"], + teamId: "123", + }, + db: { requestInviteToken: sinon.stub() }, + settingsService: { getSettings: sinon.stub() }, + emailService: { buildAndSendEmail: sinon.stub() }, + }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub(), + }; + next = sinon.stub(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should reject with an error if role validation fails", async () => { + stub = sinon.stub(jwt, "decode").callsFake(() => { + return { role: ["bad_role"], firstname: "first_name", teamId: "1" }; + }); + await issueInvitation(req, res, next); + expect(next.firstCall.args[0]).to.be.an("error"); + expect(next.firstCall.args[0]).to.be.instanceOf(joi.ValidationError); + expect(next.firstCall.args[0].status).to.equal(422); + stub.restore(); + }); + + it("should reject with an error if body validation fails", async () => { + stub = sinon.stub(jwt, "decode").callsFake(() => { + return { role: ["admin"], firstname: "first_name", teamId: "1" }; + }); + req.body = {}; + await issueInvitation(req, res, next); + expect(next.firstCall.args[0]).to.be.an("error"); + expect(next.firstCall.args[0].status).to.equal(422); + stub.restore(); + }); + + it("should reject with an error if DB operations fail", async () => { + stub = sinon.stub(jwt, "decode").callsFake(() => { + return { role: ["admin"], firstname: "first_name", teamId: "1" }; + }); + req.db.requestInviteToken.throws(new Error("DB error")); + await issueInvitation(req, res, next); + expect(next.firstCall.args[0]).to.be.an("error"); + expect(next.firstCall.args[0].message).to.equal("DB error"); + stub.restore(); + }); + + it("should send an invite successfully", async () => { + const token = "token"; + const decodedToken = { + role: "admin", + firstname: "John", + teamId: "team123", + }; + const inviteToken = { token: "inviteToken" }; + const clientHost = "http://localhost"; + + stub = sinon.stub(jwt, "decode").callsFake(() => { + return decodedToken; + }); + req.db.requestInviteToken.resolves(inviteToken); + req.settingsService.getSettings.returns({ clientHost }); + req.emailService.buildAndSendEmail.resolves(); + await issueInvitation(req, res, next); + expect(res.status.calledWith(200)).to.be.true; + expect( + res.json.calledWith({ + success: true, + msg: "Invite sent", + data: inviteToken, + }) + ).to.be.true; + stub.restore(); + }); + + it("should send an email successfully", async () => { + const token = "token"; + const decodedToken = { + role: "admin", + firstname: "John", + teamId: "team123", + }; + const inviteToken = { token: "inviteToken" }; + const clientHost = "http://localhost"; + + stub = sinon.stub(jwt, "decode").callsFake(() => { + return decodedToken; + }); + req.db.requestInviteToken.resolves(inviteToken); + req.settingsService.getSettings.returns({ clientHost }); + req.emailService.buildAndSendEmail.resolves(); + + await issueInvitation(req, res, next); + expect(req.emailService.buildAndSendEmail.calledOnce).to.be.true; + expect( + req.emailService.buildAndSendEmail.calledWith( + "employeeActivationTemplate", + { + name: "John", + link: "http://localhost/register/inviteToken", + }, + "test@test.com", + "Welcome to Uptime Monitor" + ) + ).to.be.true; + stub.restore(); + }); + + it("should continue executing if sending an email fails", async () => { + const token = "token"; + req.emailService.buildAndSendEmail.rejects(new Error("Email error")); + const decodedToken = { + role: "admin", + firstname: "John", + teamId: "team123", + }; + const inviteToken = { token: "inviteToken" }; + const clientHost = "http://localhost"; + + stub = sinon.stub(jwt, "decode").callsFake(() => { + return decodedToken; + }); + req.db.requestInviteToken.resolves(inviteToken); + req.settingsService.getSettings.returns({ clientHost }); + await issueInvitation(req, res, next); + expect(res.status.calledWith(200)).to.be.true; + expect( + res.json.calledWith({ + success: true, + msg: "Invite sent", + data: inviteToken, + }) + ).to.be.true; + stub.restore(); + }); +}); + +describe("inviteController - inviteVerifyController", () => { + beforeEach(() => { + req = { + body: { token: "token" }, + db: { + getInviteToken: sinon.stub(), + }, + }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub(), + }; + next = sinon.stub(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should reject with an error if body validation fails", async () => { + req.body = {}; + await inviteVerifyController(req, res, next); + expect(next.firstCall.args[0]).to.be.an("error"); + expect(next.firstCall.args[0].status).to.equal(422); + }); + + it("should reject with an error if DB operations fail", async () => { + req.db.getInviteToken.throws(new Error("DB error")); + await inviteVerifyController(req, res, next); + expect(next.firstCall.args[0]).to.be.an("error"); + expect(next.firstCall.args[0].message).to.equal("DB error"); + }); + + it("should return 200 and invite data when validation and invite retrieval are successful", async () => { + req.db.getInviteToken.resolves({ invite: "data" }); + await inviteVerifyController(req, res, next); + expect(res.status.calledWith(200)).to.be.true; + expect( + res.json.calledWith({ + status: "success", + msg: "Invite verified", + data: { invite: "data" }, + }) + ).to.be.true; + expect(next.called).to.be.false; + }); +}); diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 3133da80a..9e36c2135 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -10,8 +10,8 @@ const { start } = require("repl"); const roleValidatior = (role) => (value, helpers) => { const hasRole = role.some((role) => value.includes(role)); if (!hasRole) { - throw new Joi.ValidationError( - `You do not have the required authorization. Required roles: ${roles.join(", ")}` + throw new joi.ValidationError( + `You do not have the required authorization. Required roles: ${role.join(", ")}` ); } return value;