1
1
import { beforeEach , describe , expect , it , vi } from "vitest" ;
2
2
3
+ import { AuthData } from "@/APL" ;
4
+ import * as fetchRemoteJwksModule from "@/fetch-remote-jwks" ;
3
5
import { MockAdapter } from "@/test-utils/mock-adapter" ;
4
6
import { MockAPL } from "@/test-utils/mock-apl" ;
5
7
import * as verifySignatureModule from "@/verify-signature" ;
@@ -42,27 +44,6 @@ describe("SaleorWebhookValidator", () => {
42
44
middleware = new PlatformAdapterMiddleware ( adapter ) ;
43
45
} ) ;
44
46
45
- it ( "Processes valid request" , async ( ) => {
46
- vi . spyOn ( middleware , "getSaleorHeaders" ) . mockReturnValue ( validHeaders ) ;
47
-
48
- const result = await validator . validateRequest ( {
49
- allowedEvent : "PRODUCT_UPDATED" ,
50
- apl : mockAPL ,
51
- adapter,
52
- adapterMiddleware : middleware ,
53
- } ) ;
54
-
55
- expect ( result ) . toMatchObject ( {
56
- result : "ok" ,
57
- context : expect . objectContaining ( {
58
- baseUrl : "https://example-app.com/api" ,
59
- event : "product_updated" ,
60
- payload : { } ,
61
- schemaVersion : null ,
62
- } ) ,
63
- } ) ;
64
- } ) ;
65
-
66
47
it ( "Throws error on non-POST request method" , async ( ) => {
67
48
vi . spyOn ( adapter , "method" , "get" ) . mockReturnValue ( "GET" ) ;
68
49
@@ -211,6 +192,7 @@ describe("SaleorWebhookValidator", () => {
211
192
} ) ;
212
193
} ) ;
213
194
195
+ // TODO: This should be required
214
196
it ( "Fallbacks to null if version is missing in payload" , async ( ) => {
215
197
vi . spyOn ( adapter , "getRawBody" ) . mockResolvedValue ( JSON . stringify ( { } ) ) ;
216
198
vi . spyOn ( middleware , "getSaleorHeaders" ) . mockReturnValue ( validHeaders ) ;
@@ -229,4 +211,160 @@ describe("SaleorWebhookValidator", () => {
229
211
} ) ,
230
212
} ) ;
231
213
} ) ;
214
+
215
+ it ( "Returns success on valid request with signature passing validation against jwks in auth data" , async ( ) => {
216
+ vi . spyOn ( middleware , "getSaleorHeaders" ) . mockReturnValue ( validHeaders ) ;
217
+
218
+ const result = await validator . validateRequest ( {
219
+ allowedEvent : "PRODUCT_UPDATED" ,
220
+ apl : mockAPL ,
221
+ adapter,
222
+ adapterMiddleware : middleware ,
223
+ } ) ;
224
+
225
+ expect ( result ) . toMatchObject ( {
226
+ result : "ok" ,
227
+ context : expect . objectContaining ( {
228
+ baseUrl : "https://example-app.com/api" ,
229
+ event : "product_updated" ,
230
+ payload : { } ,
231
+ schemaVersion : null ,
232
+ } ) ,
233
+ } ) ;
234
+ } ) ;
235
+
236
+ describe ( "JWKS re-try validation" , ( ) => {
237
+ const authDataNoJwks = {
238
+ token : mockAPL . mockToken ,
239
+ saleorApiUrl : mockAPL . workingSaleorApiUrl ,
240
+ appId : mockAPL . mockAppId ,
241
+ jwks : null , // Simulate missing JWKS in initial auth data
242
+ } as unknown as AuthData ; // We're testing missing jwks, so this is fine
243
+
244
+ beforeEach ( ( ) => {
245
+ vi . resetAllMocks ( ) ;
246
+ vi . spyOn ( middleware , "getSaleorHeaders" ) . mockReturnValue ( validHeaders ) ;
247
+ } ) ;
248
+
249
+ it ( "Triggers JWKS refresh when initial auth data contains empty JWKS" , async ( ) => {
250
+ vi . spyOn ( mockAPL , "get" ) . mockResolvedValue ( authDataNoJwks ) ;
251
+ vi . spyOn ( verifySignatureModule , "verifySignatureWithJwks" ) . mockResolvedValueOnce ( undefined ) ;
252
+ vi . spyOn ( fetchRemoteJwksModule , "fetchRemoteJwks" ) . mockResolvedValue ( "new-jwks" ) ;
253
+
254
+ const result = await validator . validateRequest ( {
255
+ allowedEvent : "PRODUCT_UPDATED" ,
256
+ apl : mockAPL ,
257
+ adapter,
258
+ adapterMiddleware : middleware ,
259
+ } ) ;
260
+
261
+ expect ( result ) . toMatchObject ( {
262
+ result : "ok" ,
263
+ context : expect . objectContaining ( {
264
+ baseUrl : "https://example-app.com/api" ,
265
+ event : "product_updated" ,
266
+ payload : { } ,
267
+ schemaVersion : null ,
268
+ } ) ,
269
+ } ) ;
270
+
271
+ expect ( mockAPL . set ) . toHaveBeenCalledWith (
272
+ expect . objectContaining ( {
273
+ jwks : "new-jwks" ,
274
+ } )
275
+ ) ;
276
+ expect ( fetchRemoteJwksModule . fetchRemoteJwks ) . toHaveBeenCalledWith (
277
+ authDataNoJwks . saleorApiUrl
278
+ ) ;
279
+ // it's called only once because jwks was missing initially, so we skipped first validation
280
+ expect ( verifySignatureModule . verifySignatureWithJwks ) . toHaveBeenCalledTimes ( 1 ) ;
281
+ } ) ;
282
+
283
+ it ( "Triggers JWKS refresh when token signature doesn't match JWKS from existing auth data" , async ( ) => {
284
+ vi . spyOn ( verifySignatureModule , "verifySignatureWithJwks" )
285
+ . mockRejectedValueOnce ( new Error ( "Signature verification failed" ) ) // First: reject validation due to stale jwks
286
+ . mockResolvedValueOnce ( undefined ) ; // Second: resolve validation because jwks is now correct
287
+ vi . spyOn ( fetchRemoteJwksModule , "fetchRemoteJwks" ) . mockResolvedValue ( "new-jwks" ) ;
288
+
289
+ const result = await validator . validateRequest ( {
290
+ allowedEvent : "PRODUCT_UPDATED" ,
291
+ apl : mockAPL ,
292
+ adapter,
293
+ adapterMiddleware : middleware ,
294
+ } ) ;
295
+
296
+ expect ( result ) . toMatchObject ( {
297
+ result : "ok" ,
298
+ context : expect . objectContaining ( {
299
+ baseUrl : "https://example-app.com/api" ,
300
+ event : "product_updated" ,
301
+ payload : { } ,
302
+ schemaVersion : null ,
303
+ } ) ,
304
+ } ) ;
305
+
306
+ expect ( mockAPL . set ) . toHaveBeenCalledWith (
307
+ expect . objectContaining ( {
308
+ jwks : "new-jwks" ,
309
+ } )
310
+ ) ;
311
+ expect ( fetchRemoteJwksModule . fetchRemoteJwks ) . toHaveBeenCalledWith (
312
+ authDataNoJwks . saleorApiUrl
313
+ ) ;
314
+ expect ( verifySignatureModule . verifySignatureWithJwks ) . toHaveBeenCalledTimes ( 2 ) ;
315
+ } ) ;
316
+
317
+ it ( "Returns an error when new JWKS cannot be fetched" , async ( ) => {
318
+ vi . spyOn ( mockAPL , "get" ) . mockResolvedValue ( authDataNoJwks ) ;
319
+ vi . spyOn ( verifySignatureModule , "verifySignatureWithJwks" ) . mockRejectedValue (
320
+ new Error ( "Initial verification failed" )
321
+ ) ;
322
+ vi . spyOn ( fetchRemoteJwksModule , "fetchRemoteJwks" ) . mockRejectedValue (
323
+ new Error ( "JWKS fetch failed" )
324
+ ) ;
325
+
326
+ const result = await validator . validateRequest ( {
327
+ allowedEvent : "PRODUCT_UPDATED" ,
328
+ apl : mockAPL ,
329
+ adapter,
330
+ adapterMiddleware : middleware ,
331
+ } ) ;
332
+
333
+ expect ( result ) . toMatchObject ( {
334
+ result : "failure" ,
335
+ error : expect . objectContaining ( {
336
+ errorType : "SIGNATURE_VERIFICATION_FAILED" ,
337
+ message : "Fetching remote JWKS failed" ,
338
+ } ) ,
339
+ } ) ;
340
+ expect ( fetchRemoteJwksModule . fetchRemoteJwks ) . toHaveBeenCalledTimes ( 1 ) ;
341
+ } ) ;
342
+
343
+ it ( "Returns an error when signature doesn't match JWKS after re-fetching it" , async ( ) => {
344
+ vi . spyOn ( verifySignatureModule , "verifySignatureWithJwks" )
345
+ . mockRejectedValueOnce ( new Error ( "Stale JWKS" ) ) // First attempt fails
346
+ . mockRejectedValueOnce ( new Error ( "Fresh JWKS mismatch" ) ) ; // Second attempt fails
347
+ vi . spyOn ( fetchRemoteJwksModule , "fetchRemoteJwks" ) . mockResolvedValue ( "{}" ) ;
348
+
349
+ const result = await validator . validateRequest ( {
350
+ allowedEvent : "PRODUCT_UPDATED" ,
351
+ apl : mockAPL ,
352
+ adapter,
353
+ adapterMiddleware : middleware ,
354
+ } ) ;
355
+
356
+ expect ( result ) . toMatchObject ( {
357
+ result : "failure" ,
358
+ error : expect . objectContaining ( {
359
+ errorType : "SIGNATURE_VERIFICATION_FAILED" ,
360
+ message : "Request signature check failed" ,
361
+ } ) ,
362
+ } ) ;
363
+
364
+ expect ( verifySignatureModule . verifySignatureWithJwks ) . toHaveBeenCalledTimes ( 2 ) ;
365
+ expect ( fetchRemoteJwksModule . fetchRemoteJwks ) . toHaveBeenCalledWith (
366
+ authDataNoJwks . saleorApiUrl
367
+ ) ;
368
+ } ) ;
369
+ } ) ;
232
370
} ) ;
0 commit comments