Skip to content

Commit 2423e9b

Browse files
committed
test: timeout
1 parent af9b8db commit 2423e9b

File tree

1 file changed

+142
-136
lines changed

1 file changed

+142
-136
lines changed

test/e2e/openapi.test.ts

Lines changed: 142 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)