Files
bun.sh/test/js/bun/http/bun-serve-args.test.ts
2025-03-15 21:34:35 -07:00

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();
}
});
});