mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: Alistair Smith <hi@alistair.sh> Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com>
671 lines
19 KiB
TypeScript
671 lines
19 KiB
TypeScript
import { file, spawn, version } from "bun";
|
|
import { describe, expect, test } from "bun:test";
|
|
|
|
const bodyTypes = [
|
|
{
|
|
body: Request,
|
|
fn: (body?: BodyInit | null, headers?: HeadersInit) =>
|
|
new Request("http://example.com/", {
|
|
method: "POST",
|
|
body,
|
|
headers,
|
|
}),
|
|
},
|
|
{
|
|
body: Response,
|
|
fn: (body?: BodyInit | null, headers?: HeadersInit) => new Response(body, { headers }),
|
|
},
|
|
];
|
|
|
|
const bufferTypes = [
|
|
ArrayBuffer,
|
|
SharedArrayBuffer,
|
|
Buffer,
|
|
Uint8Array,
|
|
Uint8ClampedArray,
|
|
Uint16Array,
|
|
Uint32Array,
|
|
Int8Array,
|
|
Int16Array,
|
|
Int32Array,
|
|
Float16Array,
|
|
Float32Array,
|
|
Float64Array,
|
|
];
|
|
|
|
const utf8 = [
|
|
{
|
|
string: "",
|
|
buffer: new Uint8Array(0),
|
|
},
|
|
{
|
|
string: "Hello world",
|
|
buffer: new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64]),
|
|
},
|
|
{
|
|
string: "🫠",
|
|
buffer: new Uint8Array([0xf0, 0x9f, 0xab, 0xa0]),
|
|
},
|
|
{
|
|
string: "⁉️",
|
|
buffer: new Uint8Array([0xe2, 0x81, 0x89, 0xef, 0xb8, 0x8f]),
|
|
},
|
|
];
|
|
|
|
for (const { body, fn } of bodyTypes) {
|
|
describe(body.name, () => {
|
|
describe("constructor", () => {
|
|
test("undefined", () => {
|
|
expect(() => fn()).not.toThrow();
|
|
expect(fn().body).toBeNull();
|
|
});
|
|
test("null", () => {
|
|
expect(() => fn(null)).not.toThrow();
|
|
expect(fn(null).body).toBeNull();
|
|
});
|
|
describe("string", () => {
|
|
for (const { string } of utf8) {
|
|
test(`"${string}"`, async () => {
|
|
expect(() => fn(string)).not.toThrow();
|
|
expect(await fn(string).text()).toBe(string);
|
|
});
|
|
}
|
|
});
|
|
for (const bufferType of bufferTypes) {
|
|
const buffers = [
|
|
{
|
|
label: "empty buffer",
|
|
buffer: () => new bufferType(0),
|
|
},
|
|
{
|
|
label: "small buffer",
|
|
buffer: () => crypto.getRandomValues(new bufferType(1_000)),
|
|
},
|
|
{
|
|
label: "large buffer",
|
|
buffer: () => crypto.getRandomValues(new bufferType(1_000_000)),
|
|
},
|
|
];
|
|
describe(bufferType.name, () => {
|
|
for (const { label, buffer } of buffers) {
|
|
test(label, async () => {
|
|
const actual = buffer();
|
|
expect(() => fn(actual)).not.toThrow();
|
|
expect(await fn(actual).arrayBuffer()).toStrictEqual(arrayBuffer(actual));
|
|
});
|
|
}
|
|
});
|
|
}
|
|
describe("Blob", () => {
|
|
const blobs = [
|
|
{
|
|
label: "empty blob",
|
|
blob: () => new Blob(),
|
|
},
|
|
{
|
|
label: "blob with 1 part",
|
|
blob: () => new Blob(["hello"]),
|
|
},
|
|
{
|
|
label: "blob with multiple parts",
|
|
blob: () => new Blob(["hello", "world", new Blob(["!"])]),
|
|
},
|
|
{
|
|
label: "blob with content-type",
|
|
blob: () =>
|
|
new Blob(["{}"], {
|
|
type: "application/json",
|
|
}),
|
|
},
|
|
];
|
|
for (const { label, blob } of blobs) {
|
|
test(label, async () => {
|
|
const actual = blob();
|
|
expect(() => fn(actual)).not.toThrow();
|
|
expect(await fn(actual).blob()).toStrictEqual(actual);
|
|
});
|
|
}
|
|
});
|
|
describe("FormData", () => {
|
|
const forms = [
|
|
{
|
|
label: "empty form",
|
|
formData: (form: FormData) => {},
|
|
},
|
|
{
|
|
label: "form with text entry",
|
|
formData: (form: FormData) => {
|
|
form.set("value", "true");
|
|
},
|
|
},
|
|
{
|
|
label: "form with file entry",
|
|
formData: (form: FormData) => {
|
|
form.set("first", new Blob([]), "first.txt");
|
|
form.set(
|
|
"second",
|
|
new Blob(["[]"], {
|
|
type: "application/json",
|
|
}),
|
|
"second.json",
|
|
);
|
|
},
|
|
},
|
|
{
|
|
label: "form with Bun.file()",
|
|
formData: (form: FormData) => {
|
|
const url = new URL("resources/index.html", import.meta.url);
|
|
form.set("index", file(url), "index.html");
|
|
},
|
|
},
|
|
];
|
|
for (const { label, formData } of forms) {
|
|
test(label, async () => {
|
|
const actual = new FormData();
|
|
formData(actual);
|
|
expect(() => fn(actual)).not.toThrow();
|
|
expect(await fn(actual).formData()).toStrictEqual(actual);
|
|
});
|
|
}
|
|
});
|
|
describe("ReadableStream", () => {
|
|
const streams = [
|
|
{
|
|
label: "direct stream",
|
|
stream: () =>
|
|
new ReadableStream({
|
|
type: "direct",
|
|
async pull(controller) {
|
|
await controller.write("bye\n");
|
|
await controller.end();
|
|
},
|
|
}),
|
|
content: "bye\n",
|
|
skip: true, // hangs
|
|
},
|
|
{
|
|
label: "Bun.file() stream",
|
|
stream: () => {
|
|
const url = new URL("resources/index.html", import.meta.url);
|
|
const { readable } = file(url);
|
|
return readable;
|
|
},
|
|
content: /Example Domain/,
|
|
skip: true, // fails, text is empty
|
|
},
|
|
{
|
|
label: "Bun.spawn() stream",
|
|
stream: async () => {
|
|
const { stdout } = await spawn({
|
|
cmd: [process.argv0, "--version"],
|
|
});
|
|
expect(stdout).not.toBeUndefined();
|
|
return stdout as ReadableStream;
|
|
},
|
|
content: new RegExp(version),
|
|
},
|
|
{
|
|
label: "fetch() stream",
|
|
stream: async () => {
|
|
const { body } = await fetch("http://example.com/");
|
|
expect(body).not.toBeNull();
|
|
return body as ReadableStream;
|
|
},
|
|
content: /Example Domain/,
|
|
},
|
|
];
|
|
for (const { label, stream, content, skip } of streams) {
|
|
const it = skip ? test.skip : test;
|
|
it(label, async () => {
|
|
expect(async () => fn(await stream())).not.toThrow();
|
|
const text = await fn(await stream()).text();
|
|
if (typeof content === "string") {
|
|
expect(text).toBe(content);
|
|
} else {
|
|
expect(content.test(text)).toBe(true);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
test(body.name, async () => {
|
|
for (const { string, buffer } of utf8) {
|
|
expect(() => {
|
|
fn(buffer);
|
|
}).not.toThrow();
|
|
expect(await fn(buffer).text()).toBe(string);
|
|
}
|
|
});
|
|
});
|
|
for (const { string, buffer } of utf8) {
|
|
describe("arrayBuffer()", () => {
|
|
test("undefined", async () => {
|
|
expect(await fn().arrayBuffer()).toStrictEqual(new ArrayBuffer(0));
|
|
});
|
|
test("null", async () => {
|
|
expect(await fn(null).arrayBuffer()).toStrictEqual(new ArrayBuffer(0));
|
|
});
|
|
test(`"${string}"`, async () => {
|
|
expect(await fn(string).arrayBuffer()).toStrictEqual(buffer.buffer);
|
|
});
|
|
});
|
|
describe("bytes()", () => {
|
|
test("undefined", async () => {
|
|
expect(await fn().bytes()).toStrictEqual(new Uint8Array(0));
|
|
});
|
|
test("null", async () => {
|
|
expect(await fn(null).bytes()).toStrictEqual(new Uint8Array(0));
|
|
});
|
|
test(`"${string}"`, async () => {
|
|
expect(await fn(string).bytes()).toStrictEqual(new Uint8Array(buffer));
|
|
});
|
|
});
|
|
describe("text()", () => {
|
|
test("undefined", async () => {
|
|
expect(await fn().text()).toBe("");
|
|
});
|
|
test("null", async () => {
|
|
expect(await fn(null).text()).toBe("");
|
|
});
|
|
test(`"${string}"`, async () => {
|
|
expect(await fn(buffer).text()).toBe(string);
|
|
});
|
|
});
|
|
}
|
|
describe("json()", () => {
|
|
const validTests: [string, unknown][] = [
|
|
["true", true],
|
|
["1234", 1234],
|
|
['"hello"', "hello"],
|
|
["null", null],
|
|
['["abc",123]', ["abc", 123]],
|
|
["{}", {}],
|
|
["[[[[[[]]]]]]", [[[[[[]]]]]]],
|
|
['{"a":1}', { a: 1 }],
|
|
['{"emoji":"😀"}', { emoji: "😀" }],
|
|
["{}\n", {}],
|
|
["\n[]\n", []],
|
|
["\ttrue\n", true],
|
|
['\r\n{"hello":1}\r\n', { hello: 1 }],
|
|
];
|
|
for (const [actual, expected] of validTests) {
|
|
test(actual.trim(), async () => {
|
|
expect(await fn(actual).json()).toStrictEqual(expected);
|
|
});
|
|
}
|
|
const invalidTests: string[] = [
|
|
"",
|
|
" ",
|
|
"undefined",
|
|
"Infinity",
|
|
"NaN",
|
|
"10n",
|
|
"{",
|
|
"[",
|
|
"{{}}",
|
|
"()",
|
|
"[[]",
|
|
"{a:1}",
|
|
'{1:"a"}',
|
|
'{"a":1,}',
|
|
"[1,2,]",
|
|
"'hello'",
|
|
'"hello',
|
|
"😀",
|
|
];
|
|
for (const actual of invalidTests) {
|
|
test(actual || "<empty>", async () => {
|
|
expect(async () => await fn(actual).json()).toThrow(SyntaxError);
|
|
});
|
|
}
|
|
});
|
|
describe("formData()", () => {
|
|
test("undefined", () => {
|
|
expect(async () => await fn().formData()).toThrow(TypeError);
|
|
});
|
|
test("null", () => {
|
|
expect(async () => await fn(null).formData()).toThrow(TypeError);
|
|
});
|
|
const validTests = [
|
|
{
|
|
label: "multipart with no entries",
|
|
headers: {
|
|
"Content-Type": "multipart/form-data; boundary=def456",
|
|
},
|
|
body: ["--def456--"],
|
|
formData: (form: FormData) => {},
|
|
},
|
|
{
|
|
label: "multipart with text entry",
|
|
headers: {
|
|
"Content-Type": "multipart/form-data; boundary=abc123",
|
|
},
|
|
body: ["--abc123", 'Content-Disposition: form-data; name="metadata"', "", '{"ok":true}\n', "--abc123--", ""],
|
|
formData: (form: FormData) => {
|
|
form.set("metadata", '{"ok":true}\n');
|
|
},
|
|
},
|
|
{
|
|
label: "multipart with file entry",
|
|
headers: {
|
|
"Content-Type": "multipart/form-data; boundary=--456789",
|
|
},
|
|
body: [
|
|
"----456789",
|
|
'Content-Disposition: form-data; name="file"; filename="index.html"',
|
|
"Content-Type: text/html;charset=utf-8",
|
|
"",
|
|
"<html><body><h1>Hello</h1></body></html>\n",
|
|
"----456789--",
|
|
"",
|
|
],
|
|
formData: (form: FormData) => {
|
|
const file = new Blob(["<html><body><h1>Hello</h1></body></html>\n"], {
|
|
type: "text/html;charset=utf-8",
|
|
});
|
|
form.set("file", file, "index.html");
|
|
},
|
|
},
|
|
{
|
|
label: "multipart with file that has utf-8 filename (rfc5987)",
|
|
headers: {
|
|
"Content-Type": "multipart/form-data; boundary=--abcdefg",
|
|
},
|
|
body: [
|
|
"----abcdefg",
|
|
"Content-Disposition: form-data; name=\"emoji\"; filename*UTF-8''%F0%9F%9A%80.js",
|
|
"Content-Type: application/javascript;charset=utf-8",
|
|
"",
|
|
'console.log("🚀");\n',
|
|
"----abcdefg--",
|
|
"",
|
|
],
|
|
formData: (form: FormData) => {
|
|
const file = new Blob(['console.log("🚀");\n'], {
|
|
type: "application/javascript;charset=utf-8",
|
|
});
|
|
form.set("emoji", file, "🚀.js");
|
|
},
|
|
},
|
|
{
|
|
label: "multipart with unquoted name",
|
|
headers: {
|
|
"Content-Type": "multipart/form-data; boundary=--123456",
|
|
},
|
|
body: [
|
|
"----123456",
|
|
"Content-Disposition: form-data; name=value",
|
|
"Content-Type: text/plain",
|
|
"",
|
|
"goodbye",
|
|
"----123456--",
|
|
"",
|
|
],
|
|
formData: (form: FormData) => {
|
|
form.set("value", "goodbye");
|
|
},
|
|
},
|
|
{
|
|
label: "url encoded with no entries",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
body: [],
|
|
formData: (form: FormData) => {},
|
|
},
|
|
{
|
|
label: "url encoded",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
body: ["ok=true&name=bun"],
|
|
formData: (form: FormData) => {
|
|
form.set("ok", "true");
|
|
form.set("name", "bun");
|
|
},
|
|
},
|
|
{
|
|
label: "url encoded with utf-8 entry",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
|
},
|
|
body: ["emoji=%F0%9F%8F%B3%EF%B8%8F%E2%80%8D%F0%9F%8C%88"],
|
|
formData: (form: FormData) => {
|
|
form.set("emoji", "🏳️🌈");
|
|
},
|
|
},
|
|
];
|
|
for (const { label, body, headers, formData } of validTests) {
|
|
test(label, async () => {
|
|
const expected = new FormData();
|
|
formData(expected);
|
|
expect(await fn(body.join("\r\n"), headers).formData()).toStrictEqual(expected);
|
|
});
|
|
}
|
|
const invalidTests = [
|
|
{
|
|
label: "empty body",
|
|
headers: {
|
|
"Content-Type": "application/octet-stream",
|
|
},
|
|
body: [],
|
|
},
|
|
{
|
|
label: "text body",
|
|
headers: {
|
|
"Content-Type": "text/plain",
|
|
},
|
|
body: ["how are you?"],
|
|
},
|
|
{
|
|
label: "multipart with no boundary",
|
|
headers: {
|
|
"Content-Type": "multipart/form-data",
|
|
},
|
|
body: ["--abc123", 'Content-Disposition: form-data; name="value"', "", "hello", "--abc123--", ""],
|
|
},
|
|
{
|
|
label: "multipart with malformed boundary",
|
|
headers: {
|
|
"Content-Type": "multipart/form-data; boundary=abc123",
|
|
},
|
|
body: ["--", 'Content-Disposition: form-data; name="value"', "", '{"ok":true}\n', "----", ""],
|
|
},
|
|
];
|
|
for (const { label, body, headers } of invalidTests) {
|
|
test(label, () => {
|
|
expect(async () => await fn(body.join("\r\n"), headers).formData()).toThrow(TypeError);
|
|
});
|
|
}
|
|
});
|
|
describe("body", () => {
|
|
test("undefined", () => {
|
|
expect(fn().body).toBeNull();
|
|
});
|
|
test("null", () => {
|
|
expect(fn(null).body).toBeNull();
|
|
});
|
|
const tests = [
|
|
{
|
|
label: "string",
|
|
body: () => "bun",
|
|
},
|
|
{
|
|
label: "Buffer",
|
|
body: () => Buffer.from("bun", "utf-8"),
|
|
},
|
|
{
|
|
label: "Uint8Array",
|
|
body: () => new Uint8Array([0x62, 0x75, 0x6e]),
|
|
},
|
|
{
|
|
label: "Blob",
|
|
body: () => new Blob(["bun"]),
|
|
},
|
|
{
|
|
label: "ReadableStream",
|
|
body: () =>
|
|
new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue("b");
|
|
controller.enqueue("u");
|
|
controller.enqueue("n");
|
|
controller.close();
|
|
},
|
|
}),
|
|
},
|
|
];
|
|
for (const { label, body } of tests) {
|
|
test(label, async () => {
|
|
const actual = fn(body()).body;
|
|
expect(actual).not.toBeNull();
|
|
expect(actual instanceof ReadableStream).toBe(true);
|
|
const stream = actual as ReadableStream;
|
|
expect(stream.locked).toBe(false);
|
|
expect(await stream.text()).toBe("bun");
|
|
});
|
|
}
|
|
});
|
|
describe("bodyUsed", () => {
|
|
const tests: Record<string, { label?: string; body?: BodyInit | null; bodyUsed: boolean }[]> = {
|
|
"text": [
|
|
{
|
|
body: undefined,
|
|
bodyUsed: false,
|
|
},
|
|
{
|
|
body: null,
|
|
bodyUsed: false,
|
|
},
|
|
{
|
|
body: "",
|
|
bodyUsed: true,
|
|
},
|
|
{
|
|
body: "bun",
|
|
bodyUsed: true,
|
|
},
|
|
],
|
|
"json": [
|
|
{
|
|
body: "{}",
|
|
bodyUsed: true,
|
|
},
|
|
],
|
|
"arrayBuffer": [
|
|
{
|
|
body: undefined,
|
|
bodyUsed: false,
|
|
},
|
|
{
|
|
body: null,
|
|
bodyUsed: false,
|
|
},
|
|
{
|
|
label: "Uint8Array",
|
|
body: new Uint8Array([0x62, 0x75, 0x6e]),
|
|
bodyUsed: true,
|
|
},
|
|
],
|
|
"blob": [
|
|
{
|
|
body: undefined,
|
|
bodyUsed: false,
|
|
},
|
|
{
|
|
body: null,
|
|
bodyUsed: false,
|
|
},
|
|
{
|
|
label: "Blob",
|
|
body: new Blob(["bun"]),
|
|
bodyUsed: true,
|
|
},
|
|
],
|
|
"formData": [
|
|
{
|
|
label: "FormData",
|
|
body: new FormData(),
|
|
bodyUsed: true,
|
|
},
|
|
],
|
|
"clone": [
|
|
{
|
|
body: null,
|
|
bodyUsed: false,
|
|
},
|
|
{
|
|
body: null,
|
|
bodyUsed: false,
|
|
},
|
|
{
|
|
body: "bun",
|
|
bodyUsed: false,
|
|
},
|
|
],
|
|
};
|
|
for (const [property, entries] of Object.entries(tests)) {
|
|
describe(`${property}()`, () => {
|
|
for (const { label, body, bodyUsed } of entries) {
|
|
test(label || `${body}`, () => {
|
|
const result = fn(body);
|
|
expect(result).toHaveProperty("bodyUsed", false);
|
|
// @ts-expect-error
|
|
expect(() => result[property]()).not.toThrow();
|
|
expect(result).toHaveProperty("bodyUsed", bodyUsed);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("new Response()", () => {
|
|
["text", "arrayBuffer", "bytes", "blob"].map(method => {
|
|
test(method, async () => {
|
|
const result = new Response();
|
|
expect(result).toHaveProperty("bodyUsed", false);
|
|
|
|
// @ts-expect-error
|
|
await result[method]();
|
|
expect(result).toHaveProperty("bodyUsed", false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('new Request(url, {method: "POST" })', () => {
|
|
["text", "arrayBuffer", "bytes", "blob"].map(method => {
|
|
test(method, async () => {
|
|
const result = new Request("https://example.com", { method: "POST" });
|
|
expect(result).toHaveProperty("bodyUsed", false);
|
|
|
|
// @ts-expect-error
|
|
await result[method]();
|
|
expect(result).toHaveProperty("bodyUsed", false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("new Request(url)", () => {
|
|
["text", "arrayBuffer", "bytes", "blob"].map(method => {
|
|
test(method, async () => {
|
|
const result = new Request("https://example.com");
|
|
expect(result).toHaveProperty("bodyUsed", false);
|
|
|
|
// @ts-expect-error
|
|
await result[method]();
|
|
expect(result).toHaveProperty("bodyUsed", false);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function arrayBuffer(buffer: BufferSource) {
|
|
if (buffer instanceof ArrayBuffer) {
|
|
return buffer;
|
|
}
|
|
if (buffer instanceof SharedArrayBuffer) {
|
|
return new Uint8Array(new Uint8Array(buffer)).buffer;
|
|
}
|
|
return buffer.buffer;
|
|
}
|