mirror of
https://github.com/hanzoai/trpc-openapi.git
synced 2026-10-30 18:18:55 +00:00
194 lines
5.8 KiB
TypeScript
194 lines
5.8 KiB
TypeScript
import { initTRPC } from '@trpc/server';
|
|
import { NextApiRequest, NextApiResponse } from 'next';
|
|
import { IncomingHttpHeaders, IncomingMessage } from 'http';
|
|
import { NextApiRequestCookies, NextApiRequestQuery } from 'next/dist/server/api-utils';
|
|
import { Socket } from 'net';
|
|
import { z } from 'zod';
|
|
|
|
import {
|
|
CreateOpenApiNextHandlerOptions,
|
|
OpenApiMeta,
|
|
OpenApiResponse,
|
|
OpenApiRouter,
|
|
createOpenApiNextHandler,
|
|
} from '../../src';
|
|
|
|
type NextApiRequestOptions = Partial<NextApiRequestMock>;
|
|
class NextApiRequestMock extends IncomingMessage implements NextApiRequest {
|
|
public query: NextApiRequestQuery = {};
|
|
public cookies: NextApiRequestCookies = {};
|
|
public headers: IncomingHttpHeaders = {};
|
|
public env = {};
|
|
public body: unknown;
|
|
|
|
constructor(options: NextApiRequestOptions) {
|
|
super(new Socket());
|
|
|
|
this.method = options.method;
|
|
this.body = options.body;
|
|
this.query = options.query ?? {};
|
|
this.headers = options.headers ?? {};
|
|
this.env = options.env ?? {};
|
|
}
|
|
}
|
|
|
|
const createContextMock = jest.fn();
|
|
const responseMetaMock = jest.fn();
|
|
const onErrorMock = jest.fn();
|
|
|
|
const clearMocks = () => {
|
|
createContextMock.mockClear();
|
|
responseMetaMock.mockClear();
|
|
onErrorMock.mockClear();
|
|
};
|
|
|
|
const createOpenApiNextHandlerCaller = <TRouter extends OpenApiRouter>(
|
|
handlerOpts: CreateOpenApiNextHandlerOptions<TRouter>,
|
|
) => {
|
|
const openApiNextHandler = createOpenApiNextHandler({
|
|
router: handlerOpts.router,
|
|
createContext: handlerOpts.createContext ?? createContextMock,
|
|
responseMeta: handlerOpts.responseMeta ?? responseMetaMock,
|
|
onError: handlerOpts.onError ?? onErrorMock,
|
|
} as any);
|
|
|
|
return (req: {
|
|
method: string;
|
|
query: Record<string, any>;
|
|
body?: any;
|
|
headers?: Record<string, any>;
|
|
}) => {
|
|
return new Promise<{
|
|
statusCode: number;
|
|
headers: Record<string, any>;
|
|
body: OpenApiResponse;
|
|
}>(async (resolve, reject) => {
|
|
const headers = new Headers();
|
|
let body: any;
|
|
const nextResponse = {
|
|
statusCode: undefined,
|
|
setHeader: (key: string, value: any) => headers.set(key, value),
|
|
getHeaders: () => Object.fromEntries(headers.entries()),
|
|
end: (data: string) => {
|
|
body = JSON.parse(data);
|
|
},
|
|
} as unknown as NextApiResponse;
|
|
|
|
const nextRequest = new NextApiRequestMock({
|
|
method: req.method,
|
|
query: req.query,
|
|
body: req.body,
|
|
headers: req.headers,
|
|
});
|
|
|
|
try {
|
|
await openApiNextHandler(nextRequest, nextResponse);
|
|
resolve({
|
|
statusCode: nextResponse.statusCode,
|
|
headers: nextResponse.getHeaders(),
|
|
body,
|
|
});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
};
|
|
};
|
|
|
|
const t = initTRPC.meta<OpenApiMeta>().context().create();
|
|
|
|
describe('next adapter', () => {
|
|
afterEach(() => {
|
|
clearMocks();
|
|
});
|
|
|
|
test('with valid routes', async () => {
|
|
const appRouter = t.router({
|
|
sayHelloQuery: t.procedure
|
|
.meta({ openapi: { method: 'GET', path: '/say-hello' } })
|
|
.input(z.object({ name: z.string() }))
|
|
.output(z.object({ greeting: z.string() }))
|
|
.query(({ input }) => ({ greeting: `Hello ${input.name}!` })),
|
|
sayHelloMutation: t.procedure
|
|
.meta({ openapi: { method: 'POST', path: '/say-hello' } })
|
|
.input(z.object({ name: z.string() }))
|
|
.output(z.object({ greeting: z.string() }))
|
|
.mutation(({ input }) => ({ greeting: `Hello ${input.name}!` })),
|
|
sayHelloSlash: t.procedure
|
|
.meta({ openapi: { method: 'GET', path: '/say/hello' } })
|
|
.input(z.object({ name: z.string() }))
|
|
.output(z.object({ greeting: z.string() }))
|
|
.query(({ input }) => ({ greeting: `Hello ${input.name}!` })),
|
|
});
|
|
|
|
const openApiNextHandlerCaller = createOpenApiNextHandlerCaller({
|
|
router: appRouter,
|
|
});
|
|
|
|
{
|
|
const res = await openApiNextHandlerCaller({
|
|
method: 'GET',
|
|
query: { trpc: 'say-hello', name: 'Lily' },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body).toEqual({ greeting: 'Hello Lily!' });
|
|
expect(createContextMock).toHaveBeenCalledTimes(1);
|
|
expect(responseMetaMock).toHaveBeenCalledTimes(1);
|
|
expect(onErrorMock).toHaveBeenCalledTimes(0);
|
|
|
|
clearMocks();
|
|
}
|
|
{
|
|
const res = await openApiNextHandlerCaller({
|
|
method: 'POST',
|
|
query: { trpc: 'say-hello' },
|
|
body: { name: 'Lily' },
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body).toEqual({ greeting: 'Hello Lily!' });
|
|
expect(createContextMock).toHaveBeenCalledTimes(1);
|
|
expect(responseMetaMock).toHaveBeenCalledTimes(1);
|
|
expect(onErrorMock).toHaveBeenCalledTimes(0);
|
|
|
|
clearMocks();
|
|
}
|
|
{
|
|
const res = await openApiNextHandlerCaller({
|
|
method: 'GET',
|
|
query: { trpc: ['say', 'hello'], name: 'Lily' },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body).toEqual({ greeting: 'Hello Lily!' });
|
|
expect(createContextMock).toHaveBeenCalledTimes(1);
|
|
expect(responseMetaMock).toHaveBeenCalledTimes(1);
|
|
expect(onErrorMock).toHaveBeenCalledTimes(0);
|
|
}
|
|
});
|
|
|
|
test('with invalid path', async () => {
|
|
const appRouter = t.router({});
|
|
|
|
const openApiNextHandlerCaller = createOpenApiNextHandlerCaller({
|
|
router: appRouter,
|
|
});
|
|
|
|
const res = await openApiNextHandlerCaller({
|
|
method: 'GET',
|
|
query: {},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(500);
|
|
expect(res.body).toEqual({
|
|
message: 'Query "trpc" not found - is the `trpc-to-openapi` file named `[...trpc].ts`?',
|
|
code: 'INTERNAL_SERVER_ERROR',
|
|
});
|
|
expect(createContextMock).toHaveBeenCalledTimes(0);
|
|
expect(responseMetaMock).toHaveBeenCalledTimes(0);
|
|
expect(onErrorMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|