Skip to content

Commit a141701

Browse files
committed
add tests
1 parent 16f65d3 commit a141701

File tree

4 files changed

+230
-10
lines changed

4 files changed

+230
-10
lines changed

packages/hono/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
"clean": "rimraf dist *.tsbuildinfo"
2626
},
2727
"devDependencies": {
28-
"@hono/node-server": "^1.13.7",
2928
"@types/node": "^20.10.3",
3029
"@types/qs": "^6.9.10",
31-
"typescript": "^5.3.2"
30+
"typescript": "^5.3.2",
31+
"vitest": "^1.3.1"
3232
},
3333
"dependencies": {
3434
"hono": "^4.0.0",
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { Hono } from "hono";
2+
import { Stl, UnauthorizedError, z } from "stainless";
3+
import { describe, expect, test } from "vitest";
4+
import { stlApi } from "./honoPlugin";
5+
6+
const stl = new Stl({ plugins: {} });
7+
8+
describe("basic routing", () => {
9+
const api = stl.api({
10+
basePath: "/api",
11+
resources: {
12+
posts: stl.resource({
13+
summary: "posts",
14+
actions: {
15+
retrieve: stl.endpoint({
16+
endpoint: "GET /api/posts/:postId",
17+
path: z.object({ postId: z.coerce.number() }),
18+
query: z.object({ expand: z.string().array().optional() }),
19+
response: z.object({ postId: z.coerce.number() }),
20+
handler: (params) => params,
21+
}),
22+
update: stl.endpoint({
23+
endpoint: "POST /api/posts/:postId",
24+
path: z.object({ postId: z.coerce.number() }),
25+
body: z.object({ content: z.string() }),
26+
response: z.object({
27+
postId: z.coerce.number(),
28+
content: z.string(),
29+
}),
30+
handler: (params) => params,
31+
}),
32+
list: stl.endpoint({
33+
endpoint: "GET /api/posts",
34+
response: z.any().array(),
35+
handler: () => [],
36+
}),
37+
},
38+
}),
39+
comments: stl.resource({
40+
summary: "comments",
41+
actions: {
42+
retrieve: stl.endpoint({
43+
endpoint: "GET /api/comments/:commentId",
44+
path: z.object({ commentId: z.coerce.number() }),
45+
response: z.object({ commentId: z.coerce.number() }),
46+
handler: (params) => params,
47+
}),
48+
update: stl.endpoint({
49+
endpoint: "POST /api/comments/:commentId",
50+
path: z.object({ commentId: z.coerce.number() }),
51+
handler: () => {
52+
throw new UnauthorizedError();
53+
},
54+
}),
55+
},
56+
}),
57+
},
58+
});
59+
60+
const app = new Hono();
61+
app.use("*", stlApi(api));
62+
63+
test("list posts", async () => {
64+
const response = await app.request("/api/posts");
65+
expect(response).toHaveProperty("status", 200);
66+
expect(await response.json()).toMatchInlineSnapshot(`
67+
[]
68+
`);
69+
});
70+
71+
test("retrieve posts", async () => {
72+
const response = await app.request("/api/posts/5");
73+
expect(response).toHaveProperty("status", 200);
74+
expect(await response.json()).toMatchInlineSnapshot(`
75+
{
76+
"postId": 5,
77+
}
78+
`);
79+
});
80+
81+
test("retrieve posts, wrong method", async () => {
82+
const response = await app.request("/api/posts/5", {
83+
method: "PUT",
84+
});
85+
expect(response).toHaveProperty("status", 405);
86+
expect(await response.json()).toMatchInlineSnapshot(`
87+
{
88+
"message": "No handler for PUT; only GET, POST.",
89+
}
90+
`);
91+
});
92+
93+
test("update posts", async () => {
94+
const response = await app.request("/api/posts/5", {
95+
method: "POST",
96+
headers: {
97+
"content-type": "application/json",
98+
},
99+
body: JSON.stringify({ content: "hello" }),
100+
});
101+
expect(response).toHaveProperty("status", 200);
102+
expect(await response.json()).toMatchInlineSnapshot(`
103+
{
104+
"content": "hello",
105+
"postId": 5,
106+
}
107+
`);
108+
});
109+
110+
test("update posts, wrong content type", async () => {
111+
const response = await app.request("/api/posts/5", {
112+
method: "POST",
113+
headers: {
114+
"content-type": "text/plain",
115+
},
116+
body: "hello",
117+
});
118+
expect(response).toHaveProperty("status", 400);
119+
expect(await response.json()).toMatchInlineSnapshot(`
120+
{
121+
"error": "bad request",
122+
"issues": [
123+
{
124+
"code": "invalid_type",
125+
"expected": "object",
126+
"message": "Required",
127+
"path": [
128+
"<stainless request body>",
129+
],
130+
"received": "undefined",
131+
},
132+
],
133+
}
134+
`);
135+
});
136+
137+
test("retrieve comments", async () => {
138+
const response = await app.request("/api/comments/3");
139+
expect(response).toHaveProperty("status", 200);
140+
expect(await response.json()).toMatchInlineSnapshot(`
141+
{
142+
"commentId": 3,
143+
}
144+
`);
145+
});
146+
147+
test("not found", async () => {
148+
const response = await app.request("/api/not-found");
149+
expect(response).toHaveProperty("status", 404);
150+
expect(await response.json()).toMatchInlineSnapshot(`
151+
{
152+
"error": "not found",
153+
}
154+
`);
155+
});
156+
157+
test("throwing inside handler", async () => {
158+
const response = await app.request("/api/comments/3", {
159+
method: "POST",
160+
});
161+
expect(response).toHaveProperty("status", 401);
162+
expect(await response.json()).toMatchInlineSnapshot(`
163+
{
164+
"error": "unauthorized",
165+
}
166+
`);
167+
});
168+
});
169+
170+
describe("hono passthrough", () => {
171+
const baseApi = stl.api({
172+
basePath: "/api",
173+
resources: {},
174+
});
175+
176+
const app = new Hono();
177+
app.use("*", stlApi(baseApi, { handleErrors: false }));
178+
app.all("/public/*", (c) => {
179+
return c.text("public content", 200);
180+
});
181+
app.notFound((c) => {
182+
return c.text("custom not found", 404);
183+
});
184+
185+
test("public passthrough", async () => {
186+
const response = await app.request("/public/foo/bar");
187+
expect(response).toHaveProperty("status", 200);
188+
expect(await response.text()).toMatchInlineSnapshot(`"public content"`);
189+
});
190+
191+
test("not found passthrough", async () => {
192+
const response = await app.request("/api/comments");
193+
expect(response).toHaveProperty("status", 404);
194+
expect(await response.text()).toMatchInlineSnapshot(`"custom not found"`);
195+
});
196+
});

packages/hono/src/honoPlugin.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Hono, HonoRequest } from "hono";
1+
import { HonoRequest } from "hono";
2+
import { createMiddleware } from "hono/factory";
23
import { StatusCode } from "hono/utils/http-status";
34
import qs from "qs";
45
import {
@@ -21,18 +22,25 @@ declare module "stainless" {
2122
}
2223
}
2324

25+
export type StlAppOptions = {
26+
handleErrors?: boolean;
27+
};
28+
2429
const methods = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
2530

26-
function makeApp(endpoints: AnyEndpoint[]) {
31+
function makeHandler(
32+
basePath: string,
33+
endpoints: AnyEndpoint[],
34+
options?: StlAppOptions
35+
) {
2736
const stl = endpoints[0]?.stl;
2837
if (!stl) {
2938
throw new Error(`endpoints[0].stl must be defined`);
3039
}
3140

32-
const app = new Hono();
3341
const routeMatcher = makeRouteMatcher(endpoints);
3442

35-
return app.all("*", async (c) => {
43+
return createMiddleware(async (c, next) => {
3644
try {
3745
const match = routeMatcher.match(c.req.method, c.req.path);
3846
const { search } = new URL(c.req.url);
@@ -51,7 +59,11 @@ function makeApp(endpoints: AnyEndpoint[]) {
5159
{ status: 405 }
5260
);
5361
}
54-
throw new NotFoundError();
62+
if (options?.handleErrors !== false) {
63+
throw new NotFoundError();
64+
}
65+
await next();
66+
return;
5567
}
5668

5769
const [endpoint, path] = match[0][0];
@@ -77,6 +89,10 @@ function makeApp(endpoints: AnyEndpoint[]) {
7789

7890
return c.json(result);
7991
} catch (error) {
92+
if (options?.handleErrors === false) {
93+
throw error;
94+
}
95+
8096
if (isStlError(error)) {
8197
return c.json(error.response, error.statusCode as StatusCode);
8298
}
@@ -90,11 +106,16 @@ function makeApp(endpoints: AnyEndpoint[]) {
90106
});
91107
}
92108

93-
export function apiRoute({ topLevel, resources }: AnyAPIDescription) {
94-
return makeApp(
109+
export function stlApi(
110+
{ basePath, topLevel, resources }: AnyAPIDescription,
111+
options?: StlAppOptions
112+
) {
113+
return makeHandler(
114+
basePath,
95115
allEndpoints({
96116
actions: topLevel?.actions,
97117
namespacedResources: resources,
98-
})
118+
}),
119+
options
99120
);
100121
}

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)