Skip to content

Instantly share code, notes, and snippets.

@SchabaJo
Last active August 3, 2023 13:47
Show Gist options
  • Save SchabaJo/b012605f6d5d6f5b0fb99ea4e8de5b10 to your computer and use it in GitHub Desktop.
Save SchabaJo/b012605f6d5d6f5b0fb99ea4e8de5b10 to your computer and use it in GitHub Desktop.
jest-openapi + openapi-fetch
import innerCreateClient, {
FetchResponse as InnerFetchResponse,
HttpMethod,
PathsWith,
FetchOptions,
FilterKeys,
MediaType,
} from 'openapi-fetch'
type FetchResponse<T, M extends HttpMethod> = T extends { responses: unknown }
? keyof T['responses'] extends infer R
? R extends keyof T['responses']
? {
req: { path: string; method: Uppercase<M> }
status: R
body: NonNullable<FilterKeys<FilterKeys<T['responses'][R], 'content'>, MediaType>>
}
: never
: never
: never
type ClientMethods = Exclude<HttpMethod, 'delete'> | 'del'
type FetchClient<Paths extends NonNullable<unknown>> = {
[Method in ClientMethods]: Method extends HttpMethod
? <P extends PathsWith<Paths, Method>>(
url: P,
init: FetchOptions<FilterKeys<Paths[P], Method>>,
) => Promise<
FetchResponse<
Method extends infer T
? T extends Method
? T extends keyof Paths[P]
? Paths[P][T]
: unknown
: never
: never,
Method
>
>
: <P extends PathsWith<Paths, 'delete'>>(
url: P,
init: FetchOptions<FilterKeys<Paths[P], 'delete'>>,
) => Promise<
FetchResponse<
'delete' extends infer T
? T extends 'delete'
? T extends keyof Paths[P]
? Paths[P][T]
: unknown
: never
: never,
'delete'
>
>
}
function parseResponse<Paths extends NonNullable<unknown>, M extends HttpMethod>(
rawResponse: InnerFetchResponse<Paths>,
method: Uppercase<M>,
): FetchResponse<Paths, M> {
return {
req: {
path: rawResponse.response.url,
method,
},
status: rawResponse.response.status,
body: rawResponse.data || rawResponse.error,
} as FetchResponse<Paths, M>
}
export function createClient<Paths extends NonNullable<unknown>>(
clientOptions: Parameters<typeof innerCreateClient>[0],
): FetchClient<Paths> {
const { get, put, post, del, options, head, patch, trace } =
innerCreateClient<Paths>(clientOptions)
return {
get: async (url, init) => parseResponse(await get(url, init), 'GET'),
put: async (url, init) => parseResponse(await put(url, init), 'PUT'),
post: async (url, init) => parseResponse(await post(url, init), 'POST'),
del: async (url, init) => parseResponse(await del(url, init), 'DELETE'),
options: async (url, init) => parseResponse(await options(url, init), 'OPTIONS'),
head: async (url, init) => parseResponse(await head(url, init), 'HEAD'),
patch: async (url, init) => parseResponse(await patch(url, init), 'PATCH'),
trace: async (url, init) => parseResponse(await trace(url, init), 'TRACE'),
}
}
import { resolve } from 'node:path'
import SwaggerParser from '@apidevtools/swagger-parser'
import jestOpenAPI from 'jest-openapi'
import { createClient } from './client'
import type { paths } from './openapi'
const { get } = createClient<paths>({ baseUrl: 'https://api.example.com/v1' })
beforeAll(async () => {
// jest-openapi does not resolve external refs, use @apidevtools/swagger-parser instead
jestOpenAPI(await SwaggerParser.validate(resolve(__dirname, 'openapi.yaml')))
})
describe('Example API', () => {
it('should do something', async () => {
const response = await get('/endpoint')
expect(response).toSatisfyApiSpec()
})
})
import * as undici from 'undici'
declare global {
export const { fetch, FormData, Headers, Request, Response }: typeof undici
type FormData = undici.FormData
type Headers = undici.Headers
type HeadersInit = undici.HeadersInit
type BodyInit = undici.BodyInit
type Request = undici.Request
type RequestInit = undici.RequestInit
type RequestInfo = undici.RequestInfo
type RequestMode = undici.RequestMode
type RequestRedirect = undici.RequestRedirect
type RequestCredentials = undici.RequestCredentials
type RequestDestination = undici.RequestDestination
type ReferrerPolicy = undici.ReferrerPolicy
type Response = undici.Response
type ResponseInit = undici.ResponseInit
type ResponseType = undici.ResponseType
}
@SchabaJo
Copy link
Author

SchabaJo commented Aug 3, 2023

This wrapper for openapi-fetch turns its response into a superagent-like response compatible with jest-openapi.
It relies on

  • openapi-typescript to generate types from OpenAPI
  • openapi-fetch to setup a properly typed client
  • @apidevtools/swagger-parser to load OpenAPI files with support for external $refs
  • jest-openapi to ease testing using jest
  • undici to get types not supported by @types/node

It is also (hopefully properly) typed, so following code is possible:

if (response.status === 200) {
  // response body defined in OpenAPI paths.${endpoint}.${method}.responses.200
} else if (response.status === 419) {
  // response body defined in OpenAPI paths.${endpoint}.${method}.responses.419
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment