diff options
Diffstat (limited to 'test/bun.js/serve.test.ts')
-rw-r--r-- | test/bun.js/serve.test.ts | 981 |
1 files changed, 0 insertions, 981 deletions
diff --git a/test/bun.js/serve.test.ts b/test/bun.js/serve.test.ts deleted file mode 100644 index c049aad1a..000000000 --- a/test/bun.js/serve.test.ts +++ /dev/null @@ -1,981 +0,0 @@ -import { file, gc, Serve, serve, Server } from "bun"; -import { afterEach, describe, it, expect, afterAll } from "bun:test"; -import { readFileSync, writeFileSync } from "fs"; -import { resolve } from "path"; - -afterEach(() => gc(true)); - -const count = 200; -let port = 10000; -let server: Server | undefined; - -async function runTest({ port, ...serverOptions }: Serve<any>, test: (server: Serve<any>) => Promise<void> | void) { - if (server) { - server.reload({ ...serverOptions, port: 0 }); - } else { - while (!server) { - try { - server = serve({ ...serverOptions, port: 0 }); - break; - } catch (e: any) { - if (e?.message !== `Failed to start server `) { - throw e; - } - } - } - } - - await test(server); -} - -afterAll(() => { - if (server) { - server.stop(true); - server = undefined; - } -}); - -[100, 101, 418, 999].forEach(statusCode => { - it(`should response with HTTP status code (${statusCode})`, async () => { - await runTest( - { - fetch() { - return new Response("Foo Bar", { status: statusCode }); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(response.status).toBe(statusCode); - expect(await response.text()).toBe("Foo Bar"); - }, - ); - }); -}); - -[-200, 42, 12345, Math.PI].forEach(statusCode => { - it(`should ignore invalid HTTP status code (${statusCode})`, async () => { - await runTest( - { - fetch() { - return new Response("Foo Bar", { status: statusCode }); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(response.status).toBe(200); - expect(await response.text()).toBe("Foo Bar"); - }, - ); - }); -}); - -it("should display a welcome message when the response value type is incorrect", async () => { - await runTest( - { - fetch(req) { - return Symbol("invalid response type"); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - const text = await response.text(); - expect(text).toContain("Welcome to Bun!"); - }, - ); -}); - -it("request.signal works in trivial case", async () => { - var aborty = new AbortController(); - var didAbort = false; - await runTest( - { - async fetch(req) { - req.signal.addEventListener("abort", () => { - didAbort = true; - }); - expect(didAbort).toBe(false); - aborty.abort(); - await Bun.sleep(2); - return new Response("Test failed!"); - }, - }, - async server => { - try { - await fetch(`http://${server.hostname}:${server.port}`, { signal: aborty.signal }); - throw new Error("Expected fetch to throw"); - } catch (e: any) { - expect(e.name).toBe("AbortError"); - } - await Bun.sleep(1); - - expect(didAbort).toBe(true); - }, - ); -}); - -it("request.signal works in leaky case", async () => { - var aborty = new AbortController(); - var didAbort = false; - var leaky; - await runTest( - { - async fetch(req) { - leaky = req; - expect(didAbort).toBe(false); - aborty.abort(); - await Bun.sleep(2); - return new Response("Test failed!"); - }, - }, - async server => { - try { - const resp = fetch(`http://${server.hostname}:${server.port}`, { signal: aborty.signal }); - - await Bun.sleep(1); - - leaky.signal.addEventListener("abort", () => { - didAbort = true; - }); - - await resp; - - throw new Error("Expected fetch to throw"); - } catch (e: any) { - expect(e.name).toBe("AbortError"); - } - - await Bun.sleep(1); - - expect(didAbort).toBe(true); - }, - ); -}); - -it("should work for a file", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - fetch(req) { - return new Response(file(fixture)); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - }, - ); -}); - -it("request.url should log successfully", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - var expected; - await runTest( - { - fetch(req) { - expect(Bun.inspect(req).includes(expected)).toBe(true); - return new Response(file(fixture)); - }, - }, - async server => { - expected = `http://localhost:${server.port}/helloooo`; - const response = await fetch(expected); - expect(response.url).toBe(expected); - expect(await response.text()).toBe(textToExpect); - }, - ); -}); - -it("request.url should be based on the Host header", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - fetch(req) { - expect(req.url).toBe("http://example.com/helloooo"); - return new Response(file(fixture)); - }, - }, - async server => { - const expected = `http://${server.hostname}:${server.port}/helloooo`; - const response = await fetch(expected, { - headers: { - Host: "example.com", - }, - }); - expect(response.url).toBe(expected); - expect(await response.text()).toBe(textToExpect); - }, - ); -}); - -describe("streaming", () => { - describe("error handler", () => { - it("throw on pull reports an error and close the connection", async () => { - var pass = false; - await runTest( - { - error(e) { - pass = true; - return new Response("PASS", { status: 555 }); - }, - fetch(req) { - return new Response( - new ReadableStream({ - pull(controller) { - throw new Error("FAIL"); - }, - }), - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - if (response.status > 0) { - expect(response.status).toBe(555); - expect(await response.text()).toBe("PASS"); - } - expect(pass).toBe(true); - }, - ); - }); - - it("throw on pull after writing should not call the error handler", async () => { - var pass = true; - await runTest( - { - error(e) { - pass = false; - return new Response("FAIL", { status: 555 }); - }, - fetch(req) { - return new Response( - new ReadableStream({ - async pull(controller) { - controller.enqueue("PASS"); - controller.close(); - throw new Error("error"); - }, - }), - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - // connection terminated - expect(response.status).toBe(200); - expect(await response.text()).toBe("PASS"); - expect(pass).toBe(true); - }, - ); - }); - }); - - it("text from JS, one chunk", async () => { - const relative = new URL("./fetch.js.txt", import.meta.url); - const textToExpect = readFileSync(relative, "utf-8"); - await runTest( - { - fetch(req) { - return new Response( - new ReadableStream({ - start(controller) { - controller.enqueue(textToExpect); - controller.close(); - }, - }), - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - const text = await response.text(); - expect(text.length).toBe(textToExpect.length); - expect(text).toBe(textToExpect); - }, - ); - }); - it("text from JS, two chunks", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - fetch(req) { - return new Response( - new ReadableStream({ - start(controller) { - controller.enqueue(textToExpect.substring(0, 100)); - controller.enqueue(textToExpect.substring(100)); - controller.close(); - }, - }), - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - }, - ); - }); - - it("Error handler is called when a throwing stream hasn't written anything", async () => { - await runTest( - { - error(e) { - return new Response("Test Passed", { status: 200 }); - }, - - fetch(req) { - return new Response( - new ReadableStream({ - start(controller) { - throw new Error("Test Passed"); - }, - }), - { - status: 404, - }, - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(response.status).toBe(200); - expect(await response.text()).toBe("Test Passed"); - }, - ); - }); - - // Also verifies error handler reset in `.reload()` due to test above - it("text from JS throws on start with no error handler", async () => { - await runTest( - { - error: undefined, - - fetch(req) { - return new Response( - new ReadableStream({ - start(controller) { - throw new Error("Test Passed"); - }, - }), - { - status: 420, - headers: { - "x-what": "123", - }, - }, - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(response.status).toBe(500); - }, - ); - }); - - it("text from JS throws on start has error handler", async () => { - var pass = false; - var err; - await runTest( - { - error(e) { - pass = true; - err = e; - return new Response("Fail", { status: 500 }); - }, - fetch(req) { - return new Response( - new ReadableStream({ - start(controller) { - throw new TypeError("error"); - }, - }), - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(response.status).toBe(500); - expect(await response.text()).toBe("Fail"); - expect(pass).toBe(true); - expect(err?.name).toBe("TypeError"); - expect(err?.message).toBe("error"); - }, - ); - }); - - it("text from JS, 2 chunks, with delay", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - fetch(req) { - return new Response( - new ReadableStream({ - start(controller) { - controller.enqueue(textToExpect.substring(0, 100)); - queueMicrotask(() => { - controller.enqueue(textToExpect.substring(100)); - controller.close(); - }); - }, - }), - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - }, - ); - }); - - it("text from JS, 1 chunk via pull()", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - fetch(req) { - return new Response( - new ReadableStream({ - pull(controller) { - controller.enqueue(textToExpect); - controller.close(); - }, - }), - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - const text = await response.text(); - expect(text).toBe(textToExpect); - }, - ); - }); - - it("text from JS, 2 chunks, with delay in pull", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - fetch(req) { - return new Response( - new ReadableStream({ - pull(controller) { - controller.enqueue(textToExpect.substring(0, 100)); - queueMicrotask(() => { - controller.enqueue(textToExpect.substring(100)); - controller.close(); - }); - }, - }), - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - }, - ); - }); - - it("text from JS, 3 chunks, 1 empty, with delay in pull", async () => { - const textToExpect = "hello world"; - const groups = [ - ["hello", "", " world"], - ["", "hello ", "world"], - ["hello ", "world", ""], - ["hello world", "", ""], - ["", "", "hello world"], - ]; - var count = 0; - - for (const chunks of groups) { - await runTest( - { - fetch(req) { - return new Response( - new ReadableStream({ - async pull(controller) { - for (let chunk of chunks) { - controller.enqueue(Buffer.from(chunk)); - await 1; - } - await 1; - controller.close(); - }, - }), - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - count++; - }, - ); - } - expect(count).toBe(groups.length); - }); - - it("text from JS, 2 chunks, with async pull", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - fetch(req) { - return new Response( - new ReadableStream({ - async pull(controller) { - controller.enqueue(textToExpect.substring(0, 100)); - await Promise.resolve(); - controller.enqueue(textToExpect.substring(100)); - await Promise.resolve(); - controller.close(); - }, - }), - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - }, - ); - }); - - it("text from JS, 10 chunks, with async pull", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - fetch(req) { - return new Response( - new ReadableStream({ - async pull(controller) { - var remain = textToExpect; - for (let i = 0; i < 10 && remain.length > 0; i++) { - controller.enqueue(remain.substring(0, 100)); - remain = remain.substring(100); - await new Promise(resolve => queueMicrotask(resolve)); - } - - controller.enqueue(remain); - controller.close(); - }, - }), - ); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - }, - ); - }); -}); - -it("should work for a hello world", async () => { - await runTest( - { - fetch(req) { - return new Response(`Hello, world!`); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe("Hello, world!"); - }, - ); -}); - -it("should work for a blob", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - fetch(req) { - return new Response(new Blob([textToExpect])); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - }, - ); -}); - -it("should work for a blob stream", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - fetch(req) { - return new Response(new Blob([textToExpect]).stream()); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - }, - ); -}); - -it("should work for a file stream", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - fetch(req) { - return new Response(file(fixture).stream()); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - }, - ); -}); - -it("fetch should work with headers", async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - await runTest( - { - fetch(req) { - if (req.headers.get("X-Foo") !== "bar") { - return new Response("X-Foo header not set", { status: 500 }); - } - return new Response(file(fixture), { - headers: { "X-Both-Ways": "1" }, - }); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`, { - headers: { - "X-Foo": "bar", - }, - }); - expect(response.status).toBe(200); - expect(response.headers.get("X-Both-Ways")).toBe("1"); - }, - ); -}); - -it(`should work for a file ${count} times serial`, async () => { - const fixture = resolve(import.meta.dir, "./fetch.js.txt"); - const textToExpect = readFileSync(fixture, "utf-8"); - await runTest( - { - async fetch(req) { - return new Response(file(fixture)); - }, - }, - async server => { - for (let i = 0; i < count; i++) { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - } - }, - ); -}); - -it(`should work for ArrayBuffer ${count} times serial`, async () => { - const textToExpect = "hello"; - await runTest( - { - fetch(req) { - return new Response(new TextEncoder().encode(textToExpect)); - }, - }, - async server => { - for (let i = 0; i < count; i++) { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe(textToExpect); - } - }, - ); -}); - -describe("parallel", () => { - it(`should work for text ${count} times in batches of 5`, async () => { - const textToExpect = "hello"; - await runTest( - { - fetch(req) { - return new Response(textToExpect); - }, - }, - async server => { - for (let i = 0; i < count; ) { - let responses = await Promise.all([ - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - ]); - - for (let response of responses) { - expect(await response.text()).toBe(textToExpect); - } - i += responses.length; - } - }, - ); - }); - it(`should work for Uint8Array ${count} times in batches of 5`, async () => { - const textToExpect = "hello"; - await runTest( - { - fetch(req) { - return new Response(new TextEncoder().encode(textToExpect)); - }, - }, - async server => { - for (let i = 0; i < count; ) { - let responses = await Promise.all([ - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - ]); - - for (let response of responses) { - expect(await response.text()).toBe(textToExpect); - } - i += responses.length; - } - }, - ); - }); -}); - -it("should support reloading", async () => { - const first = req => new Response("first"); - const second = req => new Response("second"); - await runTest( - { - fetch: first, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe("first"); - server.reload({ fetch: second }); - const response2 = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response2.text()).toBe("second"); - }, - ); -}); - -describe("status code text", () => { - const fixture = { - 200: "OK", - 201: "Created", - 202: "Accepted", - 203: "Non-Authoritative Information", - 204: "No Content", - 205: "Reset Content", - 206: "Partial Content", - 207: "Multi-Status", - 208: "Already Reported", - 226: "IM Used", - 300: "Multiple Choices", - 301: "Moved Permanently", - 302: "Found", - 303: "See Other", - 304: "Not Modified", - 305: "Use Proxy", - 306: "Switch Proxy", - 307: "Temporary Redirect", - 308: "Permanent Redirect", - 400: "Bad Request", - 401: "Unauthorized", - 402: "Payment Required", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 406: "Not Acceptable", - 407: "Proxy Authentication Required", - 408: "Request Timeout", - 409: "Conflict", - 410: "Gone", - 411: "Length Required", - 412: "Precondition Failed", - 413: "Payload Too Large", - 414: "URI Too Long", - 415: "Unsupported Media Type", - 416: "Range Not Satisfiable", - 417: "Expectation Failed", - 418: "I'm a Teapot", - 421: "Misdirected Request", - 422: "Unprocessable Entity", - 423: "Locked", - 424: "Failed Dependency", - 425: "Too Early", - 426: "Upgrade Required", - 428: "Precondition Required", - 429: "Too Many Requests", - 431: "Request Header Fields Too Large", - 451: "Unavailable For Legal Reasons", - 500: "Internal Server Error", - 501: "Not Implemented", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 505: "HTTP Version Not Supported", - 506: "Variant Also Negotiates", - 507: "Insufficient Storage", - 508: "Loop Detected", - 510: "Not Extended", - 511: "Network Authentication Required", - }; - - for (let code in fixture) { - it(`should return ${code} ${fixture[code]}`, async () => { - await runTest( - { - fetch(req) { - return new Response("hey", { status: +code }); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(response.status).toBe(parseInt(code)); - expect(response.statusText).toBe(fixture[code]); - }, - ); - }); - } -}); - -it("should support multiple Set-Cookie headers", async () => { - await runTest( - { - fetch(req) { - return new Response("hello", { - headers: [ - ["Another-Header", "1"], - ["Set-Cookie", "foo=bar"], - ["Set-Cookie", "baz=qux"], - ], - }); - }, - }, - async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(response.headers.getAll("Set-Cookie")).toEqual(["foo=bar", "baz=qux"]); - expect(response.headers.get("Set-Cookie")).toEqual("foo=bar, baz=qux"); - - const cloned = response.clone().headers; - expect(response.headers.getAll("Set-Cookie")).toEqual(["foo=bar", "baz=qux"]); - - response.headers.delete("Set-Cookie"); - expect(response.headers.getAll("Set-Cookie")).toEqual([]); - response.headers.delete("Set-Cookie"); - expect(cloned.getAll("Set-Cookie")).toEqual(["foo=bar", "baz=qux"]); - expect(new Headers(cloned).getAll("Set-Cookie")).toEqual(["foo=bar", "baz=qux"]); - }, - ); -}); - -describe("should support Content-Range with Bun.file()", () => { - // this must be a big file so we can test potentially multiple chunks - // more than 65 KB - const full = (function () { - const fixture = resolve(import.meta.dir + "/fetch.js.txt"); - const chunk = readFileSync(fixture); - var whole = new Uint8Array(chunk.byteLength * 128); - for (var i = 0; i < 128; i++) { - whole.set(chunk, i * chunk.byteLength); - } - writeFileSync(fixture + ".big", whole); - return whole; - })(); - const fixture = resolve(import.meta.dir + "/fetch.js.txt") + ".big"; - const getServer = runTest.bind(null, { - fetch(req) { - const { searchParams } = new URL(req.url); - const start = Number(searchParams.get("start")); - const end = Number(searchParams.get("end")); - return new Response(Bun.file(fixture).slice(start, end)); - }, - }); - - const good = [ - [0, 1], - [1, 2], - [0, 10], - [10, 20], - [0, Infinity], - [10, Infinity], - [NaN, Infinity], - [full.byteLength - 10, full.byteLength], - [full.byteLength - 10, full.byteLength - 1], - [full.byteLength - 1, full.byteLength], - [0, full.byteLength], - ] as const; - - for (const [start, end] of good) { - it(`good range: ${start} - ${end}`, async () => { - await getServer(async server => { - const response = await fetch(`http://${server.hostname}:${server.port}/?start=${start}&end=${end}`, { - verbose: true, - }); - expect(await response.arrayBuffer()).toEqual(full.buffer.slice(start, end)); - expect(response.status).toBe(start > 0 || end < full.byteLength ? 206 : 200); - }); - }); - } - - const emptyRanges = [ - [0, 0], - [1, 1], - [10, 10], - [-Infinity, -Infinity], - [Infinity, Infinity], - [NaN, NaN], - [(full.byteLength / 2) | 0, (full.byteLength / 2) | 0], - [full.byteLength, full.byteLength], - [full.byteLength - 1, full.byteLength - 1], - ]; - - for (const [start, end] of emptyRanges) { - it(`empty range: ${start} - ${end}`, async () => { - await getServer(async server => { - const response = await fetch(`http://${server.hostname}:${server.port}/?start=${start}&end=${end}`); - const out = await response.arrayBuffer(); - expect(out).toEqual(new ArrayBuffer(0)); - expect(response.status).toBe(206); - }); - }); - } - - const badRanges = [ - [10, NaN], - [10, -Infinity], - [-(full.byteLength / 2) | 0, Infinity], - [-(full.byteLength / 2) | 0, -Infinity], - [full.byteLength + 100, full.byteLength], - [full.byteLength + 100, full.byteLength + 100], - [full.byteLength + 100, full.byteLength + 1], - [full.byteLength + 100, -full.byteLength], - ]; - - for (const [start, end] of badRanges) { - it(`bad range: ${start} - ${end}`, async () => { - await getServer(async server => { - const response = await fetch(`http://${server.hostname}:${server.port}/?start=${start}&end=${end}`); - const out = await response.arrayBuffer(); - expect(out).toEqual(new ArrayBuffer(0)); - expect(response.status).toBe(206); - }); - }); - } -}); |