1import { assert, it } from "@effect/vitest"
2import { ByteSize, Effect, FileSystem, Stream } from "effect"
3import { HttpBody } from "effect/http"
4
5const maxSafe = BigInt(Number.MAX_SAFE_INTEGER)
6const oversized = 9007199254740993n
7const fileInfo = (size: bigint): FileSystem.File.Info => ({ size: ByteSize.bytes(size) }) as FileSystem.File.Info
8const 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
13it.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
31it.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
53for (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
90it("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
112it("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})