mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
655 lines
14 KiB
TypeScript
655 lines
14 KiB
TypeScript
import { serve } from "bun";
|
|
import { describe, expect, test } from "bun:test";
|
|
import { tmpdirSync } from "../../../harness";
|
|
|
|
const defaultHostname = "localhost";
|
|
|
|
describe("Bun.serve basic options", () => {
|
|
test("minimal valid config", () => {
|
|
using server = serve({
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.port).toBeGreaterThan(0); // Default port
|
|
expect(server.hostname).toBe(defaultHostname);
|
|
server.stop();
|
|
});
|
|
|
|
test("port as string", () => {
|
|
using server = serve({
|
|
port: "0",
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.port).toBeGreaterThan(0);
|
|
server.stop();
|
|
});
|
|
});
|
|
|
|
describe("unix socket", () => {
|
|
const permutations = [
|
|
{
|
|
unix: Math.random().toString(32).slice(2, 15) + ".sock",
|
|
hostname: "",
|
|
},
|
|
{
|
|
unix: Math.random().toString(32).slice(2, 15) + ".sock",
|
|
hostname: undefined,
|
|
},
|
|
{
|
|
unix: Math.random().toString(32).slice(2, 15) + ".sock",
|
|
hostname: null,
|
|
},
|
|
{
|
|
unix: Buffer.from(Math.random().toString(32).slice(2, 15) + ".sock"),
|
|
hostname: null,
|
|
},
|
|
{
|
|
unix: Buffer.from(Math.random().toString(32).slice(2, 15) + ".sock"),
|
|
hostname: Buffer.from(""),
|
|
},
|
|
] as const;
|
|
|
|
for (const { unix, hostname } of permutations) {
|
|
test(`unix: ${unix} and hostname: ${hostname}`, () => {
|
|
using server = serve({
|
|
// @ts-expect-error - Testing invalid combination
|
|
unix,
|
|
// @ts-expect-error - Testing invalid combination
|
|
hostname,
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
// @ts-expect-error - Testing invalid property
|
|
expect(server.address + "").toBe(unix + "");
|
|
expect(server.port).toBeUndefined();
|
|
expect(server.hostname).toBeUndefined();
|
|
server.stop();
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("hostname and port works", () => {
|
|
const permutations = [
|
|
{
|
|
port: 0,
|
|
hostname: defaultHostname,
|
|
unix: undefined,
|
|
},
|
|
{
|
|
port: 0,
|
|
hostname: undefined,
|
|
unix: "",
|
|
},
|
|
{
|
|
port: 0,
|
|
hostname: null,
|
|
unix: "",
|
|
},
|
|
{
|
|
port: 0,
|
|
hostname: null,
|
|
unix: Buffer.from(""),
|
|
},
|
|
{
|
|
port: 0,
|
|
hostname: Buffer.from(defaultHostname),
|
|
unix: Buffer.from(""),
|
|
},
|
|
{
|
|
port: 0,
|
|
hostname: Buffer.from(defaultHostname),
|
|
unix: undefined,
|
|
},
|
|
] as const;
|
|
|
|
for (const { port, hostname, unix } of permutations) {
|
|
test(`port: ${port} and hostname: ${hostname} and unix: ${unix}`, () => {
|
|
using server = serve({
|
|
port,
|
|
// @ts-expect-error - Testing invalid combination
|
|
hostname,
|
|
// @ts-expect-error - Testing invalid combination
|
|
unix,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.port).toBeGreaterThan(0);
|
|
expect(server.hostname).toBe((hostname || defaultHostname) + "");
|
|
server.stop();
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("Bun.serve error handling", () => {
|
|
test("missing fetch handler throws", () => {
|
|
// @ts-expect-error - Testing runtime behavior
|
|
expect(() => serve({})).toThrow();
|
|
});
|
|
|
|
test("custom error handler", () => {
|
|
using server = serve({
|
|
port: 0,
|
|
error(error) {
|
|
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
},
|
|
fetch() {
|
|
throw new Error("test error");
|
|
},
|
|
});
|
|
server.stop();
|
|
});
|
|
});
|
|
|
|
describe("Bun.serve websocket options", () => {
|
|
test("basic websocket config", () => {
|
|
using server = serve({
|
|
port: 0,
|
|
websocket: {
|
|
message(ws, message) {
|
|
ws.send(message);
|
|
},
|
|
},
|
|
fetch(req, server) {
|
|
if (server.upgrade(req)) {
|
|
return;
|
|
}
|
|
return new Response("Not a websocket");
|
|
},
|
|
});
|
|
server.stop();
|
|
});
|
|
|
|
test("websocket with all handlers", () => {
|
|
using server = serve({
|
|
port: 0,
|
|
websocket: {
|
|
open(ws) {},
|
|
message(ws, message) {},
|
|
drain(ws) {},
|
|
close(ws, code, reason) {},
|
|
ping(ws, data) {},
|
|
pong(ws, data) {},
|
|
},
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
server.stop();
|
|
});
|
|
|
|
test("websocket with custom limits", () => {
|
|
using server = serve({
|
|
port: 0,
|
|
websocket: {
|
|
message(ws, message) {},
|
|
maxPayloadLength: 1024 * 1024, // 1MB
|
|
backpressureLimit: 1024 * 512, // 512KB
|
|
closeOnBackpressureLimit: true,
|
|
idleTimeout: 60, // 1 minute
|
|
},
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
server.stop();
|
|
});
|
|
|
|
test("websocket with compression options", () => {
|
|
using server = serve({
|
|
port: 0,
|
|
websocket: {
|
|
message(ws, message) {},
|
|
perMessageDeflate: {
|
|
compress: true,
|
|
decompress: "shared",
|
|
},
|
|
},
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
server.stop();
|
|
});
|
|
});
|
|
|
|
describe("Bun.serve development options", () => {
|
|
test("development mode", () => {
|
|
using server = serve({
|
|
development: true,
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.development).toBe(true);
|
|
server.stop();
|
|
});
|
|
|
|
test("custom server id", () => {
|
|
using server = serve({
|
|
id: "test-server",
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.id).toBe("test-server");
|
|
server.stop();
|
|
});
|
|
});
|
|
|
|
describe("Bun.serve static routes", () => {
|
|
test("static route handling", () => {
|
|
using server = serve({
|
|
port: 0,
|
|
static: {
|
|
"/": new Response("Home"),
|
|
"/about": new Response("About"),
|
|
},
|
|
fetch() {
|
|
return new Response("Not found");
|
|
},
|
|
});
|
|
server.stop();
|
|
});
|
|
});
|
|
|
|
describe("Bun.serve unix socket", () => {
|
|
test("unix socket config", () => {
|
|
const tmpdir = tmpdirSync();
|
|
using server = serve({
|
|
unix: tmpdir + "/test.sock",
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
server.stop();
|
|
});
|
|
|
|
test("unix socket with websocket", () => {
|
|
const tmpdir = tmpdirSync();
|
|
using server = serve({
|
|
unix: tmpdir + "/test.sock",
|
|
websocket: {
|
|
message(ws, message) {},
|
|
},
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
server.stop();
|
|
});
|
|
});
|
|
|
|
describe("Bun.serve hostname and port validation", () => {
|
|
test("hostname with port 0 gets random port", () => {
|
|
using server = serve({
|
|
hostname: "127.0.0.1",
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.port).toBeGreaterThan(0);
|
|
expect(server.hostname).toBe("127.0.0.1");
|
|
server.stop();
|
|
});
|
|
|
|
test("port with no hostname gets default hostname", () => {
|
|
using server = serve({
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.port).toBeGreaterThan(0);
|
|
expect(server.hostname).toBe(defaultHostname); // Default hostname
|
|
server.stop();
|
|
});
|
|
|
|
test("hostname with unix should throw", () => {
|
|
expect(() =>
|
|
serve({
|
|
// @ts-expect-error - Testing invalid combination
|
|
hostname: defaultHostname,
|
|
unix: "test.sock",
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
test("unix with no hostname/port is valid", () => {
|
|
const tmpdir = tmpdirSync();
|
|
using server = serve({
|
|
unix: tmpdir + "/test.sock",
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
server.stop();
|
|
});
|
|
|
|
describe("various valid hostnames", () => {
|
|
const validHostnames = [defaultHostname, "127.0.0.1", "0.0.0.0"];
|
|
|
|
for (const hostname of validHostnames) {
|
|
test(hostname, () => {
|
|
using server = serve({
|
|
hostname,
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.hostname).toBe(hostname);
|
|
server.stop();
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("various port types", () => {
|
|
const validPorts = [
|
|
[0, expect.any(Number)], // random port
|
|
["0", expect.any(Number)], // random port as string
|
|
] as const;
|
|
|
|
for (const [input, expected] of validPorts) {
|
|
test(JSON.stringify(input), () => {
|
|
using server = serve({
|
|
port: input,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
|
|
if (typeof expected === "object") {
|
|
expect(server.port).toBeGreaterThan(0);
|
|
} else {
|
|
expect(server.port).toBe(expected);
|
|
}
|
|
server.stop();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Bun.serve hostname coercion", () => {
|
|
test.todo("number hostnames coerce to string", () => {
|
|
using server = serve({
|
|
// @ts-expect-error - Testing runtime coercion
|
|
hostname: 0, // Should coerce to "0"
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.hostname).toBe("0");
|
|
server.stop();
|
|
});
|
|
|
|
test("object with toString() coerces to string", () => {
|
|
const customHostname = {
|
|
toString() {
|
|
return defaultHostname;
|
|
},
|
|
};
|
|
|
|
using server = serve({
|
|
// @ts-expect-error - Testing runtime coercion
|
|
hostname: customHostname,
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.hostname).toBe(defaultHostname);
|
|
server.stop();
|
|
});
|
|
|
|
test("invalid toString() results should throw", () => {
|
|
const invalidHostnames = [
|
|
{
|
|
toString() {
|
|
return {};
|
|
},
|
|
},
|
|
{
|
|
toString() {
|
|
return [];
|
|
},
|
|
},
|
|
{
|
|
toString() {
|
|
return null;
|
|
},
|
|
},
|
|
{
|
|
toString() {
|
|
return undefined;
|
|
},
|
|
},
|
|
{
|
|
toString() {
|
|
throw new Error("invalid toString");
|
|
},
|
|
},
|
|
{
|
|
toString() {
|
|
return Symbol("test");
|
|
},
|
|
},
|
|
];
|
|
|
|
for (const hostname of invalidHostnames) {
|
|
expect(() =>
|
|
serve({
|
|
// @ts-expect-error - Testing runtime coercion
|
|
hostname,
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
}),
|
|
).toThrow();
|
|
}
|
|
});
|
|
|
|
test("symbol hostnames should throw", () => {
|
|
expect(() =>
|
|
serve({
|
|
// @ts-expect-error - Testing runtime behavior
|
|
hostname: Symbol("test"),
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
test("coerced hostnames must still be valid", () => {
|
|
const invalidCoercions = [
|
|
{
|
|
toString() {
|
|
return "http://example.com";
|
|
},
|
|
},
|
|
{
|
|
toString() {
|
|
return "example.com:3000";
|
|
},
|
|
},
|
|
{
|
|
toString() {
|
|
return "-invalid.com";
|
|
},
|
|
},
|
|
];
|
|
|
|
for (const hostname of invalidCoercions) {
|
|
expect(() =>
|
|
serve({
|
|
// @ts-expect-error - Testing runtime coercion
|
|
hostname,
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
}),
|
|
).toThrow();
|
|
}
|
|
});
|
|
|
|
describe("falsy values should use default or throw", () => {
|
|
test("undefined should use default", () => {
|
|
using server = serve({
|
|
hostname: undefined,
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.hostname).toBe(defaultHostname);
|
|
server.stop();
|
|
});
|
|
|
|
test("null should NOT throw", () => {
|
|
expect(() => {
|
|
using server = serve({
|
|
// @ts-expect-error - Testing runtime behavior
|
|
hostname: null,
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.hostname).toBe(defaultHostname);
|
|
}).not.toThrow();
|
|
});
|
|
|
|
test("empty string should be ignored", () => {
|
|
expect(() => {
|
|
using server = serve({
|
|
hostname: "",
|
|
port: 0,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
expect(server.hostname).toBe(defaultHostname);
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Bun.serve unix socket validation", () => {
|
|
test("unix socket with hostname should throw", () => {
|
|
expect(() =>
|
|
serve({
|
|
unix: "/tmp/test.sock",
|
|
// @ts-expect-error - Testing invalid combination
|
|
hostname: defaultHostname, // Cannot combine with unix
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
describe("invalid unix socket paths should throw", () => {
|
|
const invalidPaths = [
|
|
{
|
|
toString() {
|
|
throw new Error("invalid toString");
|
|
},
|
|
toJSON() {
|
|
return "invalid toJSON";
|
|
},
|
|
},
|
|
{
|
|
toString() {
|
|
return Symbol("test");
|
|
},
|
|
toJSON() {
|
|
return "Symbol(test)";
|
|
},
|
|
},
|
|
];
|
|
|
|
for (const unix of invalidPaths) {
|
|
test(JSON.stringify(unix), () => {
|
|
expect(() =>
|
|
serve({
|
|
// @ts-expect-error - Testing invalid unix socket path
|
|
unix,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
}),
|
|
).toThrow();
|
|
});
|
|
}
|
|
});
|
|
|
|
test("unix socket path coercion", () => {
|
|
// Number should coerce to string
|
|
using server = serve({
|
|
// @ts-expect-error - Testing runtime coercion
|
|
unix: Math.ceil(Math.random() * 100000000),
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
server.stop();
|
|
|
|
// Object with toString()
|
|
const pathObj = {
|
|
toString() {
|
|
return Math.random().toString(32).slice(2, 15) + ".sock";
|
|
},
|
|
};
|
|
|
|
using server2 = serve({
|
|
// @ts-expect-error - Testing runtime coercion
|
|
unix: pathObj,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
server2.stop();
|
|
});
|
|
|
|
test("invalid unix socket path coercion should throw", () => {
|
|
const invalidCoercions = [
|
|
{
|
|
toString() {
|
|
throw new Error("invalid toString");
|
|
},
|
|
},
|
|
];
|
|
|
|
for (const unix of invalidCoercions) {
|
|
expect(() => {
|
|
using server = serve({
|
|
port: 0,
|
|
// @ts-expect-error - Testing runtime coercion
|
|
unix,
|
|
fetch() {
|
|
return new Response("ok");
|
|
},
|
|
});
|
|
server.stop();
|
|
}).toThrow();
|
|
}
|
|
});
|
|
});
|