middleware icon indicating copy to clipboard operation
middleware copied to clipboard

Testing with testClient from hono/test fails with 400 Bad Request on routes defined with @hono/zod-openapi

Open zestsystem opened this issue 1 year ago • 3 comments

Hopefully I'm doing something wrong, but I don't think it is possible to use hono/test testClient or .request method on routes created with zod-openapi. Please find below simplified repro.

// routes/event.ts
import { OpenAPIHono, createRoute } from '@hono/zod-openapi';
import type { AppContext } from '../lib/context';

export const eventRoutes = new OpenAPIHono<AppContext>()
    .openapi(
        createRoute({
            method: 'get',
            path: '/',
            request: {
                params: z.object({
                    eventId: z.string(),
                }),
            },
            responses: {
                ...json200Response(
                    z.object({
                        eventId: z.string()
                    }),
                    'Event ID',
                ),
            },
        }),
        async (c) => {
            const eventId = c.req.valid('param').eventId;
           
            return c.json({ eventId }, 200);
        },
    )
    .get('/testing', (c) => c.json({ ok: true }, 200));

// routes/event.test.ts
import { testClient } from 'hono/testing';
import { describe, it } from 'vitest';
import { eventRoutes } from './event.ts'

const testEnv = {
  HELLO: "WORLD"
};

describe('GET /event/create-event', () => {
    it('Testing routes', async ({ expect }) => {
        const successRes = await testClient(eventRoutes, testEnv)['testing'].$get();

         console.log('success res: ', successRes)
         // this passes
        expect(successRes.status).toBe(200)

        const failRes = await testClient(eventRoutes, testEnv)['index'].$get({ param: { eventId: '123' }});
        console.log('fail res: ', failRes)

       // fails with 400 Bad Request when accessing routes defined by openapi and createRoute
        expect(failRes.status).toBe(200);
    });
})

Logs: Screenshot 2024-06-10 at 3 15 02 PM

zestsystem avatar Jun 10 '24 06:06 zestsystem

Did you ever end up finding something here?

JeongJuhyeon avatar Feb 24 '25 07:02 JeongJuhyeon

I am dealing with this exact issue right now. Is anyone else having issue passing a param to a get route using @hono/zod-openapi? I am using vitest for the unit test framework.

I am able to execute the get users by id in the swagger test harness so I know the router is setup and working correctly so it has something to do with hono/testing im assuming.

import { testClient } from "hono/testing"
import { describe, expect, expectTypeOf, it } from "vitest"

import createApp from "@/lib/createApp"
import router from "@/routes/users/users.index"

const client = testClient(createApp().route("/", router))

describe("user routes", () => {

    // This works
    it("GET /users list all users", async () => {
        const response = await client.users.$get();
        expect(response.status).toBe(200);
        if (response.status === 200) {
            const json = await response.json();
            expectTypeOf(json).toBeArray();
            expect(json.length).toBe(3);
        }
    });

    // This does not work
    it("GET /users/:id get a single user", async () => {
        const id = "1";
        const response = await client.users[":id"].$get({
            param: {
                id,
            },
        });
        expect(response.status).toBe(200);
        if (response.status === 200) {
            const json = await response.json();
            expect(json.id).toBe(id);
        }
    })
})

Here is the error details AssertionError: expected 400 to be 200 // Object.is equality expect(response.status).toBe(200);

highiq avatar Feb 25 '25 14:02 highiq

The following works well. I think it's not a bug.

import { OpenAPIHono, createRoute } from '@hono/zod-openapi'
import { z } from 'zod'

export const eventRoutes = new OpenAPIHono().openapi(
  createRoute({
    method: 'get',
    path: '/users/:id',
    request: {
      params: z.object({
        id: z.string(),
      }),
    },
    responses: {
      200: {
        content: {
          'application/json': {
            schema: z.object({
              foo: z.string(),
            }),
          },
        },
        description: 'Foo',
      },
    },
  }),
  (c) => {
    return c.json({ foo: 'foo' }, 200)
  }
)

import { testClient } from 'hono/testing'

const res = await testClient(eventRoutes)['users'][':id'].$get({
  param: {
    id: '123',
  },
})

console.log(res.status) // 200 - correct

yusukebe avatar Feb 28 '25 07:02 yusukebe

I am seeing similar error. $get is successful using the test client but $post fails with 400. To give a bit more context: I was trying to solve this: https://github.com/honojs/hono/issues/1677 (one of the handler calling another handler) Inside the top handler I initialized a client using testClient

  • First $get call ✅
  • Then I do a do a $delete call which is also ✅
  • Then I do a $post call which invokes the app but then gets rejected (400).

I have spent so many hours trying to debug which handler is rejecting this request. I tried reproducing in a smaller app but unfortunately I could not.

UPDATE: After debugging turns out the headers were getting passed incorrectly. So while making the request using testClient I was passing the headers that I get from c.req like

{ headers: { ...c.req.raw.headers } }

And that was not correct.

sushovan-beemit avatar May 13 '25 03:05 sushovan-beemit

The following using $post is working correctly. Expected. I'll close this issue. If you still have a problem, please create a new issue.

import { OpenAPIHono, createRoute } from '@hono/zod-openapi'
import { testClient } from 'hono/testing'
import { z } from 'zod'

export const eventRoutes = new OpenAPIHono().openapi(
  createRoute({
    method: 'post',
    path: '/posts',
    request: {
      body: {
        content: {
          'application/json': {
            schema: z.object({
              title: z.string(),
            }),
          },
        },
      },
    },
    responses: {
      200: {
        content: {
          'application/json': {
            schema: z.object({
              foo: z.string(),
            }),
          },
        },
        description: 'Foo',
      },
    },
  }),
  (c) => {
    return c.json({ foo: 'foo' }, 200)
  }
)

const res = await testClient(eventRoutes)['posts'].$post({
  json: {
    title: 'abc',
  },
})

console.log(res.status) // 200 - correct

yusukebe avatar May 31 '25 10:05 yusukebe