1import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"
2import { assert, describe, it } from "@effect/vitest"
3import * as ByteSize from "effect/ByteSize"
4import * as Effect from "effect/Effect"
5import * as Etag from "effect/http/Etag"
6import * as HttpPlatform from "effect/http/HttpPlatform"
7import * as HttpServerResponse from "effect/http/HttpServerResponse"
8import * as Layer from "effect/Layer"
9import * as Fs from "node:fs"
10import { Readable } from "node:stream"
11import { afterEach, beforeEach, vi } from "vitest"
12import { fileSystemLayer } from "../../node-shared/test/HttpPlatform.test-utils.ts"
13
14const readResponse = (response: HttpServerResponse.HttpServerResponse) =>
15 Effect.promise(() => HttpServerResponse.toWeb(response).text())
16
17describe("NodeHttpPlatform", { concurrent: false }, () => {
18 it.effect("fileWebResponse prefers File.type over the extension", () =>
19 Effect.gen(function*() {
20 const platform = yield* HttpPlatform.HttpPlatform
21 const response = yield* platform.fileWebResponse(new File([], "script.js", { type: "application/custom" }))
22 assert.strictEqual(response.headers["content-type"], "application/custom")
23 }).pipe(Effect.provide(NodeHttpPlatform.layer)))
24
25 it.effect("fileResponse reads exact bytesToRead", () =>
26 Effect.gen(function*() {
27 const platform = yield* HttpPlatform.HttpPlatform
28 const response = yield* platform.fileResponse(`${__dirname}/fixtures/text.txt`, {
29 offset: ByteSize.bytes(6),
30 bytesToRead: ByteSize.bytes(5)
31 })
32
33 assert.strictEqual(response.headers["content-length"], "5")
34 assert.strictEqual(response.headers["content-type"], "text/plain")
35 const text = yield* readResponse(response)
36 assert.strictEqual(text, "ipsum")
37 }).pipe(Effect.provide(NodeHttpPlatform.layer)))
38
39 for (
40 const { name, offset, bytesToRead, expected } of [
41 { name: "clamps bytesToRead beyond EOF", offset: 22, bytesToRead: 100, expected: "amet\n" },
42 { name: "returns an empty body at EOF", offset: 27, bytesToRead: undefined, expected: "" },
43 { name: "returns an empty body past EOF", offset: 50, bytesToRead: undefined, expected: "" },
44 { name: "clamps bytesToRead at EOF", offset: 27, bytesToRead: 100, expected: "" },
45 { name: "clamps bytesToRead past EOF", offset: 50, bytesToRead: 100, expected: "" }
46 ]
47 ) {
48 it.effect(`fileResponse ${name}`, () =>
49 Effect.gen(function*() {
50 const platform = yield* HttpPlatform.HttpPlatform
51 const response = yield* platform.fileResponse(`${__dirname}/fixtures/text.txt`, {
52 offset: ByteSize.bytes(offset),
53 bytesToRead: bytesToRead === undefined ? undefined : ByteSize.bytes(bytesToRead)
54 })
55
56 const text = yield* readResponse(response)
57 assert.deepStrictEqual(
58 { contentLength: response.headers["content-length"], body: text },
59 { contentLength: String(expected.length), body: expected }
60 )
61 }).pipe(Effect.provide(NodeHttpPlatform.layer)))
62 }
63
64 it.effect("fileResponse supports zero bytesToRead", () =>
65 Effect.gen(function*() {
66 const platform = yield* HttpPlatform.HttpPlatform
67 const response = yield* platform.fileResponse(`${__dirname}/fixtures/text.txt`, {
68 offset: ByteSize.bytes(6),
69 bytesToRead: ByteSize.zero
70 })
71
72 assert.strictEqual(response.headers["content-length"], "0")
73 const text = yield* readResponse(response)
74 assert.strictEqual(text, "")
75 }).pipe(Effect.provide(NodeHttpPlatform.layer)))
76
77 it.effect("fileWebResponse looks up content types case-insensitively", () =>
78 Effect.gen(function*() {
79 const platform = yield* HttpPlatform.HttpPlatform
80 const response = yield* platform.fileWebResponse(new File(["<h1>Effect</h1>"], "INDEX.HTML"))
81
82 assert.strictEqual(response.headers["content-type"], "text/html")
83 }).pipe(Effect.provide(NodeHttpPlatform.layer)))
84})
85
86vi.mock("node:fs", async (importOriginal) => {
87 const original = await importOriginal<typeof Fs>()
88 return { ...original, createReadStream: vi.fn(original.createReadStream) }
89})
90
91describe("NodeHttpPlatform precision", { concurrent: false }, () => {
92 beforeEach(() => {
93 vi.mocked(Fs.createReadStream).mockImplementation(() => Readable.from([]) as Fs.ReadStream)
94 })
95 afterEach(() => {
96 vi.mocked(Fs.createReadStream).mockReset()
97 })
98
99 const layer = Layer.effect(HttpPlatform.HttpPlatform)(NodeHttpPlatform.make).pipe(
100 Layer.provide(fileSystemLayer),
101 Layer.provide(Etag.layer)
102 )
103
104 it.effect("passes a safe range to the runtime with the correct end bound", () =>
105 Effect.gen(function*() {
106 const maxSafe = Number.MAX_SAFE_INTEGER
107 const platform = yield* HttpPlatform.HttpPlatform
108 const response = yield* platform.fileResponse("precision.bin", { offset: maxSafe - 1, bytesToRead: 1 })
109 yield* readResponse(response)
110 assert.deepStrictEqual(vi.mocked(Fs.createReadStream).mock.calls, [["precision.bin", {
111 start: maxSafe - 1,
112 end: maxSafe - 1
113 }]])
114 }).pipe(Effect.provide(layer)))
115
116 it.effect("serves a whole oversized file with an exact content length and no end offset", () =>
117 Effect.gen(function*() {
118 const platform = yield* HttpPlatform.HttpPlatform
119 const response = yield* platform.fileResponse("precision.bin")
120 assert.strictEqual(response.status, 200)
121 assert.strictEqual(response.headers["content-length"], "9007199254740993")
122 yield* readResponse(response)
123 assert.deepStrictEqual(vi.mocked(Fs.createReadStream).mock.calls, [["precision.bin", {
124 start: 0,
125 end: undefined
126 }]])
127 }).pipe(Effect.provide(layer)))
128})