import { Hono } from 'hono' import type { Equal, Expect } from 'hono/utils/types' import { vi } from 'vitest' import { z } from 'zod' import { zValidator } from '../src' // eslint-disable-next-line @typescript-eslint/no-unused-vars type ExtractSchema = T extends Hono ? S : never describe('Basic', () => { const app = new Hono() const jsonSchema = z.object({ name: z.string(), age: z.number(), }) const querySchema = z .object({ name: z.string().optional(), }) .optional() const route = app.post( '/author', zValidator('json', jsonSchema), zValidator('query', querySchema), (c) => { const data = c.req.valid('json') const query = c.req.valid('query') return c.json({ success: true, message: `${data.name} is ${data.age}`, queryName: query?.name, }) } ) type Actual = ExtractSchema type Expected = { '/author': { $post: { input: { json: { name: string age: number } } & { query?: | { name?: string | undefined } | undefined } output: { success: boolean message: string queryName: string | undefined } } } } // eslint-disable-next-line @typescript-eslint/no-unused-vars type verify = Expect> it('Should return 200 response', async () => { const req = new Request('http://localhost/author?name=Metallo', { body: JSON.stringify({ name: 'Superman', age: 20, }), method: 'POST', headers: { 'Content-Type': 'application/json', }, }) const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(200) expect(await res.json()).toEqual({ success: true, message: 'Superman is 20', queryName: 'Metallo', }) }) it('Should return 400 response', async () => { const req = new Request('http://localhost/author', { body: JSON.stringify({ name: 'Superman', age: '20', }), method: 'POST', headers: { 'content-type': 'application/json', }, }) const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(400) const data = (await res.json()) as { success: boolean } expect(data['success']).toBe(false) }) }) describe('coerce', () => { const app = new Hono() const querySchema = z.object({ page: z.coerce.number(), }) const route = app.get('/page', zValidator('query', querySchema), (c) => { const { page } = c.req.valid('query') return c.json({ page }) }) type Actual = ExtractSchema type Expected = { '/page': { $get: { input: { query: { page: string | string[] } } output: { page: number } } } } // eslint-disable-next-line @typescript-eslint/no-unused-vars type verify = Expect> it('Should return 200 response', async () => { const res = await app.request('/page?page=123') expect(res).not.toBeNull() expect(res.status).toBe(200) expect(await res.json()).toEqual({ page: 123, }) }) }) describe('With Hook', () => { const app = new Hono() const schema = z.object({ id: z.number(), title: z.string(), }) app.post( '/post', zValidator('json', schema, (result, c) => { if (!result.success) { return c.text(`${result.data.id} is invalid!`, 400) } }), (c) => { const data = c.req.valid('json') return c.text(`${data.id} is valid!`) } ) it('Should return 200 response', async () => { const req = new Request('http://localhost/post', { body: JSON.stringify({ id: 123, title: 'Hello', }), method: 'POST', headers: { 'Content-Type': 'application/json', }, }) const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(200) expect(await res.text()).toBe('123 is valid!') }) it('Should return 400 response', async () => { const req = new Request('http://localhost/post', { body: JSON.stringify({ id: '123', title: 'Hello', }), method: 'POST', headers: { 'Content-Type': 'application/json', }, }) const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(400) expect(await res.text()).toBe('123 is invalid!') }) }) describe('With Async Hook', () => { const app = new Hono() const schema = z.object({ id: z.number(), title: z.string(), }) app.post( '/post', zValidator('json', schema, async (result, c) => { if (!result.success) { return c.text(`${result.data.id} is invalid!`, 400) } }), (c) => { const data = c.req.valid('json') return c.text(`${data.id} is valid!`) } ) it('Should return 200 response', async () => { const req = new Request('http://localhost/post', { body: JSON.stringify({ id: 123, title: 'Hello', }), method: 'POST', headers: { 'Content-Type': 'application/json', }, }) const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(200) expect(await res.text()).toBe('123 is valid!') }) it('Should return 400 response', async () => { const req = new Request('http://localhost/post', { body: JSON.stringify({ id: '123', title: 'Hello', }), method: 'POST', headers: { 'Content-Type': 'application/json', }, }) const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(400) expect(await res.text()).toBe('123 is invalid!') }) }) describe('With target', () => { it('should call hook for correctly validated target', async () => { const app = new Hono() const schema = z.object({ id: z.string(), }) const jsonHook = vi.fn() const paramHook = vi.fn() const queryHook = vi.fn() app.post( '/:id/post', zValidator('json', schema, jsonHook), zValidator('param', schema, paramHook), zValidator('query', schema, queryHook), (c) => { return c.text('ok') } ) const req = new Request('http://localhost/1/post?id=2', { body: JSON.stringify({ id: '3', }), method: 'POST', headers: { 'Content-Type': 'application/json', }, }) const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(200) expect(await res.text()).toBe('ok') expect(paramHook).toHaveBeenCalledWith( { data: { id: '1' }, success: true, target: 'param' }, expect.anything() ) expect(queryHook).toHaveBeenCalledWith( { data: { id: '2' }, success: true, target: 'query' }, expect.anything() ) expect(jsonHook).toHaveBeenCalledWith( { data: { id: '3' }, success: true, target: 'json' }, expect.anything() ) }) }) describe('Only Types', () => { it('Should return correct enum types for query', () => { const app = new Hono() const querySchema = z.object({ order: z.enum(['asc', 'desc']), }) const route = app.get('/', zValidator('query', querySchema), (c) => { const data = c.req.valid('query') return c.json(data) }) type Actual = ExtractSchema type Expected = { '/': { $get: { input: { query: { order: 'asc' | 'desc' } } output: { order: 'asc' | 'desc' } } } } type verify = Expect> }) }) describe('Case-Insensitive Headers', () => { it('Should ignore the case for headers in the Zod schema and return 200', () => { const app = new Hono() const headerSchema = z.object({ 'Content-Type': z.string(), ApiKey: z.string(), onlylowercase: z.string(), ONLYUPPERCASE: z.string(), }) const route = app.get('/', zValidator('header', headerSchema), (c) => { const headers = c.req.valid('header') return c.json(headers) }) type Actual = ExtractSchema type Expected = { '/': { $get: { input: { header: z.infer } output: z.infer } } } type verify = Expect> }) })