Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/be/invite-controller-tests, #924 #932

Merged
merged 1 commit into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions Server/controllers/inviteController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -93,6 +83,6 @@ const inviteVerifyController = async (req, res, next) => {
};

module.exports = {
inviteController: issueInvitation,
issueInvitation,
inviteVerifyController,
};
6 changes: 3 additions & 3 deletions Server/routes/inviteRoute.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ const { verifyJWT } = require("../middleware/verifyJWT");
const { isAllowed } = require("../middleware/isAllowed");

const {
inviteController,
issueInvitation,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Whoa, 'inviteController' is still hanging around in a few places! Let's tidy this up to keep things smooth.

  • Server/tests/controllers/inviteController.test.js
  • Server/controllers/inviteController.js
🔗 Analysis chain

Yo, this change is fire! But let's make sure we ain't trippin'.

The import change from inviteController to issueInvitation is on point, making the code clearer. It's like mom's spaghetti - simple but effective.

Let's double-check if we missed any spots where inviteController was used:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Yo, let's hunt for any leftover `inviteController` references
rg --type js 'inviteController' -g '!Server/routes/inviteRoute.js'

Length of output: 567

inviteVerifyController,
} = require("../controllers/inviteController");

router.post(
"/",
isAllowed(["admin", "superadmin"]),
verifyJWT,
inviteController
issueInvitation
);
router.post("/verify", inviteVerifyController);
router.post("/verify", issueInvitation);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Yo, hold up! Something's fishy here, and it ain't mom's spaghetti.

I'm sweating bullets here, 'cause this line's got me shook. We're using issueInvitation for the "/verify" route? That's like trying to eat spaghetti with a fork - it just don't add up!

Shouldn't we be using inviteVerifyController here instead? It's imported but not used, and it sounds like it's made for this exact purpose.

Can you double-check if this is intentional or if we need to swap in inviteVerifyController here? Don't want us to be cleaning up spaghetti code later, you feel me?


module.exports = router;
202 changes: 202 additions & 0 deletions Server/tests/controllers/inviteController.test.js
Original file line number Diff line number Diff line change
@@ -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" },

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical test

The hard-coded value "Bearer token" is used as
authorization header
.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Hold up, we got a security issue here!

Yo, I'm sweatin' bullets over here 'cause we got a hard-coded token in the authorization header. That's a no-go, my friend. We gotta keep it real and keep it secure.

Let's swap out that hard-coded "Bearer token" with a dynamic token generation. Something like:

authorization: `Bearer ${generateTestToken()}`

Don't forget to implement that generateTestToken() function to keep our tests fresh and secure.

🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 12-12: Hard-coded credentials
The hard-coded value "Bearer token" is used as authorization header.

body: {
email: "[email protected]",
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",
},
"[email protected]",
"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;
});
});
4 changes: 2 additions & 2 deletions Server/validation/joi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down