1import { assert, expect, it } from "@effect/vitest"
2import { Context, Effect, Layer, Schema, Stream, Struct } from "effect"
3import {
4 FetchHttpClient,
5 HttpClient,
6 HttpClientRequest,
7 HttpClientResponse,
8 HttpRouter,
9 HttpServer,
10 HttpServerRequest,
11 HttpServerResponse
12} from "effect/http"
13import { TestClock } from "effect/testing"
14
15const Todo = Schema.Struct({
16 userId: Schema.Number,
17 id: Schema.Number,
18 title: Schema.String,
19 completed: Schema.Boolean
20})
21const TodoWithoutId = Schema.Struct({
22 ...Struct.omit(Todo.fields, ["id"])
23})
24
25const makeJsonPlaceholder = Effect.gen(function*() {
26 const client = yield* HttpClient.HttpClient
27 const createTodo = (todo: typeof TodoWithoutId.Type) =>
28 HttpClientRequest.post("/todos").pipe(
29 HttpClientRequest.schemaBodyJson(TodoWithoutId)(todo),
30 Effect.flatMap(client.execute),
31 Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo))
32 )
33 return {
34 client,
35 createTodo
36 } as const
37})
38interface JsonPlaceholder extends Effect.Success<typeof makeJsonPlaceholder> {}
39const JsonPlaceholder = Context.Service<JsonPlaceholder>("test/JsonPlaceholder")
40const JsonPlaceholderLayer = Layer.effect(JsonPlaceholder)(makeJsonPlaceholder)
41const TestRoutes = HttpRouter.serve(HttpRouter.use(Effect.fnUntraced(function*(router) {
42 yield* router.addAll([
43 HttpRouter.route("GET", "/", Effect.succeed(HttpServerResponse.text("test"))),
44 HttpRouter.route("GET", "/redirect", Effect.succeed(HttpServerResponse.redirect("/?value=a%23b#fragment"))),
45 HttpRouter.route(
46 "GET",
47 "/stream",
48 Effect.succeed(HttpServerResponse.stream(Stream.make("test").pipe(Stream.encodeText)))
49 ),
50 HttpRouter.route(
51 "GET",
52 "/interrupt",
53 Effect.succeed(HttpServerResponse.stream(
54 Stream.make("test").pipe(Stream.concat(Stream.never), Stream.encodeText)
55 ))
56 ),
57 HttpRouter.route(
58 "GET",
59 "/todos/1",
60 Effect.succeed(HttpServerResponse.jsonUnsafe({
61 userId: 1,
62 id: 1,
63 title: "test",
64 completed: false
65 }))
66 ),
67 HttpRouter.route(
68 "POST",
69 "/todos",
70 Effect.gen(function*() {
71 const todo = yield* HttpServerRequest.schemaBodyJson(TodoWithoutId)
72 return HttpServerResponse.jsonUnsafe({ ...todo, id: 201 })
73 })
74 ),
75 HttpRouter.route("HEAD", "/todos", Effect.succeed(HttpServerResponse.empty({ status: 200 })))
76 ])
77})))
78const DenoHttpServerUrl = new URL("../../platform/deno/src/DenoHttpServer.ts", import.meta.url).href
79const TestServerLayer = Layer.unwrap(Effect.promise(() =>
80 "Deno" in globalThis
81 ? (import(DenoHttpServerUrl) as Promise<{
82 readonly layerServer: (options: {
83 readonly hostname: string
84 readonly port: number
85 readonly onListen: () => void
86 }) => Layer.Layer<HttpServer.HttpServer>
87 }>).then((DenoHttpServer) => DenoHttpServer.layerServer({ hostname: "127.0.0.1", port: 0, onListen: () => {} }))
88 : Promise.all([
89 import("@effect/platform-node/NodeHttpServer"),
90 import("node:http")
91 ]).then(([NodeHttpServer, Http]) => NodeHttpServer.layerServer(Http.createServer, { port: 0 }))
92))
93;[
94 {
95 name: "FetchHttpClient",
96 layer: FetchHttpClient.layer
97 }
98].forEach(({ layer, name }) => {
99 const layerTest = HttpServer.layerTestClient.pipe(
100 Layer.provide(layer),
101 Layer.provideMerge(TestServerLayer)
102 )
103 const testLayer = Layer.merge(JsonPlaceholderLayer, TestRoutes).pipe(
104 Layer.provideMerge(layerTest)
105 )
106
107 it.layer(layer)(name, (it) => {
108 it.effect("get", () =>
109 Effect.gen(function*() {
110 const response = yield* HttpClient.get("/").pipe(
111 Effect.flatMap((_) => _.text)
112 )
113 expect(response).toBe("test")
114 }).pipe(Effect.provide(testLayer)))
115
116 it.effect("followRedirects", () =>
117 Effect.gen(function*() {
118 const client = (yield* HttpClient.HttpClient).pipe(
119 HttpClient.followRedirects()
120 )
121 const response = yield* client.get("/redirect")
122 // Fetch follows redirects natively and retains the original request.
123 expect(new URL(response.request.url).pathname).toBe("/redirect")
124 expect(new URL(response.url).pathname).toBe("/")
125 assert.strictEqual(new URL(response.url).search, "?value=a%23b")
126 assert.strictEqual(new URL(response.url).hash, "")
127 expect(yield* response.text).toBe("test")
128 }).pipe(Effect.provide(testLayer)))
129
130 it.effect("stream", () =>
131 Effect.gen(function*() {
132 const client = yield* HttpClient.HttpClient
133 const response = yield* client.get("/stream").pipe(
134 Effect.map((_) => _.stream),
135 Stream.unwrap,
136 Stream.decodeText(),
137 Stream.mkString
138 )
139 expect(response).toBe("test")
140 }).pipe(Effect.provide(testLayer)))
141
142 it.effect("jsonplaceholder", () =>
143 Effect.gen(function*() {
144 const jp = yield* JsonPlaceholder
145 const response = yield* jp.client.get("/todos/1").pipe(
146 Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo))
147 )
148 expect(response.id).toBe(1)
149 }).pipe(Effect.provide(testLayer)))
150
151 it.effect("jsonplaceholder schemaBodyJson", () =>
152 Effect.gen(function*() {
153 const jp = yield* JsonPlaceholder
154 const response = yield* jp.createTodo({
155 userId: 1,
156 title: "test",
157 completed: false
158 })
159 expect(response.title).toBe("test")
160 }).pipe(Effect.provide(testLayer)))
161
162 it.effect("head request with schemaJson", () =>
163 Effect.gen(function*() {
164 const client = yield* HttpClient.HttpClient
165 const response = yield* client.head("/todos").pipe(
166 Effect.flatMap(
167 HttpClientResponse.schemaJson(Schema.Struct({ status: Schema.Literal(200) }))
168 )
169 )
170 expect(response).toEqual({ status: 200 })
171 }).pipe(Effect.provide(testLayer)))
172
173 it.effect("interrupt", () =>
174 Effect.gen(function*() {
175 const client = yield* HttpClient.HttpClient
176 const response = yield* client.get("/interrupt").pipe(
177 Effect.flatMap((_) => _.text),
178 Effect.timeout(1),
179 Effect.asSome,
180 Effect.catchTag("TimeoutError", () => Effect.succeedNone),
181 TestClock.withLive
182 )
183 expect(response._tag).toEqual("None")
184 }).pipe(Effect.provide(testLayer)))
185
186 it.effect("close early", () =>
187 Effect.gen(function*() {
188 const response = yield* HttpClient.get("/stream")
189 expect(response.status).toBe(200)
190 }).pipe(Effect.provide(testLayer)))
191 })
192})