@@ -255,169 +255,175 @@ describe('openapi metadata validation', () => {
255255 expect ( Object . keys ( tagsByPath ) . length ) . toBeGreaterThan ( 0 )
256256 } )
257257
258- it ( 'should validate actual API responses against OpenAPI schemas' , async ( ) => {
259- const openApiSpec = await $fetch < OpenApiSpec > ( '/_docs/openapi.json' )
260-
261- const randomNumber = ( min : number , max : number ) =>
262- Math . floor ( Math . random ( ) * ( max - min + 1 ) ) + min
263-
264- const randomValue = < T > ( arr : T [ ] ) : T => arr [ Math . floor ( Math . random ( ) * arr . length ) ]
265-
266- const testSymbols = [ 'en' , 'es' , 'fr' , 'de' , 'it' , 'pt' , 'nl' ]
267- const testCodes = [ 'E' , 'S' , 'F' , 'D' , 'I' , 'P' , 'O' ]
268-
269- // Define test cases for different endpoints with sample parameters
270- const testCases : Array < {
271- description : string
272- openApiPath : string
273- params ?: Record < string , number | string >
274- } > = [
275- {
276- description : 'Bible verse endpoint' ,
277- openApiPath : '/api/v1/bible/{symbol}/{book}/{chapter}/{verse}' ,
278- params : {
279- book : randomNumber ( 1 , 66 ) . toString ( ) ,
280- chapter : '1' ,
281- symbol : randomValue ( testSymbols ) ,
282- verse : '1'
258+ it (
259+ 'should validate actual API responses against OpenAPI schemas' ,
260+ async ( ) => {
261+ const openApiSpec = await $fetch < OpenApiSpec > ( '/_docs/openapi.json' )
262+
263+ const randomNumber = ( min : number , max : number ) =>
264+ Math . floor ( Math . random ( ) * ( max - min + 1 ) ) + min
265+
266+ const randomValue = < T > ( arr : T [ ] ) : T => arr [ Math . floor ( Math . random ( ) * arr . length ) ]
267+
268+ const testSymbols = [ 'en' , 'es' , 'fr' , 'de' , 'it' , 'pt' , 'nl' ]
269+ const testCodes = [ 'E' , 'S' , 'F' , 'D' , 'I' , 'P' , 'O' ]
270+
271+ // Define test cases for different endpoints with sample parameters
272+ const testCases : Array < {
273+ description : string
274+ openApiPath : string
275+ params ?: Record < string , number | string >
276+ } > = [
277+ {
278+ description : 'Bible verse endpoint' ,
279+ openApiPath : '/api/v1/bible/{symbol}/{book}/{chapter}/{verse}' ,
280+ params : {
281+ book : randomNumber ( 1 , 66 ) . toString ( ) ,
282+ chapter : '1' ,
283+ symbol : randomValue ( testSymbols ) ,
284+ verse : '1'
285+ }
286+ } ,
287+ {
288+ description : 'Bible books endpoint' ,
289+ openApiPath : '/api/v1/bible/{symbol}/books' ,
290+ params : { symbol : randomValue ( testSymbols ) }
291+ } ,
292+ {
293+ description : 'Yeartext endpoint' ,
294+ openApiPath : '/api/v1/wol/yeartext' ,
295+ params : { wtlocale : randomValue ( testCodes ) , year : new Date ( ) . getFullYear ( ) }
296+ } ,
297+ {
298+ description : 'Meeting schedule endpoint' ,
299+ openApiPath : '/api/v1/meeting/schedule' ,
300+ params : { langwritten : randomValue ( testCodes ) }
283301 }
284- } ,
285- {
286- description : 'Bible books endpoint' ,
287- openApiPath : '/api/v1/bible/{symbol}/books' ,
288- params : { symbol : randomValue ( testSymbols ) }
289- } ,
290- {
291- description : 'Yeartext endpoint' ,
292- openApiPath : '/api/v1/wol/yeartext' ,
293- params : { wtlocale : randomValue ( testCodes ) , year : new Date ( ) . getFullYear ( ) }
294- } ,
295- {
296- description : 'Meeting schedule endpoint' ,
297- openApiPath : '/api/v1/meeting/schedule' ,
298- params : { langwritten : randomValue ( testCodes ) }
299- }
300- ]
302+ ]
301303
302- const errors : string [ ] = [ ]
304+ const errors : string [ ] = [ ]
303305
304- for ( const testCase of testCases ) {
305- try {
306- // Get the OpenAPI schema for this endpoint
307- const endpointSpec = openApiSpec . paths [ testCase . openApiPath ] ?. get
308- if ( ! endpointSpec ) {
309- errors . push ( `${ testCase . description } : No OpenAPI spec found for ${ testCase . openApiPath } ` )
310- continue
311- }
306+ for ( const testCase of testCases ) {
307+ try {
308+ // Get the OpenAPI schema for this endpoint
309+ const endpointSpec = openApiSpec . paths [ testCase . openApiPath ] ?. get
310+ if ( ! endpointSpec ) {
311+ errors . push (
312+ `${ testCase . description } : No OpenAPI spec found for ${ testCase . openApiPath } `
313+ )
314+ continue
315+ }
312316
313- const response200 = endpointSpec . responses ?. [ '200' ]
314- if ( ! response200 || ! ( 'content' in response200 ) ) {
315- errors . push ( `${ testCase . description } : No 200 response schema defined` )
316- continue
317- }
317+ const response200 = endpointSpec . responses ?. [ '200' ]
318+ if ( ! response200 || ! ( 'content' in response200 ) ) {
319+ errors . push ( `${ testCase . description } : No 200 response schema defined` )
320+ continue
321+ }
318322
319- const schema = response200 . content ?. [ 'application/json' ] ?. schema
320- if ( ! schema ) {
321- errors . push ( `${ testCase . description } : No JSON schema defined for 200 response` )
322- continue
323- }
323+ const schema = response200 . content ?. [ 'application/json' ] ?. schema
324+ if ( ! schema ) {
325+ errors . push ( `${ testCase . description } : No JSON schema defined for 200 response` )
326+ continue
327+ }
324328
325- // Build the actual URL by replacing path parameters
326- let actualPath = testCase . openApiPath
327- const queryParams : Record < string , string > = { }
328-
329- if ( testCase . params ) {
330- for ( const [ key , value ] of Object . entries ( testCase . params ) ) {
331- const placeholder = `{${ key } }`
332- if ( actualPath . includes ( placeholder ) ) {
333- actualPath = actualPath . replace ( placeholder , String ( value ) )
334- } else {
335- queryParams [ key ] = String ( value )
329+ // Build the actual URL by replacing path parameters
330+ let actualPath = testCase . openApiPath
331+ const queryParams : Record < string , string > = { }
332+
333+ if ( testCase . params ) {
334+ for ( const [ key , value ] of Object . entries ( testCase . params ) ) {
335+ const placeholder = `{${ key } }`
336+ if ( actualPath . includes ( placeholder ) ) {
337+ actualPath = actualPath . replace ( placeholder , String ( value ) )
338+ } else {
339+ queryParams [ key ] = String ( value )
340+ }
336341 }
337342 }
338- }
339343
340- // Add query string if needed
341- const queryString =
342- Object . keys ( queryParams ) . length > 0
343- ? '?' + new URLSearchParams ( queryParams ) . toString ( )
344- : ''
344+ // Add query string if needed
345+ const queryString =
346+ Object . keys ( queryParams ) . length > 0
347+ ? '?' + new URLSearchParams ( queryParams ) . toString ( )
348+ : ''
345349
346- // Make the actual API call
347- const response = await $fetch < unknown > ( `${ actualPath } ${ queryString } ` )
350+ // Make the actual API call
351+ const response = await $fetch < unknown > ( `${ actualPath } ${ queryString } ` )
348352
349- if ( ! response ) {
350- errors . push ( `${ testCase . description } : No response received from API` )
351- continue
352- }
353+ if ( ! response ) {
354+ errors . push ( `${ testCase . description } : No response received from API` )
355+ continue
356+ }
353357
354- // Verify response has expected top-level fields from OpenAPI
355- if ( typeof response === 'object' && response !== null ) {
356- const responseObj = response as Record < string , unknown >
358+ // Verify response has expected top-level fields from OpenAPI
359+ if ( typeof response === 'object' && response !== null ) {
360+ const responseObj = response as Record < string , unknown >
357361
358- // Check required fields
359- if ( ! ( 'success' in responseObj ) ) {
360- errors . push ( `${ testCase . description } : Missing 'success' field in response` )
361- }
362+ // Check required fields
363+ if ( ! ( 'success' in responseObj ) ) {
364+ errors . push ( `${ testCase . description } : Missing 'success' field in response` )
365+ }
362366
363- if ( ! ( 'data' in responseObj ) ) {
364- errors . push ( `${ testCase . description } : Missing 'data' field in response` )
365- }
367+ if ( ! ( 'data' in responseObj ) ) {
368+ errors . push ( `${ testCase . description } : Missing 'data' field in response` )
369+ }
366370
367- if ( ! ( 'meta' in responseObj ) ) {
368- errors . push ( `${ testCase . description } : Missing 'meta' field in response` )
369- }
371+ if ( ! ( 'meta' in responseObj ) ) {
372+ errors . push ( `${ testCase . description } : Missing 'meta' field in response` )
373+ }
370374
371- // Validate meta structure
372- if ( 'meta' in responseObj && typeof responseObj . meta === 'object' ) {
373- const meta = responseObj . meta as Record < string , unknown >
374- const requiredMetaFields = [ 'requestId' , 'responseTime' , 'timestamp' , 'version' ]
375- for ( const field of requiredMetaFields ) {
376- if ( ! ( field in meta ) ) {
377- errors . push ( `${ testCase . description } : Missing 'meta.${ field } ' field in response` )
375+ // Validate meta structure
376+ if ( 'meta' in responseObj && typeof responseObj . meta === 'object' ) {
377+ const meta = responseObj . meta as Record < string , unknown >
378+ const requiredMetaFields = [ 'requestId' , 'responseTime' , 'timestamp' , 'version' ]
379+ for ( const field of requiredMetaFields ) {
380+ if ( ! ( field in meta ) ) {
381+ errors . push ( `${ testCase . description } : Missing 'meta.${ field } ' field in response` )
382+ }
378383 }
379384 }
380385 }
381- }
382386
383- // Validate the response structure using Zod
384- const apiResponseSchema = z . fromJSONSchema ( {
385- ...JSON . parse ( JSON . stringify ( schema ) . replaceAll ( '#/components/schemas/' , '#/$defs/' ) ) ,
386- $defs : JSON . parse (
387- JSON . stringify ( openApiSpec . components . schemas ) . replaceAll (
388- '#/components/schemas/' ,
389- '#/$defs/'
387+ // Validate the response structure using Zod
388+ const apiResponseSchema = z . fromJSONSchema ( {
389+ ...JSON . parse ( JSON . stringify ( schema ) . replaceAll ( '#/components/schemas/' , '#/$defs/' ) ) ,
390+ $defs : JSON . parse (
391+ JSON . stringify ( openApiSpec . components . schemas ) . replaceAll (
392+ '#/components/schemas/' ,
393+ '#/$defs/'
394+ )
390395 )
391- )
392- } )
396+ } )
393397
394- const validationResult = apiResponseSchema . safeParse ( response )
395- if ( ! validationResult . success ) {
398+ const validationResult = apiResponseSchema . safeParse ( response )
399+ if ( ! validationResult . success ) {
400+ errors . push (
401+ `${ testCase . description } : Response doesn't match API envelope structure: ${ validationResult . error . message } `
402+ )
403+ }
404+ } catch ( error ) {
396405 errors . push (
397- `${ testCase . description } : Response doesn't match API envelope structure: ${ validationResult . error . message } `
406+ `${ testCase . description } : Failed to fetch or validate - ${ error instanceof Error ? error . message : String ( error ) } `
398407 )
399408 }
400- } catch ( error ) {
401- errors . push (
402- `${ testCase . description } : Failed to fetch or validate - ${ error instanceof Error ? error . message : String ( error ) } `
403- )
404409 }
405- }
406410
407- if ( errors . length > 0 ) {
408- const errorMessage = [
409- 'API response validation failed:' ,
410- '' ,
411- ...errors . map ( ( e ) => ` - ${ e } ` ) ,
412- '' ,
413- `Total test cases: ${ testCases . length } ` ,
414- `Total errors: ${ errors . length } `
415- ] . join ( '\n' )
416-
417- // eslint-disable-next-line vitest/no-conditional-expect
418- expect . fail ( errorMessage )
419- }
420- } )
411+ if ( errors . length > 0 ) {
412+ const errorMessage = [
413+ 'API response validation failed:' ,
414+ '' ,
415+ ...errors . map ( ( e ) => ` - ${ e } ` ) ,
416+ '' ,
417+ `Total test cases: ${ testCases . length } ` ,
418+ `Total errors: ${ errors . length } `
419+ ] . join ( '\n' )
420+
421+ // eslint-disable-next-line vitest/no-conditional-expect
422+ expect . fail ( errorMessage )
423+ }
424+ } ,
425+ { timeout : 10000 }
426+ )
421427
422428 it ( 'should not have type objects without properties defined' , async ( ) => {
423429 const openApiSpec = await $fetch < OpenApiSpec > ( '/_docs/openapi.json' )
0 commit comments