Skip to content

Commit cfcf6b6

Browse files
feat: adds unit tests for impersonation session feature (#2452)
* added unit tests for impersonation session route * fixed test identation and spacing * fixed unecessary imports and variable in tests * fixed comments on tests * fixed naming mistake in test * fixed failing test
1 parent 411c988 commit cfcf6b6

File tree

5 files changed

+449
-6
lines changed

5 files changed

+449
-6
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { expect } from "chai";
2+
import sinon from "sinon";
3+
import { NextFunction, Response } from "express";
4+
import { addAuthorizationForImpersonation } from "../../../middlewares/addAuthorizationForImpersonation";
5+
import { ImpersonationRequestResponse, ImpersonationSessionRequest } from "../../../types/impersonationRequest";
6+
import { INVALID_ACTION_PARAM, OPERATION_NOT_ALLOWED } from "../../../constants/requests";
7+
8+
describe("addAuthorizationForImpersonation", () => {
9+
let req;
10+
let res: Partial<Response> & {
11+
boom: {
12+
badRequest: sinon.SinonSpy;
13+
unauthorized: sinon.SinonSpy;
14+
forbidden: sinon.SinonSpy;
15+
badImplementation: sinon.SinonSpy;
16+
};
17+
};
18+
let next: sinon.SinonSpy;
19+
let boomBadRequest: sinon.SinonSpy;
20+
let boomForbidden: sinon.SinonSpy;
21+
let boomUnauthorized: sinon.SinonSpy;
22+
23+
beforeEach(() => {
24+
boomBadRequest = sinon.spy();
25+
boomForbidden = sinon.spy();
26+
boomUnauthorized = sinon.spy();
27+
28+
req = {
29+
query: {},
30+
userData: {
31+
roles: {
32+
super_user: true,
33+
},
34+
},
35+
};
36+
37+
res = {
38+
boom: {
39+
badRequest: boomBadRequest,
40+
badImplementation: sinon.spy(),
41+
forbidden: boomForbidden,
42+
unauthorized: boomUnauthorized,
43+
},
44+
};
45+
46+
next = sinon.spy();
47+
});
48+
49+
it("should call next when user has super_user role and action is START", () => {
50+
req.query = { action: "START" };
51+
52+
addAuthorizationForImpersonation(
53+
req as ImpersonationSessionRequest,
54+
res as ImpersonationRequestResponse,
55+
next as NextFunction
56+
);
57+
58+
expect(next.calledOnce).to.be.true;
59+
expect(boomBadRequest.notCalled).to.be.true;
60+
});
61+
62+
it("should not call next if user doesn't have super_user role", () => {
63+
req.query = { action: "START" };
64+
req.userData = { roles: {} };
65+
66+
addAuthorizationForImpersonation(
67+
req as ImpersonationSessionRequest,
68+
res as ImpersonationRequestResponse,
69+
next
70+
);
71+
72+
expect(boomUnauthorized.calledOnce).to.be.true;
73+
expect(next.notCalled).to.be.true;
74+
});
75+
76+
it("should call next directly for action=STOP if an impersonation session is in progress", () => {
77+
req.query = { action: "STOP" };
78+
req.isImpersonating = true;
79+
80+
addAuthorizationForImpersonation(
81+
req as ImpersonationSessionRequest,
82+
res as ImpersonationRequestResponse,
83+
next
84+
);
85+
86+
expect(next.calledOnce).to.be.true;
87+
});
88+
89+
it("should return 403 Forbidden if action=STOP and an impersonation session is not in progress", () => {
90+
req.query = { action: "STOP" };
91+
req.isImpersonating = false;
92+
93+
addAuthorizationForImpersonation(
94+
req as ImpersonationSessionRequest,
95+
res as ImpersonationRequestResponse,
96+
next
97+
);
98+
99+
expect(boomForbidden.calledOnce).to.be.true;
100+
expect(next.notCalled).to.be.true;
101+
});
102+
103+
it("should call badRequest for invalid action", () => {
104+
req.query = { action: "INVALID" };
105+
106+
addAuthorizationForImpersonation(
107+
req as ImpersonationSessionRequest,
108+
res as ImpersonationRequestResponse,
109+
next
110+
);
111+
112+
expect(boomBadRequest.calledOnce).to.be.true;
113+
expect(boomBadRequest.firstCall.args[0]).to.equal(INVALID_ACTION_PARAM);
114+
expect(next.notCalled).to.be.true;
115+
});
116+
117+
it("should call badRequest if action is missing", () => {
118+
req.query = {};
119+
120+
addAuthorizationForImpersonation(
121+
req as ImpersonationSessionRequest,
122+
res as ImpersonationRequestResponse,
123+
next
124+
);
125+
126+
expect(boomBadRequest.calledOnce).to.be.true;
127+
expect(boomBadRequest.firstCall.args[0]).to.equal(INVALID_ACTION_PARAM);
128+
expect(next.notCalled).to.be.true;
129+
});
130+
});

test/unit/middlewares/authenticate.test.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,88 @@ describe("Authentication Middleware", function () {
113113
expect(nextSpy.notCalled).to.equal(true);
114114
});
115115
});
116+
117+
describe("Impersonation and Refresh Logic", function () {
118+
it("should allow impersonation and set userData of impersonated user", async function () {
119+
const user = { id: "user123", roles: { restricted: false } };
120+
121+
const verifyAuthTokenStub = Sinon.stub(authService, "verifyAuthToken").returns({
122+
userId: "admin",
123+
impersonatedUserId: user.id,
124+
});
125+
126+
const retrieveUsersStub = Sinon.stub(dataAccess, "retrieveUsers").resolves({ user });
127+
128+
req.cookies = {
129+
[config.get("userToken.cookieName")]: "validToken",
130+
};
131+
132+
req.method = "GET";
133+
req.baseUrl = "/impersonation";
134+
req.path = "/abc123";
135+
req.query = {};
136+
137+
await authMiddleware(req, res, nextSpy);
138+
139+
expect(verifyAuthTokenStub.calledOnce).to.equal(true);
140+
expect(retrieveUsersStub.calledOnce).to.equal(true);
141+
142+
expect(req.userData.id).to.equal(user.id);
143+
expect(req.isImpersonating).to.equal(true);
144+
expect(nextSpy.calledOnce).to.equal(true);
145+
expect(res.boom.unauthorized.notCalled).to.equal(true);
146+
expect(res.boom.forbidden.notCalled).to.equal(true);
147+
});
148+
149+
it("should restrict all write/update type requests during impersonation", async function () {
150+
req.method = "POST";
151+
req.baseUrl = "/impersonation";
152+
req.path = "/abc123";
153+
req.query = {};
154+
155+
Sinon.stub(authService, "verifyAuthToken").returns({ userId: "admin", impersonatedUserId: "impUser" });
156+
Sinon.stub(dataAccess, "retrieveUsers").resolves({ user: { id: "impUser", roles: {} } });
157+
158+
await authMiddleware(req, res, nextSpy);
159+
160+
expect(req.isImpersonating).to.equal(true);
161+
expect(res.boom.forbidden.calledOnce).to.equal(true);
162+
expect(res.boom.forbidden.firstCall.args[0]).to.include("Only viewing is permitted");
163+
});
164+
165+
it("should allow PATCH STOP request during impersonation", async function () {
166+
req.method = "PATCH";
167+
req.baseUrl = "/impersonation";
168+
req.path = "/randomId";
169+
req.query = { action: "STOP" };
170+
171+
Sinon.stub(authService, "verifyAuthToken").returns({ userId: "admin", impersonatedUserId: "impUser" });
172+
Sinon.stub(dataAccess, "retrieveUsers").resolves({ user: { id: "impUser", roles: {} } });
173+
174+
await authMiddleware(req, res, nextSpy);
175+
176+
expect(req.isImpersonating).to.equal(true);
177+
expect(nextSpy.calledOnce).to.equal(true);
178+
});
179+
180+
it("should refresh token if expired and within TTL", async function () {
181+
const now = Math.floor(Date.now() / 1000);
182+
req.cookies[config.get("userToken.cookieName")] = "expiredToken";
183+
184+
Sinon.stub(authService, "verifyAuthToken").throws({ name: "TokenExpiredError" });
185+
Sinon.stub(authService, "decodeAuthToken").returns({
186+
userId: "user123",
187+
impersonatedUserId: "impUserId",
188+
iat: now - 10,
189+
});
190+
Sinon.stub(authService, "generateAuthToken").returns("newToken");
191+
Sinon.stub(dataAccess, "retrieveUsers").resolves({ user: { id: "user123", roles: {} } });
192+
193+
await authMiddleware(req, res, nextSpy);
194+
195+
expect(res.cookie.calledOnce).to.equal(true);
196+
expect(res.cookie.firstCall.args[1]).to.equal("newToken");
197+
expect(nextSpy.calledOnce).to.equal(true);
198+
});
199+
});
116200
});

test/unit/middlewares/impersonationRequests.test.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@ import {
44
createImpersonationRequestValidator,
55
getImpersonationRequestByIdValidator,
66
getImpersonationRequestsValidator,
7-
updateImpersonationRequestValidator
7+
updateImpersonationRequestValidator,
8+
impersonationSessionValidator
89
} from "../../../middlewares/validators/impersonationRequests";
910
import {
1011
CreateImpersonationRequest,
1112
CreateImpersonationRequestBody,
1213
ImpersonationRequestResponse,
1314
GetImpersonationControllerRequest,
15+
ImpersonationSessionRequest,
1416
GetImpersonationRequestByIdRequest,
1517
UpdateImpersonationRequest,
1618
UpdateImpersonationRequestStatusBody,
1719
} from "../../../types/impersonationRequest";
1820
import { Request, Response } from "express";
19-
import { getImpersonationRequestById } from "../../../models/impersonationRequests";
2021

2122
const { expect } = chai;
2223

@@ -232,4 +233,32 @@ describe("Impersonation Request Validators", function () {
232233
expect(nextSpy.called).to.be.false;
233234
});
234235
});
236+
237+
describe("impersonationSessionValidator", function(){
238+
it("should validate for a valid impersonation session request", async function () {
239+
req = {
240+
query: {action:"START", dev:"true"}
241+
};
242+
await impersonationSessionValidator(req as ImpersonationSessionRequest,res as ImpersonationRequestResponse, nextSpy);
243+
expect(nextSpy.calledOnce).to.be.true;
244+
});
245+
246+
it("should not validate for an invalid session request on missing action", async function () {
247+
req = {
248+
query:{action:""}
249+
};
250+
await impersonationSessionValidator(req as ImpersonationSessionRequest,res as ImpersonationRequestResponse, nextSpy);
251+
expect(res.boom.badRequest.calledOnce).to.be.true;
252+
expect(nextSpy.called).to.be.false;
253+
});
254+
255+
it("should invalidate if action field is not of correct type", async function () {
256+
req = {
257+
query:{action:"ACTIVE",dev:"true"}
258+
};
259+
await impersonationSessionValidator(req as ImpersonationSessionRequest,res as ImpersonationRequestResponse, nextSpy);
260+
expect(res.boom.badRequest.calledOnce).to.be.true;
261+
expect(nextSpy.called).to.be.false;
262+
});
263+
})
235264
});

test/unit/services/authService.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,30 @@ describe("authService", function () {
2828

2929
return done();
3030
});
31+
32+
describe("generateImpersonationAuthToken", function () {
33+
const payload = { userId: "devUser123", impersonatedUserId: "testUser456" };
34+
35+
it("should generate a valid JWT with correct payload", function (done) {
36+
const jwtToken = authService.generateImpersonationAuthToken(payload);
37+
const decoded = authService.verifyAuthToken(jwtToken); // Assuming verifyAuthToken uses the same public key
38+
39+
expect(decoded).to.have.all.keys("userId", "impersonatedUserId", "iat", "exp");
40+
expect(decoded.userId).to.equal(payload.userId);
41+
expect(decoded.impersonatedUserId).to.equal(payload.impersonatedUserId);
42+
43+
return done();
44+
});
45+
46+
it("should decode the impersonation JWT without verifying", function (done) {
47+
const jwtToken = authService.generateImpersonationAuthToken(payload);
48+
const decoded = authService.decodeAuthToken(jwtToken); // No signature verification
49+
50+
expect(decoded).to.have.all.keys("userId", "impersonatedUserId", "iat", "exp");
51+
expect(decoded.userId).to.equal(payload.userId);
52+
expect(decoded.impersonatedUserId).to.equal(payload.impersonatedUserId);
53+
54+
return done();
55+
});
56+
});
3157
});

0 commit comments

Comments
 (0)