1import { assert, describe, it } from "@effect/vitest"
2import { Effect, Layer, Option } from "effect"
3import { HttpRouter, type HttpServerRequest, HttpServerResponse } from "effect/http"
4
5const echoUrl = (request: HttpServerRequest.HttpServerRequest) => Effect.succeed(HttpServerResponse.text(request.url))
6
7const layerPrefixed = (prefix: string) =>
8 Layer.effect(
9 HttpRouter.HttpRouter,
10 Effect.map(HttpRouter.HttpRouter, (router) => router.prefixed(prefix))
11 )
12
13const fetchText = (app: Layer.Layer<never, never, HttpRouter.HttpRouter>, path: string) =>
14 Effect.acquireUseRelease(
15 Effect.sync(() => HttpRouter.toWebHandler(app, { disableLogger: true })),
16 ({ handler }) =>
17 Effect.promise(async () => {
18 const response = await handler(new Request(`http://localhost${path}`))
19 return await response.text()
20 }),
21 ({ dispose }) => Effect.promise(dispose)
22 )
23
24describe("HttpRouter", () => {
25 it("normalizes the prefix stored by prefixRoute", () => {
26 const route = HttpRouter.prefixRoute(
27 HttpRouter.route("GET", "/users", HttpServerResponse.text("ok")),
28 "/api/"
29 )
30
31 assert.deepStrictEqual(route.prefix, Option.some("/api"))
32 })
33
34 it("handles prefixed routes with toWebHandler", async () => {
35 const { dispose, handler } = HttpRouter.toWebHandler(
36 Effect.gen(function*() {
37 const router = yield* HttpRouter.HttpRouter
38 yield* router.prefixed("/api").add("GET", "/hello", HttpServerResponse.text("hi"))
39 }).pipe(Layer.effectDiscard)
40 )
41
42 try {
43 const response = await handler(new Request("http://localhost/api/hello"))
44 assert.strictEqual(response.status, 200)
45 assert.strictEqual(await response.text(), "hi")
46 } finally {
47 await dispose()
48 }
49 })
50
51 for (
52 const { prefix, requestUrl } of [
53 { prefix: "/", requestUrl: "/users" },
54 { prefix: "/api/", requestUrl: "/api/users" }
55 ]
56 ) {
57 it.effect(`preserves the local URL for the ${prefix} prefix`, () =>
58 Effect.gen(function*() {
59 const routes = HttpRouter.use((router) => router.prefixed(prefix).add("GET", "/users", echoUrl))
60
61 const body = yield* fetchText(routes, requestUrl)
62
63 assert.strictEqual(body, "/users")
64 }))
65 }
66
67 it.effect("nests a prefixed sub-router beneath a prefixed parent router", () =>
68 Effect.gen(function*() {
69 const routes = HttpRouter.use((router) => router.prefixed("/app").add("GET", "/users", echoUrl)).pipe(
70 Layer.provide(layerPrefixed("/api"))
71 )
72
73 const body = yield* fetchText(routes, "/api/app/users")
74
75 assert.strictEqual(body, "/users")
76 }))
77
78 it.effect("nests a prefixRoute prefix beneath a prefixed parent router", () =>
79 Effect.gen(function*() {
80 const users = HttpRouter.prefixRoute(HttpRouter.route("GET", "/users", echoUrl), "/app")
81 const routes = HttpRouter.use((router) => router.addAll([users])).pipe(
82 Layer.provide(layerPrefixed("/api"))
83 )
84
85 const body = yield* fetchText(routes, "/api/app/users")
86
87 assert.strictEqual(body, "/users")
88 }))
89})