| 1 | import { assert, it } from "@effect/vitest" |
| 2 | import { ByteSize, Effect, FileSystem, Stream } from "effect" |
| 3 | import { HttpBody } from "effect/http" |
| 4 | |
| 5 | const maxSafe = BigInt(Number.MAX_SAFE_INTEGER) |
| 6 | const oversized = 9007199254740993n |
| 7 | const fileInfo = (size: bigint): FileSystem.File.Info => ({ size: ByteSize.bytes(size) }) as FileSystem.File.Info |
| 8 | const fromInfo = (size: bigint, options?: Parameters<typeof HttpBody.fileFromInfo>[2]) => |
| 9 | HttpBody.fileFromInfo("x", fileInfo(size), options).pipe( |
| 10 | Effect.provideService(FileSystem.FileSystem, FileSystem.makeNoop({ stream: () => Stream.empty })) |
| 11 | ) |
| 12 | |
| 13 | it.each([ |
| 14 | { name: "maximum safe final length", size: oversized, options: { offset: 2 }, expected: Number.MAX_SAFE_INTEGER }, |
| 15 | { |
| 16 | name: "exact subtraction before clamping", |
| 17 | size: oversized + 3n, |
| 18 | options: { offset: oversized, bytesToRead: 4 }, |
| 19 | expected: 3 |
| 20 | }, |
| 21 | { name: "safe byte count on an oversized file", size: oversized, options: { bytesToRead: 2 }, expected: 2 }, |
| 22 | { name: "oversized byte count clamped to a small file", size: 6n, options: { bytesToRead: oversized }, expected: 6 }, |
| 23 | { name: "offset at EOF", size: 6n, options: { offset: 6 }, expected: 0 }, |
| 24 | { name: "offset past EOF", size: 6n, options: { offset: 7, bytesToRead: 2 }, expected: 0 }, |
| 25 | { name: "zero byte count", size: oversized, options: { bytesToRead: 0 }, expected: 0 } |
| 26 | ])("fileFromInfo handles $name", async ({ expected, options, size }) => { |
| 27 | const body = await Effect.runPromise(fromInfo(size, options)) |
| 28 | assert.strictEqual(body.contentLength, expected) |
| 29 | }) |
| 30 | |
| 31 | it.each([ |
| 32 | { name: "first unsafe final length", size: maxSafe + 1n, options: {} }, |
| 33 | { |
| 34 | name: "unsafe length after applying both bounds", |
| 35 | size: oversized + 3n, |
| 36 | options: { offset: 1, bytesToRead: oversized } |
| 37 | }, |
| 38 | // Bypass the input type to exercise runtime validation. |
| 39 | { name: "malformed byte count", size: 6n, options: { bytesToRead: "garbage" as ByteSize.Input } }, |
| 40 | { name: "negative offset with zero bytes requested", size: 6n, options: { offset: -1n, bytesToRead: 0 } }, |
| 41 | { name: "negative byte count at EOF", size: 6n, options: { offset: 6, bytesToRead: -1 } }, |
| 42 | { name: "fractional offset", size: 6n, options: { offset: 1.5 } }, |
| 43 | { name: "non-finite byte count", size: 6n, options: { bytesToRead: Infinity } }, |
| 44 | { name: "unsafe numeric offset", size: 6n, options: { offset: Number.MAX_SAFE_INTEGER + 1 } }, |
| 45 | { name: "unsafe numeric byte count", size: 6n, options: { bytesToRead: Number.MAX_SAFE_INTEGER + 1 } } |
| 46 | ])("fileFromInfo rejects $name with typed BadArgument", async ({ options, size }) => { |
| 47 | const error = await Effect.runPromise(fromInfo(size, options).pipe(Effect.flip)) |
| 48 | assert.strictEqual(error._tag, "PlatformError") |
| 49 | assert.strictEqual(error.reason._tag, "BadArgument") |
| 50 | assert.strictEqual(error.reason.method, "fileFromInfo") |
| 51 | }) |
| 52 | |
| 53 | for (const constructor of ["file", "fileFromInfo"] as const) { |
| 54 | it(`${constructor} defers range validation and filesystem access until evaluation`, async () => { |
| 55 | let reads = 0 |
| 56 | let statCalls = 0 |
| 57 | const options = { |
| 58 | get offset() { |
| 59 | reads++ |
| 60 | // Bypass the input type to exercise deferred runtime validation. |
| 61 | return "garbage" as ByteSize.Input |
| 62 | } |
| 63 | } |
| 64 | // Construct outside Effect.gen so eager validation fails the test. |
| 65 | const info = fileInfo(6n) |
| 66 | const body = (constructor === "file" ? HttpBody.file("x", options) : HttpBody.fileFromInfo("x", info, options)) |
| 67 | .pipe( |
| 68 | Effect.provideService( |
| 69 | FileSystem.FileSystem, |
| 70 | FileSystem.makeNoop({ |
| 71 | stat: () => { |
| 72 | statCalls++ |
| 73 | return Effect.succeed(info) |
| 74 | }, |
| 75 | stream: () => assert.fail("invalid ranges must fail before creating a stream") |
| 76 | }) |
| 77 | ) |
| 78 | ) |
| 79 | assert.strictEqual(reads, 0) |
| 80 | assert.strictEqual(statCalls, 0) |
| 81 | const error = await Effect.runPromise(body.pipe(Effect.flip)) |
| 82 | assert.strictEqual(error._tag, "PlatformError") |
| 83 | assert.strictEqual(error.reason._tag, "BadArgument") |
| 84 | assert.strictEqual(error.reason.method, constructor) |
| 85 | assert.isAbove(reads, 0) |
| 86 | assert.strictEqual(statCalls, constructor === "file" ? 1 : 0) |
| 87 | }) |
| 88 | } |
| 89 | |
| 90 | it("fileFromInfo preserves a small selected range and stream options", async () => { |
| 91 | const options = { offset: "2 B", bytesToRead: ByteSize.bytes(2), contentType: "text/plain", chunkSize: 2 } as const |
| 92 | const body = await Effect.runPromise( |
| 93 | HttpBody.fileFromInfo("x", fileInfo(6n), options).pipe( |
| 94 | Effect.provideService( |
| 95 | FileSystem.FileSystem, |
| 96 | FileSystem.makeNoop({ |
| 97 | stream: (path, range) => { |
| 98 | assert.strictEqual(path, "x") |
| 99 | assert.deepStrictEqual(range, options) |
| 100 | return Stream.succeed(new Uint8Array([3, 4])) |
| 101 | } |
| 102 | }) |
| 103 | ) |
| 104 | ) |
| 105 | ) |
| 106 | const bytes = await Effect.runPromise(Stream.mkUint8Array(body.stream)) |
| 107 | assert.deepStrictEqual(Array.from(bytes), [3, 4]) |
| 108 | assert.strictEqual(body.contentLength, bytes.length) |
| 109 | assert.strictEqual(body.contentType, "text/plain") |
| 110 | }) |
| 111 | |
| 112 | it("identifies HTTP body errors", () => { |
| 113 | assert.isTrue(HttpBody.isHttpBodyError(new HttpBody.HttpBodyError({ reason: { _tag: "JsonError" } }))) |
| 114 | assert.isFalse(HttpBody.isHttpBodyError({ _tag: "HttpBodyError" })) |
| 115 | assert.isFalse(HttpBody.isHttpBodyError(null)) |
| 116 | }) |