| 1 | import type * as BunFileSystem from "@effect/platform-bun/BunFileSystem" |
| 2 | import * as BunHttpPlatform from "@effect/platform-bun/BunHttpPlatform" |
| 3 | import { assert, describe, it } from "@effect/vitest" |
| 4 | import * as ByteSize from "effect/ByteSize" |
| 5 | import * as Effect from "effect/Effect" |
| 6 | import type * as HttpBody from "effect/http/HttpBody" |
| 7 | import * as HttpPlatform from "effect/http/HttpPlatform" |
| 8 | import * as HttpServerResponse from "effect/http/HttpServerResponse" |
| 9 | import { afterEach, beforeEach, vi } from "vitest" |
| 10 | |
| 11 | const readBody = (body: HttpBody.HttpBody) => { |
| 12 | assert.strictEqual(body._tag, "Raw") |
| 13 | return Effect.promise(() => new Response((body as HttpBody.Raw).body as BodyInit).text()) |
| 14 | } |
| 15 | |
| 16 | // Bun sets Content-Length when sending the raw file, so inspect the HTTP response. |
| 17 | const readResponse = (response: HttpServerResponse.HttpServerResponse) => |
| 18 | Effect.acquireUseRelease( |
| 19 | Effect.sync(() => |
| 20 | Bun.serve({ |
| 21 | hostname: "127.0.0.1", |
| 22 | port: 0, |
| 23 | fetch: () => HttpServerResponse.toWeb(response) |
| 24 | }) |
| 25 | ), |
| 26 | (server) => |
| 27 | Effect.promise(async (signal) => { |
| 28 | const response = await fetch(server.url, { signal }) |
| 29 | return { contentLength: response.headers.get("content-length"), body: await response.text() } |
| 30 | }), |
| 31 | (server) => |
| 32 | Effect.promise(async () => { |
| 33 | await server.stop(true) |
| 34 | }) |
| 35 | ) |
| 36 | |
| 37 | describe("BunHttpPlatform", { concurrent: false }, () => { |
| 38 | for ( |
| 39 | const { name, offset, bytesToRead, expected } of [ |
| 40 | { name: "clamps bytesToRead beyond EOF", offset: 1, bytesToRead: 10, expected: "bcd" }, |
| 41 | { name: "returns an empty body at EOF", offset: 4, bytesToRead: undefined, expected: "" }, |
| 42 | { name: "returns an empty body past EOF", offset: 9, bytesToRead: undefined, expected: "" }, |
| 43 | { name: "clamps bytesToRead at EOF", offset: 4, bytesToRead: 10, expected: "" }, |
| 44 | { name: "clamps bytesToRead past EOF", offset: 9, bytesToRead: 10, expected: "" } |
| 45 | ] |
| 46 | ) { |
| 47 | it.effect(`fileWebResponse ${name}`, () => |
| 48 | Effect.gen(function*() { |
| 49 | const platform = yield* HttpPlatform.HttpPlatform |
| 50 | const file = new File(["abcd"], "file.txt", { type: "text/plain", lastModified: 0 }) |
| 51 | const response = yield* platform.fileWebResponse(file, { offset, bytesToRead }) |
| 52 | assert.deepStrictEqual( |
| 53 | yield* readResponse(response), |
| 54 | { contentLength: String(expected.length), body: expected } |
| 55 | ) |
| 56 | }).pipe(Effect.provide(BunHttpPlatform.layer))) |
| 57 | } |
| 58 | |
| 59 | for ( |
| 60 | const { name, offsetFromEnd, bytesToRead } of [ |
| 61 | { name: "clamps bytesToRead beyond EOF", offsetFromEnd: -5, bytesToRead: 100 }, |
| 62 | { name: "returns an empty body at EOF", offsetFromEnd: 0, bytesToRead: undefined }, |
| 63 | { name: "returns an empty body past EOF", offsetFromEnd: 5, bytesToRead: undefined }, |
| 64 | { name: "clamps bytesToRead at EOF", offsetFromEnd: 0, bytesToRead: 100 }, |
| 65 | { name: "clamps bytesToRead past EOF", offsetFromEnd: 5, bytesToRead: 100 } |
| 66 | ] |
| 67 | ) { |
| 68 | it.effect(`fileResponse ${name}`, () => |
| 69 | Effect.gen(function*() { |
| 70 | const platform = yield* HttpPlatform.HttpPlatform |
| 71 | const file = Bun.file(import.meta.filename) |
| 72 | const contents = yield* Effect.promise(() => file.text()) |
| 73 | const expected = offsetFromEnd < 0 ? contents.slice(offsetFromEnd) : "" |
| 74 | const response = yield* platform.fileResponse(import.meta.filename, { |
| 75 | offset: ByteSize.bytes(file.size + offsetFromEnd), |
| 76 | bytesToRead: bytesToRead === undefined ? undefined : ByteSize.bytes(bytesToRead) |
| 77 | }) |
| 78 | assert.deepStrictEqual( |
| 79 | yield* readResponse(response), |
| 80 | { contentLength: String(expected.length), body: expected } |
| 81 | ) |
| 82 | }).pipe(Effect.provide(BunHttpPlatform.layer))) |
| 83 | } |
| 84 | |
| 85 | it.effect("fileWebResponse honors offset and bytesToRead including zero", () => |
| 86 | Effect.gen(function*() { |
| 87 | const platform = yield* HttpPlatform.HttpPlatform |
| 88 | const file = new File(["abcd"], "file.txt", { type: "text/plain", lastModified: 0 }) |
| 89 | const sliced = yield* platform.fileWebResponse(file, { offset: 1, bytesToRead: 2 }) |
| 90 | const empty = yield* platform.fileWebResponse(file, { offset: 1, bytesToRead: 0 }) |
| 91 | |
| 92 | assert.deepStrictEqual( |
| 93 | { sliced: yield* readBody(sliced.body), empty: yield* readBody(empty.body) }, |
| 94 | { sliced: "bc", empty: "" } |
| 95 | ) |
| 96 | }).pipe(Effect.provide(BunHttpPlatform.layer))) |
| 97 | |
| 98 | it.effect("fileResponse supports exact and empty ranges", () => |
| 99 | Effect.gen(function*() { |
| 100 | const platform = yield* HttpPlatform.HttpPlatform |
| 101 | const exact = yield* platform.fileResponse(import.meta.filename, { |
| 102 | bytesToRead: ByteSize.bytes(6) |
| 103 | }) |
| 104 | const empty = yield* platform.fileResponse(import.meta.filename, { |
| 105 | offset: ByteSize.bytes(1), |
| 106 | bytesToRead: ByteSize.zero |
| 107 | }) |
| 108 | |
| 109 | assert.deepStrictEqual( |
| 110 | { exact: yield* readBody(exact.body), empty: yield* readBody(empty.body) }, |
| 111 | { exact: "import", empty: "" } |
| 112 | ) |
| 113 | }).pipe(Effect.provide(BunHttpPlatform.layer))) |
| 114 | }) |
| 115 | |
| 116 | const precision = vi.hoisted(() => ({ enabled: false })) |
| 117 | |
| 118 | vi.mock("@effect/platform-bun/BunFileSystem", async (importOriginal) => { |
| 119 | const original = await importOriginal<typeof BunFileSystem>() |
| 120 | const { Layer, Effect } = await import("effect") |
| 121 | const { fileSystemLayer } = await import("../../node-shared/test/HttpPlatform.test-utils.ts") |
| 122 | return { layer: Layer.unwrap(Effect.sync(() => precision.enabled ? fileSystemLayer : original.layer)) } |
| 123 | }) |
| 124 | |
| 125 | describe("BunHttpPlatform precision", { concurrent: false }, () => { |
| 126 | const slice = vi.fn((_start?: number, _end?: number) => new Blob([])) |
| 127 | |
| 128 | beforeEach(() => { |
| 129 | precision.enabled = true |
| 130 | slice.mockClear() |
| 131 | vi.spyOn(Bun, "file").mockReturnValue({ slice } as unknown as ReturnType<typeof Bun.file>) |
| 132 | }) |
| 133 | afterEach(() => { |
| 134 | precision.enabled = false |
| 135 | vi.restoreAllMocks() |
| 136 | }) |
| 137 | |
| 138 | it.effect("passes a safe range to the runtime with the correct end bound", () => |
| 139 | Effect.gen(function*() { |
| 140 | const maxSafe = Number.MAX_SAFE_INTEGER |
| 141 | const platform = yield* HttpPlatform.HttpPlatform |
| 142 | yield* platform.fileResponse("precision.bin", { offset: maxSafe - 1, bytesToRead: 1 }) |
| 143 | assert.deepStrictEqual(slice.mock.calls, [[maxSafe - 1, maxSafe]]) |
| 144 | }).pipe(Effect.provide(BunHttpPlatform.layer))) |
| 145 | |
| 146 | it.effect("serves a whole oversized file without slicing it", () => |
| 147 | Effect.gen(function*() { |
| 148 | const platform = yield* HttpPlatform.HttpPlatform |
| 149 | const response = yield* platform.fileResponse("precision.bin") |
| 150 | assert.strictEqual(response.status, 200) |
| 151 | assert.strictEqual(response.headers["content-length"], "9007199254740993") |
| 152 | assert.strictEqual(response.body._tag, "Raw") |
| 153 | if (response.body._tag === "Raw") { |
| 154 | assert.strictEqual(response.body.body, vi.mocked(Bun.file).mock.results[0].value) |
| 155 | } |
| 156 | assert.strictEqual(vi.mocked(Bun.file).mock.calls.length, 1) |
| 157 | assert.deepStrictEqual(slice.mock.calls, []) |
| 158 | }).pipe(Effect.provide(BunHttpPlatform.layer))) |
| 159 | }) |