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