1import { assert, describe, it } from "@effect/vitest"
2import { ByteSize, Effect, FileSystem, Layer, Option, Stream } from "effect"
3import { HttpPlatform } from "effect/http"
4
5describe("HttpPlatform", () => {
6 const oversized = 9007199254740993n
7 const layer = HttpPlatform.layer.pipe(Layer.provide(FileSystem.layerNoop({
8 stat: () =>
9 Effect.succeed({
10 type: "File",
11 size: ByteSize.bytes(oversized),
12 mtime: Option.none()
13 } as FileSystem.File.Info),
14 stream: () => Stream.empty
15 })))
16
17 it.effect("fileResponse infers the content type from the extension", () =>
18 Effect.gen(function*() {
19 const platform = yield* HttpPlatform.HttpPlatform
20 const response = yield* platform.fileResponse("script.js")
21 assert.strictEqual(response.headers["content-type"], "text/javascript")
22 }).pipe(Effect.provide(layer)))
23
24 it.effect("fileResponse honors contentType over a conflicting header", () =>
25 Effect.gen(function*() {
26 const platform = yield* HttpPlatform.HttpPlatform
27 const response = yield* platform.fileResponse("script.js", {
28 contentType: "application/custom",
29 headers: { "content-type": "application/header" }
30 })
31 assert.strictEqual(response.headers["content-type"], "application/custom")
32 }).pipe(Effect.provide(layer)))
33
34 it.effect("fileWebResponse honors contentType over the file type", () =>
35 Effect.gen(function*() {
36 const platform = yield* HttpPlatform.HttpPlatform
37 const response = yield* platform.fileWebResponse(new File([], "script.js", { type: "text/plain" }), {
38 contentType: "text/javascript"
39 })
40 assert.strictEqual(response.headers["content-type"], "text/javascript")
41 }).pipe(Effect.provide(layer)))
42
43 it.effect("serves a whole oversized file with an exact content-length header", () =>
44 Effect.gen(function*() {
45 const platform = yield* HttpPlatform.HttpPlatform
46 const response = yield* platform.fileResponse("file.bin")
47 assert.strictEqual(response.headers["content-length"], oversized.toString())
48 }).pipe(Effect.provide(layer)))
49
50 for (
51 const [name, options] of [
52 ["unsafe bigint offset", { offset: oversized }],
53 ["unsafe end from safe bounds", { offset: Number.MAX_SAFE_INTEGER, bytesToRead: 2 }],
54 ["unsafe number offset", { offset: Number.MAX_SAFE_INTEGER + 1 }],
55 ["unsafe number byte count", { bytesToRead: Number.MAX_SAFE_INTEGER + 1 }]
56 ] as const
57 ) {
58 it.effect(`rejects ${name} with BadArgument`, () =>
59 Effect.gen(function*() {
60 const platform = yield* HttpPlatform.HttpPlatform
61 const error = yield* Effect.flip(platform.fileResponse("file.bin", options))
62 assert.strictEqual(error.reason._tag, "BadArgument")
63 }).pipe(Effect.provide(layer)))
64 }
65
66 const file = {
67 name: "file.bin",
68 lastModified: 0,
69 size: 4,
70 type: "application/octet-stream",
71 stream: () => new Blob([new Uint8Array([1, 2, 3, 4])]).stream()
72 }
73
74 it.effect("honors Web file offset and byte count", () =>
75 Effect.gen(function*() {
76 const platform = yield* HttpPlatform.HttpPlatform
77 const response = yield* platform.fileWebResponse(file, { offset: 1, bytesToRead: 2 })
78 assert.strictEqual(response.body._tag, "Stream")
79 if (response.body._tag === "Stream") {
80 assert.strictEqual(response.body.contentLength, 2)
81 const bytes = yield* Stream.mkUint8Array(response.body.stream)
82 assert.deepStrictEqual(Array.from(bytes), [2, 3])
83 }
84 }).pipe(
85 Effect.provide(HttpPlatform.layer),
86 Effect.provideService(FileSystem.FileSystem, {} as any)
87 ))
88
89 for (
90 const { name, offset, bytesToRead, expected } of [
91 { name: "clamps bytesToRead beyond EOF", offset: 1, bytesToRead: 10, expected: [2, 3, 4] },
92 { name: "returns an empty body at EOF", offset: 4, bytesToRead: undefined, expected: [] },
93 { name: "returns an empty body past EOF", offset: 9, bytesToRead: undefined, expected: [] },
94 { name: "clamps bytesToRead at EOF", offset: 4, bytesToRead: 10, expected: [] },
95 { name: "clamps bytesToRead past EOF", offset: 9, bytesToRead: 10, expected: [] }
96 ]
97 ) {
98 it.effect(`fileWebResponse ${name}`, () =>
99 Effect.gen(function*() {
100 const platform = yield* HttpPlatform.HttpPlatform
101 const response = yield* platform.fileWebResponse(file, { offset, bytesToRead })
102 assert.strictEqual(response.body._tag, "Stream")
103 if (response.body._tag === "Stream") {
104 const bytes = yield* Stream.mkUint8Array(response.body.stream)
105 assert.deepStrictEqual(
106 {
107 contentLength: response.body.contentLength,
108 header: response.headers["content-length"],
109 body: Array.from(bytes)
110 },
111 { contentLength: expected.length, header: String(expected.length), body: expected }
112 )
113 }
114 }).pipe(
115 Effect.provide(HttpPlatform.layer),
116 Effect.provideService(FileSystem.FileSystem, {} as any)
117 ))
118 }
119
120 it.effect("honors Web file chunk size", () =>
121 Effect.gen(function*() {
122 const platform = yield* HttpPlatform.HttpPlatform
123 const response = yield* platform.fileWebResponse(file, { offset: 0, bytesToRead: 4, chunkSize: 2 })
124 assert.strictEqual(response.body._tag, "Stream")
125 if (response.body._tag === "Stream") {
126 assert.strictEqual(response.body.contentLength, 4)
127 const chunks = yield* Stream.runCollect(response.body.stream)
128 assert.deepStrictEqual(chunks.map((chunk) => Array.from(chunk)), [[1, 2], [3, 4]])
129 }
130 }).pipe(
131 Effect.provide(HttpPlatform.layer),
132 Effect.provideService(FileSystem.FileSystem, {} as any)
133 ))
134
135 it.effect("keeps the source chunking by default", () =>
136 Effect.gen(function*() {
137 const platform = yield* HttpPlatform.HttpPlatform
138 const response = yield* platform.fileWebResponse(file)
139 assert.strictEqual(response.body._tag, "Stream")
140 if (response.body._tag === "Stream") {
141 const chunks = yield* Stream.runCollect(response.body.stream)
142 assert.deepStrictEqual(chunks.map((chunk) => Array.from(chunk)), [[1, 2, 3, 4]])
143 }
144 }).pipe(
145 Effect.provide(HttpPlatform.layer),
146 Effect.provideService(FileSystem.FileSystem, {} as any)
147 ))
148})