| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { ByteSize, Effect, FileSystem, Layer, Option, Stream } from "effect" |
| 3 | import { HttpPlatform } from "effect/http" |
| 4 | |
| 5 | describe("HttpPlatform", () => { |
| 6 | const oversized = 9007199254740993n |
| 7 | const layer = HttpPlatform.layer.pipe(Layer.provide(FileSystem.layerNoop({ |
| 8 | stat: () => |
| 9 | Effect.succeed({ |
| 10 | type: "File", |
| 11 | size: ByteSize.bytes(oversized), |
| 12 | mtime: Option.none() |
| 13 | } as FileSystem.File.Info), |
| 14 | stream: () => Stream.empty |
| 15 | }))) |
| 16 | |
| 17 | it.effect("fileResponse infers the content type from the extension", () => |
| 18 | Effect.gen(function*() { |
| 19 | const platform = yield* HttpPlatform.HttpPlatform |
| 20 | const response = yield* platform.fileResponse("script.js") |
| 21 | assert.strictEqual(response.headers["content-type"], "text/javascript") |
| 22 | }).pipe(Effect.provide(layer))) |
| 23 | |
| 24 | it.effect("fileResponse honors contentType over a conflicting header", () => |
| 25 | Effect.gen(function*() { |
| 26 | const platform = yield* HttpPlatform.HttpPlatform |
| 27 | const response = yield* platform.fileResponse("script.js", { |
| 28 | contentType: "application/custom", |
| 29 | headers: { "content-type": "application/header" } |
| 30 | }) |
| 31 | assert.strictEqual(response.headers["content-type"], "application/custom") |
| 32 | }).pipe(Effect.provide(layer))) |
| 33 | |
| 34 | it.effect("fileWebResponse honors contentType over the file type", () => |
| 35 | Effect.gen(function*() { |
| 36 | const platform = yield* HttpPlatform.HttpPlatform |
| 37 | const response = yield* platform.fileWebResponse(new File([], "script.js", { type: "text/plain" }), { |
| 38 | contentType: "text/javascript" |
| 39 | }) |
| 40 | assert.strictEqual(response.headers["content-type"], "text/javascript") |
| 41 | }).pipe(Effect.provide(layer))) |
| 42 | |
| 43 | it.effect("serves a whole oversized file with an exact content-length header", () => |
| 44 | Effect.gen(function*() { |
| 45 | const platform = yield* HttpPlatform.HttpPlatform |
| 46 | const response = yield* platform.fileResponse("file.bin") |
| 47 | assert.strictEqual(response.headers["content-length"], oversized.toString()) |
| 48 | }).pipe(Effect.provide(layer))) |
| 49 | |
| 50 | for ( |
| 51 | const [name, options] of [ |
| 52 | ["unsafe bigint offset", { offset: oversized }], |
| 53 | ["unsafe end from safe bounds", { offset: Number.MAX_SAFE_INTEGER, bytesToRead: 2 }], |
| 54 | ["unsafe number offset", { offset: Number.MAX_SAFE_INTEGER + 1 }], |
| 55 | ["unsafe number byte count", { bytesToRead: Number.MAX_SAFE_INTEGER + 1 }] |
| 56 | ] as const |
| 57 | ) { |
| 58 | it.effect(`rejects ${name} with BadArgument`, () => |
| 59 | Effect.gen(function*() { |
| 60 | const platform = yield* HttpPlatform.HttpPlatform |
| 61 | const error = yield* Effect.flip(platform.fileResponse("file.bin", options)) |
| 62 | assert.strictEqual(error.reason._tag, "BadArgument") |
| 63 | }).pipe(Effect.provide(layer))) |
| 64 | } |
| 65 | |
| 66 | const file = { |
| 67 | name: "file.bin", |
| 68 | lastModified: 0, |
| 69 | size: 4, |
| 70 | type: "application/octet-stream", |
| 71 | stream: () => new Blob([new Uint8Array([1, 2, 3, 4])]).stream() |
| 72 | } |
| 73 | |
| 74 | it.effect("honors Web file offset and byte count", () => |
| 75 | Effect.gen(function*() { |
| 76 | const platform = yield* HttpPlatform.HttpPlatform |
| 77 | const response = yield* platform.fileWebResponse(file, { offset: 1, bytesToRead: 2 }) |
| 78 | assert.strictEqual(response.body._tag, "Stream") |
| 79 | if (response.body._tag === "Stream") { |
| 80 | assert.strictEqual(response.body.contentLength, 2) |
| 81 | const bytes = yield* Stream.mkUint8Array(response.body.stream) |
| 82 | assert.deepStrictEqual(Array.from(bytes), [2, 3]) |
| 83 | } |
| 84 | }).pipe( |
| 85 | Effect.provide(HttpPlatform.layer), |
| 86 | Effect.provideService(FileSystem.FileSystem, {} as any) |
| 87 | )) |
| 88 | |
| 89 | for ( |
| 90 | const { name, offset, bytesToRead, expected } of [ |
| 91 | { name: "clamps bytesToRead beyond EOF", offset: 1, bytesToRead: 10, expected: [2, 3, 4] }, |
| 92 | { name: "returns an empty body at EOF", offset: 4, bytesToRead: undefined, expected: [] }, |
| 93 | { name: "returns an empty body past EOF", offset: 9, bytesToRead: undefined, expected: [] }, |
| 94 | { name: "clamps bytesToRead at EOF", offset: 4, bytesToRead: 10, expected: [] }, |
| 95 | { name: "clamps bytesToRead past EOF", offset: 9, bytesToRead: 10, expected: [] } |
| 96 | ] |
| 97 | ) { |
| 98 | it.effect(`fileWebResponse ${name}`, () => |
| 99 | Effect.gen(function*() { |
| 100 | const platform = yield* HttpPlatform.HttpPlatform |
| 101 | const response = yield* platform.fileWebResponse(file, { offset, bytesToRead }) |
| 102 | assert.strictEqual(response.body._tag, "Stream") |
| 103 | if (response.body._tag === "Stream") { |
| 104 | const bytes = yield* Stream.mkUint8Array(response.body.stream) |
| 105 | assert.deepStrictEqual( |
| 106 | { |
| 107 | contentLength: response.body.contentLength, |
| 108 | header: response.headers["content-length"], |
| 109 | body: Array.from(bytes) |
| 110 | }, |
| 111 | { contentLength: expected.length, header: String(expected.length), body: expected } |
| 112 | ) |
| 113 | } |
| 114 | }).pipe( |
| 115 | Effect.provide(HttpPlatform.layer), |
| 116 | Effect.provideService(FileSystem.FileSystem, {} as any) |
| 117 | )) |
| 118 | } |
| 119 | |
| 120 | it.effect("honors Web file chunk size", () => |
| 121 | Effect.gen(function*() { |
| 122 | const platform = yield* HttpPlatform.HttpPlatform |
| 123 | const response = yield* platform.fileWebResponse(file, { offset: 0, bytesToRead: 4, chunkSize: 2 }) |
| 124 | assert.strictEqual(response.body._tag, "Stream") |
| 125 | if (response.body._tag === "Stream") { |
| 126 | assert.strictEqual(response.body.contentLength, 4) |
| 127 | const chunks = yield* Stream.runCollect(response.body.stream) |
| 128 | assert.deepStrictEqual(chunks.map((chunk) => Array.from(chunk)), [[1, 2], [3, 4]]) |
| 129 | } |
| 130 | }).pipe( |
| 131 | Effect.provide(HttpPlatform.layer), |
| 132 | Effect.provideService(FileSystem.FileSystem, {} as any) |
| 133 | )) |
| 134 | |
| 135 | it.effect("keeps the source chunking by default", () => |
| 136 | Effect.gen(function*() { |
| 137 | const platform = yield* HttpPlatform.HttpPlatform |
| 138 | const response = yield* platform.fileWebResponse(file) |
| 139 | assert.strictEqual(response.body._tag, "Stream") |
| 140 | if (response.body._tag === "Stream") { |
| 141 | const chunks = yield* Stream.runCollect(response.body.stream) |
| 142 | assert.deepStrictEqual(chunks.map((chunk) => Array.from(chunk)), [[1, 2, 3, 4]]) |
| 143 | } |
| 144 | }).pipe( |
| 145 | Effect.provide(HttpPlatform.layer), |
| 146 | Effect.provideService(FileSystem.FileSystem, {} as any) |
| 147 | )) |
| 148 | }) |