mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
What happened: when moving to uSockets for the http client, I forgot to call `SSL_set_tlsext_host_name` and uSockets apparently doesn't do that
508 lines
15 KiB
JavaScript
508 lines
15 KiB
JavaScript
import { it, describe, expect } from "bun:test";
|
|
import fs from "fs";
|
|
import { gc } from "./gc";
|
|
|
|
const exampleFixture = fs.readFileSync(
|
|
import.meta.path.substring(0, import.meta.path.lastIndexOf("/")) +
|
|
"/fetch.js.txt",
|
|
"utf8"
|
|
);
|
|
|
|
describe("fetch", () => {
|
|
const urls = ["https://example.com", "http://example.com"];
|
|
for (let url of urls) {
|
|
gc();
|
|
it(url, async () => {
|
|
gc();
|
|
const response = await fetch(url, {}, { verbose: true });
|
|
gc();
|
|
const text = await response.text();
|
|
gc();
|
|
expect(exampleFixture).toBe(text);
|
|
});
|
|
}
|
|
});
|
|
|
|
it("simultaneous HTTPS fetch", async () => {
|
|
const urls = ["https://example.com", "https://www.example.com"];
|
|
for (let batch = 0; batch < 4; batch++) {
|
|
const promises = new Array(20);
|
|
for (let i = 0; i < 20; i++) {
|
|
promises[i] = fetch(urls[i % 2]);
|
|
}
|
|
const result = await Promise.all(promises);
|
|
expect(result.length).toBe(20);
|
|
for (let i = 0; i < 20; i++) {
|
|
expect(result[i].status).toBe(200);
|
|
expect(await result[i].text()).toBe(exampleFixture);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("website with tlsextname", async () => {
|
|
// irony
|
|
await fetch("https://bun.sh", { method: "HEAD" });
|
|
});
|
|
|
|
function testBlobInterface(blobbyConstructor, hasBlobFn) {
|
|
for (let withGC of [false, true]) {
|
|
for (let jsonObject of [
|
|
{ hello: true },
|
|
{
|
|
hello:
|
|
"😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳",
|
|
},
|
|
]) {
|
|
it(`${jsonObject.hello === true ? "latin1" : "utf16"} json${
|
|
withGC ? " (with gc) " : ""
|
|
}`, async () => {
|
|
if (withGC) gc();
|
|
var response = blobbyConstructor(JSON.stringify(jsonObject));
|
|
if (withGC) gc();
|
|
expect(JSON.stringify(await response.json())).toBe(
|
|
JSON.stringify(jsonObject)
|
|
);
|
|
if (withGC) gc();
|
|
});
|
|
|
|
it(`${
|
|
jsonObject.hello === true ? "latin1" : "utf16"
|
|
} arrayBuffer -> json${withGC ? " (with gc) " : ""}`, async () => {
|
|
if (withGC) gc();
|
|
var response = blobbyConstructor(
|
|
new TextEncoder().encode(JSON.stringify(jsonObject))
|
|
);
|
|
if (withGC) gc();
|
|
expect(JSON.stringify(await response.json())).toBe(
|
|
JSON.stringify(jsonObject)
|
|
);
|
|
if (withGC) gc();
|
|
});
|
|
|
|
it(`${jsonObject.hello === true ? "latin1" : "utf16"} text${
|
|
withGC ? " (with gc) " : ""
|
|
}`, async () => {
|
|
if (withGC) gc();
|
|
var response = blobbyConstructor(JSON.stringify(jsonObject));
|
|
if (withGC) gc();
|
|
expect(await response.text()).toBe(JSON.stringify(jsonObject));
|
|
if (withGC) gc();
|
|
});
|
|
|
|
it(`${
|
|
jsonObject.hello === true ? "latin1" : "utf16"
|
|
} arrayBuffer -> text${withGC ? " (with gc) " : ""}`, async () => {
|
|
if (withGC) gc();
|
|
var response = blobbyConstructor(
|
|
new TextEncoder().encode(JSON.stringify(jsonObject))
|
|
);
|
|
if (withGC) gc();
|
|
expect(await response.text()).toBe(JSON.stringify(jsonObject));
|
|
if (withGC) gc();
|
|
});
|
|
|
|
it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer${
|
|
withGC ? " (with gc) " : ""
|
|
}`, async () => {
|
|
if (withGC) gc();
|
|
|
|
var response = blobbyConstructor(JSON.stringify(jsonObject));
|
|
if (withGC) gc();
|
|
|
|
const bytes = new TextEncoder().encode(JSON.stringify(jsonObject));
|
|
if (withGC) gc();
|
|
|
|
const compare = new Uint8Array(await response.arrayBuffer());
|
|
if (withGC) gc();
|
|
|
|
for (let i = 0; i < compare.length; i++) {
|
|
if (withGC) gc();
|
|
|
|
expect(compare[i]).toBe(bytes[i]);
|
|
if (withGC) gc();
|
|
}
|
|
if (withGC) gc();
|
|
});
|
|
|
|
it(`${
|
|
jsonObject.hello === true ? "latin1" : "utf16"
|
|
} arrayBuffer -> arrayBuffer${withGC ? " (with gc) " : ""}`, async () => {
|
|
if (withGC) gc();
|
|
|
|
var response = blobbyConstructor(
|
|
new TextEncoder().encode(JSON.stringify(jsonObject))
|
|
);
|
|
if (withGC) gc();
|
|
|
|
const bytes = new TextEncoder().encode(JSON.stringify(jsonObject));
|
|
if (withGC) gc();
|
|
|
|
const compare = new Uint8Array(await response.arrayBuffer());
|
|
if (withGC) gc();
|
|
|
|
for (let i = 0; i < compare.length; i++) {
|
|
if (withGC) gc();
|
|
|
|
expect(compare[i]).toBe(bytes[i]);
|
|
if (withGC) gc();
|
|
}
|
|
if (withGC) gc();
|
|
});
|
|
|
|
hasBlobFn &&
|
|
it(`${jsonObject.hello === true ? "latin1" : "utf16"} blob${
|
|
withGC ? " (with gc) " : ""
|
|
}`, async () => {
|
|
if (withGC) gc();
|
|
const text = JSON.stringify(jsonObject);
|
|
var response = blobbyConstructor(text);
|
|
if (withGC) gc();
|
|
const size = new TextEncoder().encode(text).byteLength;
|
|
if (withGC) gc();
|
|
const blobed = await response.blob();
|
|
if (withGC) gc();
|
|
expect(blobed instanceof Blob).toBe(true);
|
|
if (withGC) gc();
|
|
expect(blobed.size).toBe(size);
|
|
if (withGC) gc();
|
|
blobed.type = "";
|
|
if (withGC) gc();
|
|
expect(blobed.type).toBe("");
|
|
if (withGC) gc();
|
|
blobed.type = "application/json";
|
|
if (withGC) gc();
|
|
expect(blobed.type).toBe("application/json");
|
|
if (withGC) gc();
|
|
const out = await blobed.text();
|
|
expect(out).toBe(text);
|
|
if (withGC) gc();
|
|
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
if (withGC) gc();
|
|
expect(out).toBe(text);
|
|
const first = await blobed.arrayBuffer();
|
|
const initial = first[0];
|
|
first[0] = 254;
|
|
const second = await blobed.arrayBuffer();
|
|
expect(second[0]).toBe(initial);
|
|
expect(first[0]).toBe(254);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
describe("Blob", () => {
|
|
testBlobInterface((data) => new Blob([data]));
|
|
|
|
var blobConstructorValues = [
|
|
["123", "456"],
|
|
["123", 456],
|
|
["123", "456", "789"],
|
|
["123", 456, 789],
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
[Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 9])],
|
|
[Uint8Array.from([1, 2, 3, 4]), "5678", 9],
|
|
[new Blob([Uint8Array.from([1, 2, 3, 4])]), "5678", 9],
|
|
[
|
|
new Blob([
|
|
new TextEncoder().encode(
|
|
"😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳"
|
|
),
|
|
]),
|
|
],
|
|
[
|
|
new TextEncoder().encode(
|
|
"😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳"
|
|
),
|
|
],
|
|
];
|
|
|
|
var expected = [
|
|
"123456",
|
|
"123456",
|
|
"123456789",
|
|
"123456789",
|
|
"123456789",
|
|
"\x01\x02\x03\x04\x05\x06\x07\t",
|
|
"\x01\x02\x03\x0456789",
|
|
"\x01\x02\x03\x0456789",
|
|
"😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳",
|
|
"😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳",
|
|
];
|
|
|
|
it(`blobConstructorValues`, async () => {
|
|
for (let i = 0; i < blobConstructorValues.length; i++) {
|
|
var response = new Blob(blobConstructorValues[i]);
|
|
const res = await response.text();
|
|
if (res !== expected[i]) {
|
|
throw new Error(
|
|
`Failed: ${expected[i]
|
|
.split("")
|
|
.map((a) => a.charCodeAt(0))}, received: ${res
|
|
.split("")
|
|
.map((a) => a.charCodeAt(0))}`
|
|
);
|
|
}
|
|
|
|
expect(res).toBe(expected[i]);
|
|
}
|
|
});
|
|
|
|
for (let withGC of [false, true]) {
|
|
it(`Blob.slice() ${withGC ? " with gc" : ""}`, async () => {
|
|
var parts = ["hello", " ", "world"];
|
|
if (withGC) gc();
|
|
var str = parts.join("");
|
|
if (withGC) gc();
|
|
var combined = new Blob(parts);
|
|
if (withGC) gc();
|
|
for (let part of parts) {
|
|
if (withGC) gc();
|
|
expect(
|
|
await combined
|
|
.slice(str.indexOf(part), str.indexOf(part) + part.length)
|
|
.text()
|
|
).toBe(part);
|
|
if (withGC) gc();
|
|
}
|
|
if (withGC) gc();
|
|
for (let part of parts) {
|
|
if (withGC) gc();
|
|
expect(
|
|
await combined
|
|
.slice(str.indexOf(part), str.indexOf(part) + part.length)
|
|
.text()
|
|
).toBe(part);
|
|
if (withGC) gc();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
{
|
|
const sample = new TextEncoder().encode("Hello World!");
|
|
const typedArrays = [
|
|
Uint8Array,
|
|
Uint8ClampedArray,
|
|
Int8Array,
|
|
Uint16Array,
|
|
Int16Array,
|
|
Uint32Array,
|
|
Int32Array,
|
|
Float32Array,
|
|
Float64Array,
|
|
];
|
|
const Constructors = [Blob, Response, Request];
|
|
|
|
for (let withGC of [false, true]) {
|
|
for (let TypedArray of typedArrays) {
|
|
for (let Constructor of Constructors) {
|
|
it(`${Constructor.name} arrayBuffer() with ${TypedArray.name}${
|
|
withGC ? " with gc" : ""
|
|
}`, async () => {
|
|
const data = new TypedArray(sample);
|
|
if (withGC) gc();
|
|
const input =
|
|
Constructor === Blob
|
|
? [data]
|
|
: Constructor === Request
|
|
? { body: data }
|
|
: data;
|
|
if (withGC) gc();
|
|
const blob = new Constructor(input);
|
|
if (withGC) gc();
|
|
const out = await blob.arrayBuffer();
|
|
if (withGC) gc();
|
|
expect(out instanceof ArrayBuffer).toBe(true);
|
|
if (withGC) gc();
|
|
expect(out.byteLength).toBe(data.byteLength);
|
|
if (withGC) gc();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
describe("Response", () => {
|
|
describe("Response.json", () => {
|
|
it("works", async () => {
|
|
const inputs = [
|
|
"hellooo",
|
|
[[123], 456, 789],
|
|
{ hello: "world" },
|
|
{ ok: "😉 😌 😍 🥰 😘 " },
|
|
];
|
|
for (let input of inputs) {
|
|
const output = JSON.stringify(input);
|
|
expect(await Response.json(input).text()).toBe(output);
|
|
}
|
|
// JSON.stringify() returns undefined
|
|
expect(await Response.json().text()).toBe("");
|
|
// JSON.stringify("") returns '""'
|
|
expect(await Response.json("").text()).toBe('""');
|
|
});
|
|
it("sets the content-type header", () => {
|
|
let response = Response.json("hello");
|
|
expect(response.type).toBe("basic");
|
|
expect(response.headers.get("content-type")).toBe(
|
|
"application/json;charset=utf-8"
|
|
);
|
|
expect(response.status).toBe(200);
|
|
});
|
|
it("supports number status code", () => {
|
|
let response = Response.json("hello", 407);
|
|
expect(response.type).toBe("basic");
|
|
expect(response.headers.get("content-type")).toBe(
|
|
"application/json;charset=utf-8"
|
|
);
|
|
expect(response.status).toBe(407);
|
|
});
|
|
|
|
it("supports headers", () => {
|
|
var response = Response.json("hello", {
|
|
headers: {
|
|
"content-type": "potato",
|
|
"x-hello": "world",
|
|
},
|
|
status: 408,
|
|
});
|
|
|
|
expect(response.headers.get("x-hello")).toBe("world");
|
|
expect(response.status).toBe(408);
|
|
});
|
|
});
|
|
describe("Response.redirect", () => {
|
|
it("works", () => {
|
|
const inputs = [
|
|
"http://example.com",
|
|
"http://example.com/",
|
|
"http://example.com/hello",
|
|
"http://example.com/hello/",
|
|
"http://example.com/hello/world",
|
|
"http://example.com/hello/world/",
|
|
];
|
|
for (let input of inputs) {
|
|
expect(Response.redirect(input).headers.get("Location")).toBe(input);
|
|
}
|
|
});
|
|
|
|
it("supports headers", () => {
|
|
var response = Response.redirect("https://example.com", {
|
|
headers: {
|
|
"content-type": "potato",
|
|
"x-hello": "world",
|
|
Location: "https://wrong.com",
|
|
},
|
|
status: 408,
|
|
});
|
|
expect(response.headers.get("x-hello")).toBe("world");
|
|
expect(response.headers.get("Location")).toBe("https://example.com");
|
|
expect(response.status).toBe(302);
|
|
expect(response.type).toBe("basic");
|
|
expect(response.ok).toBe(false);
|
|
});
|
|
});
|
|
describe("Response.error", () => {
|
|
it("works", () => {
|
|
expect(Response.error().type).toBe("error");
|
|
expect(Response.error().ok).toBe(false);
|
|
expect(Response.error().status).toBe(0);
|
|
});
|
|
});
|
|
it("clone", async () => {
|
|
gc();
|
|
var body = new Response("<div>hello</div>", {
|
|
headers: {
|
|
"content-type": "text/html; charset=utf-8",
|
|
},
|
|
});
|
|
gc();
|
|
var clone = body.clone();
|
|
gc();
|
|
body.headers.set("content-type", "text/plain");
|
|
gc();
|
|
expect(clone.headers.get("content-type")).toBe("text/html; charset=utf-8");
|
|
gc();
|
|
expect(body.headers.get("content-type")).toBe("text/plain");
|
|
gc();
|
|
expect(await clone.text()).toBe("<div>hello</div>");
|
|
gc();
|
|
});
|
|
it("invalid json", async () => {
|
|
gc();
|
|
var body = new Response("<div>hello</div>", {
|
|
headers: {
|
|
"content-type": "text/html; charset=utf-8",
|
|
},
|
|
});
|
|
try {
|
|
await body.json();
|
|
expect(false).toBe(true);
|
|
} catch (exception) {
|
|
expect(exception instanceof SyntaxError);
|
|
}
|
|
});
|
|
|
|
testBlobInterface((data) => new Response(data), true);
|
|
});
|
|
|
|
describe("Request", () => {
|
|
it("clone", async () => {
|
|
gc();
|
|
var body = new Request("https://hello.com", {
|
|
headers: {
|
|
"content-type": "text/html; charset=utf-8",
|
|
},
|
|
body: "<div>hello</div>",
|
|
});
|
|
gc();
|
|
expect(body.headers.get("content-type")).toBe("text/html; charset=utf-8");
|
|
gc();
|
|
var clone = body.clone();
|
|
gc();
|
|
body.headers.set("content-type", "text/plain");
|
|
gc();
|
|
expect(clone.headers.get("content-type")).toBe("text/html; charset=utf-8");
|
|
gc();
|
|
expect(body.headers.get("content-type")).toBe("text/plain");
|
|
gc();
|
|
expect(await clone.text()).toBe("<div>hello</div>");
|
|
gc();
|
|
});
|
|
|
|
testBlobInterface(
|
|
(data) => new Request("https://hello.com", { body: data }),
|
|
true
|
|
);
|
|
});
|
|
|
|
describe("Headers", () => {
|
|
it("writes", async () => {
|
|
var headers = new Headers({
|
|
"content-type": "text/html; charset=utf-8",
|
|
});
|
|
gc();
|
|
expect(headers.get("content-type")).toBe("text/html; charset=utf-8");
|
|
gc();
|
|
headers.delete("content-type");
|
|
gc();
|
|
expect(headers.get("content-type")).toBe(null);
|
|
gc();
|
|
headers.append("content-type", "text/plain");
|
|
gc();
|
|
expect(headers.get("content-type")).toBe("text/plain");
|
|
gc();
|
|
headers.append("content-type", "text/plain");
|
|
gc();
|
|
expect(headers.get("content-type")).toBe("text/plain, text/plain");
|
|
gc();
|
|
headers.set("content-type", "text/html; charset=utf-8");
|
|
gc();
|
|
expect(headers.get("content-type")).toBe("text/html; charset=utf-8");
|
|
|
|
headers.delete("content-type");
|
|
gc();
|
|
expect(headers.get("content-type")).toBe(null);
|
|
gc();
|
|
});
|
|
});
|