1
- /* eslint-disable @typescript-eslint/no-explicit-any */
1
+ import { ReadableStream } from 'stream/web'
2
2
import { MockProxy , mock , mockReset } from 'jest-mock-extended'
3
3
import { AjvOpenApiValidator } from '@restfulhead/ajv-openapi-request-response-validator'
4
4
import { HttpRequest , HttpResponseInit , PostInvocationContext , PreInvocationContext } from '@azure/functions'
5
5
import {
6
6
ValidatorHookOptions ,
7
7
configureValidationPostInvocationHandler ,
8
8
configureValidationPreInvocationHandler ,
9
+ DEFAULT_HOOK_OPTIONS ,
9
10
} from '../../src/validation-hook-setup'
10
11
11
12
describe ( 'The app validator' , ( ) => {
@@ -191,7 +192,7 @@ describe('The app validator', () => {
191
192
[ 'code' ] ,
192
193
expect . anything ( )
193
194
)
194
- expect ( mockValidator . validateRequestBody ) . toHaveBeenCalledTimes ( 0 )
195
+ expect ( mockValidator . validateRequestBody ) . toHaveBeenCalledWith ( '/api/v1/health' , 'GET' , undefined , true , expect . anything ( ) )
195
196
} )
196
197
197
198
it ( 'should fail with query parameter validation error' , async ( ) => {
@@ -215,6 +216,65 @@ describe('The app validator', () => {
215
216
expect ( mockValidator . validateRequestBody ) . toHaveBeenCalledTimes ( 0 )
216
217
} )
217
218
219
+ it ( 'should fail with request body validation error' , async ( ) => {
220
+ mockValidator . validateQueryParams . mockReturnValueOnce ( { errors : undefined , normalizedParams : { } } )
221
+ mockValidator . validateRequestBody . mockReturnValueOnce ( [ MOCK_ERROR ] )
222
+ const handler = configureValidationPreInvocationHandler ( mockValidator )
223
+ const ctx = getMockPreContext ( 'api/v1/health?something' , JSON . stringify ( { status : 'ok' } ) )
224
+ await handler ( ctx )
225
+
226
+ const request : HttpRequest = { ...DEFAULT_HTTP_GET_REQUEST , query : new URLSearchParams ( 'something' ) }
227
+ const functionResult = await ctx . functionHandler ( request , MOCK_PRE_CONTEXT . invocationContext )
228
+
229
+ expect ( functionResult ) . toEqual ( { status : 400 , body : JSON . stringify ( { errors : [ MOCK_ERROR ] } ) , headers : JSON_HEADERS } )
230
+ expect ( mockValidator . validateRequestBody ) . toHaveBeenCalledWith ( '/api/v1/health?something' , 'GET' , undefined , true , expect . anything ( ) )
231
+ } )
232
+
233
+ it ( 'should exlcude request body validation error' , async ( ) => {
234
+ mockValidator . validateQueryParams . mockReturnValueOnce ( { errors : undefined , normalizedParams : { } } )
235
+ mockValidator . validateRequestBody . mockReturnValueOnce ( [ MOCK_ERROR ] )
236
+ const handler = configureValidationPreInvocationHandler ( mockValidator , {
237
+ ...DEFAULT_HOOK_OPTIONS ,
238
+ exclude : [
239
+ {
240
+ path : '/api/v1/health' ,
241
+ method : 'GET' ,
242
+ validation : false ,
243
+ } ,
244
+ ] ,
245
+ } )
246
+ const ctx = getMockPreContext ( 'api/v1/health' , JSON . stringify ( { status : 'ok' } ) )
247
+ await handler ( ctx )
248
+
249
+ const request : HttpRequest = { ...DEFAULT_HTTP_GET_REQUEST , query : new URLSearchParams ( 'something' ) }
250
+ const functionResult = await ctx . functionHandler ( request , MOCK_PRE_CONTEXT . invocationContext )
251
+
252
+ expect ( functionResult ) . toEqual ( { status : 200 , body : '{"status":"ok"}' } )
253
+ expect ( mockValidator . validateRequestBody ) . not . toHaveBeenCalled ( )
254
+ } )
255
+
256
+ it ( 'should exlcude query validation' , async ( ) => {
257
+ mockValidator . validateQueryParams . mockReturnValueOnce ( { errors : [ MOCK_ERROR ] , normalizedParams : { } } )
258
+ const handler = configureValidationPreInvocationHandler ( mockValidator , {
259
+ ...DEFAULT_HOOK_OPTIONS ,
260
+ exclude : [
261
+ {
262
+ path : 'api/v1/health' ,
263
+ method : 'GET' ,
264
+ validation : false ,
265
+ } ,
266
+ ] ,
267
+ } )
268
+ const ctx = getMockPreContext ( 'api/v1/health' , JSON . stringify ( { status : 'ok' } ) )
269
+ await handler ( ctx )
270
+
271
+ const request : HttpRequest = { ...DEFAULT_HTTP_GET_REQUEST , query : new URLSearchParams ( 'something' ) }
272
+ const functionResult = await ctx . functionHandler ( request , MOCK_PRE_CONTEXT . invocationContext )
273
+
274
+ expect ( functionResult ) . toEqual ( { status : 200 , body : '{"status":"ok"}' } )
275
+ expect ( mockValidator . validateRequestBody ) . not . toHaveBeenCalled ( )
276
+ } )
277
+
218
278
it ( 'should pass post request without body' , async ( ) => {
219
279
mockValidator . validateQueryParams . mockReturnValueOnce ( { errors : undefined , normalizedParams : { } } )
220
280
const handler = configureValidationPreInvocationHandler ( mockValidator )
@@ -257,6 +317,44 @@ describe('The app validator', () => {
257
317
expect ( mockValidator . validateRequestBody ) . toHaveBeenCalledWith ( '/api/v1/messages' , 'POST' , { hello : 'world' } , true , expect . anything ( ) )
258
318
} )
259
319
320
+ it ( 'should fail missing request content type' , async ( ) => {
321
+ mockValidator . validateQueryParams . mockReturnValueOnce ( { errors : undefined , normalizedParams : { } } )
322
+ const handler = configureValidationPreInvocationHandler ( mockValidator )
323
+ const ctx = getMockPreContext ( 'api/v1/messages' , JSON . stringify ( { status : 'ok' } ) )
324
+ await handler ( ctx )
325
+
326
+ const request : HttpRequest = DEFAULT_HTTP_POST_REQUEST ( { hello : 'world' } )
327
+ request . headers . delete ( 'Content-Type' )
328
+ const functionResult = await ctx . functionHandler ( request , MOCK_PRE_CONTEXT . invocationContext )
329
+
330
+ expect ( functionResult ) . toEqual ( {
331
+ status : 400 ,
332
+ body : '{"errors":[{"status":400,"code":"Validation-missing-content-type-header","title":"The request header \'Content-Type\' is missing"}]}' ,
333
+ headers : { 'Content-Type' : 'application/json' } ,
334
+ } )
335
+
336
+ expect ( mockValidator . validateRequestBody ) . not . toHaveBeenCalled ( )
337
+ } )
338
+
339
+ it ( 'should fail wrong request content type' , async ( ) => {
340
+ mockValidator . validateQueryParams . mockReturnValueOnce ( { errors : undefined , normalizedParams : { } } )
341
+ const handler = configureValidationPreInvocationHandler ( mockValidator )
342
+ const ctx = getMockPreContext ( 'api/v1/messages' , JSON . stringify ( { status : 'ok' } ) )
343
+ await handler ( ctx )
344
+
345
+ const request : HttpRequest = DEFAULT_HTTP_POST_REQUEST ( { hello : 'world' } )
346
+ request . headers . set ( 'Content-Type' , 'text/plain' )
347
+ const functionResult = await ctx . functionHandler ( request , MOCK_PRE_CONTEXT . invocationContext )
348
+
349
+ expect ( functionResult ) . toEqual ( {
350
+ status : 400 ,
351
+ body : '{"errors":[{"status":400,"code":"Validation-invalid-content-type-header","title":"The content type \'text/plain\' is not supported."}]}' ,
352
+ headers : { 'Content-Type' : 'application/json' } ,
353
+ } )
354
+
355
+ expect ( mockValidator . validateRequestBody ) . not . toHaveBeenCalled ( )
356
+ } )
357
+
260
358
it ( 'should fail with post request body validation error' , async ( ) => {
261
359
mockValidator . validateQueryParams . mockReturnValueOnce ( { errors : undefined , normalizedParams : { } } )
262
360
mockValidator . validateRequestBody . mockReturnValueOnce ( [ MOCK_ERROR ] )
@@ -305,10 +403,132 @@ describe('The app validator', () => {
305
403
306
404
expect ( ctx . result ) . toEqual ( {
307
405
status : 500 ,
406
+ body : '{"errors":[{"status":500,"code":"Validation","title":"Response body validation failed"}]}' ,
407
+ headers : {
408
+ 'Content-Type' : 'application/json' ,
409
+ } ,
410
+ } )
411
+ expect ( mockValidator . validateResponseBody ) . toHaveBeenCalledWith ( '/api/v1/health' , 'GET' , 200 , { hello : 'ok' } , true , expect . anything ( ) )
412
+ } )
413
+
414
+ it ( 'should pass with warning for non json response body' , async ( ) => {
415
+ const handler = configureValidationPostInvocationHandler ( mockValidator , {
416
+ responseBodyValidationMode : { returnErrorResponse : false , strict : true , logLevel : 'info' } ,
417
+ queryParameterValidationMode : false ,
418
+ requestBodyValidationMode : false ,
419
+ } )
420
+ const ctx = getMockPostContext ( 'api/v1/health' , { ...DEFAULT_HTTP_GET_REQUEST } , { status : 200 , body : 'hello\nworld' } )
421
+ await handler ( ctx )
422
+
423
+ expect ( ctx . result ) . toEqual ( { status : 200 , body : 'hello\nworld' } )
424
+ expect ( mockValidator . validateResponseBody ) . not . toHaveBeenCalled ( )
425
+ } )
426
+
427
+ it ( 'should fail for non json response body' , async ( ) => {
428
+ const handler = configureValidationPostInvocationHandler ( mockValidator , withResponseValidation )
429
+ const ctx = getMockPostContext ( 'api/v1/health' , { ...DEFAULT_HTTP_GET_REQUEST } , { status : 200 , body : 'hello\nworld' } )
430
+ await handler ( ctx )
431
+
432
+ expect ( ctx . result ) . toEqual ( {
433
+ status : 500 ,
434
+ body : '{"errors":[{"status":500,"code":"Validation-invalid-content-type-header","title":"Response body validation failed"}]}' ,
435
+ headers : {
436
+ 'Content-Type' : 'application/json' ,
437
+ } ,
438
+ } )
439
+ expect ( mockValidator . validateResponseBody ) . not . toHaveBeenCalled ( )
440
+ } )
441
+
442
+ it ( 'should handle path request exclusions 1' , async ( ) => {
443
+ mockValidator . validateQueryParams . mockReturnValueOnce ( { errors : [ MOCK_ERROR ] , normalizedParams : { } } )
444
+ mockValidator . validateRequestBody . mockReturnValueOnce ( [ MOCK_ERROR ] )
445
+ const handler = configureValidationPreInvocationHandler ( mockValidator , {
446
+ ...DEFAULT_HOOK_OPTIONS ,
447
+ exclude : [
448
+ {
449
+ path : '/api/v1/health' ,
450
+ method : 'GET' ,
451
+ validation : {
452
+ queryParameter : false ,
453
+ requestBody : false ,
454
+ responseBody : true ,
455
+ } ,
456
+ } ,
457
+ ] ,
458
+ } )
459
+ const ctx = getMockPreContext ( 'api/v1/health' , JSON . stringify ( { status : 'ok' } ) )
460
+ await handler ( ctx )
461
+
462
+ const request : HttpRequest = { ...DEFAULT_HTTP_GET_REQUEST , query : new URLSearchParams ( 'something' ) }
463
+ const functionResult = await ctx . functionHandler ( request , MOCK_PRE_CONTEXT . invocationContext )
464
+
465
+ expect ( functionResult ) . toEqual ( { status : 200 , body : '{"status":"ok"}' } )
466
+ expect ( mockValidator . validateRequestBody ) . not . toHaveBeenCalled ( )
467
+ expect ( mockValidator . validateQueryParams ) . not . toHaveBeenCalled ( )
468
+ } )
469
+
470
+ it ( 'should handle path request exclusions2' , async ( ) => {
471
+ mockValidator . validateQueryParams . mockReturnValueOnce ( { errors : [ MOCK_ERROR ] , normalizedParams : { } } )
472
+ mockValidator . validateRequestBody . mockReturnValueOnce ( [ MOCK_ERROR ] )
473
+ const handler = configureValidationPreInvocationHandler ( mockValidator , {
474
+ ...DEFAULT_HOOK_OPTIONS ,
475
+ exclude : [
476
+ {
477
+ path : '/api/v1/health' ,
478
+ method : 'GET' ,
479
+ validation : {
480
+ queryParameter : false ,
481
+ requestBody : {
482
+ returnErrorResponse : true ,
483
+ strict : false ,
484
+ logLevel : 'info' ,
485
+ } ,
486
+ responseBody : true ,
487
+ } ,
488
+ } ,
489
+ ] ,
490
+ } )
491
+ const ctx = getMockPreContext ( 'api/v1/health' , JSON . stringify ( { status : 'ok' } ) )
492
+ await handler ( ctx )
493
+
494
+ const request : HttpRequest = { ...DEFAULT_HTTP_GET_REQUEST , query : new URLSearchParams ( 'something' ) }
495
+ const functionResult = await ctx . functionHandler ( request , MOCK_PRE_CONTEXT . invocationContext )
496
+
497
+ expect ( functionResult ) . toEqual ( {
308
498
body : '{"errors":[{"status":400,"code":"ValidationError","title":"Validation failed"}]}' ,
309
499
headers : {
310
500
'Content-Type' : 'application/json' ,
311
501
} ,
502
+ status : 400 ,
503
+ } )
504
+ expect ( mockValidator . validateRequestBody ) . toHaveBeenCalledWith ( '/api/v1/health' , 'GET' , undefined , false , expect . anything ( ) )
505
+ expect ( mockValidator . validateQueryParams ) . not . toHaveBeenCalled ( )
506
+ } )
507
+
508
+ it ( 'should handle path response exclusions' , async ( ) => {
509
+ mockValidator . validateResponseBody . mockReturnValueOnce ( [ MOCK_ERROR ] )
510
+ const handler = configureValidationPostInvocationHandler ( mockValidator , {
511
+ ...DEFAULT_HOOK_OPTIONS ,
512
+ exclude : [
513
+ {
514
+ path : '/api/v1/health' ,
515
+ method : 'GET' ,
516
+ validation : {
517
+ queryParameter : false ,
518
+ requestBody : false ,
519
+ responseBody : true ,
520
+ } ,
521
+ } ,
522
+ ] ,
523
+ } )
524
+ const ctx = getMockPostContext ( 'api/v1/health' , { ...DEFAULT_HTTP_GET_REQUEST } , { status : 200 , jsonBody : { hello : 'ok' } } )
525
+ await handler ( ctx )
526
+ expect ( ctx . result ) . toEqual ( {
527
+ body : '{"errors":[{"status":500,"code":"Validation","title":"Response body validation failed"}]}' ,
528
+ headers : {
529
+ 'Content-Type' : 'application/json' ,
530
+ } ,
531
+ status : 500 ,
312
532
} )
313
533
expect ( mockValidator . validateResponseBody ) . toHaveBeenCalledWith ( '/api/v1/health' , 'GET' , 200 , { hello : 'ok' } , true , expect . anything ( ) )
314
534
} )
0 commit comments