| 1 | import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform" |
| 2 | import { assert, describe, it } from "@effect/vitest" |
| 3 | import * as ByteSize from "effect/ByteSize" |
| 4 | import * as Effect from "effect/Effect" |
| 5 | import * as Etag from "effect/http/Etag" |
| 6 | import * as HttpPlatform from "effect/http/HttpPlatform" |
| 7 | import * as HttpServerResponse from "effect/http/HttpServerResponse" |
| 8 | import * as Layer from "effect/Layer" |
| 9 | import * as Fs from "node:fs" |
| 10 | import { Readable } from "node:stream" |
| 11 | import { afterEach, beforeEach, vi } from "vitest" |
| 12 | import { fileSystemLayer } from "../../node-shared/test/HttpPlatform.test-utils.ts" |
| 13 | |
| 14 | const readResponse = (response: HttpServerResponse.HttpServerResponse) => |
| 15 | Effect.promise(() => HttpServerResponse.toWeb(response).text()) |
| 16 | |
| 17 | describe("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 | |
| 86 | vi.mock("node:fs", async (importOriginal) => { |
| 87 | const original = await importOriginal<typeof Fs>() |
| 88 | return { ...original, createReadStream: vi.fn(original.createReadStream) } |
| 89 | }) |
| 90 | |
| 91 | describe("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 | }) |