From 9c8d1dc5df2dfcb4eb3ab7d1baf88fe81c7408a0 Mon Sep 17 00:00:00 2001 From: Christian Samaniego <199278128+christiansamaniego-okta@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:17:29 -0500 Subject: [PATCH 1/4] add tests for decoder service --- tests/token-decoder.service.test.ts | 257 ++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 tests/token-decoder.service.test.ts diff --git a/tests/token-decoder.service.test.ts b/tests/token-decoder.service.test.ts new file mode 100644 index 00000000..2d4b6d69 --- /dev/null +++ b/tests/token-decoder.service.test.ts @@ -0,0 +1,257 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { err, ok } from "neverthrow"; +import { extractJwt } from "@/features/common/services/utils"; +import { + validateSymmetricSecret, + validateAsymmetricKey, + validateJwtFormat, + isHmacAlg, + isDigitalSignatureAlg, + getStringifiedHeaderAndPayload, + isSupportedAlg, + parseStringIntoValidJsonObject, + verifyMACedJwt, +} from "@/features/common/services/jwt.service"; +import { downloadPublicKeyIfPossible } from "@/features/decoder/services/public-key.service"; +import { AsymmetricKeyFormatValues } from "@/features/common/values/asymmetric-key-format.values"; +import { EncodingValues } from "@/features/common/values/encoding.values"; +import { JwtSignatureStatusValues } from "@/features/common/values/jwt-signature-status.values"; +import { JwtTypeValues } from "@/features/common/values/jwt-type.values"; +import { StringValues } from "@/features/common/values/string.values"; +import { + DebuggerInputValues +} from "@/features/common/values/debugger-input.values"; +import { DebuggerTaskValues } from "@/features/common/values/debugger-task.values"; +import { TokenDecoderService } from "@/features/decoder/services/token-decoder.service"; + +// Create Mocks +vi.mock("@/features/common/services/utils", () => ({ + extractJwt: vi.fn(), +})); + +vi.mock("@/features/common/services/jwt.service", () => ({ + validateSymmetricSecret: vi.fn(), + validateAsymmetricKey: vi.fn(), + validateJwtFormat: vi.fn(), + isHmacAlg: vi.fn(), + isDigitalSignatureAlg: vi.fn(), + getStringifiedHeaderAndPayload: vi.fn(), + isSupportedAlg: vi.fn(), + parseStringIntoValidJsonObject: vi.fn(), + verifyMACedJwt: vi.fn(), + verifyDigitallySignedJwt: vi.fn(), +})); + +vi.mock("@/features/decoder/services/public-key.service", () => ({ + downloadPublicKeyIfPossible: vi.fn(), +})); + +vi.mock("@/features/debugger/services/debugger.store", () => ({ + useDebuggerStore: vi.fn(() => ({ + getState: vi.fn(() => ({ + setStash$: vi.fn(), + })), + })), +})); + +// Typed Mocks +const viExtractJwt = vi.mocked(extractJwt); +const viValidateSymmetricSecret = vi.mocked(validateSymmetricSecret); +const viValidateAsymmetricKey = vi.mocked(validateAsymmetricKey); +const viValidateJwtFormat = vi.mocked(validateJwtFormat); +const viIsHmacAlg = vi.mocked(isHmacAlg); +const viIsDigitalSignatureAlg = vi.mocked(isDigitalSignatureAlg); +const viGetStringifiedHeaderAndPayload = vi.mocked( + getStringifiedHeaderAndPayload, +); +const viIsSupportedAlg = vi.mocked(isSupportedAlg); +const viParseStringIntoValidJsonObject = vi.mocked( + parseStringIntoValidJsonObject, +); +const viVerifyMACedJwt = vi.mocked(verifyMACedJwt); +const viDownloadPublicKeyIfPossible = vi.mocked(downloadPublicKeyIfPossible); + +describe("TokenDecoderService.handleJwtChange", () => { + const mockJwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + const mockParams = { + alg: "HS256", + symmetricSecretKey: "secret", + symmetricSecretKeyEncoding: EncodingValues.UTF8, + asymmetricPublicKey: "key", + asymmetricPublicKeyFormat: AsymmetricKeyFormatValues.PEM, + newToken: mockJwt, + }; + + const mockDecodedHeader = { alg: "HS256", typ: "JWT" }; + const mockDecodedPayload = { + sub: "1234567890", + name: "John Doe", + iat: 1516239022, + }; + const mockStringifiedHeader = '{ "alg": "HS256", "typ": "JWT" }'; + const mockStringifiedPayload = + '{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }'; + + const mockCompactVerifyResult = { + payload: new Uint8Array(1), + protectedHeader: mockDecodedHeader, + }; + + // Reset mocks + beforeEach(() => { + vi.resetAllMocks(); + + viExtractJwt.mockImplementation((t) => t); + viIsHmacAlg.mockImplementation((alg) => alg.startsWith("HS")); + viIsDigitalSignatureAlg.mockImplementation( + (alg) => alg.startsWith("RS") || alg.startsWith("ES"), + ); + viIsSupportedAlg.mockReturnValue(true); + viValidateSymmetricSecret.mockResolvedValue(ok(new Uint8Array([1, 2, 3]))); + viValidateAsymmetricKey.mockResolvedValue(ok({} as CryptoKey)); + viGetStringifiedHeaderAndPayload.mockReturnValue( + ok({ + header: mockStringifiedHeader, + payload: mockStringifiedPayload, + }), + ); + viDownloadPublicKeyIfPossible.mockResolvedValue( + err({ + message: "No jku/x5u/kid", + task: DebuggerTaskValues.VERIFY, + input: DebuggerInputValues.JWT, + }), + ); + viParseStringIntoValidJsonObject.mockReturnValue( + err("Not JSON"), + ); + }); + + it("should return decoding errors if JWT format is invalid", async () => { + const error = { + message: "Invalid format", + input: DebuggerInputValues.JWT, + task: DebuggerTaskValues.DECODE, + }; + viValidateJwtFormat.mockReturnValue(err(error)); + + const result = await TokenDecoderService.handleJwtChange(mockParams); + + expect(viValidateJwtFormat).toHaveBeenCalledWith(mockParams.newToken); + expect(result.decodingErrors).toEqual([error.message]); + expect(result.signatureStatus).toBe(JwtSignatureStatusValues.WARNING); + expect(result.signatureWarnings).toEqual([ + StringValues.editor.signatureWarning, + ]); + expect(result.decodedHeader).toBe(""); + expect(result.decodedPayload).toBe(""); + }); + + it("should show partial header/payload if format is invalid but data exists", async () => { + const error = { + message: "Invalid payload JSON", + input: DebuggerInputValues.JWT, + data: { header: mockDecodedHeader, payload: { sub: 123 } }, + task: DebuggerTaskValues.DECODE, + }; + viValidateJwtFormat.mockReturnValue(err(error)); + + viGetStringifiedHeaderAndPayload + .mockImplementationOnce( + () => ok({ header: mockStringifiedHeader, payload: "" }), + ) // For header + .mockImplementationOnce(() => ok({ header: "", payload: '{ "sub": 123 }' })); // For payload + + const result = await TokenDecoderService.handleJwtChange(mockParams); + + expect(result.decodingErrors).toEqual([error.message]); + expect(result.alg).toBe(mockDecodedHeader.alg); + expect(result.decodedHeader).toBe(mockStringifiedHeader); + expect(result.decodedPayload).toBe('{ "sub": 123 }'); + expect(result.signatureStatus).toBe(JwtSignatureStatusValues.WARNING); + }); + + it("should handle Unsecured JWT (alg: none)", async () => { + const unsecuredJwt = "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjMifQ."; + const decoded = { header: { alg: "none" }, payload: { sub: "123" } }; + viValidateJwtFormat.mockReturnValue( + ok({ + type: JwtTypeValues.Unsecured, + signingAlgorithm: "none", + decoded: decoded, + }), + ); + viGetStringifiedHeaderAndPayload.mockReturnValue( + ok({ + header: '{ "alg": "none" }', + payload: '{ "sub": "123" }', + }), + ); + + const result = await TokenDecoderService.handleJwtChange({ + ...mockParams, + newToken: unsecuredJwt, + }); + + expect(result.alg).toBe("none"); + expect(result.decodedHeader).toBe('{ "alg": "none" }'); + expect(result.decodedPayload).toBe('{ "sub": "123" }'); + expect(result.signatureStatus).toBe(JwtSignatureStatusValues.WARNING); + expect(result.signatureWarnings).toEqual([ + expect.stringContaining("Unsecured JWT"), + ]); + expect(result.verificationInputErrors).toEqual([ + "Can't verify signature for an Unsecured JWT.", + ]); + }); + + it("should return VALID for a valid HMAC token and secret", async () => { + viValidateJwtFormat.mockReturnValue( + ok({ + type: JwtTypeValues.MACed, + signingAlgorithm: mockDecodedHeader.alg, + decoded: { header: mockDecodedHeader, payload: mockDecodedPayload }, + }), + ); + viVerifyMACedJwt.mockResolvedValue(ok(mockCompactVerifyResult)); + + const result = await TokenDecoderService.handleJwtChange(mockParams); + + expect(viValidateSymmetricSecret).toHaveBeenCalledWith({ + symmetricSecretKey: mockParams.symmetricSecretKey, + symmetricSecretKeyEncoding: mockParams.symmetricSecretKeyEncoding, + }); + expect(viVerifyMACedJwt).toHaveBeenCalledWith({ + jwt: mockParams.newToken, + symmetricSecretKey: mockParams.symmetricSecretKey, + symmetricSecretKeyEncoding: mockParams.symmetricSecretKeyEncoding, + }); + expect(result.signatureStatus).toBe(JwtSignatureStatusValues.VALID); + expect(result.verificationInputErrors).toBeNull(); + expect(result.symmetricSecretKey).toBe(mockParams.symmetricSecretKey); + expect(result.controlledSymmetricSecretKey).toBeDefined(); + }); + + it("should return INVALID for a valid HMAC token and *incorrect* secret", async () => { + viValidateJwtFormat.mockReturnValue( + ok({ + type: JwtTypeValues.MACed, + signingAlgorithm: mockDecodedHeader.alg, + decoded: { header: mockDecodedHeader, payload: mockDecodedPayload }, + }), + ); + const error = { + message: "Invalid signature", + task: DebuggerTaskValues.VERIFY, + input: DebuggerInputValues.KEY, + }; + viVerifyMACedJwt.mockResolvedValue(err(error)); + + const result = await TokenDecoderService.handleJwtChange(mockParams); + + expect(viVerifyMACedJwt).toHaveBeenCalled(); + expect(result.signatureStatus).toBe(JwtSignatureStatusValues.INVALID); + expect(result.verificationInputErrors).toEqual([error.message]); + }); +}); \ No newline at end of file From c006f5413ec30ede152d79405830e1145bced662 Mon Sep 17 00:00:00 2001 From: Christian Samaniego <199278128+christiansamaniego-okta@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:36:21 -0500 Subject: [PATCH 2/4] add test to address empty token --- tests/token-decoder.service.test.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/token-decoder.service.test.ts b/tests/token-decoder.service.test.ts index 2d4b6d69..77d18029 100644 --- a/tests/token-decoder.service.test.ts +++ b/tests/token-decoder.service.test.ts @@ -57,7 +57,6 @@ vi.mock("@/features/debugger/services/debugger.store", () => ({ // Typed Mocks const viExtractJwt = vi.mocked(extractJwt); const viValidateSymmetricSecret = vi.mocked(validateSymmetricSecret); -const viValidateAsymmetricKey = vi.mocked(validateAsymmetricKey); const viValidateJwtFormat = vi.mocked(validateJwtFormat); const viIsHmacAlg = vi.mocked(isHmacAlg); const viIsDigitalSignatureAlg = vi.mocked(isDigitalSignatureAlg); @@ -71,7 +70,7 @@ const viParseStringIntoValidJsonObject = vi.mocked( const viVerifyMACedJwt = vi.mocked(verifyMACedJwt); const viDownloadPublicKeyIfPossible = vi.mocked(downloadPublicKeyIfPossible); -describe("TokenDecoderService.handleJwtChange", () => { +describe("handleJwtChange", () => { const mockJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; const mockParams = { @@ -109,7 +108,6 @@ describe("TokenDecoderService.handleJwtChange", () => { ); viIsSupportedAlg.mockReturnValue(true); viValidateSymmetricSecret.mockResolvedValue(ok(new Uint8Array([1, 2, 3]))); - viValidateAsymmetricKey.mockResolvedValue(ok({} as CryptoKey)); viGetStringifiedHeaderAndPayload.mockReturnValue( ok({ header: mockStringifiedHeader, @@ -128,6 +126,31 @@ describe("TokenDecoderService.handleJwtChange", () => { ); }); + it("should return a warning if the new token is empty", async () => { + const error = { + message: "JWT must not be empty.", + input: DebuggerInputValues.JWT, + task: DebuggerTaskValues.DECODE, + }; + + viExtractJwt.mockReturnValue(""); + viValidateJwtFormat.mockReturnValue(err(error)); + + const result = await TokenDecoderService.handleJwtChange({ + ...mockParams, + newToken: " ", + }); + + expect(viExtractJwt).toHaveBeenCalledWith(" "); + expect(result.jwt).toBe(""); + expect(result.signatureStatus).toBe(JwtSignatureStatusValues.WARNING); + expect(result.signatureWarnings).toEqual([ + StringValues.editor.signatureWarning, + ]); + expect(result.decodedHeader).toBe(""); + expect(result.decodedPayload).toBe(""); + }); + it("should return decoding errors if JWT format is invalid", async () => { const error = { message: "Invalid format", From ce8db22f52aca979b9ff39de0069149e0cf29995 Mon Sep 17 00:00:00 2001 From: Christian Samaniego <199278128+christiansamaniego-okta@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:37:18 -0500 Subject: [PATCH 3/4] remove unused import --- tests/token-decoder.service.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/token-decoder.service.test.ts b/tests/token-decoder.service.test.ts index 77d18029..dae352e4 100644 --- a/tests/token-decoder.service.test.ts +++ b/tests/token-decoder.service.test.ts @@ -3,7 +3,6 @@ import { err, ok } from "neverthrow"; import { extractJwt } from "@/features/common/services/utils"; import { validateSymmetricSecret, - validateAsymmetricKey, validateJwtFormat, isHmacAlg, isDigitalSignatureAlg, From 01cc17f04c6384feea48188c7671c15bba97fc45 Mon Sep 17 00:00:00 2001 From: Christian Samaniego <199278128+christiansamaniego-okta@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:38:33 -0500 Subject: [PATCH 4/4] remove unused mock function --- tests/token-decoder.service.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/token-decoder.service.test.ts b/tests/token-decoder.service.test.ts index dae352e4..bef5acaa 100644 --- a/tests/token-decoder.service.test.ts +++ b/tests/token-decoder.service.test.ts @@ -30,7 +30,6 @@ vi.mock("@/features/common/services/utils", () => ({ vi.mock("@/features/common/services/jwt.service", () => ({ validateSymmetricSecret: vi.fn(), - validateAsymmetricKey: vi.fn(), validateJwtFormat: vi.fn(), isHmacAlg: vi.fn(), isDigitalSignatureAlg: vi.fn(),