honojs-middleware/packages/zod-openapi/test/index.test.ts

459 lines
9.6 KiB
TypeScript

/* eslint-disable node/no-extraneous-import */
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Env } from 'hono'
import type { Hono } from 'hono'
import { describe, it, expect, expectTypeOf } from 'vitest'
import type { Schema } from 'zod'
import { OpenAPIHono, createRoute, z } from '../src'
describe('Basic - params', () => {
const ParamsSchema = z.object({
id: z
.string()
.min(3)
.openapi({
param: {
name: 'id',
in: 'path',
},
example: '1212121',
}),
})
const UserSchema = z
.object({
id: z.number().openapi({
example: 123,
}),
name: z.string().openapi({
example: 'John Doe',
}),
age: z.number().openapi({
example: 42,
}),
})
.openapi('User')
const ErrorSchema = z
.object({
ok: z.boolean().openapi({
example: false,
}),
})
.openapi('Error')
const route = createRoute({
method: 'get',
path: '/users/:id',
request: {
params: ParamsSchema,
},
responses: {
200: {
content: {
'application/json': {
schema: UserSchema,
},
},
description: 'Get the user',
},
400: {
content: {
'application/json': {
schema: ErrorSchema,
},
},
description: 'Error!',
},
},
})
const app = new OpenAPIHono()
app.openapi(
route,
(c) => {
const { id } = c.req.valid('param')
return c.jsonT({
id: Number(id),
age: 20,
name: 'Ultra-man',
})
},
(result, c) => {
if (!result.success) {
const res = c.jsonT(
{
ok: false,
},
400
)
return res
}
}
)
app.doc('/doc', {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'My API',
},
})
it('Should return 200 response with correct contents', async () => {
const res = await app.request('/users/123')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
id: 123,
age: 20,
name: 'Ultra-man',
})
})
it('Should return 400 response with correct contents', async () => {
const res = await app.request('/users/1')
expect(res.status).toBe(400)
expect(await res.json()).toEqual({ ok: false })
})
it('Should return OpenAPI documents', async () => {
const res = await app.request('/doc')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
openapi: '3.0.0',
info: { version: '1.0.0', title: 'My API' },
components: {
schemas: {
User: {
type: 'object',
properties: {
id: { type: 'number', example: 123 },
name: { type: 'string', example: 'John Doe' },
age: { type: 'number', example: 42 },
},
required: ['id', 'name', 'age'],
},
Error: {
type: 'object',
properties: { ok: { type: 'boolean', example: false } },
required: ['ok'],
},
},
parameters: {},
},
paths: {
'/users/:id': {
get: {
parameters: [
{
schema: { type: 'string', minLength: 3, example: '1212121' },
required: true,
name: 'id',
in: 'path',
},
],
responses: {
'200': {
description: 'Get the user',
content: { 'application/json': { schema: { $ref: '#/components/schemas/User' } } },
},
'400': {
description: 'Error!',
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
},
},
},
},
},
})
})
})
describe('Query', () => {
const QuerySchema = z.object({
page: z.string().openapi({
example: '123',
}),
})
const BooksSchema = z
.object({
titles: z.array(z.string().openapi({})),
page: z.number().openapi({}),
})
.openapi('Post')
const route = createRoute({
method: 'get',
path: '/books',
request: {
query: QuerySchema,
},
responses: {
200: {
content: {
'application/json': {
schema: BooksSchema,
},
},
description: 'Get books',
},
},
})
const app = new OpenAPIHono()
app.openapi(route, (c) => {
const { page } = c.req.valid('query')
return c.jsonT({
titles: ['Good title'],
page: Number(page),
})
})
it('Should return 200 response with correct contents', async () => {
const res = await app.request('/books?page=123')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
titles: ['Good title'],
page: 123,
})
})
it('Should return 400 response with correct contents', async () => {
const res = await app.request('/books')
expect(res.status).toBe(400)
})
})
describe('JSON', () => {
const RequestSchema = z.object({
id: z.number().openapi({}),
title: z.string().openapi({}),
})
const PostSchema = z
.object({
id: z.number().openapi({}),
title: z.string().openapi({}),
})
.openapi('Post')
const route = createRoute({
method: 'post',
path: '/posts',
request: {
body: {
content: {
'application/json': {
schema: RequestSchema,
},
},
},
},
responses: {
200: {
content: {
'application/json': {
schema: PostSchema,
},
},
description: 'Post a post',
},
},
})
const app = new OpenAPIHono()
app.openapi(route, (c) => {
const { id, title } = c.req.valid('json')
return c.jsonT({
id,
title,
})
})
it('Should return 200 response with correct contents', async () => {
const req = new Request('http://localhost/posts', {
method: 'POST',
body: JSON.stringify({
id: 123,
title: 'Good title',
}),
headers: {
'Content-Type': 'application/json',
},
})
const res = await app.request(req)
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
id: 123,
title: 'Good title',
})
})
it('Should return 400 response with correct contents', async () => {
const req = new Request('http://localhost/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
})
const res = await app.request(req)
expect(res.status).toBe(400)
})
})
describe('Form', () => {
const RequestSchema = z.object({
id: z.string().openapi({}),
title: z.string().openapi({}),
})
const PostSchema = z
.object({
id: z.number().openapi({}),
title: z.string().openapi({}),
})
.openapi('Post')
const route = createRoute({
method: 'post',
path: '/posts',
request: {
body: {
content: {
'application/x-www-form-urlencoded': {
schema: RequestSchema,
},
},
},
},
responses: {
200: {
content: {
'application/json': {
schema: PostSchema,
},
},
description: 'Post a post',
},
},
})
const app = new OpenAPIHono()
app.openapi(route, (c) => {
const { id, title } = c.req.valid('form')
return c.jsonT({
id: Number(id),
title,
})
})
it('Should return 200 response with correct contents', async () => {
const searchParams = new URLSearchParams()
searchParams.append('id', '123')
searchParams.append('title', 'Good title')
const req = new Request('http://localhost/posts', {
method: 'POST',
body: searchParams,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
const res = await app.request(req)
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
id: 123,
title: 'Good title',
})
})
it('Should return 400 response with correct contents', async () => {
const req = new Request('http://localhost/posts', {
method: 'POST',
})
const res = await app.request(req)
expect(res.status).toBe(400)
})
})
describe('Types', () => {
const RequestSchema = z.object({
id: z.number().openapi({}),
title: z.string().openapi({}),
})
const PostSchema = z
.object({
id: z.number().openapi({}),
message: z.string().openapi({}),
})
.openapi('Post')
const route = createRoute({
method: 'post',
path: '/posts',
request: {
body: {
content: {
'application/json': {
schema: RequestSchema,
},
},
},
},
responses: {
200: {
content: {
'application/json': {
schema: PostSchema,
},
},
description: 'Post a post',
},
},
})
const app = new OpenAPIHono()
const appRoutes = app.openapi(route, (c) => {
const data = c.req.valid('json')
return c.jsonT({
id: data.id,
message: 'Success',
})
})
it('Should return correct types', () => {
type H = Hono<
Env,
Schema<{
'/posts': {
$post: {
input: {
json: {
title: string
id: number
}
}
output: {
id: number
message: string
}
}
}
}>,
'/'
>
expectTypeOf(appRoutes).toMatchTypeOf<H>
})
})