Skip to content

Commit 20c9c1a

Browse files
feat: add unit tests for authentication middelware (#2254)
* feat: add unit tests for authentication middelware * fix: add missing config import statement * feat: add more expectation for test cases * fix: refactor set cookies and add assertions in stubs
1 parent 4d9c711 commit 20c9c1a

File tree

4 files changed

+119
-1
lines changed

4 files changed

+119
-1
lines changed

models/userStatus.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const { userState } = require("../constants/userStatus");
2323
const discordRoleModel = firestore.collection("discord-roles");
2424
const memberRoleModel = firestore.collection("member-group-roles");
2525
const usersCollection = firestore.collection("users");
26+
const config = require("config");
2627
const DISCORD_BASE_URL = config.get("services.discordBot.baseUrl");
2728
const { generateAuthTokenForCloudflare } = require("../utils/discord-actions");
2829

services/discordMembersService.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const jwt = require("jsonwebtoken");
2-
2+
const config = require("config");
33
const DISCORD_BASE_URL = config.get("services.discordBot.baseUrl");
44

55
/**
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
const Sinon = require("sinon");
2+
const { expect } = require("chai");
3+
const authMiddleware = require("../../../middlewares/authenticate");
4+
const authService = require("../../../services/authService");
5+
const dataAccess = require("../../../services/dataAccessLayer");
6+
const config = require("config");
7+
8+
describe("Authentication Middleware", function () {
9+
let req, res, nextSpy;
10+
11+
beforeEach(function () {
12+
req = {
13+
cookies: {
14+
[config.get("userToken.cookieName")]: "validToken",
15+
},
16+
headers: {},
17+
};
18+
res = {
19+
cookie: Sinon.spy(),
20+
boom: {
21+
unauthorized: Sinon.spy(),
22+
forbidden: Sinon.spy(),
23+
},
24+
};
25+
nextSpy = Sinon.spy();
26+
});
27+
28+
afterEach(function () {
29+
Sinon.restore();
30+
});
31+
32+
describe("Token Verification", function () {
33+
it("should allow unrestricted user with valid token", async function () {
34+
const user = { id: "user123", roles: { restricted: false } };
35+
const verifyAuthTokenStub = Sinon.stub(authService, "verifyAuthToken").returns({ userId: user.id });
36+
const retrieveUsersStub = Sinon.stub(dataAccess, "retrieveUsers").resolves({ user });
37+
38+
await authMiddleware(req, res, nextSpy);
39+
40+
expect(verifyAuthTokenStub.calledOnce).to.equal(true);
41+
expect(verifyAuthTokenStub.returnValues[0]).to.deep.equal({ userId: user.id });
42+
expect(verifyAuthTokenStub.calledWith("validToken")).to.equal(true);
43+
44+
expect(retrieveUsersStub.calledOnce).to.equal(true);
45+
const retrievedValue = await retrieveUsersStub.returnValues[0];
46+
expect(retrievedValue).to.deep.equal({ user });
47+
48+
expect(nextSpy.calledOnce).to.equal(true);
49+
expect(res.boom.unauthorized.notCalled).to.equal(true);
50+
expect(res.boom.forbidden.notCalled).to.equal(true);
51+
});
52+
53+
it("should deny restricted user access for non-GET requests", async function () {
54+
req.method = "POST";
55+
const user = { id: "user123", roles: { restricted: true } };
56+
const verifyAuthTokenStub = Sinon.stub(authService, "verifyAuthToken").returns({ userId: user.id });
57+
const retrieveUsersStub = Sinon.stub(dataAccess, "retrieveUsers").resolves({ user });
58+
59+
await authMiddleware(req, res, nextSpy);
60+
61+
expect(verifyAuthTokenStub.calledOnce).to.equal(true);
62+
expect(verifyAuthTokenStub.returnValues[0]).to.deep.equal({ userId: user.id });
63+
expect(verifyAuthTokenStub.calledWith("validToken")).to.equal(true);
64+
65+
expect(retrieveUsersStub.calledOnce).to.equal(true);
66+
const retrievedValue = await retrieveUsersStub.returnValues[0];
67+
expect(retrievedValue).to.deep.equal({ user });
68+
69+
expect(res.boom.forbidden.calledOnce).to.equal(true);
70+
expect(res.boom.forbidden.firstCall.args[0]).to.equal("You are restricted from performing this action");
71+
expect(nextSpy.notCalled).to.equal(true);
72+
});
73+
74+
it("should deny access with invalid token", async function () {
75+
req.cookies[config.get("userToken.cookieName")] = "invalidToken";
76+
const verifyAuthTokenStub = Sinon.stub(authService, "verifyAuthToken").throws(new Error("Invalid token"));
77+
78+
await authMiddleware(req, res, nextSpy);
79+
80+
expect(verifyAuthTokenStub.calledOnce).to.equal(true);
81+
expect(verifyAuthTokenStub.threw()).to.equal(true);
82+
expect(verifyAuthTokenStub.exceptions[0].message).to.equal("Invalid token");
83+
expect(verifyAuthTokenStub.calledWith("invalidToken")).to.equal(true);
84+
85+
expect(res.boom.unauthorized.calledOnce).to.equal(true);
86+
expect(res.boom.unauthorized.firstCall.args[0]).to.equal("Unauthenticated User");
87+
expect(nextSpy.notCalled).to.equal(true);
88+
});
89+
});
90+
91+
describe("Error Handling", function () {
92+
it("should deny access when token is missing in production", async function () {
93+
const originalEnv = process.env.NODE_ENV;
94+
process.env.NODE_ENV = "production";
95+
96+
await authMiddleware(req, res, nextSpy);
97+
98+
expect(res.boom.unauthorized.calledOnce).to.equal(true);
99+
process.env.NODE_ENV = originalEnv;
100+
});
101+
102+
it("should handle unexpected errors gracefully", async function () {
103+
const verifyAuthTokenStub = Sinon.stub(authService, "verifyAuthToken").throws(new Error("Unexpected error"));
104+
105+
await authMiddleware(req, res, nextSpy);
106+
107+
expect(verifyAuthTokenStub.calledOnce).to.equal(true);
108+
expect(verifyAuthTokenStub.threw()).to.equal(true);
109+
expect(verifyAuthTokenStub.exceptions[0].message).to.equal("Unexpected error");
110+
111+
expect(res.boom.unauthorized.calledOnce).to.equal(true);
112+
expect(res.boom.unauthorized.firstCall.args[0]).to.equal("Unauthenticated User");
113+
expect(nextSpy.notCalled).to.equal(true);
114+
});
115+
});
116+
});

utils/discord-actions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const jwt = require("jsonwebtoken");
2+
const config = require("config");
23
const { getDiscordMemberDetails } = require("../services/discordMembersService");
34
const DISCORD_BASE_URL = config.get("services.discordBot.baseUrl");
45
const RDS_SERVERLESS_PRIVATE_KEY = config.get("rdsServerlessBot.rdsServerLessPrivateKey");

0 commit comments

Comments
 (0)