effect-start 0.17.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Development.d.ts +7 -2
- package/dist/Development.js +12 -6
- package/dist/PlatformRuntime.d.ts +4 -0
- package/dist/PlatformRuntime.js +9 -0
- package/dist/Route.d.ts +6 -2
- package/dist/Route.js +22 -0
- package/dist/RouteHttp.d.ts +1 -1
- package/dist/RouteHttp.js +12 -19
- package/dist/RouteMount.d.ts +2 -1
- package/dist/Start.d.ts +1 -5
- package/dist/Start.js +1 -8
- package/dist/Unique.d.ts +50 -0
- package/dist/Unique.js +187 -0
- package/dist/bun/BunHttpServer.js +5 -6
- package/dist/bun/BunRoute.d.ts +1 -1
- package/dist/bun/BunRoute.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node/Effectify.d.ts +209 -0
- package/dist/node/Effectify.js +19 -0
- package/dist/node/FileSystem.d.ts +3 -5
- package/dist/node/FileSystem.js +42 -62
- package/dist/node/PlatformError.d.ts +46 -0
- package/dist/node/PlatformError.js +43 -0
- package/dist/testing/TestLogger.js +1 -1
- package/package.json +10 -5
- package/src/Development.ts +13 -18
- package/src/PlatformRuntime.ts +11 -0
- package/src/Route.ts +31 -2
- package/src/RouteHttp.ts +15 -31
- package/src/RouteMount.ts +1 -1
- package/src/Start.ts +1 -15
- package/src/Unique.ts +232 -0
- package/src/bun/BunHttpServer.ts +6 -9
- package/src/bun/BunRoute.ts +3 -3
- package/src/index.ts +1 -0
- package/src/node/Effectify.ts +262 -0
- package/src/node/FileSystem.ts +59 -97
- package/src/node/PlatformError.ts +102 -0
- package/src/testing/TestLogger.ts +1 -1
- package/dist/Random.d.ts +0 -5
- package/dist/Random.js +0 -49
- package/src/Commander.test.ts +0 -1639
- package/src/ContentNegotiation.test.ts +0 -603
- package/src/Development.test.ts +0 -119
- package/src/Entity.test.ts +0 -592
- package/src/FileRouterPattern.test.ts +0 -147
- package/src/FileRouter_files.test.ts +0 -64
- package/src/FileRouter_path.test.ts +0 -145
- package/src/FileRouter_tree.test.ts +0 -132
- package/src/Http.test.ts +0 -319
- package/src/HttpAppExtra.test.ts +0 -103
- package/src/HttpUtils.test.ts +0 -85
- package/src/PathPattern.test.ts +0 -648
- package/src/Random.ts +0 -59
- package/src/RouteBody.test.ts +0 -232
- package/src/RouteHook.test.ts +0 -40
- package/src/RouteHttp.test.ts +0 -2909
- package/src/RouteMount.test.ts +0 -481
- package/src/RouteSchema.test.ts +0 -427
- package/src/RouteSse.test.ts +0 -249
- package/src/RouteTree.test.ts +0 -494
- package/src/RouteTrie.test.ts +0 -322
- package/src/RouterPattern.test.ts +0 -676
- package/src/Values.test.ts +0 -263
- package/src/bun/BunBundle.test.ts +0 -268
- package/src/bun/BunBundle_imports.test.ts +0 -48
- package/src/bun/BunHttpServer.test.ts +0 -251
- package/src/bun/BunImportTrackerPlugin.test.ts +0 -77
- package/src/bun/BunRoute.test.ts +0 -162
- package/src/bundler/BundleHttp.test.ts +0 -132
- package/src/effect/HttpRouter.test.ts +0 -548
- package/src/experimental/EncryptedCookies.test.ts +0 -488
- package/src/hyper/HyperHtml.test.ts +0 -209
- package/src/hyper/HyperRoute.test.tsx +0 -197
- package/src/middlewares/BasicAuthMiddleware.test.ts +0 -84
- package/src/testing/TestHttpClient.test.ts +0 -83
- package/src/testing/TestLogger.test.ts +0 -51
- package/src/x/datastar/Datastar.test.ts +0 -266
- package/src/x/tailwind/TailwindPlugin.test.ts +0 -333
package/src/RouteHttp.test.ts
DELETED
|
@@ -1,2909 +0,0 @@
|
|
|
1
|
-
import * as test from "bun:test"
|
|
2
|
-
import * as Effect from "effect/Effect"
|
|
3
|
-
import * as Option from "effect/Option"
|
|
4
|
-
import * as Ref from "effect/Ref"
|
|
5
|
-
import * as Schedule from "effect/Schedule"
|
|
6
|
-
import * as Schema from "effect/Schema"
|
|
7
|
-
import * as Stream from "effect/Stream"
|
|
8
|
-
import * as Tracer from "effect/Tracer"
|
|
9
|
-
import * as Entity from "./Entity.ts"
|
|
10
|
-
import * as Http from "./Http.ts"
|
|
11
|
-
import * as Route from "./Route.ts"
|
|
12
|
-
import * as RouteHttp from "./RouteHttp.ts"
|
|
13
|
-
import * as RouteSchema from "./RouteSchema.ts"
|
|
14
|
-
import * as RouteTree from "./RouteTree.ts"
|
|
15
|
-
import * as TestLogger from "./testing/TestLogger.ts"
|
|
16
|
-
|
|
17
|
-
test.it("converts string to text/plain for Route.text", async () => {
|
|
18
|
-
const handler = RouteHttp.toWebHandler(
|
|
19
|
-
Route.get(
|
|
20
|
-
Route.text("Hello World"),
|
|
21
|
-
),
|
|
22
|
-
)
|
|
23
|
-
const response = await Http.fetch(handler, { path: "/text" })
|
|
24
|
-
|
|
25
|
-
test
|
|
26
|
-
.expect(response.status)
|
|
27
|
-
.toBe(200)
|
|
28
|
-
test
|
|
29
|
-
.expect(response.headers.get("Content-Type"))
|
|
30
|
-
.toBe("text/plain; charset=utf-8")
|
|
31
|
-
test
|
|
32
|
-
.expect(await response.text())
|
|
33
|
-
.toBe("Hello World")
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test.it("converts string to text/html for Route.html", async () => {
|
|
37
|
-
const handler = RouteHttp.toWebHandler(
|
|
38
|
-
Route.get(Route.html("<h1>Hello</h1>")),
|
|
39
|
-
)
|
|
40
|
-
const response = await Http.fetch(handler, { path: "/html" })
|
|
41
|
-
|
|
42
|
-
test
|
|
43
|
-
.expect(response.status)
|
|
44
|
-
.toBe(200)
|
|
45
|
-
test
|
|
46
|
-
.expect(response.headers.get("Content-Type"))
|
|
47
|
-
.toBe("text/html; charset=utf-8")
|
|
48
|
-
test
|
|
49
|
-
.expect(await response.text())
|
|
50
|
-
.toBe("<h1>Hello</h1>")
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test.it("converts object to JSON for Route.json", async () => {
|
|
54
|
-
const handler = RouteHttp.toWebHandler(
|
|
55
|
-
Route.get(
|
|
56
|
-
Route.json({ message: "hello", count: 42 }),
|
|
57
|
-
),
|
|
58
|
-
)
|
|
59
|
-
const response = await Http.fetch(handler, { path: "/json" })
|
|
60
|
-
|
|
61
|
-
test
|
|
62
|
-
.expect(response.status)
|
|
63
|
-
.toBe(200)
|
|
64
|
-
test
|
|
65
|
-
.expect(response.headers.get("Content-Type"))
|
|
66
|
-
.toBe("application/json")
|
|
67
|
-
test
|
|
68
|
-
.expect(await response.json())
|
|
69
|
-
.toEqual({ message: "hello", count: 42 })
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test.it("converts array to JSON for Route.json", async () => {
|
|
73
|
-
const handler = RouteHttp.toWebHandler(
|
|
74
|
-
Route.get(
|
|
75
|
-
Route.json([1, 2, 3]),
|
|
76
|
-
),
|
|
77
|
-
)
|
|
78
|
-
const response = await Http.fetch(handler, { path: "/array" })
|
|
79
|
-
|
|
80
|
-
test
|
|
81
|
-
.expect(response.status)
|
|
82
|
-
.toBe(200)
|
|
83
|
-
test
|
|
84
|
-
.expect(response.headers.get("Content-Type"))
|
|
85
|
-
.toBe("application/json")
|
|
86
|
-
test
|
|
87
|
-
.expect(await response.json())
|
|
88
|
-
.toEqual([1, 2, 3])
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test.it("handles method-specific routes", async () => {
|
|
92
|
-
const handler = RouteHttp.toWebHandler(
|
|
93
|
-
Route
|
|
94
|
-
.get(Route.text("get resource"))
|
|
95
|
-
.post(Route.text("post resource")),
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
const getResponse = await Http.fetch(handler, {
|
|
99
|
-
path: "/resource",
|
|
100
|
-
method: "GET",
|
|
101
|
-
})
|
|
102
|
-
test
|
|
103
|
-
.expect(await getResponse.text())
|
|
104
|
-
.toBe("get resource")
|
|
105
|
-
|
|
106
|
-
const postResponse = await Http.fetch(handler, {
|
|
107
|
-
path: "/resource",
|
|
108
|
-
method: "POST",
|
|
109
|
-
})
|
|
110
|
-
test
|
|
111
|
-
.expect(await postResponse.text())
|
|
112
|
-
.toBe("post resource")
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
test.it("handles errors by returning 500 response", () =>
|
|
116
|
-
Effect
|
|
117
|
-
.gen(function*() {
|
|
118
|
-
const runtime = yield* Effect.runtime<TestLogger.TestLogger>()
|
|
119
|
-
const handler = RouteHttp.toWebHandlerRuntime(runtime)(
|
|
120
|
-
Route.get(
|
|
121
|
-
Route.text(function*(): Generator<any, string, any> {
|
|
122
|
-
return yield* Effect.fail(new Error("Something went wrong"))
|
|
123
|
-
}),
|
|
124
|
-
),
|
|
125
|
-
)
|
|
126
|
-
const response = yield* Effect.promise(() =>
|
|
127
|
-
Http.fetch(handler, { path: "/error" })
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
test
|
|
131
|
-
.expect(response.status)
|
|
132
|
-
.toBe(500)
|
|
133
|
-
|
|
134
|
-
const text = yield* Effect.promise(() => response.text())
|
|
135
|
-
test
|
|
136
|
-
.expect(text)
|
|
137
|
-
.toContain("Something went wrong")
|
|
138
|
-
|
|
139
|
-
const messages = yield* TestLogger.messages
|
|
140
|
-
test
|
|
141
|
-
.expect(messages.some((m) => m.includes("Something went wrong")))
|
|
142
|
-
.toBe(true)
|
|
143
|
-
})
|
|
144
|
-
.pipe(Effect.provide(TestLogger.layer()), Effect.runPromise))
|
|
145
|
-
|
|
146
|
-
test.it("handles defects by returning 500 response", () =>
|
|
147
|
-
Effect
|
|
148
|
-
.gen(function*() {
|
|
149
|
-
const runtime = yield* Effect.runtime<TestLogger.TestLogger>()
|
|
150
|
-
const handler = RouteHttp.toWebHandlerRuntime(runtime)(
|
|
151
|
-
Route.get(
|
|
152
|
-
Route.text(function*() {
|
|
153
|
-
return yield* Effect.die("Unexpected error")
|
|
154
|
-
|
|
155
|
-
return "Hello"
|
|
156
|
-
}),
|
|
157
|
-
),
|
|
158
|
-
)
|
|
159
|
-
const response = yield* Effect.promise(() =>
|
|
160
|
-
Http.fetch(handler, { path: "/defect" })
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
test
|
|
164
|
-
.expect(response.status)
|
|
165
|
-
.toBe(500)
|
|
166
|
-
|
|
167
|
-
const messages = yield* TestLogger.messages
|
|
168
|
-
test
|
|
169
|
-
.expect(messages.some((m) => m.includes("Unexpected error")))
|
|
170
|
-
.toBe(true)
|
|
171
|
-
})
|
|
172
|
-
.pipe(Effect.provide(TestLogger.layer()), Effect.runPromise))
|
|
173
|
-
|
|
174
|
-
test.it("includes descriptor properties in handler context", async () => {
|
|
175
|
-
let capturedMethod: string | undefined
|
|
176
|
-
|
|
177
|
-
const handler = RouteHttp.toWebHandler(
|
|
178
|
-
Route.get(
|
|
179
|
-
Route.text(function*(ctx) {
|
|
180
|
-
capturedMethod = ctx.method
|
|
181
|
-
return "ok"
|
|
182
|
-
}),
|
|
183
|
-
),
|
|
184
|
-
)
|
|
185
|
-
await Http.fetch(handler, { path: "/test" })
|
|
186
|
-
|
|
187
|
-
test
|
|
188
|
-
.expect(capturedMethod)
|
|
189
|
-
.toBe("GET")
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
test.it("includes request in handler context", async () => {
|
|
193
|
-
let capturedRequest: Request | undefined
|
|
194
|
-
|
|
195
|
-
const handler = RouteHttp.toWebHandler(
|
|
196
|
-
Route.get(
|
|
197
|
-
Route.text(function*(ctx) {
|
|
198
|
-
test
|
|
199
|
-
.expectTypeOf(ctx.request)
|
|
200
|
-
.toEqualTypeOf<Request>()
|
|
201
|
-
capturedRequest = ctx.request
|
|
202
|
-
return "ok"
|
|
203
|
-
}),
|
|
204
|
-
),
|
|
205
|
-
)
|
|
206
|
-
await Http.fetch(handler, {
|
|
207
|
-
path: "/test",
|
|
208
|
-
headers: { "x-custom": "value" },
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
test
|
|
212
|
-
.expect(capturedRequest)
|
|
213
|
-
.toBeInstanceOf(Request)
|
|
214
|
-
test
|
|
215
|
-
.expect(capturedRequest?.headers.get("x-custom"))
|
|
216
|
-
.toBe("value")
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
test.it("returns 405 for wrong method", async () => {
|
|
220
|
-
const handler = RouteHttp.toWebHandler(
|
|
221
|
-
Route.get(Route.text("users")),
|
|
222
|
-
)
|
|
223
|
-
const response = await Http.fetch(handler, {
|
|
224
|
-
path: "/users",
|
|
225
|
-
method: "POST",
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
test
|
|
229
|
-
.expect(response.status)
|
|
230
|
-
.toBe(405)
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
test.it("supports POST method", async () => {
|
|
234
|
-
const handler = RouteHttp.toWebHandler(
|
|
235
|
-
Route.post(Route.text("created")),
|
|
236
|
-
)
|
|
237
|
-
const response = await Http.fetch(handler, {
|
|
238
|
-
path: "/users",
|
|
239
|
-
method: "POST",
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
test
|
|
243
|
-
.expect(response.status)
|
|
244
|
-
.toBe(200)
|
|
245
|
-
test
|
|
246
|
-
.expect(await response.text())
|
|
247
|
-
.toBe("created")
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
test.it("selects json when Accept prefers application/json", async () => {
|
|
251
|
-
const handler = RouteHttp.toWebHandler(
|
|
252
|
-
Route
|
|
253
|
-
.get(Route.json({ type: "json" }))
|
|
254
|
-
.get(Route.html("<div>html</div>")),
|
|
255
|
-
)
|
|
256
|
-
const response = await Http.fetch(handler, {
|
|
257
|
-
path: "/data",
|
|
258
|
-
headers: { Accept: "application/json" },
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
test
|
|
262
|
-
.expect(response.headers.get("Content-Type"))
|
|
263
|
-
.toBe("application/json")
|
|
264
|
-
test
|
|
265
|
-
.expect(await response.json())
|
|
266
|
-
.toEqual({ type: "json" })
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
test.it("selects html when Accept prefers text/html", async () => {
|
|
270
|
-
const handler = RouteHttp.toWebHandler(
|
|
271
|
-
Route
|
|
272
|
-
.get(Route.json({ type: "json" }))
|
|
273
|
-
.get(Route.html("<div>html</div>")),
|
|
274
|
-
)
|
|
275
|
-
const response = await Http.fetch(handler, {
|
|
276
|
-
path: "/data",
|
|
277
|
-
headers: { Accept: "text/html" },
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
test
|
|
281
|
-
.expect(response.headers.get("Content-Type"))
|
|
282
|
-
.toBe("text/html; charset=utf-8")
|
|
283
|
-
test
|
|
284
|
-
.expect(await response.text())
|
|
285
|
-
.toBe("<div>html</div>")
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
test.it("selects text/plain when Accept prefers it", async () => {
|
|
289
|
-
const handler = RouteHttp.toWebHandler(
|
|
290
|
-
Route
|
|
291
|
-
.get(Route.text("plain text"))
|
|
292
|
-
.get(Route.json({ type: "json" })),
|
|
293
|
-
)
|
|
294
|
-
const response = await Http.fetch(handler, {
|
|
295
|
-
path: "/data",
|
|
296
|
-
headers: { Accept: "text/plain" },
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
test
|
|
300
|
-
.expect(response.headers.get("Content-Type"))
|
|
301
|
-
.toBe("text/plain; charset=utf-8")
|
|
302
|
-
test
|
|
303
|
-
.expect(await response.text())
|
|
304
|
-
.toBe("plain text")
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
test.it("returns first candidate when no Accept header", async () => {
|
|
308
|
-
const handler = RouteHttp.toWebHandler(
|
|
309
|
-
Route
|
|
310
|
-
.get(Route.json({ type: "json" }))
|
|
311
|
-
.get(Route.html("<div>html</div>")),
|
|
312
|
-
)
|
|
313
|
-
const response = await Http.fetch(handler, { path: "/data" })
|
|
314
|
-
|
|
315
|
-
test
|
|
316
|
-
.expect(response.headers.get("Content-Type"))
|
|
317
|
-
.toBe("application/json")
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
test.it("handles Accept with quality values", async () => {
|
|
321
|
-
const handler = RouteHttp.toWebHandler(
|
|
322
|
-
Route
|
|
323
|
-
.get(Route.json({ type: "json" }))
|
|
324
|
-
.get(Route.html("<div>html</div>")),
|
|
325
|
-
)
|
|
326
|
-
const response = await Http.fetch(handler, {
|
|
327
|
-
path: "/data",
|
|
328
|
-
headers: { Accept: "text/html;q=0.9, application/json;q=1.0" },
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
test
|
|
332
|
-
.expect(response.headers.get("Content-Type"))
|
|
333
|
-
.toBe("application/json")
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
test.it("handles Accept: */*", async () => {
|
|
337
|
-
const handler = RouteHttp.toWebHandler(
|
|
338
|
-
Route
|
|
339
|
-
.get(Route.json({ type: "json" }))
|
|
340
|
-
.get(Route.html("<div>html</div>")),
|
|
341
|
-
)
|
|
342
|
-
const response = await Http.fetch(handler, {
|
|
343
|
-
path: "/data",
|
|
344
|
-
headers: { Accept: "*/*" },
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
test
|
|
348
|
-
.expect(response.headers.get("Content-Type"))
|
|
349
|
-
.toBe("application/json")
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
test.it("returns 406 when Accept doesn't match available formats", async () => {
|
|
353
|
-
const handler = RouteHttp.toWebHandler(
|
|
354
|
-
Route.get(Route.json({ type: "json" })),
|
|
355
|
-
)
|
|
356
|
-
const response = await Http.fetch(handler, {
|
|
357
|
-
path: "/data",
|
|
358
|
-
headers: { Accept: "text/html" },
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
test
|
|
362
|
-
.expect(response.status)
|
|
363
|
-
.toBe(406)
|
|
364
|
-
test
|
|
365
|
-
.expect(await response.json())
|
|
366
|
-
.toEqual({ status: 406, message: "not acceptable" })
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
test.it("returns 406 when Accept doesn't match any of multiple formats", async () => {
|
|
370
|
-
const handler = RouteHttp.toWebHandler(
|
|
371
|
-
Route
|
|
372
|
-
.get(Route.json({ type: "json" }))
|
|
373
|
-
.get(Route.html("<div>html</div>")),
|
|
374
|
-
)
|
|
375
|
-
const response = await Http.fetch(handler, {
|
|
376
|
-
path: "/data",
|
|
377
|
-
headers: { Accept: "image/png" },
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
test
|
|
381
|
-
.expect(response.status)
|
|
382
|
-
.toBe(406)
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
test.it("definition order determines priority when no Accept header", async () => {
|
|
386
|
-
const handler = RouteHttp.toWebHandler(
|
|
387
|
-
Route
|
|
388
|
-
.get(Route.text("plain"))
|
|
389
|
-
.get(Route.html("<div>html</div>")),
|
|
390
|
-
)
|
|
391
|
-
const response = await Http.fetch(handler, { path: "/data" })
|
|
392
|
-
|
|
393
|
-
test
|
|
394
|
-
.expect(response.headers.get("Content-Type"))
|
|
395
|
-
.toBe("text/plain; charset=utf-8")
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
test.it("falls back to html when no Accept header and no json or text", async () => {
|
|
399
|
-
const handler = RouteHttp.toWebHandler(
|
|
400
|
-
Route.get(Route.html("<div>html</div>")),
|
|
401
|
-
)
|
|
402
|
-
const response = await Http.fetch(handler, { path: "/data" })
|
|
403
|
-
|
|
404
|
-
test
|
|
405
|
-
.expect(response.headers.get("Content-Type"))
|
|
406
|
-
.toBe("text/html; charset=utf-8")
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
test.it("Route.text matches any text/* Accept header", async () => {
|
|
410
|
-
const handler = RouteHttp.toWebHandler(
|
|
411
|
-
Route.get(
|
|
412
|
-
Route.text(function*() {
|
|
413
|
-
return Entity.make("event: message\ndata: hello\n\n", {
|
|
414
|
-
headers: { "content-type": "text/event-stream" },
|
|
415
|
-
})
|
|
416
|
-
}),
|
|
417
|
-
),
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
const response = await Http.fetch(handler, {
|
|
421
|
-
path: "/events",
|
|
422
|
-
headers: { Accept: "text/event-stream" },
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
test
|
|
426
|
-
.expect(response.status)
|
|
427
|
-
.toBe(200)
|
|
428
|
-
test
|
|
429
|
-
.expect(response.headers.get("Content-Type"))
|
|
430
|
-
.toBe("text/event-stream")
|
|
431
|
-
test
|
|
432
|
-
.expect(await response.text())
|
|
433
|
-
.toBe("event: message\ndata: hello\n\n")
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
test.it("Route.text matches text/markdown Accept header", async () => {
|
|
437
|
-
const handler = RouteHttp.toWebHandler(
|
|
438
|
-
Route.get(
|
|
439
|
-
Route.text(function*() {
|
|
440
|
-
return Entity.make("# Hello", {
|
|
441
|
-
headers: { "content-type": "text/markdown" },
|
|
442
|
-
})
|
|
443
|
-
}),
|
|
444
|
-
),
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
const response = await Http.fetch(handler, {
|
|
448
|
-
path: "/doc",
|
|
449
|
-
headers: { Accept: "text/markdown" },
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
test
|
|
453
|
-
.expect(response.status)
|
|
454
|
-
.toBe(200)
|
|
455
|
-
test
|
|
456
|
-
.expect(response.headers.get("Content-Type"))
|
|
457
|
-
.toBe("text/markdown")
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
test.describe("walkHandles", () => {
|
|
461
|
-
test.it("yields handlers for static routes", () => {
|
|
462
|
-
const tree = RouteTree.make({
|
|
463
|
-
"/users": Route.get(Route.text("users list")),
|
|
464
|
-
"/admin": Route.get(Route.text("admin")),
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
468
|
-
|
|
469
|
-
test
|
|
470
|
-
.expect("/users" in handles)
|
|
471
|
-
.toBe(true)
|
|
472
|
-
test
|
|
473
|
-
.expect("/admin" in handles)
|
|
474
|
-
.toBe(true)
|
|
475
|
-
})
|
|
476
|
-
|
|
477
|
-
test.it("yields handlers for parameterized routes", () => {
|
|
478
|
-
const tree = RouteTree.make({
|
|
479
|
-
"/users/:id": Route.get(Route.text("user detail")),
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
483
|
-
|
|
484
|
-
test
|
|
485
|
-
.expect("/users/:id" in handles)
|
|
486
|
-
.toBe(true)
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
test.it("preserves optional param syntax", () => {
|
|
490
|
-
const tree = RouteTree.make({
|
|
491
|
-
"/files/:name?": Route.get(Route.text("files")),
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
495
|
-
|
|
496
|
-
test
|
|
497
|
-
.expect("/files/:name?" in handles)
|
|
498
|
-
.toBe(true)
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
test.it("preserves wildcard param syntax", () => {
|
|
502
|
-
const tree = RouteTree.make({
|
|
503
|
-
"/docs/:path*": Route.get(Route.text("docs")),
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
507
|
-
|
|
508
|
-
test
|
|
509
|
-
.expect("/docs/:path*" in handles)
|
|
510
|
-
.toBe(true)
|
|
511
|
-
})
|
|
512
|
-
})
|
|
513
|
-
|
|
514
|
-
test.describe("middleware chain", () => {
|
|
515
|
-
test.it("passes enriched context to handler", async () => {
|
|
516
|
-
const handler = RouteHttp.toWebHandler(
|
|
517
|
-
Route
|
|
518
|
-
.use(Route.filter({ context: { answer: 42 } }))
|
|
519
|
-
.get(Route.text(function*(ctx) {
|
|
520
|
-
return `The answer is ${ctx.answer}`
|
|
521
|
-
})),
|
|
522
|
-
)
|
|
523
|
-
const response = await Http.fetch(handler, { path: "/test" })
|
|
524
|
-
|
|
525
|
-
test
|
|
526
|
-
.expect(response.status)
|
|
527
|
-
.toBe(200)
|
|
528
|
-
test
|
|
529
|
-
.expect(await response.text())
|
|
530
|
-
.toBe("The answer is 42")
|
|
531
|
-
})
|
|
532
|
-
|
|
533
|
-
test.it("composes multiple middlewares with cumulative context", async () => {
|
|
534
|
-
const handler = RouteHttp.toWebHandler(
|
|
535
|
-
Route
|
|
536
|
-
.use(Route.filter({ context: { a: 1 } }))
|
|
537
|
-
.use(Route.filter({ context: { b: 2 } }))
|
|
538
|
-
.get(Route.text(function*(ctx) {
|
|
539
|
-
return `a=${ctx.a},b=${ctx.b}`
|
|
540
|
-
})),
|
|
541
|
-
)
|
|
542
|
-
const response = await Http.fetch(handler, { path: "/test" })
|
|
543
|
-
|
|
544
|
-
test
|
|
545
|
-
.expect(await response.text())
|
|
546
|
-
.toBe("a=1,b=2")
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
test.it("later middleware can access earlier context", async () => {
|
|
550
|
-
const handler = RouteHttp.toWebHandler(
|
|
551
|
-
Route
|
|
552
|
-
.use(Route.filter({ context: { x: 10 } }))
|
|
553
|
-
.use(Route.filter(function*(ctx) {
|
|
554
|
-
return { context: { doubled: ctx.x * 2 } }
|
|
555
|
-
}))
|
|
556
|
-
.get(Route.text(function*(ctx) {
|
|
557
|
-
return `doubled=${ctx.doubled}`
|
|
558
|
-
})),
|
|
559
|
-
)
|
|
560
|
-
const response = await Http.fetch(handler, { path: "/test" })
|
|
561
|
-
|
|
562
|
-
test
|
|
563
|
-
.expect(await response.text())
|
|
564
|
-
.toBe("doubled=20")
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
test.it("middleware error short-circuits chain", () =>
|
|
568
|
-
Effect
|
|
569
|
-
.gen(function*() {
|
|
570
|
-
const runtime = yield* Effect.runtime<TestLogger.TestLogger>()
|
|
571
|
-
const handler = RouteHttp.toWebHandlerRuntime(runtime)(
|
|
572
|
-
Route
|
|
573
|
-
.use(Route.filter(function*() {
|
|
574
|
-
return yield* Effect.fail(new Error("middleware failed"))
|
|
575
|
-
}))
|
|
576
|
-
.get(Route.text("should not reach")),
|
|
577
|
-
)
|
|
578
|
-
const response = yield* Effect.promise(() =>
|
|
579
|
-
Http.fetch(handler, { path: "/test" })
|
|
580
|
-
)
|
|
581
|
-
|
|
582
|
-
test
|
|
583
|
-
.expect(response.status)
|
|
584
|
-
.toBe(500)
|
|
585
|
-
test
|
|
586
|
-
.expect(yield* Effect.promise(() => response.text()))
|
|
587
|
-
.toContain("middleware failed")
|
|
588
|
-
|
|
589
|
-
const messages = yield* TestLogger.messages
|
|
590
|
-
test
|
|
591
|
-
.expect(messages.some((m) => m.includes("middleware failed")))
|
|
592
|
-
.toBe(true)
|
|
593
|
-
})
|
|
594
|
-
.pipe(Effect.provide(TestLogger.layer()), Effect.runPromise))
|
|
595
|
-
|
|
596
|
-
test.it("applies middleware to all methods", async () => {
|
|
597
|
-
const handler = RouteHttp.toWebHandler(
|
|
598
|
-
Route
|
|
599
|
-
.use(Route.filter({ context: { shared: true } }))
|
|
600
|
-
.get(Route.text(function*(ctx) {
|
|
601
|
-
return `GET:${ctx.shared}`
|
|
602
|
-
}))
|
|
603
|
-
.post(Route.text(function*(ctx) {
|
|
604
|
-
return `POST:${ctx.shared}`
|
|
605
|
-
})),
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
const getResponse = await Http.fetch(handler, {
|
|
609
|
-
path: "/test",
|
|
610
|
-
method: "GET",
|
|
611
|
-
})
|
|
612
|
-
test
|
|
613
|
-
.expect(await getResponse.text())
|
|
614
|
-
.toBe("GET:true")
|
|
615
|
-
|
|
616
|
-
const postResponse = await Http.fetch(handler, {
|
|
617
|
-
path: "/test",
|
|
618
|
-
method: "POST",
|
|
619
|
-
})
|
|
620
|
-
test
|
|
621
|
-
.expect(await postResponse.text())
|
|
622
|
-
.toBe("POST:true")
|
|
623
|
-
})
|
|
624
|
-
|
|
625
|
-
test.it("method-specific middleware enriches context for that method", async () => {
|
|
626
|
-
const handler = RouteHttp.toWebHandler(
|
|
627
|
-
Route.get(
|
|
628
|
-
Route.filter({ context: { methodSpecific: true } }),
|
|
629
|
-
Route.text(function*(ctx) {
|
|
630
|
-
return `methodSpecific=${ctx.methodSpecific}`
|
|
631
|
-
}),
|
|
632
|
-
),
|
|
633
|
-
)
|
|
634
|
-
const response = await Http.fetch(handler, { path: "/test" })
|
|
635
|
-
|
|
636
|
-
test
|
|
637
|
-
.expect(await response.text())
|
|
638
|
-
.toBe("methodSpecific=true")
|
|
639
|
-
})
|
|
640
|
-
|
|
641
|
-
test.it("wildcard and method-specific middlewares compose in order", async () => {
|
|
642
|
-
const handler = RouteHttp.toWebHandler(
|
|
643
|
-
Route
|
|
644
|
-
.use(Route.filter({ context: { a: 1 } }))
|
|
645
|
-
.get(
|
|
646
|
-
Route.filter({ context: { b: 2 } }),
|
|
647
|
-
Route.text(function*(ctx) {
|
|
648
|
-
return `a=${ctx.a},b=${ctx.b}`
|
|
649
|
-
}),
|
|
650
|
-
),
|
|
651
|
-
)
|
|
652
|
-
const response = await Http.fetch(handler, { path: "/test" })
|
|
653
|
-
|
|
654
|
-
test
|
|
655
|
-
.expect(await response.text())
|
|
656
|
-
.toBe("a=1,b=2")
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
test.it("method-specific middleware only affects its method", async () => {
|
|
660
|
-
const handler = RouteHttp.toWebHandler(
|
|
661
|
-
Route
|
|
662
|
-
.get(
|
|
663
|
-
Route.filter({ context: { getOnly: true } }),
|
|
664
|
-
Route.text(function*(ctx) {
|
|
665
|
-
return `GET:${ctx.getOnly}`
|
|
666
|
-
}),
|
|
667
|
-
)
|
|
668
|
-
.post(Route.text(function*(ctx) {
|
|
669
|
-
return `POST:${(ctx as any).getOnly}`
|
|
670
|
-
})),
|
|
671
|
-
)
|
|
672
|
-
|
|
673
|
-
const getResponse = await Http.fetch(handler, {
|
|
674
|
-
path: "/test",
|
|
675
|
-
method: "GET",
|
|
676
|
-
})
|
|
677
|
-
test
|
|
678
|
-
.expect(await getResponse.text())
|
|
679
|
-
.toBe("GET:true")
|
|
680
|
-
|
|
681
|
-
const postResponse = await Http.fetch(handler, {
|
|
682
|
-
path: "/test",
|
|
683
|
-
method: "POST",
|
|
684
|
-
})
|
|
685
|
-
test
|
|
686
|
-
.expect(await postResponse.text())
|
|
687
|
-
.toBe("POST:undefined")
|
|
688
|
-
})
|
|
689
|
-
|
|
690
|
-
test.it("json middleware wraps json response content", async () => {
|
|
691
|
-
const handler = RouteHttp.toWebHandler(
|
|
692
|
-
Route
|
|
693
|
-
.use(
|
|
694
|
-
Route.json(function*(_ctx, next) {
|
|
695
|
-
const value = yield* next().json
|
|
696
|
-
return { data: value }
|
|
697
|
-
}),
|
|
698
|
-
)
|
|
699
|
-
.get(
|
|
700
|
-
Route.json({ message: "hello", count: 42 }),
|
|
701
|
-
),
|
|
702
|
-
)
|
|
703
|
-
const response = await Http.fetch(handler, { path: "/test" })
|
|
704
|
-
|
|
705
|
-
test
|
|
706
|
-
.expect(response.status)
|
|
707
|
-
.toBe(200)
|
|
708
|
-
test
|
|
709
|
-
.expect(response.headers.get("Content-Type"))
|
|
710
|
-
.toBe("application/json")
|
|
711
|
-
test
|
|
712
|
-
.expect(await response.json())
|
|
713
|
-
.toEqual({ data: { message: "hello", count: 42 } })
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
test.it("multiple json middlewares compose in order", async () => {
|
|
717
|
-
const handler = RouteHttp.toWebHandler(
|
|
718
|
-
Route
|
|
719
|
-
.use(
|
|
720
|
-
Route.json(function*(_ctx, next) {
|
|
721
|
-
const value = yield* next().json
|
|
722
|
-
return { outer: value }
|
|
723
|
-
}),
|
|
724
|
-
)
|
|
725
|
-
.use(
|
|
726
|
-
Route.json(function*(_ctx, next) {
|
|
727
|
-
const value = yield* next().json
|
|
728
|
-
return { inner: value }
|
|
729
|
-
}),
|
|
730
|
-
)
|
|
731
|
-
.get(
|
|
732
|
-
Route.json({ original: true }),
|
|
733
|
-
),
|
|
734
|
-
)
|
|
735
|
-
const response = await Http.fetch(handler, { path: "/test" })
|
|
736
|
-
|
|
737
|
-
test
|
|
738
|
-
.expect(await response.json())
|
|
739
|
-
.toEqual({ outer: { inner: { original: true } } })
|
|
740
|
-
})
|
|
741
|
-
|
|
742
|
-
test.it("json middleware passes through non-json responses", async () => {
|
|
743
|
-
const handler = RouteHttp.toWebHandler(
|
|
744
|
-
Route
|
|
745
|
-
.use(
|
|
746
|
-
Route.json(function*(_ctx, next) {
|
|
747
|
-
const value = yield* next().json
|
|
748
|
-
return { wrapped: value }
|
|
749
|
-
}),
|
|
750
|
-
)
|
|
751
|
-
.get(Route.json({ type: "json" }))
|
|
752
|
-
.get(Route.text("plain text")),
|
|
753
|
-
)
|
|
754
|
-
|
|
755
|
-
const textResponse = await Http.fetch(handler, {
|
|
756
|
-
path: "/test",
|
|
757
|
-
headers: { Accept: "text/plain" },
|
|
758
|
-
})
|
|
759
|
-
test
|
|
760
|
-
.expect(textResponse.headers.get("Content-Type"))
|
|
761
|
-
.toBe("text/plain; charset=utf-8")
|
|
762
|
-
test
|
|
763
|
-
.expect(await textResponse.text())
|
|
764
|
-
.toBe("plain text")
|
|
765
|
-
|
|
766
|
-
const jsonResponse = await Http.fetch(handler, {
|
|
767
|
-
path: "/test",
|
|
768
|
-
headers: { Accept: "application/json" },
|
|
769
|
-
})
|
|
770
|
-
test
|
|
771
|
-
.expect(await jsonResponse.json())
|
|
772
|
-
.toEqual({ wrapped: { type: "json" } })
|
|
773
|
-
})
|
|
774
|
-
|
|
775
|
-
test.it("text middleware wraps text response content", async () => {
|
|
776
|
-
const handler = RouteHttp.toWebHandler(
|
|
777
|
-
Route
|
|
778
|
-
.use(
|
|
779
|
-
Route.text(function*(_ctx, next) {
|
|
780
|
-
const value = yield* next().text
|
|
781
|
-
return `wrapped: ${value}`
|
|
782
|
-
}),
|
|
783
|
-
)
|
|
784
|
-
.get(Route.text("hello")),
|
|
785
|
-
)
|
|
786
|
-
const response = await Http.fetch(handler, { path: "/test" })
|
|
787
|
-
|
|
788
|
-
test
|
|
789
|
-
.expect(response.headers.get("Content-Type"))
|
|
790
|
-
.toBe("text/plain; charset=utf-8")
|
|
791
|
-
test
|
|
792
|
-
.expect(await response.text())
|
|
793
|
-
.toBe("wrapped: hello")
|
|
794
|
-
})
|
|
795
|
-
|
|
796
|
-
test.it("html middleware wraps html response content", async () => {
|
|
797
|
-
const handler = RouteHttp.toWebHandler(
|
|
798
|
-
Route
|
|
799
|
-
.use(
|
|
800
|
-
Route.html(function*(_ctx, next) {
|
|
801
|
-
const value = yield* next().text
|
|
802
|
-
return `<div>${value}</div>`
|
|
803
|
-
}),
|
|
804
|
-
)
|
|
805
|
-
.get(Route.html("<span>content</span>")),
|
|
806
|
-
)
|
|
807
|
-
const response = await Http.fetch(handler, { path: "/test" })
|
|
808
|
-
|
|
809
|
-
test
|
|
810
|
-
.expect(response.headers.get("Content-Type"))
|
|
811
|
-
.toBe("text/html; charset=utf-8")
|
|
812
|
-
test
|
|
813
|
-
.expect(await response.text())
|
|
814
|
-
.toBe("<div><span>content</span></div>")
|
|
815
|
-
})
|
|
816
|
-
|
|
817
|
-
test.it("bytes middleware wraps bytes response content", async () => {
|
|
818
|
-
const encoder = new TextEncoder()
|
|
819
|
-
const decoder = new TextDecoder()
|
|
820
|
-
|
|
821
|
-
const handler = RouteHttp.toWebHandler(
|
|
822
|
-
Route
|
|
823
|
-
.use(
|
|
824
|
-
Route.bytes(function*(_ctx, next) {
|
|
825
|
-
const value = yield* next().bytes
|
|
826
|
-
const text = decoder.decode(value)
|
|
827
|
-
return encoder.encode(`wrapped:${text}`)
|
|
828
|
-
}),
|
|
829
|
-
)
|
|
830
|
-
.get(Route.bytes(encoder.encode("data"))),
|
|
831
|
-
)
|
|
832
|
-
const response = await Http.fetch(handler, { path: "/test" })
|
|
833
|
-
|
|
834
|
-
test
|
|
835
|
-
.expect(response.headers.get("Content-Type"))
|
|
836
|
-
.toBe("application/octet-stream")
|
|
837
|
-
test
|
|
838
|
-
.expect(await response.text())
|
|
839
|
-
.toBe("wrapped:data")
|
|
840
|
-
})
|
|
841
|
-
|
|
842
|
-
test.it("chains middlewares in order", async () => {
|
|
843
|
-
const calls: string[] = []
|
|
844
|
-
|
|
845
|
-
const handler = RouteHttp.toWebHandler(
|
|
846
|
-
Route
|
|
847
|
-
.use(
|
|
848
|
-
// always called
|
|
849
|
-
Route.filter({
|
|
850
|
-
context: {
|
|
851
|
-
name: "Johnny",
|
|
852
|
-
},
|
|
853
|
-
}),
|
|
854
|
-
// called 1st
|
|
855
|
-
// next is related handler with same format (here format="text" descriptor)
|
|
856
|
-
Route.text(function*(_ctx, next) {
|
|
857
|
-
calls.push("wildcard text 1")
|
|
858
|
-
return "1st layout: " + (yield* next().text)
|
|
859
|
-
}),
|
|
860
|
-
// never called because it's unrelated (different format descriptor)
|
|
861
|
-
Route.json(function*(_ctx, next) {
|
|
862
|
-
calls.push("wildcard json")
|
|
863
|
-
return { data: yield* next().json }
|
|
864
|
-
}),
|
|
865
|
-
// called 2nd
|
|
866
|
-
// no other related handler in the same method,
|
|
867
|
-
// continue traversing RouteHttp middleware chain
|
|
868
|
-
Route.text(function*(_ctx, next) {
|
|
869
|
-
calls.push("wildcard text 2")
|
|
870
|
-
return "2nd layout: " + (yield* next().text)
|
|
871
|
-
}),
|
|
872
|
-
)
|
|
873
|
-
.get(
|
|
874
|
-
// never called because doesn't pass content negotiation check in RouteHttp middleware
|
|
875
|
-
Route.json(function*(_ctx) {
|
|
876
|
-
calls.push("method json")
|
|
877
|
-
return { ok: true }
|
|
878
|
-
}),
|
|
879
|
-
// called 3rd
|
|
880
|
-
Route.text(function*(_ctx, next) {
|
|
881
|
-
calls.push("method text 1")
|
|
882
|
-
return "Prefix: " + (yield* next().text)
|
|
883
|
-
}),
|
|
884
|
-
// called 4th - terminal, no next() call
|
|
885
|
-
Route.text(function*(ctx) {
|
|
886
|
-
calls.push("method text 2")
|
|
887
|
-
return `Hello, ${ctx.name}`
|
|
888
|
-
}),
|
|
889
|
-
),
|
|
890
|
-
)
|
|
891
|
-
|
|
892
|
-
const response = await Http.fetch(handler, {
|
|
893
|
-
path: "/test",
|
|
894
|
-
headers: { Accept: "text/plain" },
|
|
895
|
-
})
|
|
896
|
-
|
|
897
|
-
test
|
|
898
|
-
.expect(calls)
|
|
899
|
-
.toEqual([
|
|
900
|
-
"wildcard text 1",
|
|
901
|
-
"wildcard text 2",
|
|
902
|
-
"method text 1",
|
|
903
|
-
"method text 2",
|
|
904
|
-
])
|
|
905
|
-
|
|
906
|
-
test
|
|
907
|
-
.expect(response.status)
|
|
908
|
-
.toBe(200)
|
|
909
|
-
test
|
|
910
|
-
.expect(response.headers.get("Content-Type"))
|
|
911
|
-
.toBe("text/plain; charset=utf-8")
|
|
912
|
-
test
|
|
913
|
-
.expect(await response.text())
|
|
914
|
-
.toBe("1st layout: 2nd layout: Prefix: Hello, Johnny")
|
|
915
|
-
})
|
|
916
|
-
|
|
917
|
-
test.it("schema headers parsing works with HttpServerRequest service", async () => {
|
|
918
|
-
const handler = RouteHttp.toWebHandler(
|
|
919
|
-
Route.get(
|
|
920
|
-
RouteSchema.schemaHeaders(
|
|
921
|
-
Schema.Struct({
|
|
922
|
-
"x-test": Schema.String,
|
|
923
|
-
}),
|
|
924
|
-
),
|
|
925
|
-
Route.text(function*(ctx) {
|
|
926
|
-
return `header=${ctx.headers["x-test"]}`
|
|
927
|
-
}),
|
|
928
|
-
),
|
|
929
|
-
)
|
|
930
|
-
const response = await Http.fetch(handler, {
|
|
931
|
-
path: "/test",
|
|
932
|
-
headers: { "x-test": "test-value" },
|
|
933
|
-
})
|
|
934
|
-
|
|
935
|
-
test
|
|
936
|
-
.expect(response.status)
|
|
937
|
-
.toBe(200)
|
|
938
|
-
test
|
|
939
|
-
.expect(await response.text())
|
|
940
|
-
.toBe("header=test-value")
|
|
941
|
-
})
|
|
942
|
-
|
|
943
|
-
test.it("merges headers", async () => {
|
|
944
|
-
const handler = RouteHttp.toWebHandler(
|
|
945
|
-
Route
|
|
946
|
-
.use(RouteSchema.schemaHeaders(
|
|
947
|
-
Schema.Struct({
|
|
948
|
-
"x-shared": Schema.String,
|
|
949
|
-
}),
|
|
950
|
-
))
|
|
951
|
-
.get(
|
|
952
|
-
RouteSchema.schemaHeaders(
|
|
953
|
-
Schema.Struct({
|
|
954
|
-
"x-get-only": Schema.String,
|
|
955
|
-
}),
|
|
956
|
-
),
|
|
957
|
-
Route.text(function*(ctx) {
|
|
958
|
-
return `shared=${ctx.headers["x-shared"]},getOnly=${
|
|
959
|
-
ctx.headers["x-get-only"]
|
|
960
|
-
}`
|
|
961
|
-
}),
|
|
962
|
-
),
|
|
963
|
-
)
|
|
964
|
-
const response = await Http.fetch(handler, {
|
|
965
|
-
path: "/test",
|
|
966
|
-
headers: {
|
|
967
|
-
"x-shared": "shared-value",
|
|
968
|
-
"x-get-only": "get-value",
|
|
969
|
-
},
|
|
970
|
-
})
|
|
971
|
-
|
|
972
|
-
test
|
|
973
|
-
.expect(response.status)
|
|
974
|
-
.toBe(200)
|
|
975
|
-
test
|
|
976
|
-
.expect(await response.text())
|
|
977
|
-
.toBe("shared=shared-value,getOnly=get-value")
|
|
978
|
-
})
|
|
979
|
-
})
|
|
980
|
-
|
|
981
|
-
test.describe("toWebHandler type constraints", () => {
|
|
982
|
-
test.it("accepts routes with method", () => {
|
|
983
|
-
RouteHttp.toWebHandler(Route.get(Route.text("hello")))
|
|
984
|
-
})
|
|
985
|
-
|
|
986
|
-
test.it("accepts multiple routes with methods", () => {
|
|
987
|
-
RouteHttp.toWebHandler(
|
|
988
|
-
Route.get(Route.text("hello")).post(Route.text("world")),
|
|
989
|
-
)
|
|
990
|
-
})
|
|
991
|
-
|
|
992
|
-
test.it("rejects routes without method", () => {
|
|
993
|
-
const noMethod = Route.empty.pipe(Route.text("hello"))
|
|
994
|
-
// @ts-expect-error
|
|
995
|
-
RouteHttp.toWebHandler(noMethod)
|
|
996
|
-
})
|
|
997
|
-
|
|
998
|
-
test.it("rejects mixed routes where one has method and one doesn't", () => {
|
|
999
|
-
const withMethod = Route.get(Route.text("hello"))
|
|
1000
|
-
const withoutMethod = Route.empty.pipe(Route.text("hello"))
|
|
1001
|
-
const mixed = [...withMethod, ...withoutMethod] as const
|
|
1002
|
-
// @ts-expect-error
|
|
1003
|
-
RouteHttp.toWebHandler(mixed)
|
|
1004
|
-
})
|
|
1005
|
-
})
|
|
1006
|
-
|
|
1007
|
-
test.describe("streaming responses", () => {
|
|
1008
|
-
test.it("streams text response", async () => {
|
|
1009
|
-
const handler = RouteHttp.toWebHandler(
|
|
1010
|
-
Route.get(
|
|
1011
|
-
Route.text(function*() {
|
|
1012
|
-
return Stream.make("Hello", " ", "World")
|
|
1013
|
-
}),
|
|
1014
|
-
),
|
|
1015
|
-
)
|
|
1016
|
-
const response = await Http.fetch(handler, { path: "/stream" })
|
|
1017
|
-
|
|
1018
|
-
test
|
|
1019
|
-
.expect(response.headers.get("Content-Type"))
|
|
1020
|
-
.toBe("text/plain; charset=utf-8")
|
|
1021
|
-
test
|
|
1022
|
-
.expect(await response.text())
|
|
1023
|
-
.toBe("Hello World")
|
|
1024
|
-
})
|
|
1025
|
-
|
|
1026
|
-
test.it("streams html response", async () => {
|
|
1027
|
-
const handler = RouteHttp.toWebHandler(
|
|
1028
|
-
Route.get(
|
|
1029
|
-
Route.html(function*() {
|
|
1030
|
-
return Stream.make("<div>", "content", "</div>")
|
|
1031
|
-
}),
|
|
1032
|
-
),
|
|
1033
|
-
)
|
|
1034
|
-
const response = await Http.fetch(handler, { path: "/stream" })
|
|
1035
|
-
|
|
1036
|
-
test
|
|
1037
|
-
.expect(response.headers.get("Content-Type"))
|
|
1038
|
-
.toBe("text/html; charset=utf-8")
|
|
1039
|
-
test
|
|
1040
|
-
.expect(await response.text())
|
|
1041
|
-
.toBe("<div>content</div>")
|
|
1042
|
-
})
|
|
1043
|
-
|
|
1044
|
-
test.it("streams bytes response", async () => {
|
|
1045
|
-
const encoder = new TextEncoder()
|
|
1046
|
-
const handler = RouteHttp.toWebHandler(
|
|
1047
|
-
Route.get(
|
|
1048
|
-
Route.bytes(function*() {
|
|
1049
|
-
return Stream.make(
|
|
1050
|
-
encoder.encode("chunk1"),
|
|
1051
|
-
encoder.encode("chunk2"),
|
|
1052
|
-
)
|
|
1053
|
-
}),
|
|
1054
|
-
),
|
|
1055
|
-
)
|
|
1056
|
-
const response = await Http.fetch(handler, { path: "/stream" })
|
|
1057
|
-
|
|
1058
|
-
test
|
|
1059
|
-
.expect(response.headers.get("Content-Type"))
|
|
1060
|
-
.toBe("application/octet-stream")
|
|
1061
|
-
test
|
|
1062
|
-
.expect(await response.text())
|
|
1063
|
-
.toBe("chunk1chunk2")
|
|
1064
|
-
})
|
|
1065
|
-
|
|
1066
|
-
test.it("handles stream errors gracefully", async () => {
|
|
1067
|
-
const handler = RouteHttp.toWebHandler(
|
|
1068
|
-
Route.get(
|
|
1069
|
-
Route.text(function*() {
|
|
1070
|
-
return Stream.make("start").pipe(
|
|
1071
|
-
Stream.concat(Stream.fail(new Error("stream error"))),
|
|
1072
|
-
)
|
|
1073
|
-
}),
|
|
1074
|
-
),
|
|
1075
|
-
)
|
|
1076
|
-
const response = await Http.fetch(handler, { path: "/error" })
|
|
1077
|
-
|
|
1078
|
-
test
|
|
1079
|
-
.expect(response.status)
|
|
1080
|
-
.toBe(200)
|
|
1081
|
-
|
|
1082
|
-
await test
|
|
1083
|
-
.expect(response.text())
|
|
1084
|
-
.rejects
|
|
1085
|
-
.toThrow("stream error")
|
|
1086
|
-
})
|
|
1087
|
-
})
|
|
1088
|
-
|
|
1089
|
-
test.describe("schema handlers", () => {
|
|
1090
|
-
test.it("parses headers, cookies, and search params together", async () => {
|
|
1091
|
-
const handler = RouteHttp.toWebHandler(
|
|
1092
|
-
Route.get(
|
|
1093
|
-
RouteSchema.schemaHeaders(
|
|
1094
|
-
Schema.Struct({
|
|
1095
|
-
"x-api-key": Schema.String,
|
|
1096
|
-
}),
|
|
1097
|
-
),
|
|
1098
|
-
RouteSchema.schemaCookies(
|
|
1099
|
-
Schema.Struct({
|
|
1100
|
-
session: Schema.String,
|
|
1101
|
-
}),
|
|
1102
|
-
),
|
|
1103
|
-
RouteSchema.schemaSearchParams(
|
|
1104
|
-
Schema.Struct({
|
|
1105
|
-
page: Schema.NumberFromString,
|
|
1106
|
-
limit: Schema.optional(Schema.NumberFromString),
|
|
1107
|
-
}),
|
|
1108
|
-
),
|
|
1109
|
-
Route.json(function*(ctx) {
|
|
1110
|
-
return {
|
|
1111
|
-
apiKey: ctx.headers["x-api-key"],
|
|
1112
|
-
session: ctx.cookies.session,
|
|
1113
|
-
page: ctx.searchParams.page,
|
|
1114
|
-
limit: ctx.searchParams.limit,
|
|
1115
|
-
}
|
|
1116
|
-
}),
|
|
1117
|
-
),
|
|
1118
|
-
)
|
|
1119
|
-
|
|
1120
|
-
const response = await Http.fetch(handler, {
|
|
1121
|
-
path: "/test?page=2&limit=10",
|
|
1122
|
-
headers: {
|
|
1123
|
-
"x-api-key": "secret-key",
|
|
1124
|
-
cookie: "session=abc123",
|
|
1125
|
-
},
|
|
1126
|
-
})
|
|
1127
|
-
|
|
1128
|
-
test
|
|
1129
|
-
.expect(response.status)
|
|
1130
|
-
.toBe(200)
|
|
1131
|
-
test
|
|
1132
|
-
.expect(await response.json())
|
|
1133
|
-
.toEqual({
|
|
1134
|
-
apiKey: "secret-key",
|
|
1135
|
-
session: "abc123",
|
|
1136
|
-
page: 2,
|
|
1137
|
-
limit: 10,
|
|
1138
|
-
})
|
|
1139
|
-
})
|
|
1140
|
-
|
|
1141
|
-
test.it("parses JSON body with headers", async () => {
|
|
1142
|
-
const handler = RouteHttp.toWebHandler(
|
|
1143
|
-
Route.post(
|
|
1144
|
-
RouteSchema.schemaHeaders(
|
|
1145
|
-
Schema.Struct({
|
|
1146
|
-
"content-type": Schema.String,
|
|
1147
|
-
}),
|
|
1148
|
-
),
|
|
1149
|
-
RouteSchema.schemaBodyJson(
|
|
1150
|
-
Schema.Struct({
|
|
1151
|
-
name: Schema.String,
|
|
1152
|
-
age: Schema.Number,
|
|
1153
|
-
}),
|
|
1154
|
-
),
|
|
1155
|
-
Route.json(function*(ctx) {
|
|
1156
|
-
return {
|
|
1157
|
-
contentType: ctx.headers["content-type"],
|
|
1158
|
-
name: ctx.body.name,
|
|
1159
|
-
age: ctx.body.age,
|
|
1160
|
-
}
|
|
1161
|
-
}),
|
|
1162
|
-
),
|
|
1163
|
-
)
|
|
1164
|
-
|
|
1165
|
-
const response = await Http.fetch(handler, {
|
|
1166
|
-
path: "/users",
|
|
1167
|
-
method: "POST",
|
|
1168
|
-
headers: { "Content-Type": "application/json" },
|
|
1169
|
-
body: JSON.stringify({ name: "Alice", age: 30 }),
|
|
1170
|
-
})
|
|
1171
|
-
|
|
1172
|
-
test
|
|
1173
|
-
.expect(response.status)
|
|
1174
|
-
.toBe(200)
|
|
1175
|
-
test
|
|
1176
|
-
.expect(await response.json())
|
|
1177
|
-
.toEqual({
|
|
1178
|
-
contentType: "application/json",
|
|
1179
|
-
name: "Alice",
|
|
1180
|
-
age: 30,
|
|
1181
|
-
})
|
|
1182
|
-
})
|
|
1183
|
-
|
|
1184
|
-
test.it("parses URL-encoded body", async () => {
|
|
1185
|
-
const handler = RouteHttp.toWebHandler(
|
|
1186
|
-
Route.post(
|
|
1187
|
-
RouteSchema.schemaBodyUrlParams(
|
|
1188
|
-
Schema.Struct({
|
|
1189
|
-
username: Schema.String,
|
|
1190
|
-
password: Schema.String,
|
|
1191
|
-
}),
|
|
1192
|
-
),
|
|
1193
|
-
Route.json(function*(ctx) {
|
|
1194
|
-
return {
|
|
1195
|
-
username: ctx.body.username,
|
|
1196
|
-
hasPassword: ctx.body.password.length > 0,
|
|
1197
|
-
}
|
|
1198
|
-
}),
|
|
1199
|
-
),
|
|
1200
|
-
)
|
|
1201
|
-
|
|
1202
|
-
const response = await Http.fetch(handler, {
|
|
1203
|
-
path: "/login",
|
|
1204
|
-
method: "POST",
|
|
1205
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1206
|
-
body: "username=alice&password=secret",
|
|
1207
|
-
})
|
|
1208
|
-
|
|
1209
|
-
test
|
|
1210
|
-
.expect(response.status)
|
|
1211
|
-
.toBe(200)
|
|
1212
|
-
test
|
|
1213
|
-
.expect(await response.json())
|
|
1214
|
-
.toEqual({
|
|
1215
|
-
username: "alice",
|
|
1216
|
-
hasPassword: true,
|
|
1217
|
-
})
|
|
1218
|
-
})
|
|
1219
|
-
|
|
1220
|
-
test.it("returns 400 on schema validation failure", () =>
|
|
1221
|
-
Effect
|
|
1222
|
-
.gen(function*() {
|
|
1223
|
-
const runtime = yield* Effect.runtime<TestLogger.TestLogger>()
|
|
1224
|
-
const handler = RouteHttp.toWebHandlerRuntime(runtime)(
|
|
1225
|
-
Route.get(
|
|
1226
|
-
RouteSchema.schemaSearchParams(
|
|
1227
|
-
Schema.Struct({
|
|
1228
|
-
count: Schema.NumberFromString,
|
|
1229
|
-
}),
|
|
1230
|
-
),
|
|
1231
|
-
Route.text("ok"),
|
|
1232
|
-
),
|
|
1233
|
-
)
|
|
1234
|
-
|
|
1235
|
-
const response = yield* Effect.promise(() =>
|
|
1236
|
-
Http.fetch(handler, { path: "/test?count=not-a-number" })
|
|
1237
|
-
)
|
|
1238
|
-
|
|
1239
|
-
test
|
|
1240
|
-
.expect(response.status)
|
|
1241
|
-
.toBe(400)
|
|
1242
|
-
|
|
1243
|
-
const messages = yield* TestLogger.messages
|
|
1244
|
-
test
|
|
1245
|
-
.expect(messages.some((m) => m.includes("ParseError")))
|
|
1246
|
-
.toBe(true)
|
|
1247
|
-
})
|
|
1248
|
-
.pipe(Effect.provide(TestLogger.layer()), Effect.runPromise))
|
|
1249
|
-
|
|
1250
|
-
test.it("handles missing required fields", () =>
|
|
1251
|
-
Effect
|
|
1252
|
-
.gen(function*() {
|
|
1253
|
-
const runtime = yield* Effect.runtime<TestLogger.TestLogger>()
|
|
1254
|
-
const handler = RouteHttp.toWebHandlerRuntime(runtime)(
|
|
1255
|
-
Route.get(
|
|
1256
|
-
RouteSchema.schemaHeaders(
|
|
1257
|
-
Schema.Struct({
|
|
1258
|
-
"x-required": Schema.String,
|
|
1259
|
-
}),
|
|
1260
|
-
),
|
|
1261
|
-
Route.text("ok"),
|
|
1262
|
-
),
|
|
1263
|
-
)
|
|
1264
|
-
|
|
1265
|
-
const response = yield* Effect.promise(() =>
|
|
1266
|
-
Http.fetch(handler, { path: "/test" })
|
|
1267
|
-
)
|
|
1268
|
-
|
|
1269
|
-
test
|
|
1270
|
-
.expect(response.status)
|
|
1271
|
-
.toBe(400)
|
|
1272
|
-
|
|
1273
|
-
const messages = yield* TestLogger.messages
|
|
1274
|
-
test
|
|
1275
|
-
.expect(messages.some((m) => m.includes("x-required")))
|
|
1276
|
-
.toBe(true)
|
|
1277
|
-
})
|
|
1278
|
-
.pipe(Effect.provide(TestLogger.layer()), Effect.runPromise))
|
|
1279
|
-
|
|
1280
|
-
test.it("parses multipart form data with file", async () => {
|
|
1281
|
-
const handler = RouteHttp.toWebHandler(
|
|
1282
|
-
Route.post(
|
|
1283
|
-
RouteSchema.schemaBodyMultipart(
|
|
1284
|
-
Schema.Struct({
|
|
1285
|
-
title: Schema.String,
|
|
1286
|
-
file: Schema.Array(RouteSchema.File),
|
|
1287
|
-
}),
|
|
1288
|
-
),
|
|
1289
|
-
Route.json(function*(ctx) {
|
|
1290
|
-
const file = ctx.body.file[0]
|
|
1291
|
-
return {
|
|
1292
|
-
title: ctx.body.title,
|
|
1293
|
-
fileName: file.name,
|
|
1294
|
-
contentType: file.contentType,
|
|
1295
|
-
size: file.content.length,
|
|
1296
|
-
}
|
|
1297
|
-
}),
|
|
1298
|
-
),
|
|
1299
|
-
)
|
|
1300
|
-
|
|
1301
|
-
const formData = new FormData()
|
|
1302
|
-
formData.append("title", "My Upload")
|
|
1303
|
-
formData.append(
|
|
1304
|
-
"file",
|
|
1305
|
-
new Blob(["hello world"], { type: "text/plain" }),
|
|
1306
|
-
"test.txt",
|
|
1307
|
-
)
|
|
1308
|
-
|
|
1309
|
-
const response = await Http.fetch(handler, {
|
|
1310
|
-
path: "/upload",
|
|
1311
|
-
method: "POST",
|
|
1312
|
-
body: formData,
|
|
1313
|
-
})
|
|
1314
|
-
|
|
1315
|
-
test
|
|
1316
|
-
.expect(response.status)
|
|
1317
|
-
.toBe(200)
|
|
1318
|
-
|
|
1319
|
-
const json = await response.json()
|
|
1320
|
-
test
|
|
1321
|
-
.expect(json.title)
|
|
1322
|
-
.toBe("My Upload")
|
|
1323
|
-
test
|
|
1324
|
-
.expect(json.fileName)
|
|
1325
|
-
.toBe("test.txt")
|
|
1326
|
-
test
|
|
1327
|
-
.expect(json.contentType)
|
|
1328
|
-
.toContain("text/plain")
|
|
1329
|
-
test
|
|
1330
|
-
.expect(json.size)
|
|
1331
|
-
.toBe(11)
|
|
1332
|
-
})
|
|
1333
|
-
|
|
1334
|
-
test.it("handles multiple files with same field name", async () => {
|
|
1335
|
-
const handler = RouteHttp.toWebHandler(
|
|
1336
|
-
Route.post(
|
|
1337
|
-
RouteSchema.schemaBodyMultipart(
|
|
1338
|
-
Schema.Struct({
|
|
1339
|
-
documents: Schema.Array(RouteSchema.File),
|
|
1340
|
-
}),
|
|
1341
|
-
),
|
|
1342
|
-
Route.json(function*(ctx) {
|
|
1343
|
-
return {
|
|
1344
|
-
count: ctx.body.documents.length,
|
|
1345
|
-
names: ctx.body.documents.map((f) => f.name),
|
|
1346
|
-
sizes: ctx.body.documents.map((f) => f.content.length),
|
|
1347
|
-
}
|
|
1348
|
-
}),
|
|
1349
|
-
),
|
|
1350
|
-
)
|
|
1351
|
-
|
|
1352
|
-
const formData = new FormData()
|
|
1353
|
-
formData.append(
|
|
1354
|
-
"documents",
|
|
1355
|
-
new Blob(["first file content"], { type: "text/plain" }),
|
|
1356
|
-
"doc1.txt",
|
|
1357
|
-
)
|
|
1358
|
-
formData.append(
|
|
1359
|
-
"documents",
|
|
1360
|
-
new Blob(["second file content"], { type: "text/plain" }),
|
|
1361
|
-
"doc2.txt",
|
|
1362
|
-
)
|
|
1363
|
-
formData.append(
|
|
1364
|
-
"documents",
|
|
1365
|
-
new Blob(["third file content"], { type: "text/plain" }),
|
|
1366
|
-
"doc3.txt",
|
|
1367
|
-
)
|
|
1368
|
-
|
|
1369
|
-
const response = await Http.fetch(handler, {
|
|
1370
|
-
path: "/upload",
|
|
1371
|
-
method: "POST",
|
|
1372
|
-
body: formData,
|
|
1373
|
-
})
|
|
1374
|
-
|
|
1375
|
-
test
|
|
1376
|
-
.expect(response.status)
|
|
1377
|
-
.toBe(200)
|
|
1378
|
-
|
|
1379
|
-
const json = await response.json()
|
|
1380
|
-
test
|
|
1381
|
-
.expect(json.count)
|
|
1382
|
-
.toBe(3)
|
|
1383
|
-
test
|
|
1384
|
-
.expect(json.names)
|
|
1385
|
-
.toEqual(["doc1.txt", "doc2.txt", "doc3.txt"])
|
|
1386
|
-
test
|
|
1387
|
-
.expect(json.sizes)
|
|
1388
|
-
.toEqual([18, 19, 18])
|
|
1389
|
-
})
|
|
1390
|
-
|
|
1391
|
-
test.it("handles single file upload", async () => {
|
|
1392
|
-
const handler = RouteHttp.toWebHandler(
|
|
1393
|
-
Route.post(
|
|
1394
|
-
RouteSchema.schemaBodyMultipart(
|
|
1395
|
-
Schema.Struct({
|
|
1396
|
-
image: Schema.Array(RouteSchema.File),
|
|
1397
|
-
}),
|
|
1398
|
-
),
|
|
1399
|
-
Route.json(function*(ctx) {
|
|
1400
|
-
const image = ctx.body.image[0]
|
|
1401
|
-
return {
|
|
1402
|
-
name: image.name,
|
|
1403
|
-
type: image.contentType,
|
|
1404
|
-
size: image.content.length,
|
|
1405
|
-
}
|
|
1406
|
-
}),
|
|
1407
|
-
),
|
|
1408
|
-
)
|
|
1409
|
-
|
|
1410
|
-
const formData = new FormData()
|
|
1411
|
-
formData.append(
|
|
1412
|
-
"image",
|
|
1413
|
-
new Blob(["fake image data"], { type: "image/png" }),
|
|
1414
|
-
"avatar.png",
|
|
1415
|
-
)
|
|
1416
|
-
|
|
1417
|
-
const response = await Http.fetch(handler, {
|
|
1418
|
-
path: "/upload",
|
|
1419
|
-
method: "POST",
|
|
1420
|
-
body: formData,
|
|
1421
|
-
})
|
|
1422
|
-
|
|
1423
|
-
test
|
|
1424
|
-
.expect(response.status)
|
|
1425
|
-
.toBe(200)
|
|
1426
|
-
|
|
1427
|
-
const json = await response.json()
|
|
1428
|
-
test
|
|
1429
|
-
.expect(json.name)
|
|
1430
|
-
.toBe("avatar.png")
|
|
1431
|
-
test
|
|
1432
|
-
.expect(json.type)
|
|
1433
|
-
.toContain("image/png")
|
|
1434
|
-
test
|
|
1435
|
-
.expect(json.size)
|
|
1436
|
-
.toBe(15)
|
|
1437
|
-
})
|
|
1438
|
-
|
|
1439
|
-
test.it("handles multiple string values for same field", async () => {
|
|
1440
|
-
const handler = RouteHttp.toWebHandler(
|
|
1441
|
-
Route.post(
|
|
1442
|
-
RouteSchema.schemaBodyMultipart(
|
|
1443
|
-
Schema.Struct({
|
|
1444
|
-
tags: Schema.Array(Schema.String),
|
|
1445
|
-
title: Schema.String,
|
|
1446
|
-
}),
|
|
1447
|
-
),
|
|
1448
|
-
Route.json(function*(ctx) {
|
|
1449
|
-
return {
|
|
1450
|
-
title: ctx.body.title,
|
|
1451
|
-
// Schema returns readonly array, but Json type expects mutable array
|
|
1452
|
-
tags: [...ctx.body.tags],
|
|
1453
|
-
}
|
|
1454
|
-
}),
|
|
1455
|
-
),
|
|
1456
|
-
)
|
|
1457
|
-
|
|
1458
|
-
const formData = new FormData()
|
|
1459
|
-
formData.append("title", "My Post")
|
|
1460
|
-
formData.append("tags", "javascript")
|
|
1461
|
-
formData.append("tags", "typescript")
|
|
1462
|
-
formData.append("tags", "effect")
|
|
1463
|
-
|
|
1464
|
-
const response = await Http.fetch(handler, {
|
|
1465
|
-
path: "/upload",
|
|
1466
|
-
method: "POST",
|
|
1467
|
-
body: formData,
|
|
1468
|
-
})
|
|
1469
|
-
|
|
1470
|
-
test
|
|
1471
|
-
.expect(response.status)
|
|
1472
|
-
.toBe(200)
|
|
1473
|
-
|
|
1474
|
-
const json = await response.json()
|
|
1475
|
-
test
|
|
1476
|
-
.expect(json.title)
|
|
1477
|
-
.toBe("My Post")
|
|
1478
|
-
test
|
|
1479
|
-
.expect(json.tags)
|
|
1480
|
-
.toEqual(["javascript", "typescript", "effect"])
|
|
1481
|
-
})
|
|
1482
|
-
|
|
1483
|
-
test.it("schema validation: single value with Schema.String succeeds", async () => {
|
|
1484
|
-
const handler = RouteHttp.toWebHandler(
|
|
1485
|
-
Route.post(
|
|
1486
|
-
RouteSchema.schemaBodyMultipart(
|
|
1487
|
-
Schema.Struct({
|
|
1488
|
-
name: Schema.String,
|
|
1489
|
-
}),
|
|
1490
|
-
),
|
|
1491
|
-
Route.json(function*(ctx) {
|
|
1492
|
-
return { name: ctx.body.name }
|
|
1493
|
-
}),
|
|
1494
|
-
),
|
|
1495
|
-
)
|
|
1496
|
-
|
|
1497
|
-
const formData = new FormData()
|
|
1498
|
-
formData.append("name", "John")
|
|
1499
|
-
|
|
1500
|
-
const response = await Http.fetch(handler, {
|
|
1501
|
-
path: "/test",
|
|
1502
|
-
method: "POST",
|
|
1503
|
-
body: formData,
|
|
1504
|
-
})
|
|
1505
|
-
|
|
1506
|
-
test
|
|
1507
|
-
.expect(response.status)
|
|
1508
|
-
.toBe(200)
|
|
1509
|
-
|
|
1510
|
-
const json = await response.json()
|
|
1511
|
-
test
|
|
1512
|
-
.expect(json.name)
|
|
1513
|
-
.toBe("John")
|
|
1514
|
-
})
|
|
1515
|
-
|
|
1516
|
-
test.it("schema validation: multiple values with Schema.String fails with detailed error", () =>
|
|
1517
|
-
Effect
|
|
1518
|
-
.gen(function*() {
|
|
1519
|
-
const runtime = yield* Effect.runtime<TestLogger.TestLogger>()
|
|
1520
|
-
const handler = RouteHttp.toWebHandlerRuntime(runtime)(
|
|
1521
|
-
Route.post(
|
|
1522
|
-
RouteSchema.schemaBodyMultipart(
|
|
1523
|
-
Schema.Struct({
|
|
1524
|
-
name: Schema.String,
|
|
1525
|
-
}),
|
|
1526
|
-
),
|
|
1527
|
-
Route.json(function*(ctx) {
|
|
1528
|
-
return { name: ctx.body.name }
|
|
1529
|
-
}),
|
|
1530
|
-
),
|
|
1531
|
-
)
|
|
1532
|
-
|
|
1533
|
-
const formData = new FormData()
|
|
1534
|
-
formData.append("name", "John")
|
|
1535
|
-
formData.append("name", "Jane")
|
|
1536
|
-
|
|
1537
|
-
const response = yield* Effect.promise(() =>
|
|
1538
|
-
Http.fetch(handler, {
|
|
1539
|
-
path: "/test",
|
|
1540
|
-
method: "POST",
|
|
1541
|
-
body: formData,
|
|
1542
|
-
})
|
|
1543
|
-
)
|
|
1544
|
-
|
|
1545
|
-
test
|
|
1546
|
-
.expect(response.status)
|
|
1547
|
-
.toBe(400)
|
|
1548
|
-
|
|
1549
|
-
const body = yield* Effect.promise(() => response.json())
|
|
1550
|
-
|
|
1551
|
-
test
|
|
1552
|
-
.expect(body.message)
|
|
1553
|
-
.toContain("ParseError")
|
|
1554
|
-
test
|
|
1555
|
-
.expect(body.message)
|
|
1556
|
-
.toContain("Expected string, actual [\"John\",\"Jane\"]")
|
|
1557
|
-
|
|
1558
|
-
const messages = yield* TestLogger.messages
|
|
1559
|
-
test
|
|
1560
|
-
.expect(messages.some((m) => m.includes("ParseError")))
|
|
1561
|
-
.toBe(true)
|
|
1562
|
-
})
|
|
1563
|
-
.pipe(Effect.provide(TestLogger.layer()), Effect.runPromise))
|
|
1564
|
-
|
|
1565
|
-
test.it("logs validation errors to console", () =>
|
|
1566
|
-
Effect
|
|
1567
|
-
.gen(function*() {
|
|
1568
|
-
const testLogger = yield* TestLogger.TestLogger
|
|
1569
|
-
const runtime = yield* Effect.runtime<TestLogger.TestLogger>()
|
|
1570
|
-
|
|
1571
|
-
const handler = RouteHttp.toWebHandlerRuntime(runtime)(
|
|
1572
|
-
Route.post(
|
|
1573
|
-
RouteSchema.schemaBodyMultipart(
|
|
1574
|
-
Schema.Struct({
|
|
1575
|
-
name: Schema.String,
|
|
1576
|
-
}),
|
|
1577
|
-
),
|
|
1578
|
-
Route.json(function*(ctx) {
|
|
1579
|
-
return { name: ctx.body.name }
|
|
1580
|
-
}),
|
|
1581
|
-
),
|
|
1582
|
-
)
|
|
1583
|
-
|
|
1584
|
-
const formData = new FormData()
|
|
1585
|
-
formData.append("name", "John")
|
|
1586
|
-
formData.append("name", "Jane")
|
|
1587
|
-
|
|
1588
|
-
yield* Effect.promise(() =>
|
|
1589
|
-
Http.fetch(handler, {
|
|
1590
|
-
path: "/test",
|
|
1591
|
-
method: "POST",
|
|
1592
|
-
body: formData,
|
|
1593
|
-
})
|
|
1594
|
-
)
|
|
1595
|
-
|
|
1596
|
-
const messages = yield* Ref.get(testLogger.messages)
|
|
1597
|
-
const errorLogs = messages.filter((msg) => msg.includes("[Error]"))
|
|
1598
|
-
|
|
1599
|
-
test
|
|
1600
|
-
.expect(errorLogs.length)
|
|
1601
|
-
.toBeGreaterThan(0)
|
|
1602
|
-
|
|
1603
|
-
test
|
|
1604
|
-
.expect(errorLogs[0])
|
|
1605
|
-
.toContain("ParseError")
|
|
1606
|
-
test
|
|
1607
|
-
.expect(errorLogs[0])
|
|
1608
|
-
.toContain("Expected string, actual [\"John\",\"Jane\"]")
|
|
1609
|
-
})
|
|
1610
|
-
.pipe(Effect.provide(TestLogger.layer()), Effect.runPromise))
|
|
1611
|
-
|
|
1612
|
-
test.it("composes shared middleware with method-specific schema", async () => {
|
|
1613
|
-
const handler = RouteHttp.toWebHandler(
|
|
1614
|
-
Route
|
|
1615
|
-
.use(RouteSchema.schemaHeaders(
|
|
1616
|
-
Schema.Struct({
|
|
1617
|
-
"x-api-version": Schema.String,
|
|
1618
|
-
}),
|
|
1619
|
-
))
|
|
1620
|
-
.post(
|
|
1621
|
-
RouteSchema.schemaBodyJson(
|
|
1622
|
-
Schema.Struct({
|
|
1623
|
-
action: Schema.String,
|
|
1624
|
-
}),
|
|
1625
|
-
),
|
|
1626
|
-
Route.json(function*(ctx) {
|
|
1627
|
-
return {
|
|
1628
|
-
version: ctx.headers["x-api-version"],
|
|
1629
|
-
action: ctx.body.action,
|
|
1630
|
-
}
|
|
1631
|
-
}),
|
|
1632
|
-
)
|
|
1633
|
-
.get(
|
|
1634
|
-
RouteSchema.schemaSearchParams(
|
|
1635
|
-
Schema.Struct({
|
|
1636
|
-
id: Schema.String,
|
|
1637
|
-
}),
|
|
1638
|
-
),
|
|
1639
|
-
Route.json(function*(ctx) {
|
|
1640
|
-
return {
|
|
1641
|
-
version: ctx.headers["x-api-version"],
|
|
1642
|
-
id: ctx.searchParams.id,
|
|
1643
|
-
}
|
|
1644
|
-
}),
|
|
1645
|
-
),
|
|
1646
|
-
)
|
|
1647
|
-
|
|
1648
|
-
const postResponse = await Http.fetch(handler, {
|
|
1649
|
-
path: "/api",
|
|
1650
|
-
method: "POST",
|
|
1651
|
-
headers: {
|
|
1652
|
-
"x-api-version": "v2",
|
|
1653
|
-
"Content-Type": "application/json",
|
|
1654
|
-
},
|
|
1655
|
-
body: JSON.stringify({ action: "create" }),
|
|
1656
|
-
})
|
|
1657
|
-
|
|
1658
|
-
test
|
|
1659
|
-
.expect(await postResponse.json())
|
|
1660
|
-
.toEqual({ version: "v2", action: "create" })
|
|
1661
|
-
|
|
1662
|
-
const getResponse = await Http.fetch(handler, {
|
|
1663
|
-
path: "/api?id=123",
|
|
1664
|
-
method: "GET",
|
|
1665
|
-
headers: { "x-api-version": "v2" },
|
|
1666
|
-
})
|
|
1667
|
-
|
|
1668
|
-
test
|
|
1669
|
-
.expect(await getResponse.json())
|
|
1670
|
-
.toEqual({ version: "v2", id: "123" })
|
|
1671
|
-
})
|
|
1672
|
-
|
|
1673
|
-
test.it("handles cookies with equals sign in value", async () => {
|
|
1674
|
-
const handler = RouteHttp.toWebHandler(
|
|
1675
|
-
Route.get(
|
|
1676
|
-
RouteSchema.schemaCookies(
|
|
1677
|
-
Schema.Struct({
|
|
1678
|
-
token: Schema.String,
|
|
1679
|
-
}),
|
|
1680
|
-
),
|
|
1681
|
-
Route.json(function*(ctx) {
|
|
1682
|
-
return { token: ctx.cookies.token }
|
|
1683
|
-
}),
|
|
1684
|
-
),
|
|
1685
|
-
)
|
|
1686
|
-
|
|
1687
|
-
const response = await Http.fetch(handler, {
|
|
1688
|
-
path: "/test",
|
|
1689
|
-
headers: { cookie: "token=abc=123==" },
|
|
1690
|
-
})
|
|
1691
|
-
|
|
1692
|
-
test
|
|
1693
|
-
.expect(response.status)
|
|
1694
|
-
.toBe(200)
|
|
1695
|
-
test
|
|
1696
|
-
.expect(await response.json())
|
|
1697
|
-
.toEqual({ token: "abc=123==" })
|
|
1698
|
-
})
|
|
1699
|
-
|
|
1700
|
-
test.it("handles multiple search params with same key", async () => {
|
|
1701
|
-
const handler = RouteHttp.toWebHandler(
|
|
1702
|
-
Route.get(
|
|
1703
|
-
RouteSchema.schemaSearchParams(
|
|
1704
|
-
Schema.Struct({
|
|
1705
|
-
tags: Schema.Array(Schema.String),
|
|
1706
|
-
}),
|
|
1707
|
-
),
|
|
1708
|
-
Route.json(function*(ctx) {
|
|
1709
|
-
return { tags: [...ctx.searchParams.tags] }
|
|
1710
|
-
}),
|
|
1711
|
-
),
|
|
1712
|
-
)
|
|
1713
|
-
|
|
1714
|
-
const response = await Http.fetch(handler, {
|
|
1715
|
-
path: "/test?tags=one&tags=two&tags=three",
|
|
1716
|
-
})
|
|
1717
|
-
|
|
1718
|
-
test
|
|
1719
|
-
.expect(response.status)
|
|
1720
|
-
.toBe(200)
|
|
1721
|
-
test
|
|
1722
|
-
.expect(await response.json())
|
|
1723
|
-
.toEqual({ tags: ["one", "two", "three"] })
|
|
1724
|
-
})
|
|
1725
|
-
|
|
1726
|
-
test.it("parses path params from RouteTree", async () => {
|
|
1727
|
-
const tree = RouteTree.make({
|
|
1728
|
-
"/folders/:folderId/files/:fileId": Route.get(
|
|
1729
|
-
RouteSchema.schemaPathParams(
|
|
1730
|
-
Schema.Struct({
|
|
1731
|
-
folderId: Schema.String,
|
|
1732
|
-
fileId: Schema.NumberFromString,
|
|
1733
|
-
}),
|
|
1734
|
-
),
|
|
1735
|
-
Route.json(function*(ctx) {
|
|
1736
|
-
return {
|
|
1737
|
-
folderId: ctx.pathParams.folderId,
|
|
1738
|
-
fileId: ctx.pathParams.fileId,
|
|
1739
|
-
}
|
|
1740
|
-
}),
|
|
1741
|
-
),
|
|
1742
|
-
})
|
|
1743
|
-
|
|
1744
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
1745
|
-
const handler = handles["/folders/:folderId/files/:fileId"]
|
|
1746
|
-
|
|
1747
|
-
const response = await Http.fetch(handler, {
|
|
1748
|
-
path: "/folders/abc123/files/42",
|
|
1749
|
-
})
|
|
1750
|
-
|
|
1751
|
-
test
|
|
1752
|
-
.expect(response.status)
|
|
1753
|
-
.toBe(200)
|
|
1754
|
-
test
|
|
1755
|
-
.expect(await response.json())
|
|
1756
|
-
.toEqual({
|
|
1757
|
-
folderId: "abc123",
|
|
1758
|
-
fileId: 42,
|
|
1759
|
-
})
|
|
1760
|
-
})
|
|
1761
|
-
|
|
1762
|
-
test.it("path params validation fails on invalid input", () =>
|
|
1763
|
-
Effect
|
|
1764
|
-
.gen(function*() {
|
|
1765
|
-
const runtime = yield* Effect.runtime<TestLogger.TestLogger>()
|
|
1766
|
-
const handler = RouteHttp.toWebHandlerRuntime(runtime)(
|
|
1767
|
-
Route.get(
|
|
1768
|
-
RouteSchema.schemaPathParams(
|
|
1769
|
-
Schema.Struct({
|
|
1770
|
-
userId: Schema.NumberFromString,
|
|
1771
|
-
}),
|
|
1772
|
-
),
|
|
1773
|
-
Route.text("ok"),
|
|
1774
|
-
),
|
|
1775
|
-
)
|
|
1776
|
-
|
|
1777
|
-
const response = yield* Effect.promise(() =>
|
|
1778
|
-
Http.fetch(handler, { path: "/users/not-a-number" })
|
|
1779
|
-
)
|
|
1780
|
-
|
|
1781
|
-
test
|
|
1782
|
-
.expect(response.status)
|
|
1783
|
-
.toBe(400)
|
|
1784
|
-
|
|
1785
|
-
const messages = yield* TestLogger.messages
|
|
1786
|
-
test
|
|
1787
|
-
.expect(messages.some((m) => m.includes("ParseError")))
|
|
1788
|
-
.toBe(true)
|
|
1789
|
-
})
|
|
1790
|
-
.pipe(Effect.provide(TestLogger.layer()), Effect.runPromise))
|
|
1791
|
-
|
|
1792
|
-
test.it("combines path params with headers and body", async () => {
|
|
1793
|
-
const tree = RouteTree.make({
|
|
1794
|
-
"/projects/:projectId/tasks": Route.post(
|
|
1795
|
-
RouteSchema.schemaPathParams(
|
|
1796
|
-
Schema.Struct({
|
|
1797
|
-
projectId: Schema.String,
|
|
1798
|
-
}),
|
|
1799
|
-
),
|
|
1800
|
-
RouteSchema.schemaHeaders(
|
|
1801
|
-
Schema.Struct({
|
|
1802
|
-
"x-api-key": Schema.String,
|
|
1803
|
-
}),
|
|
1804
|
-
),
|
|
1805
|
-
RouteSchema.schemaBodyJson(
|
|
1806
|
-
Schema.Struct({
|
|
1807
|
-
title: Schema.String,
|
|
1808
|
-
}),
|
|
1809
|
-
),
|
|
1810
|
-
Route.json(function*(ctx) {
|
|
1811
|
-
return {
|
|
1812
|
-
projectId: ctx.pathParams.projectId,
|
|
1813
|
-
apiKey: ctx.headers["x-api-key"],
|
|
1814
|
-
title: ctx.body.title,
|
|
1815
|
-
}
|
|
1816
|
-
}),
|
|
1817
|
-
),
|
|
1818
|
-
})
|
|
1819
|
-
|
|
1820
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
1821
|
-
const handler = handles["/projects/:projectId/tasks"]
|
|
1822
|
-
|
|
1823
|
-
const response = await Http.fetch(handler, {
|
|
1824
|
-
path: "/projects/proj-999/tasks",
|
|
1825
|
-
method: "POST",
|
|
1826
|
-
headers: { "x-api-key": "secret" },
|
|
1827
|
-
body: { title: "New Task" },
|
|
1828
|
-
})
|
|
1829
|
-
|
|
1830
|
-
test
|
|
1831
|
-
.expect(response.status)
|
|
1832
|
-
.toBe(200)
|
|
1833
|
-
test
|
|
1834
|
-
.expect(await response.json())
|
|
1835
|
-
.toEqual({
|
|
1836
|
-
projectId: "proj-999",
|
|
1837
|
-
apiKey: "secret",
|
|
1838
|
-
title: "New Task",
|
|
1839
|
-
})
|
|
1840
|
-
})
|
|
1841
|
-
})
|
|
1842
|
-
|
|
1843
|
-
test.describe("request abort handling", () => {
|
|
1844
|
-
test.it("returns 499 and runs finalizers when request is aborted", async () => {
|
|
1845
|
-
let finalizerRan = false
|
|
1846
|
-
|
|
1847
|
-
const handler = RouteHttp.toWebHandler(
|
|
1848
|
-
Route.get(
|
|
1849
|
-
Route.text(function*() {
|
|
1850
|
-
yield* Effect.addFinalizer(() =>
|
|
1851
|
-
Effect.sync(() => {
|
|
1852
|
-
finalizerRan = true
|
|
1853
|
-
})
|
|
1854
|
-
)
|
|
1855
|
-
yield* Effect.sleep("10 seconds")
|
|
1856
|
-
return "should not reach"
|
|
1857
|
-
}),
|
|
1858
|
-
),
|
|
1859
|
-
)
|
|
1860
|
-
|
|
1861
|
-
const { request, abort } = Http.createAbortableRequest({ path: "/abort" })
|
|
1862
|
-
|
|
1863
|
-
const responsePromise = handler(request)
|
|
1864
|
-
|
|
1865
|
-
await Effect.runPromise(Effect.sleep("10 millis"))
|
|
1866
|
-
abort()
|
|
1867
|
-
|
|
1868
|
-
const response = await responsePromise
|
|
1869
|
-
|
|
1870
|
-
test
|
|
1871
|
-
.expect(response.status)
|
|
1872
|
-
.toBe(499)
|
|
1873
|
-
test
|
|
1874
|
-
.expect(finalizerRan)
|
|
1875
|
-
.toBe(true)
|
|
1876
|
-
})
|
|
1877
|
-
|
|
1878
|
-
test.it("uses clientAbortFiberId to identify client disconnects", async () => {
|
|
1879
|
-
let interruptedBy: string | undefined
|
|
1880
|
-
|
|
1881
|
-
const handler = RouteHttp.toWebHandler(
|
|
1882
|
-
Route.get(
|
|
1883
|
-
Route.text(
|
|
1884
|
-
Effect
|
|
1885
|
-
.gen(function*() {
|
|
1886
|
-
yield* Effect.sleep("10 seconds")
|
|
1887
|
-
return "should not reach"
|
|
1888
|
-
})
|
|
1889
|
-
.pipe(
|
|
1890
|
-
Effect.onInterrupt((interruptors) =>
|
|
1891
|
-
Effect.sync(() => {
|
|
1892
|
-
for (const id of interruptors) {
|
|
1893
|
-
interruptedBy = String(id)
|
|
1894
|
-
}
|
|
1895
|
-
})
|
|
1896
|
-
),
|
|
1897
|
-
),
|
|
1898
|
-
),
|
|
1899
|
-
),
|
|
1900
|
-
)
|
|
1901
|
-
|
|
1902
|
-
const { request, abort } = Http.createAbortableRequest({ path: "/abort" })
|
|
1903
|
-
|
|
1904
|
-
const responsePromise = handler(request)
|
|
1905
|
-
|
|
1906
|
-
await Effect.runPromise(Effect.sleep("10 millis"))
|
|
1907
|
-
abort()
|
|
1908
|
-
|
|
1909
|
-
await responsePromise
|
|
1910
|
-
|
|
1911
|
-
test
|
|
1912
|
-
.expect(interruptedBy)
|
|
1913
|
-
.toContain("-499")
|
|
1914
|
-
})
|
|
1915
|
-
|
|
1916
|
-
test.it("interrupts streaming response when request is aborted", async () => {
|
|
1917
|
-
let finalizerRan = false
|
|
1918
|
-
|
|
1919
|
-
const handler = RouteHttp.toWebHandler(
|
|
1920
|
-
Route.get(
|
|
1921
|
-
Route.text(function*() {
|
|
1922
|
-
yield* Effect.addFinalizer(() =>
|
|
1923
|
-
Effect.sync(() => {
|
|
1924
|
-
finalizerRan = true
|
|
1925
|
-
})
|
|
1926
|
-
)
|
|
1927
|
-
return Stream.fromSchedule(Schedule.spaced("100 millis")).pipe(
|
|
1928
|
-
Stream.map((n) => `event ${n}\n`),
|
|
1929
|
-
Stream.take(100),
|
|
1930
|
-
)
|
|
1931
|
-
}),
|
|
1932
|
-
),
|
|
1933
|
-
)
|
|
1934
|
-
|
|
1935
|
-
const { request, abort } = Http.createAbortableRequest({ path: "/stream" })
|
|
1936
|
-
|
|
1937
|
-
const response = await handler(request)
|
|
1938
|
-
|
|
1939
|
-
test
|
|
1940
|
-
.expect(response.status)
|
|
1941
|
-
.toBe(200)
|
|
1942
|
-
|
|
1943
|
-
const reader = response.body!.getReader()
|
|
1944
|
-
const firstChunk = await reader.read()
|
|
1945
|
-
|
|
1946
|
-
test
|
|
1947
|
-
.expect(firstChunk.done)
|
|
1948
|
-
.toBe(false)
|
|
1949
|
-
|
|
1950
|
-
abort()
|
|
1951
|
-
|
|
1952
|
-
await Effect.runPromise(Effect.sleep("50 millis"))
|
|
1953
|
-
|
|
1954
|
-
test
|
|
1955
|
-
.expect(finalizerRan)
|
|
1956
|
-
.toBe(true)
|
|
1957
|
-
})
|
|
1958
|
-
})
|
|
1959
|
-
|
|
1960
|
-
test.describe("tracing", () => {
|
|
1961
|
-
test.it("creates span with correct name and kind", async () => {
|
|
1962
|
-
let capturedSpan: Tracer.Span | undefined
|
|
1963
|
-
|
|
1964
|
-
const handler = RouteHttp.toWebHandler(
|
|
1965
|
-
Route.get(
|
|
1966
|
-
Route.text(function*() {
|
|
1967
|
-
const span = yield* Effect.currentSpan
|
|
1968
|
-
capturedSpan = span
|
|
1969
|
-
return "ok"
|
|
1970
|
-
}),
|
|
1971
|
-
),
|
|
1972
|
-
)
|
|
1973
|
-
|
|
1974
|
-
await Http.fetch(handler, { path: "/test" })
|
|
1975
|
-
|
|
1976
|
-
test
|
|
1977
|
-
.expect(capturedSpan)
|
|
1978
|
-
.toBeDefined()
|
|
1979
|
-
test
|
|
1980
|
-
.expect(capturedSpan?.name)
|
|
1981
|
-
.toBe("http.server GET")
|
|
1982
|
-
test
|
|
1983
|
-
.expect(capturedSpan?.kind)
|
|
1984
|
-
.toBe("server")
|
|
1985
|
-
})
|
|
1986
|
-
|
|
1987
|
-
test.it("adds request attributes to span", async () => {
|
|
1988
|
-
let capturedSpan: Tracer.Span | undefined
|
|
1989
|
-
|
|
1990
|
-
const handler = RouteHttp.toWebHandler(
|
|
1991
|
-
Route.get(
|
|
1992
|
-
Route.text(function*() {
|
|
1993
|
-
const span = yield* Effect.currentSpan
|
|
1994
|
-
capturedSpan = span
|
|
1995
|
-
return "ok"
|
|
1996
|
-
}),
|
|
1997
|
-
),
|
|
1998
|
-
)
|
|
1999
|
-
|
|
2000
|
-
await Http.fetch(handler, {
|
|
2001
|
-
path: "/users?page=1&limit=10",
|
|
2002
|
-
headers: { "user-agent": "test-agent" },
|
|
2003
|
-
})
|
|
2004
|
-
|
|
2005
|
-
test
|
|
2006
|
-
.expect(capturedSpan?.attributes.get("http.request.method"))
|
|
2007
|
-
.toBe("GET")
|
|
2008
|
-
test
|
|
2009
|
-
.expect(capturedSpan?.attributes.get("url.path"))
|
|
2010
|
-
.toBe("/users")
|
|
2011
|
-
test
|
|
2012
|
-
.expect(capturedSpan?.attributes.get("url.query"))
|
|
2013
|
-
.toBe("page=1&limit=10")
|
|
2014
|
-
test
|
|
2015
|
-
.expect(capturedSpan?.attributes.get("url.scheme"))
|
|
2016
|
-
.toBe("http")
|
|
2017
|
-
test
|
|
2018
|
-
.expect(capturedSpan?.attributes.get("user_agent.original"))
|
|
2019
|
-
.toBe("test-agent")
|
|
2020
|
-
})
|
|
2021
|
-
|
|
2022
|
-
test.it("adds response status code to span", async () => {
|
|
2023
|
-
let capturedSpan: Tracer.Span | undefined
|
|
2024
|
-
|
|
2025
|
-
const handler = RouteHttp.toWebHandler(
|
|
2026
|
-
Route.get(
|
|
2027
|
-
Route.text(function*() {
|
|
2028
|
-
const span = yield* Effect.currentSpan
|
|
2029
|
-
capturedSpan = span
|
|
2030
|
-
return "ok"
|
|
2031
|
-
}),
|
|
2032
|
-
),
|
|
2033
|
-
)
|
|
2034
|
-
|
|
2035
|
-
const response = await Http.fetch(handler, { path: "/test" })
|
|
2036
|
-
|
|
2037
|
-
test
|
|
2038
|
-
.expect(response.status)
|
|
2039
|
-
.toBe(200)
|
|
2040
|
-
|
|
2041
|
-
await Effect.runPromise(Effect.sleep("10 millis"))
|
|
2042
|
-
|
|
2043
|
-
test
|
|
2044
|
-
.expect(capturedSpan?.attributes.get("http.response.status_code"))
|
|
2045
|
-
.toBe(200)
|
|
2046
|
-
})
|
|
2047
|
-
|
|
2048
|
-
test.it("parses W3C traceparent header for parent span", async () => {
|
|
2049
|
-
let capturedSpan: Tracer.Span | undefined
|
|
2050
|
-
|
|
2051
|
-
const handler = RouteHttp.toWebHandler(
|
|
2052
|
-
Route.get(
|
|
2053
|
-
Route.text(function*() {
|
|
2054
|
-
const span = yield* Effect.currentSpan
|
|
2055
|
-
capturedSpan = span
|
|
2056
|
-
return "ok"
|
|
2057
|
-
}),
|
|
2058
|
-
),
|
|
2059
|
-
)
|
|
2060
|
-
|
|
2061
|
-
await Http.fetch(handler, {
|
|
2062
|
-
path: "/test",
|
|
2063
|
-
headers: {
|
|
2064
|
-
traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
|
|
2065
|
-
},
|
|
2066
|
-
})
|
|
2067
|
-
|
|
2068
|
-
test
|
|
2069
|
-
.expect(capturedSpan?.parent)
|
|
2070
|
-
.toBeDefined()
|
|
2071
|
-
|
|
2072
|
-
const parent = Option.getOrUndefined(
|
|
2073
|
-
capturedSpan?.parent ?? Option.none(),
|
|
2074
|
-
) as Tracer.AnySpan | undefined
|
|
2075
|
-
test
|
|
2076
|
-
.expect(parent?.traceId)
|
|
2077
|
-
.toBe("0af7651916cd43dd8448eb211c80319c")
|
|
2078
|
-
test
|
|
2079
|
-
.expect(parent?.spanId)
|
|
2080
|
-
.toBe("b7ad6b7169203331")
|
|
2081
|
-
})
|
|
2082
|
-
|
|
2083
|
-
test.it("parses B3 single header for parent span", async () => {
|
|
2084
|
-
let capturedSpan: Tracer.Span | undefined
|
|
2085
|
-
|
|
2086
|
-
const handler = RouteHttp.toWebHandler(
|
|
2087
|
-
Route.get(
|
|
2088
|
-
Route.text(function*() {
|
|
2089
|
-
const span = yield* Effect.currentSpan
|
|
2090
|
-
capturedSpan = span
|
|
2091
|
-
return "ok"
|
|
2092
|
-
}),
|
|
2093
|
-
),
|
|
2094
|
-
)
|
|
2095
|
-
|
|
2096
|
-
await Http.fetch(handler, {
|
|
2097
|
-
path: "/test",
|
|
2098
|
-
headers: {
|
|
2099
|
-
b3: "80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1",
|
|
2100
|
-
},
|
|
2101
|
-
})
|
|
2102
|
-
|
|
2103
|
-
test
|
|
2104
|
-
.expect(capturedSpan?.parent)
|
|
2105
|
-
.toBeDefined()
|
|
2106
|
-
|
|
2107
|
-
const parent = Option.getOrUndefined(
|
|
2108
|
-
capturedSpan?.parent ?? Option.none(),
|
|
2109
|
-
) as Tracer.AnySpan | undefined
|
|
2110
|
-
test
|
|
2111
|
-
.expect(parent?.traceId)
|
|
2112
|
-
.toBe("80f198ee56343ba864fe8b2a57d3eff7")
|
|
2113
|
-
test
|
|
2114
|
-
.expect(parent?.spanId)
|
|
2115
|
-
.toBe("e457b5a2e4d86bd1")
|
|
2116
|
-
})
|
|
2117
|
-
|
|
2118
|
-
test.it("parses X-B3 multi headers for parent span", async () => {
|
|
2119
|
-
let capturedSpan: Tracer.Span | undefined
|
|
2120
|
-
|
|
2121
|
-
const handler = RouteHttp.toWebHandler(
|
|
2122
|
-
Route.get(
|
|
2123
|
-
Route.text(function*() {
|
|
2124
|
-
const span = yield* Effect.currentSpan
|
|
2125
|
-
capturedSpan = span
|
|
2126
|
-
return "ok"
|
|
2127
|
-
}),
|
|
2128
|
-
),
|
|
2129
|
-
)
|
|
2130
|
-
|
|
2131
|
-
await Http.fetch(handler, {
|
|
2132
|
-
path: "/test",
|
|
2133
|
-
headers: {
|
|
2134
|
-
"x-b3-traceid": "463ac35c9f6413ad48485a3953bb6124",
|
|
2135
|
-
"x-b3-spanid": "0020000000000001",
|
|
2136
|
-
"x-b3-sampled": "1",
|
|
2137
|
-
},
|
|
2138
|
-
})
|
|
2139
|
-
|
|
2140
|
-
test
|
|
2141
|
-
.expect(capturedSpan?.parent)
|
|
2142
|
-
.toBeDefined()
|
|
2143
|
-
|
|
2144
|
-
const parent = Option.getOrUndefined(
|
|
2145
|
-
capturedSpan?.parent ?? Option.none(),
|
|
2146
|
-
) as Tracer.AnySpan | undefined
|
|
2147
|
-
test
|
|
2148
|
-
.expect(parent?.traceId)
|
|
2149
|
-
.toBe("463ac35c9f6413ad48485a3953bb6124")
|
|
2150
|
-
test
|
|
2151
|
-
.expect(parent?.spanId)
|
|
2152
|
-
.toBe("0020000000000001")
|
|
2153
|
-
})
|
|
2154
|
-
|
|
2155
|
-
test.it("withTracerDisabledWhen disables tracing for matching requests", () =>
|
|
2156
|
-
Effect
|
|
2157
|
-
.gen(function*() {
|
|
2158
|
-
let spanCapturedOnHealth = false
|
|
2159
|
-
let spanCapturedOnUsers = false
|
|
2160
|
-
|
|
2161
|
-
const runtime = yield* RouteHttp.withTracerDisabledWhen(
|
|
2162
|
-
Effect.runtime<never>(),
|
|
2163
|
-
(req) => new URL(req.url).pathname === "/health",
|
|
2164
|
-
)
|
|
2165
|
-
const handler = RouteHttp.toWebHandlerRuntime(runtime)(
|
|
2166
|
-
Route.get(
|
|
2167
|
-
Route.text(function*() {
|
|
2168
|
-
const spanResult = yield* Effect.option(Effect.currentSpan)
|
|
2169
|
-
if (Option.isSome(spanResult)) {
|
|
2170
|
-
const path = spanResult.value.attributes.get("url.path")
|
|
2171
|
-
if (path === "/health") spanCapturedOnHealth = true
|
|
2172
|
-
if (path === "/users") spanCapturedOnUsers = true
|
|
2173
|
-
}
|
|
2174
|
-
return "ok"
|
|
2175
|
-
}),
|
|
2176
|
-
),
|
|
2177
|
-
)
|
|
2178
|
-
|
|
2179
|
-
yield* Effect.promise(() => Http.fetch(handler, { path: "/health" }))
|
|
2180
|
-
yield* Effect.promise(() => Http.fetch(handler, { path: "/users" }))
|
|
2181
|
-
|
|
2182
|
-
test
|
|
2183
|
-
.expect(spanCapturedOnHealth)
|
|
2184
|
-
.toBe(false)
|
|
2185
|
-
test
|
|
2186
|
-
.expect(spanCapturedOnUsers)
|
|
2187
|
-
.toBe(true)
|
|
2188
|
-
})
|
|
2189
|
-
.pipe(Effect.runPromise))
|
|
2190
|
-
|
|
2191
|
-
test.it("withSpanNameGenerator customizes span name", () =>
|
|
2192
|
-
Effect
|
|
2193
|
-
.gen(function*() {
|
|
2194
|
-
let capturedSpan: Tracer.Span | undefined
|
|
2195
|
-
|
|
2196
|
-
const runtime = yield* RouteHttp.withSpanNameGenerator(
|
|
2197
|
-
Effect.runtime<never>(),
|
|
2198
|
-
(req) => {
|
|
2199
|
-
const url = new URL(req.url)
|
|
2200
|
-
return `${req.method} ${url.pathname}`
|
|
2201
|
-
},
|
|
2202
|
-
)
|
|
2203
|
-
const handler = RouteHttp.toWebHandlerRuntime(runtime)(
|
|
2204
|
-
Route.get(
|
|
2205
|
-
Route.text(function*() {
|
|
2206
|
-
const span = yield* Effect.currentSpan
|
|
2207
|
-
capturedSpan = span
|
|
2208
|
-
return "ok"
|
|
2209
|
-
}),
|
|
2210
|
-
),
|
|
2211
|
-
)
|
|
2212
|
-
|
|
2213
|
-
yield* Effect.promise(() => Http.fetch(handler, { path: "/users" }))
|
|
2214
|
-
|
|
2215
|
-
test
|
|
2216
|
-
.expect(capturedSpan?.name)
|
|
2217
|
-
.toBe("GET /users")
|
|
2218
|
-
})
|
|
2219
|
-
.pipe(Effect.runPromise))
|
|
2220
|
-
|
|
2221
|
-
test.it("adds http.route attribute when route has path", async () => {
|
|
2222
|
-
let capturedSpan: Tracer.Span | undefined
|
|
2223
|
-
|
|
2224
|
-
const tree = RouteTree.make({
|
|
2225
|
-
"/users/:id": Route.get(
|
|
2226
|
-
Route.text(function*() {
|
|
2227
|
-
const span = yield* Effect.currentSpan
|
|
2228
|
-
capturedSpan = span
|
|
2229
|
-
return "ok"
|
|
2230
|
-
}),
|
|
2231
|
-
),
|
|
2232
|
-
})
|
|
2233
|
-
|
|
2234
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
2235
|
-
const handler = handles["/users/:id"]
|
|
2236
|
-
|
|
2237
|
-
await Http.fetch(handler, { path: "/users/123" })
|
|
2238
|
-
|
|
2239
|
-
test
|
|
2240
|
-
.expect(capturedSpan?.attributes.get("http.route"))
|
|
2241
|
-
.toBe("/users/:id")
|
|
2242
|
-
})
|
|
2243
|
-
})
|
|
2244
|
-
|
|
2245
|
-
test.describe("RouteTree layer routes", () => {
|
|
2246
|
-
test.it("layer routes execute in order before path routes", async () => {
|
|
2247
|
-
const calls: string[] = []
|
|
2248
|
-
|
|
2249
|
-
const tree = RouteTree.make({
|
|
2250
|
-
"*": Route
|
|
2251
|
-
.use(Route.filter(function*() {
|
|
2252
|
-
calls.push("layer1")
|
|
2253
|
-
return { context: {} }
|
|
2254
|
-
}))
|
|
2255
|
-
.use(Route.filter(function*() {
|
|
2256
|
-
calls.push("layer2")
|
|
2257
|
-
return { context: {} }
|
|
2258
|
-
})),
|
|
2259
|
-
"/test": Route.get(Route.text(function*() {
|
|
2260
|
-
calls.push("handler")
|
|
2261
|
-
return "ok"
|
|
2262
|
-
})),
|
|
2263
|
-
})
|
|
2264
|
-
|
|
2265
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
2266
|
-
const response = await Http.fetch(handles["/test"], { path: "/test" })
|
|
2267
|
-
|
|
2268
|
-
test
|
|
2269
|
-
.expect(response.status)
|
|
2270
|
-
.toBe(200)
|
|
2271
|
-
test
|
|
2272
|
-
.expect(calls)
|
|
2273
|
-
.toEqual(["layer1", "layer2", "handler"])
|
|
2274
|
-
})
|
|
2275
|
-
|
|
2276
|
-
test.it("layer routes apply to all paths in the tree", async () => {
|
|
2277
|
-
const calls: string[] = []
|
|
2278
|
-
|
|
2279
|
-
const tree = RouteTree.make({
|
|
2280
|
-
"*": Route.use(Route.filter(function*() {
|
|
2281
|
-
calls.push("layer")
|
|
2282
|
-
return { context: {} }
|
|
2283
|
-
})),
|
|
2284
|
-
"/users": Route.get(Route.text(function*() {
|
|
2285
|
-
calls.push("users")
|
|
2286
|
-
return "users"
|
|
2287
|
-
})),
|
|
2288
|
-
"/admin": Route.get(Route.text(function*() {
|
|
2289
|
-
calls.push("admin")
|
|
2290
|
-
return "admin"
|
|
2291
|
-
})),
|
|
2292
|
-
})
|
|
2293
|
-
|
|
2294
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
2295
|
-
|
|
2296
|
-
calls.length = 0
|
|
2297
|
-
await Http.fetch(handles["/users"], { path: "/users" })
|
|
2298
|
-
test
|
|
2299
|
-
.expect(calls)
|
|
2300
|
-
.toEqual(["layer", "users"])
|
|
2301
|
-
|
|
2302
|
-
calls.length = 0
|
|
2303
|
-
await Http.fetch(handles["/admin"], { path: "/admin" })
|
|
2304
|
-
test
|
|
2305
|
-
.expect(calls)
|
|
2306
|
-
.toEqual(["layer", "admin"])
|
|
2307
|
-
})
|
|
2308
|
-
|
|
2309
|
-
test.it("layer execution does not leak between requests", async () => {
|
|
2310
|
-
let layerCallCount = 0
|
|
2311
|
-
|
|
2312
|
-
const tree = RouteTree.make({
|
|
2313
|
-
"*": Route.use(Route.filter(function*() {
|
|
2314
|
-
layerCallCount++
|
|
2315
|
-
return { context: {} }
|
|
2316
|
-
})),
|
|
2317
|
-
"/test": Route.get(Route.text("ok")),
|
|
2318
|
-
})
|
|
2319
|
-
|
|
2320
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
2321
|
-
|
|
2322
|
-
layerCallCount = 0
|
|
2323
|
-
await Http.fetch(handles["/test"], { path: "/test" })
|
|
2324
|
-
test
|
|
2325
|
-
.expect(layerCallCount)
|
|
2326
|
-
.toBe(1)
|
|
2327
|
-
|
|
2328
|
-
await Http.fetch(handles["/test"], { path: "/test" })
|
|
2329
|
-
test
|
|
2330
|
-
.expect(layerCallCount)
|
|
2331
|
-
.toBe(2)
|
|
2332
|
-
})
|
|
2333
|
-
|
|
2334
|
-
test.it("nested tree inherits parent layer routes", async () => {
|
|
2335
|
-
const calls: string[] = []
|
|
2336
|
-
|
|
2337
|
-
const apiTree = RouteTree.make({
|
|
2338
|
-
"/users": Route.get(Route.text(function*() {
|
|
2339
|
-
calls.push("users")
|
|
2340
|
-
return "users"
|
|
2341
|
-
})),
|
|
2342
|
-
})
|
|
2343
|
-
|
|
2344
|
-
const tree = RouteTree.make({
|
|
2345
|
-
"*": Route.use(Route.filter(function*() {
|
|
2346
|
-
calls.push("root-layer")
|
|
2347
|
-
return { context: {} }
|
|
2348
|
-
})),
|
|
2349
|
-
"/api": apiTree,
|
|
2350
|
-
})
|
|
2351
|
-
|
|
2352
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
2353
|
-
await Http.fetch(handles["/api/users"], { path: "/api/users" })
|
|
2354
|
-
|
|
2355
|
-
test
|
|
2356
|
-
.expect(calls)
|
|
2357
|
-
.toEqual(["root-layer", "users"])
|
|
2358
|
-
})
|
|
2359
|
-
|
|
2360
|
-
test.it("layer routes can short-circuit with error", () =>
|
|
2361
|
-
Effect
|
|
2362
|
-
.gen(function*() {
|
|
2363
|
-
const runtime = yield* Effect.runtime<TestLogger.TestLogger>()
|
|
2364
|
-
let handlerExecuted = false
|
|
2365
|
-
|
|
2366
|
-
const tree = RouteTree.make({
|
|
2367
|
-
"*": Route.use(Route.filter(function*() {
|
|
2368
|
-
return yield* Effect.fail(new Error("layer rejected"))
|
|
2369
|
-
})),
|
|
2370
|
-
"/test": Route.get(Route.text(function*() {
|
|
2371
|
-
handlerExecuted = true
|
|
2372
|
-
return "should not reach"
|
|
2373
|
-
})),
|
|
2374
|
-
})
|
|
2375
|
-
|
|
2376
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree, runtime))
|
|
2377
|
-
|
|
2378
|
-
const response = yield* Effect.promise(() =>
|
|
2379
|
-
Http.fetch(handles["/test"], { path: "/test" })
|
|
2380
|
-
)
|
|
2381
|
-
|
|
2382
|
-
test
|
|
2383
|
-
.expect(response.status)
|
|
2384
|
-
.toBe(500)
|
|
2385
|
-
|
|
2386
|
-
test
|
|
2387
|
-
.expect(handlerExecuted)
|
|
2388
|
-
.toBe(false)
|
|
2389
|
-
|
|
2390
|
-
const text = yield* Effect.promise(() => response.text())
|
|
2391
|
-
test
|
|
2392
|
-
.expect(text)
|
|
2393
|
-
.toContain("layer rejected")
|
|
2394
|
-
|
|
2395
|
-
const messages = yield* TestLogger.messages
|
|
2396
|
-
test
|
|
2397
|
-
.expect(messages.some((m) => m.includes("layer rejected")))
|
|
2398
|
-
.toBe(true)
|
|
2399
|
-
})
|
|
2400
|
-
.pipe(Effect.provide(TestLogger.layer()), Effect.runPromise))
|
|
2401
|
-
|
|
2402
|
-
test.it("layer middleware wraps response content with json", async () => {
|
|
2403
|
-
const tree = RouteTree.make({
|
|
2404
|
-
"*": Route.use(
|
|
2405
|
-
Route.json(function*(_ctx, next) {
|
|
2406
|
-
const value = yield* next().json
|
|
2407
|
-
return { wrapped: value }
|
|
2408
|
-
}),
|
|
2409
|
-
),
|
|
2410
|
-
"/data": Route.get(Route.json({ original: true })),
|
|
2411
|
-
})
|
|
2412
|
-
|
|
2413
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
2414
|
-
const response = await Http.fetch(handles["/data"], { path: "/data" })
|
|
2415
|
-
|
|
2416
|
-
test
|
|
2417
|
-
.expect(await response.json())
|
|
2418
|
-
.toEqual({ wrapped: { original: true } })
|
|
2419
|
-
})
|
|
2420
|
-
|
|
2421
|
-
test.it("layer middleware wraps response content with text", async () => {
|
|
2422
|
-
const tree = RouteTree.make({
|
|
2423
|
-
"*": Route.use(
|
|
2424
|
-
Route.text(function*(_ctx, next) {
|
|
2425
|
-
const value = yield* next().text
|
|
2426
|
-
return `Layout: ${value}`
|
|
2427
|
-
}),
|
|
2428
|
-
),
|
|
2429
|
-
"/page": Route.get(Route.text("Page Content")),
|
|
2430
|
-
})
|
|
2431
|
-
|
|
2432
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
2433
|
-
const response = await Http.fetch(handles["/page"], { path: "/page" })
|
|
2434
|
-
|
|
2435
|
-
test
|
|
2436
|
-
.expect(await response.text())
|
|
2437
|
-
.toBe("Layout: Page Content")
|
|
2438
|
-
})
|
|
2439
|
-
|
|
2440
|
-
test.it("multiple layers execute in definition order", async () => {
|
|
2441
|
-
const calls: string[] = []
|
|
2442
|
-
|
|
2443
|
-
const tree = RouteTree.make({
|
|
2444
|
-
"*": Route
|
|
2445
|
-
.use(Route.filter(function*() {
|
|
2446
|
-
calls.push("layer1")
|
|
2447
|
-
return { context: {} }
|
|
2448
|
-
}))
|
|
2449
|
-
.use(Route.filter(function*() {
|
|
2450
|
-
calls.push("layer2")
|
|
2451
|
-
return { context: {} }
|
|
2452
|
-
})),
|
|
2453
|
-
"/test": Route.get(Route.text(function*() {
|
|
2454
|
-
calls.push("handler")
|
|
2455
|
-
return "ok"
|
|
2456
|
-
})),
|
|
2457
|
-
})
|
|
2458
|
-
|
|
2459
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
2460
|
-
await Http.fetch(handles["/test"], { path: "/test" })
|
|
2461
|
-
|
|
2462
|
-
test
|
|
2463
|
-
.expect(calls)
|
|
2464
|
-
.toEqual(["layer1", "layer2", "handler"])
|
|
2465
|
-
})
|
|
2466
|
-
|
|
2467
|
-
test.it("format negotiation excludes middleware formats", async () => {
|
|
2468
|
-
const tree = RouteTree.make({
|
|
2469
|
-
"*": Route.use(
|
|
2470
|
-
Route.json(function*(_ctx, next) {
|
|
2471
|
-
const value = yield* next().json
|
|
2472
|
-
return { wrapped: value }
|
|
2473
|
-
}),
|
|
2474
|
-
),
|
|
2475
|
-
"/": Route.get(Route.html("<h1>Hello</h1>")),
|
|
2476
|
-
})
|
|
2477
|
-
|
|
2478
|
-
const handles = Object.fromEntries(RouteHttp.walkHandles(tree))
|
|
2479
|
-
const response = await Http.fetch(handles["/"], { path: "/" })
|
|
2480
|
-
|
|
2481
|
-
test
|
|
2482
|
-
.expect(response.status)
|
|
2483
|
-
.toBe(200)
|
|
2484
|
-
test
|
|
2485
|
-
.expect(response.headers.get("Content-Type"))
|
|
2486
|
-
.toBe("text/html; charset=utf-8")
|
|
2487
|
-
test
|
|
2488
|
-
.expect(await response.text())
|
|
2489
|
-
.toBe("<h1>Hello</h1>")
|
|
2490
|
-
})
|
|
2491
|
-
})
|
|
2492
|
-
|
|
2493
|
-
test.describe("Route.render (format=*)", () => {
|
|
2494
|
-
test.it("accepts any Accept header", async () => {
|
|
2495
|
-
const handler = RouteHttp.toWebHandler(
|
|
2496
|
-
Route.get(
|
|
2497
|
-
Route.render(function*() {
|
|
2498
|
-
return Stream.make("event: message\ndata: hello\n\n")
|
|
2499
|
-
}),
|
|
2500
|
-
),
|
|
2501
|
-
)
|
|
2502
|
-
|
|
2503
|
-
const response = await Http.fetch(handler, {
|
|
2504
|
-
path: "/events",
|
|
2505
|
-
headers: { Accept: "text/event-stream" },
|
|
2506
|
-
})
|
|
2507
|
-
|
|
2508
|
-
test
|
|
2509
|
-
.expect(response.status)
|
|
2510
|
-
.toBe(200)
|
|
2511
|
-
test
|
|
2512
|
-
.expect(await response.text())
|
|
2513
|
-
.toBe("event: message\ndata: hello\n\n")
|
|
2514
|
-
})
|
|
2515
|
-
|
|
2516
|
-
test.it("works without Accept header", async () => {
|
|
2517
|
-
const handler = RouteHttp.toWebHandler(
|
|
2518
|
-
Route.get(
|
|
2519
|
-
Route.render(function*() {
|
|
2520
|
-
return "raw response"
|
|
2521
|
-
}),
|
|
2522
|
-
),
|
|
2523
|
-
)
|
|
2524
|
-
|
|
2525
|
-
const response = await Http.fetch(handler, { path: "/raw" })
|
|
2526
|
-
|
|
2527
|
-
test
|
|
2528
|
-
.expect(response.status)
|
|
2529
|
-
.toBe(200)
|
|
2530
|
-
test
|
|
2531
|
-
.expect(await response.text())
|
|
2532
|
-
.toBe("raw response")
|
|
2533
|
-
})
|
|
2534
|
-
|
|
2535
|
-
test.it("does not participate in content negotiation", async () => {
|
|
2536
|
-
const handler = RouteHttp.toWebHandler(
|
|
2537
|
-
Route
|
|
2538
|
-
.get(Route.json({ type: "json" }))
|
|
2539
|
-
.get(Route.render(function*() {
|
|
2540
|
-
return "fallback"
|
|
2541
|
-
})),
|
|
2542
|
-
)
|
|
2543
|
-
|
|
2544
|
-
const jsonResponse = await Http.fetch(handler, {
|
|
2545
|
-
path: "/data",
|
|
2546
|
-
headers: { Accept: "application/json" },
|
|
2547
|
-
})
|
|
2548
|
-
test
|
|
2549
|
-
.expect(await jsonResponse.json())
|
|
2550
|
-
.toEqual({ type: "json" })
|
|
2551
|
-
|
|
2552
|
-
const eventStreamResponse = await Http.fetch(handler, {
|
|
2553
|
-
path: "/data",
|
|
2554
|
-
headers: { Accept: "text/event-stream" },
|
|
2555
|
-
})
|
|
2556
|
-
test
|
|
2557
|
-
.expect(eventStreamResponse.status)
|
|
2558
|
-
.toBe(200)
|
|
2559
|
-
test
|
|
2560
|
-
.expect(await eventStreamResponse.text())
|
|
2561
|
-
.toBe("fallback")
|
|
2562
|
-
})
|
|
2563
|
-
|
|
2564
|
-
test.it("is always called regardless of Accept header when only handle routes exist", async () => {
|
|
2565
|
-
const handler = RouteHttp.toWebHandler(
|
|
2566
|
-
Route.get(
|
|
2567
|
-
Route.render(function*() {
|
|
2568
|
-
return "any format"
|
|
2569
|
-
}),
|
|
2570
|
-
),
|
|
2571
|
-
)
|
|
2572
|
-
|
|
2573
|
-
const responses = await Promise.all([
|
|
2574
|
-
Http.fetch(handler, {
|
|
2575
|
-
path: "/",
|
|
2576
|
-
headers: { Accept: "text/event-stream" },
|
|
2577
|
-
}),
|
|
2578
|
-
Http.fetch(handler, { path: "/", headers: { Accept: "image/png" } }),
|
|
2579
|
-
Http.fetch(handler, { path: "/", headers: { Accept: "*/*" } }),
|
|
2580
|
-
Http.fetch(handler, { path: "/" }),
|
|
2581
|
-
])
|
|
2582
|
-
|
|
2583
|
-
for (const response of responses) {
|
|
2584
|
-
test
|
|
2585
|
-
.expect(response.status)
|
|
2586
|
-
.toBe(200)
|
|
2587
|
-
test
|
|
2588
|
-
.expect(await response.text())
|
|
2589
|
-
.toBe("any format")
|
|
2590
|
-
}
|
|
2591
|
-
})
|
|
2592
|
-
|
|
2593
|
-
test.it("can return Entity with custom headers", async () => {
|
|
2594
|
-
const handler = RouteHttp.toWebHandler(
|
|
2595
|
-
Route.get(
|
|
2596
|
-
Route.render(function*() {
|
|
2597
|
-
return Entity.make(Stream.make("data: hello\n\n"), {
|
|
2598
|
-
headers: {
|
|
2599
|
-
"content-type": "text/event-stream",
|
|
2600
|
-
"cache-control": "no-cache",
|
|
2601
|
-
},
|
|
2602
|
-
})
|
|
2603
|
-
}),
|
|
2604
|
-
),
|
|
2605
|
-
)
|
|
2606
|
-
|
|
2607
|
-
const response = await Http.fetch(handler, {
|
|
2608
|
-
path: "/events",
|
|
2609
|
-
headers: { Accept: "text/event-stream" },
|
|
2610
|
-
})
|
|
2611
|
-
|
|
2612
|
-
test
|
|
2613
|
-
.expect(response.status)
|
|
2614
|
-
.toBe(200)
|
|
2615
|
-
test
|
|
2616
|
-
.expect(response.headers.get("content-type"))
|
|
2617
|
-
.toBe("text/event-stream")
|
|
2618
|
-
test
|
|
2619
|
-
.expect(response.headers.get("cache-control"))
|
|
2620
|
-
.toBe("no-cache")
|
|
2621
|
-
test
|
|
2622
|
-
.expect(await response.text())
|
|
2623
|
-
.toBe("data: hello\n\n")
|
|
2624
|
-
})
|
|
2625
|
-
|
|
2626
|
-
test.it("handle middleware wraps handle handler", async () => {
|
|
2627
|
-
const handler = RouteHttp.toWebHandler(
|
|
2628
|
-
Route
|
|
2629
|
-
.use(
|
|
2630
|
-
Route.render(function*(_ctx, next) {
|
|
2631
|
-
const value = yield* next().text
|
|
2632
|
-
return `wrapped: ${value}`
|
|
2633
|
-
}),
|
|
2634
|
-
)
|
|
2635
|
-
.get(
|
|
2636
|
-
Route.render(function*() {
|
|
2637
|
-
return "inner"
|
|
2638
|
-
}),
|
|
2639
|
-
),
|
|
2640
|
-
)
|
|
2641
|
-
|
|
2642
|
-
const response = await Http.fetch(handler, {
|
|
2643
|
-
path: "/",
|
|
2644
|
-
headers: { Accept: "text/event-stream" },
|
|
2645
|
-
})
|
|
2646
|
-
|
|
2647
|
-
test
|
|
2648
|
-
.expect(response.status)
|
|
2649
|
-
.toBe(200)
|
|
2650
|
-
test
|
|
2651
|
-
.expect(await response.text())
|
|
2652
|
-
.toBe("wrapped: inner")
|
|
2653
|
-
})
|
|
2654
|
-
|
|
2655
|
-
test.it("render middleware always runs even when specific format is selected", async () => {
|
|
2656
|
-
const calls: string[] = []
|
|
2657
|
-
|
|
2658
|
-
const handler = RouteHttp.toWebHandler(
|
|
2659
|
-
Route
|
|
2660
|
-
.use(
|
|
2661
|
-
Route.render(function*(_ctx, next) {
|
|
2662
|
-
calls.push("render middleware")
|
|
2663
|
-
return next().stream
|
|
2664
|
-
}),
|
|
2665
|
-
)
|
|
2666
|
-
.get(
|
|
2667
|
-
Route.json(function*() {
|
|
2668
|
-
calls.push("json handler")
|
|
2669
|
-
return { type: "json" }
|
|
2670
|
-
}),
|
|
2671
|
-
),
|
|
2672
|
-
)
|
|
2673
|
-
|
|
2674
|
-
const response = await Http.fetch(handler, {
|
|
2675
|
-
path: "/",
|
|
2676
|
-
headers: { Accept: "application/json" },
|
|
2677
|
-
})
|
|
2678
|
-
|
|
2679
|
-
test
|
|
2680
|
-
.expect(response.status)
|
|
2681
|
-
.toBe(200)
|
|
2682
|
-
test
|
|
2683
|
-
.expect(calls)
|
|
2684
|
-
.toEqual(["render middleware", "json handler"])
|
|
2685
|
-
test
|
|
2686
|
-
.expect(await response.json())
|
|
2687
|
-
.toEqual({ type: "json" })
|
|
2688
|
-
})
|
|
2689
|
-
|
|
2690
|
-
test.it("next() from render matches both render and selected format routes", async () => {
|
|
2691
|
-
const calls: string[] = []
|
|
2692
|
-
|
|
2693
|
-
const handler = RouteHttp.toWebHandler(
|
|
2694
|
-
Route
|
|
2695
|
-
.use(
|
|
2696
|
-
Route.render(function*(_ctx, next) {
|
|
2697
|
-
calls.push("render middleware 1")
|
|
2698
|
-
return next().stream
|
|
2699
|
-
}),
|
|
2700
|
-
Route.render(function*(_ctx, next) {
|
|
2701
|
-
calls.push("render middleware 2")
|
|
2702
|
-
return next().stream
|
|
2703
|
-
}),
|
|
2704
|
-
Route.json(function*(_ctx, next) {
|
|
2705
|
-
calls.push("json middleware")
|
|
2706
|
-
return yield* next().json
|
|
2707
|
-
}),
|
|
2708
|
-
)
|
|
2709
|
-
.get(
|
|
2710
|
-
Route.json(function*() {
|
|
2711
|
-
calls.push("json handler")
|
|
2712
|
-
return { type: "json" }
|
|
2713
|
-
}),
|
|
2714
|
-
),
|
|
2715
|
-
)
|
|
2716
|
-
|
|
2717
|
-
const response = await Http.fetch(handler, {
|
|
2718
|
-
path: "/",
|
|
2719
|
-
headers: { Accept: "application/json" },
|
|
2720
|
-
})
|
|
2721
|
-
|
|
2722
|
-
test
|
|
2723
|
-
.expect(response.status)
|
|
2724
|
-
.toBe(200)
|
|
2725
|
-
test
|
|
2726
|
-
.expect(calls)
|
|
2727
|
-
.toEqual([
|
|
2728
|
-
"render middleware 1",
|
|
2729
|
-
"render middleware 2",
|
|
2730
|
-
"json middleware",
|
|
2731
|
-
"json handler",
|
|
2732
|
-
])
|
|
2733
|
-
})
|
|
2734
|
-
|
|
2735
|
-
test.it("render handler runs when no specific format matches", async () => {
|
|
2736
|
-
const calls: string[] = []
|
|
2737
|
-
|
|
2738
|
-
const handler = RouteHttp.toWebHandler(
|
|
2739
|
-
Route
|
|
2740
|
-
.get(
|
|
2741
|
-
Route.json(function*() {
|
|
2742
|
-
calls.push("json")
|
|
2743
|
-
return { type: "json" }
|
|
2744
|
-
}),
|
|
2745
|
-
Route.render(function*() {
|
|
2746
|
-
calls.push("render")
|
|
2747
|
-
return "render output"
|
|
2748
|
-
}),
|
|
2749
|
-
),
|
|
2750
|
-
)
|
|
2751
|
-
|
|
2752
|
-
const eventStreamResponse = await Http.fetch(handler, {
|
|
2753
|
-
path: "/",
|
|
2754
|
-
headers: { Accept: "text/event-stream" },
|
|
2755
|
-
})
|
|
2756
|
-
|
|
2757
|
-
test
|
|
2758
|
-
.expect(eventStreamResponse.status)
|
|
2759
|
-
.toBe(200)
|
|
2760
|
-
test
|
|
2761
|
-
.expect(calls)
|
|
2762
|
-
.toEqual(["render"])
|
|
2763
|
-
test
|
|
2764
|
-
.expect(await eventStreamResponse.text())
|
|
2765
|
-
.toBe("render output")
|
|
2766
|
-
})
|
|
2767
|
-
|
|
2768
|
-
test.it("render used as fallback when Accept doesn't match other formats", async () => {
|
|
2769
|
-
const handler = RouteHttp.toWebHandler(
|
|
2770
|
-
Route
|
|
2771
|
-
.get(
|
|
2772
|
-
Route.json({ type: "json" }),
|
|
2773
|
-
Route.html("<h1>html</h1>"),
|
|
2774
|
-
Route.render(function*() {
|
|
2775
|
-
return "fallback for unknown accept"
|
|
2776
|
-
}),
|
|
2777
|
-
),
|
|
2778
|
-
)
|
|
2779
|
-
|
|
2780
|
-
const eventStreamResponse = await Http.fetch(handler, {
|
|
2781
|
-
path: "/",
|
|
2782
|
-
headers: { Accept: "text/event-stream" },
|
|
2783
|
-
})
|
|
2784
|
-
|
|
2785
|
-
test
|
|
2786
|
-
.expect(eventStreamResponse.status)
|
|
2787
|
-
.toBe(200)
|
|
2788
|
-
test
|
|
2789
|
-
.expect(await eventStreamResponse.text())
|
|
2790
|
-
.toBe("fallback for unknown accept")
|
|
2791
|
-
})
|
|
2792
|
-
|
|
2793
|
-
test.it("handler context includes format=*", () => {
|
|
2794
|
-
Route.get(
|
|
2795
|
-
Route.render(function*(ctx) {
|
|
2796
|
-
test
|
|
2797
|
-
.expectTypeOf(ctx.format)
|
|
2798
|
-
.toEqualTypeOf<"*">()
|
|
2799
|
-
return "ok"
|
|
2800
|
-
}),
|
|
2801
|
-
)
|
|
2802
|
-
})
|
|
2803
|
-
|
|
2804
|
-
test.it("streams work correctly with render", async () => {
|
|
2805
|
-
const handler = RouteHttp.toWebHandler(
|
|
2806
|
-
Route.get(
|
|
2807
|
-
Route.render(function*() {
|
|
2808
|
-
return Stream.make("chunk1", "chunk2", "chunk3")
|
|
2809
|
-
}),
|
|
2810
|
-
),
|
|
2811
|
-
)
|
|
2812
|
-
|
|
2813
|
-
const response = await Http.fetch(handler, {
|
|
2814
|
-
path: "/stream",
|
|
2815
|
-
headers: { Accept: "text/event-stream" },
|
|
2816
|
-
})
|
|
2817
|
-
|
|
2818
|
-
test
|
|
2819
|
-
.expect(response.status)
|
|
2820
|
-
.toBe(200)
|
|
2821
|
-
test
|
|
2822
|
-
.expect(await response.text())
|
|
2823
|
-
.toBe("chunk1chunk2chunk3")
|
|
2824
|
-
})
|
|
2825
|
-
|
|
2826
|
-
test.it("multiple render middlewares chain correctly", async () => {
|
|
2827
|
-
const handler = RouteHttp.toWebHandler(
|
|
2828
|
-
Route
|
|
2829
|
-
.use(
|
|
2830
|
-
Route.render(function*(_ctx, next) {
|
|
2831
|
-
const value = yield* next().text
|
|
2832
|
-
return `outer(${value})`
|
|
2833
|
-
}),
|
|
2834
|
-
Route.render(function*(_ctx, next) {
|
|
2835
|
-
const value = yield* next().text
|
|
2836
|
-
return `inner(${value})`
|
|
2837
|
-
}),
|
|
2838
|
-
)
|
|
2839
|
-
.get(
|
|
2840
|
-
Route.render(function*() {
|
|
2841
|
-
return "content"
|
|
2842
|
-
}),
|
|
2843
|
-
),
|
|
2844
|
-
)
|
|
2845
|
-
|
|
2846
|
-
const response = await Http.fetch(handler, { path: "/" })
|
|
2847
|
-
|
|
2848
|
-
test
|
|
2849
|
-
.expect(response.status)
|
|
2850
|
-
.toBe(200)
|
|
2851
|
-
test
|
|
2852
|
-
.expect(await response.text())
|
|
2853
|
-
.toBe("outer(inner(content))")
|
|
2854
|
-
})
|
|
2855
|
-
|
|
2856
|
-
test.it("render middleware can wrap text handler", async () => {
|
|
2857
|
-
const handler = RouteHttp.toWebHandler(
|
|
2858
|
-
Route
|
|
2859
|
-
.use(
|
|
2860
|
-
Route.render(function*(_ctx, next) {
|
|
2861
|
-
const value = yield* next().text
|
|
2862
|
-
return `[${value}]`
|
|
2863
|
-
}),
|
|
2864
|
-
)
|
|
2865
|
-
.get(
|
|
2866
|
-
Route.text("hello"),
|
|
2867
|
-
),
|
|
2868
|
-
)
|
|
2869
|
-
|
|
2870
|
-
const response = await Http.fetch(handler, {
|
|
2871
|
-
path: "/",
|
|
2872
|
-
headers: { Accept: "text/plain" },
|
|
2873
|
-
})
|
|
2874
|
-
|
|
2875
|
-
test
|
|
2876
|
-
.expect(response.status)
|
|
2877
|
-
.toBe(200)
|
|
2878
|
-
test
|
|
2879
|
-
.expect(await response.text())
|
|
2880
|
-
.toBe("[hello]")
|
|
2881
|
-
})
|
|
2882
|
-
|
|
2883
|
-
test.it("render middleware can wrap html handler", async () => {
|
|
2884
|
-
const handler = RouteHttp.toWebHandler(
|
|
2885
|
-
Route
|
|
2886
|
-
.use(
|
|
2887
|
-
Route.render(function*(_ctx, next) {
|
|
2888
|
-
const value = yield* next().text
|
|
2889
|
-
return `<!DOCTYPE html>${value}`
|
|
2890
|
-
}),
|
|
2891
|
-
)
|
|
2892
|
-
.get(
|
|
2893
|
-
Route.html("<body>content</body>"),
|
|
2894
|
-
),
|
|
2895
|
-
)
|
|
2896
|
-
|
|
2897
|
-
const response = await Http.fetch(handler, {
|
|
2898
|
-
path: "/",
|
|
2899
|
-
headers: { Accept: "text/html" },
|
|
2900
|
-
})
|
|
2901
|
-
|
|
2902
|
-
test
|
|
2903
|
-
.expect(response.status)
|
|
2904
|
-
.toBe(200)
|
|
2905
|
-
test
|
|
2906
|
-
.expect(await response.text())
|
|
2907
|
-
.toBe("<!DOCTYPE html><body>content</body>")
|
|
2908
|
-
})
|
|
2909
|
-
})
|