Skip to content

Commit 9c8d1dc

Browse files
add tests for decoder service
1 parent a62bf14 commit 9c8d1dc

File tree

1 file changed

+257
-0
lines changed

1 file changed

+257
-0
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import { err, ok } from "neverthrow";
3+
import { extractJwt } from "@/features/common/services/utils";
4+
import {
5+
validateSymmetricSecret,
6+
validateAsymmetricKey,
7+
validateJwtFormat,
8+
isHmacAlg,
9+
isDigitalSignatureAlg,
10+
getStringifiedHeaderAndPayload,
11+
isSupportedAlg,
12+
parseStringIntoValidJsonObject,
13+
verifyMACedJwt,
14+
} from "@/features/common/services/jwt.service";
15+
import { downloadPublicKeyIfPossible } from "@/features/decoder/services/public-key.service";
16+
import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values";
17+
import { EncodingValues } from "@/features/common/values/encoding.values";
18+
import { JwtSignatureStatusValues } from "@/features/common/values/jwt-signature-status.values";
19+
import { JwtTypeValues } from "@/features/common/values/jwt-type.values";
20+
import { StringValues } from "@/features/common/values/string.values";
21+
import {
22+
DebuggerInputValues
23+
} from "@/features/common/values/debugger-input.values";
24+
import { DebuggerTaskValues } from "@/features/common/values/debugger-task.values";
25+
import { TokenDecoderService } from "@/features/decoder/services/token-decoder.service";
26+
27+
// Create Mocks
28+
vi.mock("@/features/common/services/utils", () => ({
29+
extractJwt: vi.fn(),
30+
}));
31+
32+
vi.mock("@/features/common/services/jwt.service", () => ({
33+
validateSymmetricSecret: vi.fn(),
34+
validateAsymmetricKey: vi.fn(),
35+
validateJwtFormat: vi.fn(),
36+
isHmacAlg: vi.fn(),
37+
isDigitalSignatureAlg: vi.fn(),
38+
getStringifiedHeaderAndPayload: vi.fn(),
39+
isSupportedAlg: vi.fn(),
40+
parseStringIntoValidJsonObject: vi.fn(),
41+
verifyMACedJwt: vi.fn(),
42+
verifyDigitallySignedJwt: vi.fn(),
43+
}));
44+
45+
vi.mock("@/features/decoder/services/public-key.service", () => ({
46+
downloadPublicKeyIfPossible: vi.fn(),
47+
}));
48+
49+
vi.mock("@/features/debugger/services/debugger.store", () => ({
50+
useDebuggerStore: vi.fn(() => ({
51+
getState: vi.fn(() => ({
52+
setStash$: vi.fn(),
53+
})),
54+
})),
55+
}));
56+
57+
// Typed Mocks
58+
const viExtractJwt = vi.mocked(extractJwt);
59+
const viValidateSymmetricSecret = vi.mocked(validateSymmetricSecret);
60+
const viValidateAsymmetricKey = vi.mocked(validateAsymmetricKey);
61+
const viValidateJwtFormat = vi.mocked(validateJwtFormat);
62+
const viIsHmacAlg = vi.mocked(isHmacAlg);
63+
const viIsDigitalSignatureAlg = vi.mocked(isDigitalSignatureAlg);
64+
const viGetStringifiedHeaderAndPayload = vi.mocked(
65+
getStringifiedHeaderAndPayload,
66+
);
67+
const viIsSupportedAlg = vi.mocked(isSupportedAlg);
68+
const viParseStringIntoValidJsonObject = vi.mocked(
69+
parseStringIntoValidJsonObject,
70+
);
71+
const viVerifyMACedJwt = vi.mocked(verifyMACedJwt);
72+
const viDownloadPublicKeyIfPossible = vi.mocked(downloadPublicKeyIfPossible);
73+
74+
describe("TokenDecoderService.handleJwtChange", () => {
75+
const mockJwt =
76+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
77+
const mockParams = {
78+
alg: "HS256",
79+
symmetricSecretKey: "secret",
80+
symmetricSecretKeyEncoding: EncodingValues.UTF8,
81+
asymmetricPublicKey: "key",
82+
asymmetricPublicKeyFormat: AsymmetricKeyFormatValues.PEM,
83+
newToken: mockJwt,
84+
};
85+
86+
const mockDecodedHeader = { alg: "HS256", typ: "JWT" };
87+
const mockDecodedPayload = {
88+
sub: "1234567890",
89+
name: "John Doe",
90+
iat: 1516239022,
91+
};
92+
const mockStringifiedHeader = '{ "alg": "HS256", "typ": "JWT" }';
93+
const mockStringifiedPayload =
94+
'{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }';
95+
96+
const mockCompactVerifyResult = {
97+
payload: new Uint8Array(1),
98+
protectedHeader: mockDecodedHeader,
99+
};
100+
101+
// Reset mocks
102+
beforeEach(() => {
103+
vi.resetAllMocks();
104+
105+
viExtractJwt.mockImplementation((t) => t);
106+
viIsHmacAlg.mockImplementation((alg) => alg.startsWith("HS"));
107+
viIsDigitalSignatureAlg.mockImplementation(
108+
(alg) => alg.startsWith("RS") || alg.startsWith("ES"),
109+
);
110+
viIsSupportedAlg.mockReturnValue(true);
111+
viValidateSymmetricSecret.mockResolvedValue(ok(new Uint8Array([1, 2, 3])));
112+
viValidateAsymmetricKey.mockResolvedValue(ok({} as CryptoKey));
113+
viGetStringifiedHeaderAndPayload.mockReturnValue(
114+
ok({
115+
header: mockStringifiedHeader,
116+
payload: mockStringifiedPayload,
117+
}),
118+
);
119+
viDownloadPublicKeyIfPossible.mockResolvedValue(
120+
err({
121+
message: "No jku/x5u/kid",
122+
task: DebuggerTaskValues.VERIFY,
123+
input: DebuggerInputValues.JWT,
124+
}),
125+
);
126+
viParseStringIntoValidJsonObject.mockReturnValue(
127+
err("Not JSON"),
128+
);
129+
});
130+
131+
it("should return decoding errors if JWT format is invalid", async () => {
132+
const error = {
133+
message: "Invalid format",
134+
input: DebuggerInputValues.JWT,
135+
task: DebuggerTaskValues.DECODE,
136+
};
137+
viValidateJwtFormat.mockReturnValue(err(error));
138+
139+
const result = await TokenDecoderService.handleJwtChange(mockParams);
140+
141+
expect(viValidateJwtFormat).toHaveBeenCalledWith(mockParams.newToken);
142+
expect(result.decodingErrors).toEqual([error.message]);
143+
expect(result.signatureStatus).toBe(JwtSignatureStatusValues.WARNING);
144+
expect(result.signatureWarnings).toEqual([
145+
StringValues.editor.signatureWarning,
146+
]);
147+
expect(result.decodedHeader).toBe("");
148+
expect(result.decodedPayload).toBe("");
149+
});
150+
151+
it("should show partial header/payload if format is invalid but data exists", async () => {
152+
const error = {
153+
message: "Invalid payload JSON",
154+
input: DebuggerInputValues.JWT,
155+
data: { header: mockDecodedHeader, payload: { sub: 123 } },
156+
task: DebuggerTaskValues.DECODE,
157+
};
158+
viValidateJwtFormat.mockReturnValue(err(error));
159+
160+
viGetStringifiedHeaderAndPayload
161+
.mockImplementationOnce(
162+
() => ok({ header: mockStringifiedHeader, payload: "" }),
163+
) // For header
164+
.mockImplementationOnce(() => ok({ header: "", payload: '{ "sub": 123 }' })); // For payload
165+
166+
const result = await TokenDecoderService.handleJwtChange(mockParams);
167+
168+
expect(result.decodingErrors).toEqual([error.message]);
169+
expect(result.alg).toBe(mockDecodedHeader.alg);
170+
expect(result.decodedHeader).toBe(mockStringifiedHeader);
171+
expect(result.decodedPayload).toBe('{ "sub": 123 }');
172+
expect(result.signatureStatus).toBe(JwtSignatureStatusValues.WARNING);
173+
});
174+
175+
it("should handle Unsecured JWT (alg: none)", async () => {
176+
const unsecuredJwt = "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjMifQ.";
177+
const decoded = { header: { alg: "none" }, payload: { sub: "123" } };
178+
viValidateJwtFormat.mockReturnValue(
179+
ok({
180+
type: JwtTypeValues.Unsecured,
181+
signingAlgorithm: "none",
182+
decoded: decoded,
183+
}),
184+
);
185+
viGetStringifiedHeaderAndPayload.mockReturnValue(
186+
ok({
187+
header: '{ "alg": "none" }',
188+
payload: '{ "sub": "123" }',
189+
}),
190+
);
191+
192+
const result = await TokenDecoderService.handleJwtChange({
193+
...mockParams,
194+
newToken: unsecuredJwt,
195+
});
196+
197+
expect(result.alg).toBe("none");
198+
expect(result.decodedHeader).toBe('{ "alg": "none" }');
199+
expect(result.decodedPayload).toBe('{ "sub": "123" }');
200+
expect(result.signatureStatus).toBe(JwtSignatureStatusValues.WARNING);
201+
expect(result.signatureWarnings).toEqual([
202+
expect.stringContaining("Unsecured JWT"),
203+
]);
204+
expect(result.verificationInputErrors).toEqual([
205+
"Can't verify signature for an Unsecured JWT.",
206+
]);
207+
});
208+
209+
it("should return VALID for a valid HMAC token and secret", async () => {
210+
viValidateJwtFormat.mockReturnValue(
211+
ok({
212+
type: JwtTypeValues.MACed,
213+
signingAlgorithm: mockDecodedHeader.alg,
214+
decoded: { header: mockDecodedHeader, payload: mockDecodedPayload },
215+
}),
216+
);
217+
viVerifyMACedJwt.mockResolvedValue(ok(mockCompactVerifyResult));
218+
219+
const result = await TokenDecoderService.handleJwtChange(mockParams);
220+
221+
expect(viValidateSymmetricSecret).toHaveBeenCalledWith({
222+
symmetricSecretKey: mockParams.symmetricSecretKey,
223+
symmetricSecretKeyEncoding: mockParams.symmetricSecretKeyEncoding,
224+
});
225+
expect(viVerifyMACedJwt).toHaveBeenCalledWith({
226+
jwt: mockParams.newToken,
227+
symmetricSecretKey: mockParams.symmetricSecretKey,
228+
symmetricSecretKeyEncoding: mockParams.symmetricSecretKeyEncoding,
229+
});
230+
expect(result.signatureStatus).toBe(JwtSignatureStatusValues.VALID);
231+
expect(result.verificationInputErrors).toBeNull();
232+
expect(result.symmetricSecretKey).toBe(mockParams.symmetricSecretKey);
233+
expect(result.controlledSymmetricSecretKey).toBeDefined();
234+
});
235+
236+
it("should return INVALID for a valid HMAC token and *incorrect* secret", async () => {
237+
viValidateJwtFormat.mockReturnValue(
238+
ok({
239+
type: JwtTypeValues.MACed,
240+
signingAlgorithm: mockDecodedHeader.alg,
241+
decoded: { header: mockDecodedHeader, payload: mockDecodedPayload },
242+
}),
243+
);
244+
const error = {
245+
message: "Invalid signature",
246+
task: DebuggerTaskValues.VERIFY,
247+
input: DebuggerInputValues.KEY,
248+
};
249+
viVerifyMACedJwt.mockResolvedValue(err(error));
250+
251+
const result = await TokenDecoderService.handleJwtChange(mockParams);
252+
253+
expect(viVerifyMACedJwt).toHaveBeenCalled();
254+
expect(result.signatureStatus).toBe(JwtSignatureStatusValues.INVALID);
255+
expect(result.verificationInputErrors).toEqual([error.message]);
256+
});
257+
});

0 commit comments

Comments
 (0)