Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 49 additions & 22 deletions src/open-api/utils/open-api-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
import { getAcceptedContentTypes } from './open-api-utils.js'
import { getAcceptedContentTypes, getResponseStatus } from './open-api-utils.js'

it('returns a single content type as-is', () => {
expect(
getAcceptedContentTypes(new Headers([['accept', 'text/html']])),
).toEqual(['text/html'])
})
describe(getAcceptedContentTypes, () => {
it('returns a single content type as-is', () => {
expect(
getAcceptedContentTypes(new Headers([['accept', 'text/html']])),
).toEqual(['text/html'])
})

it('ignores whitespace separating multiple content types', () => {
expect(
getAcceptedContentTypes(
new Headers([['accept', 'text/html, application/xhtml+xml, */*']]),
),
).toEqual(['text/html', 'application/xhtml+xml', '*/*'])
})
it('ignores whitespace separating multiple content types', () => {
expect(
getAcceptedContentTypes(
new Headers([['accept', 'text/html, application/xhtml+xml, */*']]),
),
).toEqual(['text/html', 'application/xhtml+xml', '*/*'])
})

it('removes an empty content type', () => {
expect(getAcceptedContentTypes(new Headers([['accept', ', ,']]))).toEqual([])
it('removes an empty content type', () => {
expect(getAcceptedContentTypes(new Headers([['accept', ', ,']]))).toEqual(
[],
)

expect(
getAcceptedContentTypes(new Headers([['accept', 'text/html, , */*']])),
).toEqual(['text/html', '*/*'])
})
expect(
getAcceptedContentTypes(new Headers([['accept', 'text/html, , */*']])),
).toEqual(['text/html', '*/*'])
})

describe.skip('complex "accept" headers', () => {
it('supports weight reordering', () => {
it.skip('supports weight reordering', () => {
expect(
getAcceptedContentTypes(
new Headers([
Expand All @@ -36,7 +38,7 @@ describe.skip('complex "accept" headers', () => {
).toEqual(['text/html', 'text/x-c', 'text/x-dvi', 'text/plain'])
})

it('supports specificity reordering', () => {
it.skip('supports specificity reordering', () => {
expect(
getAcceptedContentTypes(
new Headers([
Expand All @@ -46,3 +48,28 @@ describe.skip('complex "accept" headers', () => {
).toEqual(['text/plain;format=flowed', 'text/plain', 'text/*', '*/*'])
})
})

describe(getResponseStatus, () => {
it('returns 200 if 200 response is defined', () => {
expect(getResponseStatus({ 200: { description: '' } })).toBe('200')
})

it('returns the first 2xx code if 200 response is not defined', () => {
expect(
getResponseStatus({
201: { description: '' },
}),
).toBe('201')

expect(
getResponseStatus({
201: { description: '' },
202: { description: '' },
}),
).toBe('201')
})

it('returns undefined as the fallback', () => {
expect(getResponseStatus({})).toBeUndefined()
})
})
102 changes: 51 additions & 51 deletions src/open-api/utils/open-api-utils.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,77 @@
import type { ResponseResolver } from 'msw'
import { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
import { OpenAPI, OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
import { seedSchema } from '@yellow-ticket/seed-json-schema'
import { toString } from './to-string.js'
import { STATUS_CODES } from './status-codes.js'

/**
* @note Manually type the `responses` object to be dereferenced.
*/
type ResponsesObject =
| {
[index: string]: OpenAPIV2.ResponseObject | undefined
default?: OpenAPIV2.ResponseObject
}
| {
[code: string]: OpenAPIV3.ResponseObject
}
| {
[code: string]: OpenAPIV3_1.ResponseObject
}

type ResponseObject = OpenAPIV3.ResponseObject | OpenAPIV3_1.ResponseObject

/**
* Create a resolver function based on the responses defined for a given operation.
*/
export function createResponseResolver(
operation: OpenAPI.Operation,
): ResponseResolver {
return ({ request }) => {
const { responses } = operation
const responses = operation.responses as ResponsesObject

// Treat operations that describe no responses as not implemented.
if (responses == null) {
return new Response('Not Implemented', {
status: 501,
statusText: 'Not Implemented',
})
}
const explicitResponseStatus = new URL(request.url).searchParams.get(
'response',
)

const responseStatus =
explicitResponseStatus || getResponseStatus(responses)

const responseObject = responseStatus
? responses[responseStatus]
: responses.default

if (Object.keys(responses).length === 0) {
if (responseObject == null) {
return new Response('Not Implemented', {
status: 501,
statusText: 'Not Implemented',
})
}

let responseObject: OpenAPIV3.ResponseObject | OpenAPIV3_1.ResponseObject

const url = new URL(request.url)
const explicitResponseStatus = url.searchParams.get('response')

if (explicitResponseStatus) {
const responseByStatus = responses[
explicitResponseStatus
] as OpenAPIV3.ResponseObject

if (!responseByStatus) {
return new Response('Not Implemented', {
status: 501,
statusText: 'Not Implemented',
})
}

responseObject = responseByStatus
} else {
const fallbackResponse =
(responses['200'] as
| OpenAPIV3.ResponseObject
| OpenAPIV3_1.ResponseObject) ||
(responses.default as
| OpenAPIV3.ResponseObject
| OpenAPIV3_1.ResponseObject)

if (!fallbackResponse) {
return new Response('Not Implemented', {
status: 501,
statusText: 'Not Implemented',
})
}

responseObject = fallbackResponse
}

const status = Number(explicitResponseStatus || '200')
const normalizedStatus = Number(responseStatus || '200')

return new Response(toBody(request, responseObject), {
status,
statusText: STATUS_CODES[status],
status: normalizedStatus,
statusText: STATUS_CODES[normalizedStatus],
headers: toHeaders(request, responseObject),
})
}
}

export function getResponseStatus(
responses: ResponsesObject,
): string | undefined {
if (responses['200']) {
return '200'
}

for (const status in responses) {
if (status.startsWith('2')) {
return status
}
}
}

/**
* Get the Fetch API `Headers` from the OpenAPI response object.
*/
Expand Down Expand Up @@ -156,7 +156,7 @@ export function toHeaders(
*/
export function toBody(
request: Request,
responseObject: OpenAPIV3.ResponseObject | OpenAPIV3_1.ResponseObject,
responseObject: ResponseObject,
): RequestInit['body'] {
const { content } = responseObject

Expand Down
110 changes: 110 additions & 0 deletions test/oas/oas-json-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,113 @@ it('respects the "Accept" request header', async () => {
}),
)
})

it('responds with 201 to a POST request with a 201 response defined', async () => {
const handlers = await fromOpenApi(
createOpenApiSpec({
paths: {
'/resource': {
post: {
responses: {
201: { description: 'Created' },
},
},
},
},
}),
)

const response = await withHandlers(handlers, () => {
return fetch('http://localhost/resource', {
method: 'POST',
body: JSON.stringify({ username: 'example' }),
headers: {
'Content-Type': 'application/json',
},
})
})

expect(response.status).toEqual(201)
})

it('responds with 204 to a DELETE request with a 204 response defined', async () => {
const handlers = await fromOpenApi(
createOpenApiSpec({
paths: {
'/resource': {
delete: {
responses: {
204: { description: 'No Content' },
},
},
},
},
}),
)

const response = await withHandlers(handlers, () => {
return fetch('http://localhost/resource', {
method: 'DELETE',
})
})

expect(response.status).toEqual(204)
})

it('responds with 201 to a POST request even if defined out of order', async () => {
const handlers = await fromOpenApi(
createOpenApiSpec({
paths: {
'/resource': {
post: {
responses: {
404: { description: 'Not Found' },
201: { description: 'Created' },
},
},
},
},
}),
)

const response = await withHandlers(handlers, () => {
return fetch('http://localhost/resource', {
method: 'POST',
body: JSON.stringify({ username: 'example' }),
headers: {
'Content-Type': 'application/json',
},
})
})

expect(response.status).toEqual(201)
})

it('responds with 201 to a POST request even if default is defined', async () => {
const handlers = await fromOpenApi(
createOpenApiSpec({
paths: {
'/resource': {
post: {
responses: {
201: { description: 'Created' },
default: { description: 'An unexpected error occurred' },
},
},
},
},
}),
)

const response = await withHandlers(handlers, () => {
return fetch('http://localhost/resource', {
method: 'POST',
body: JSON.stringify({ username: 'example' }),
headers: {
'Content-Type': 'application/json',
},
})
})

expect(response.status).toEqual(201)
})