diff options
Diffstat (limited to 'test/js/web')
-rw-r--r-- | test/js/web/abort/abort.signal.ts | 24 | ||||
-rw-r--r-- | test/js/web/abort/abort.test.ts | 21 | ||||
-rw-r--r-- | test/js/web/confirm-fixture.js | 3 | ||||
-rw-r--r-- | test/js/web/console/console-log.expected.txt | 27 | ||||
-rw-r--r-- | test/js/web/console/console-log.js | 18 | ||||
-rw-r--r-- | test/js/web/encoding/text-decoder.test.js | 29 | ||||
-rw-r--r-- | test/js/web/encoding/text-encoder.test.js | 2 | ||||
-rw-r--r-- | test/js/web/fetch/blob.test.ts | 29 | ||||
-rw-r--r-- | test/js/web/fetch/body-stream.test.ts | 29 | ||||
-rw-r--r-- | test/js/web/fetch/fetch.stream.test.ts | 143 | ||||
-rw-r--r-- | test/js/web/fetch/fetch.test.ts | 425 | ||||
-rw-r--r-- | test/js/web/fetch/response.test.ts | 32 | ||||
-rw-r--r-- | test/js/web/html/URLSearchParams.test.ts | 2 | ||||
-rw-r--r-- | test/js/web/request/request.test.ts | 37 | ||||
-rw-r--r-- | test/js/web/timers/performance.test.js | 25 | ||||
-rw-r--r-- | test/js/web/url/url.test.ts | 66 | ||||
-rw-r--r-- | test/js/web/web-globals.test.js | 41 | ||||
-rw-r--r-- | test/js/web/worker.test.ts | 2 |
18 files changed, 945 insertions, 10 deletions
diff --git a/test/js/web/abort/abort.signal.ts b/test/js/web/abort/abort.signal.ts new file mode 100644 index 000000000..da402f637 --- /dev/null +++ b/test/js/web/abort/abort.signal.ts @@ -0,0 +1,24 @@ +import type { Server } from "bun"; + +const server = Bun.serve({ + port: 0, + async fetch() { + const signal = AbortSignal.timeout(1); + return await fetch("https://bun.sh", { signal }); + }, +}); + +function hostname(server: Server) { + if (server.hostname.startsWith(":")) return `[${server.hostname}]`; + return server.hostname; +} + +let url = `http://${hostname(server)}:${server.port}/`; + +const responses: Response[] = []; +for (let i = 0; i < 10; i++) { + responses.push(await fetch(url)); +} +server.stop(true); +// we fail if any of the requests succeeded +process.exit(responses.every(res => res.status === 500) ? 0 : 1); diff --git a/test/js/web/abort/abort.test.ts b/test/js/web/abort/abort.test.ts index 4895e0d13..ef0b07a18 100644 --- a/test/js/web/abort/abort.test.ts +++ b/test/js/web/abort/abort.test.ts @@ -18,4 +18,25 @@ describe("AbortSignal", () => { expect(stderr?.toString()).not.toContain("✗"); }); + + test("AbortSignal.timeout(n) should not freeze the process", async () => { + const fileName = join(import.meta.dir, "abort.signal.ts"); + + const server = Bun.spawn({ + cmd: [bunExe(), fileName], + env: bunEnv, + cwd: tmpdir(), + }); + + const exitCode = await Promise.race([ + server.exited, + (async () => { + await Bun.sleep(5000); + server.kill(); + return 2; + })(), + ]); + + expect(exitCode).toBe(0); + }); }); diff --git a/test/js/web/confirm-fixture.js b/test/js/web/confirm-fixture.js new file mode 100644 index 000000000..908570bce --- /dev/null +++ b/test/js/web/confirm-fixture.js @@ -0,0 +1,3 @@ +const result = confirm("What is your answer?"); + +console.error(result ? "Yes" : "No"); diff --git a/test/js/web/console/console-log.expected.txt b/test/js/web/console/console-log.expected.txt index 332322665..39db03722 100644 --- a/test/js/web/console/console-log.expected.txt +++ b/test/js/web/console/console-log.expected.txt @@ -35,7 +35,7 @@ Promise { <pending> } [Function] [Function] [class Foo] -[class] +[class (anonymous)] {} [Function: foooo] /FooRegex/ @@ -50,3 +50,28 @@ String 123 should be 2nd word, 456 == 456 and percent s %s == What okay [ {}, {}, {}, {} ] +{ + level1: { + level2: { + level3: [Object ...] + } + } +} +{ + "1": [Object ...] +} +{ + "1": [Object ...] +} +{ + "1": { + "2": [Object ...] + } +} +{ + "1": { + "2": { + "3": 3 + } + } +} diff --git a/test/js/web/console/console-log.js b/test/js/web/console/console-log.js index 4db40aaac..95f419781 100644 --- a/test/js/web/console/console-log.js +++ b/test/js/web/console/console-log.js @@ -58,3 +58,21 @@ infinteLoop.bar = infinteLoop; console.log(infinteLoop, "am"); console.log(new Array(4).fill({})); +const nestedObject = { + level1: { + level2: { + level3: { + level4: { + level5: { + name: "Deeply nested object", + }, + }, + }, + }, + }, +}; +console.log(nestedObject); +console.dir({ 1: { 2: { 3: 3 } } }, { depth: 0, colors: false }, "Some ignored arg"); +console.dir({ 1: { 2: { 3: 3 } } }, { depth: -1, colors: false }, "Some ignored arg"); +console.dir({ 1: { 2: { 3: 3 } } }, { depth: 1.2, colors: false }, "Some ignored arg"); +console.dir({ 1: { 2: { 3: 3 } } }, { depth: Infinity, colors: false }, "Some ignored arg"); diff --git a/test/js/web/encoding/text-decoder.test.js b/test/js/web/encoding/text-decoder.test.js index 4991cf361..3685a5f6d 100644 --- a/test/js/web/encoding/text-decoder.test.js +++ b/test/js/web/encoding/text-decoder.test.js @@ -250,7 +250,7 @@ describe("TextDecoder", () => { it("constructor should set values", () => { const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }); expect(decoder.fatal).toBe(true); - // expect(decoder.ignoreBOM).toBe(false); // currently the getter for ignoreBOM doesn't work and always returns undefined + expect(decoder.ignoreBOM).toBe(false); }); it("should throw on invalid input", () => { @@ -258,6 +258,33 @@ describe("TextDecoder", () => { const decoder = new TextDecoder("utf-8", { fatal: 10, ignoreBOM: {} }); }).toThrow(); }); + + it("should support undifined", () => { + const decoder = new TextDecoder(undefined); + expect(decoder.encoding).toBe("utf-8"); + }); +}); + +describe("TextDecoder ignoreBOM", () => { + it.each([ + { + encoding: "utf-8", + bytes: [0xef, 0xbb, 0xbf, 0x61, 0x62, 0x63], + }, + { + encoding: "utf-16le", + bytes: [0xff, 0xfe, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00], + }, + ])("should ignoreBOM for: %o", ({ encoding, bytes }) => { + const BOM = "\uFEFF"; + const array = new Uint8Array(bytes); + + const decoder_ignore_bom = new TextDecoder(encoding, { ignoreBOM: true }); + expect(decoder_ignore_bom.decode(array)).toStrictEqual(`${BOM}abc`); + + const decoder_not_ignore_bom = new TextDecoder(encoding, { ignoreBOM: false }); + expect(decoder_not_ignore_bom.decode(array)).toStrictEqual("abc"); + }); }); it("truncated sequences", () => { diff --git a/test/js/web/encoding/text-encoder.test.js b/test/js/web/encoding/text-encoder.test.js index 1bf2057bc..78940a6eb 100644 --- a/test/js/web/encoding/text-encoder.test.js +++ b/test/js/web/encoding/text-encoder.test.js @@ -111,7 +111,7 @@ describe("TextEncoder", () => { const fixture = new Uint8Array(await Bun.file(import.meta.dir + "/utf8-encoding-fixture.bin").arrayBuffer()); const length = 0x110000; let textEncoder = new TextEncoder(); - let textDecoder = new TextDecoder(); + let textDecoder = new TextDecoder("utf-8", { ignoreBOM: true }); let encodeOut = new Uint8Array(length * 4); let encodeIntoOut = new Uint8Array(length * 4); let encodeIntoBuffer = new Uint8Array(4); diff --git a/test/js/web/fetch/blob.test.ts b/test/js/web/fetch/blob.test.ts index ba44f8c1b..ea8d86a7a 100644 --- a/test/js/web/fetch/blob.test.ts +++ b/test/js/web/fetch/blob.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "bun:test"; -test("Blob.slice", () => { +test("Blob.slice", async () => { const blob = new Blob(["Bun", "Foo"]); const b1 = blob.slice(0, 3, "Text/HTML"); expect(b1 instanceof Blob).toBeTruthy(); @@ -26,6 +26,33 @@ test("Blob.slice", () => { expect(blob.slice(null, "-123").size).toBe(6); expect(blob.slice(0, 10).size).toBe(blob.size); expect(blob.slice("text/plain;charset=utf-8").type).toBe("text/plain;charset=utf-8"); + + // test Blob.slice().slice(), issue#6252 + expect(await blob.slice(0, 4).slice(0, 3).text()).toBe("Bun"); + expect(await blob.slice(0, 4).slice(1, 3).text()).toBe("un"); + expect(await blob.slice(1, 4).slice(0, 3).text()).toBe("unF"); + expect(await blob.slice(1, 4).slice(1, 3).text()).toBe("nF"); + expect(await blob.slice(1, 4).slice(2, 3).text()).toBe("F"); + expect(await blob.slice(1, 4).slice(3, 3).text()).toBe(""); + expect(await blob.slice(1, 4).slice(4, 3).text()).toBe(""); + // test negative start + expect(await blob.slice(1, 4).slice(-1, 3).text()).toBe("F"); + expect(await blob.slice(1, 4).slice(-2, 3).text()).toBe("nF"); + expect(await blob.slice(1, 4).slice(-3, 3).text()).toBe("unF"); + expect(await blob.slice(1, 4).slice(-4, 3).text()).toBe("unF"); + expect(await blob.slice(1, 4).slice(-5, 3).text()).toBe("unF"); + expect(await blob.slice(-1, 4).slice(-1, 3).text()).toBe(""); + expect(await blob.slice(-2, 4).slice(-1, 3).text()).toBe(""); + expect(await blob.slice(-3, 4).slice(-1, 3).text()).toBe("F"); + expect(await blob.slice(-4, 4).slice(-1, 3).text()).toBe("F"); + expect(await blob.slice(-5, 4).slice(-1, 3).text()).toBe("F"); + expect(await blob.slice(-5, 4).slice(-2, 3).text()).toBe("nF"); + expect(await blob.slice(-5, 4).slice(-3, 3).text()).toBe("unF"); + expect(await blob.slice(-5, 4).slice(-4, 3).text()).toBe("unF"); + expect(await blob.slice(-4, 4).slice(-3, 3).text()).toBe("nF"); + expect(await blob.slice(-5, 4).slice(-4, 3).text()).toBe("unF"); + expect(await blob.slice(-3, 4).slice(-2, 3).text()).toBe("F"); + expect(await blob.slice(-blob.size, 4).slice(-blob.size, 3).text()).toBe("Bun"); }); test("new Blob", () => { diff --git a/test/js/web/fetch/body-stream.test.ts b/test/js/web/fetch/body-stream.test.ts index 8e2baf92a..8f7675528 100644 --- a/test/js/web/fetch/body-stream.test.ts +++ b/test/js/web/fetch/body-stream.test.ts @@ -13,6 +13,35 @@ var port = 0; ]; const useRequestObjectValues = [true, false]; + test("Should not crash when not returning a promise when stream is in progress", async () => { + var called = false; + await runInServer( + { + async fetch() { + var stream = new ReadableStream({ + type: "direct", + pull(controller) { + controller.write("hey"); + setTimeout(() => { + controller.end(); + }, 100); + }, + }); + + return new Response(stream); + }, + }, + async url => { + called = true; + expect(await fetch(url).then(res => res.text())).toContain( + "Welcome to Bun! To get started, return a Response object.", + ); + }, + ); + + expect(called).toBe(true); + }); + for (let RequestPrototypeMixin of BodyMixin) { for (let useRequestObject of useRequestObjectValues) { describe(`Request.prototoype.${RequestPrototypeMixin.name}() ${ diff --git a/test/js/web/fetch/fetch.stream.test.ts b/test/js/web/fetch/fetch.stream.test.ts index 98271ee79..cc518e397 100644 --- a/test/js/web/fetch/fetch.stream.test.ts +++ b/test/js/web/fetch/fetch.stream.test.ts @@ -28,6 +28,142 @@ const smallText = Buffer.from("Hello".repeat(16)); const empty = Buffer.alloc(0); describe("fetch() with streaming", () => { + it(`should be able to fail properly when reading from readable stream`, async () => { + let server: Server | null = null; + try { + server = Bun.serve({ + port: 0, + async fetch(req) { + return new Response( + new ReadableStream({ + async start(controller) { + controller.enqueue("Hello, World!"); + await Bun.sleep(1000); + controller.enqueue("Hello, World!"); + controller.close(); + }, + }), + { + status: 200, + headers: { + "Content-Type": "text/plain", + }, + }, + ); + }, + }); + + const server_url = `http://${server.hostname}:${server.port}`; + try { + const res = await fetch(server_url, { signal: AbortSignal.timeout(20) }); + const reader = res.body?.getReader(); + while (true) { + const { done } = await reader?.read(); + if (done) break; + } + expect(true).toBe("unreachable"); + } catch (err: any) { + if (err.name !== "TimeoutError") throw err; + expect(err.message).toBe("The operation timed out."); + } + } finally { + server?.stop(); + } + }); + + it(`should be locked after start buffering`, async () => { + let server: Server | null = null; + try { + server = Bun.serve({ + port: 0, + fetch(req) { + return new Response( + new ReadableStream({ + async start(controller) { + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.close(); + }, + }), + { + status: 200, + headers: { + "Content-Type": "text/plain", + }, + }, + ); + }, + }); + + const server_url = `http://${server.hostname}:${server.port}`; + const res = await fetch(server_url); + try { + const promise = res.text(); // start buffering + res.body?.getReader(); // get a reader + const result = await promise; // should throw the right error + expect(result).toBe("unreachable"); + } catch (err: any) { + if (err.name !== "TypeError") throw err; + expect(err.message).toBe("ReadableStream is locked"); + } + } finally { + server?.stop(); + } + }); + + it(`should be locked after start buffering when calling getReader`, async () => { + let server: Server | null = null; + try { + server = Bun.serve({ + port: 0, + fetch(req) { + return new Response( + new ReadableStream({ + async start(controller) { + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.close(); + }, + }), + { + status: 200, + headers: { + "Content-Type": "text/plain", + }, + }, + ); + }, + }); + + const server_url = `http://${server.hostname}:${server.port}`; + const res = await fetch(server_url); + try { + const body = res.body as ReadableStream<Uint8Array>; + const promise = res.text(); // start buffering + body.getReader(); // get a reader + const result = await promise; // should throw the right error + expect(result).toBe("unreachable"); + } catch (err: any) { + if (err.name !== "TypeError") throw err; + expect(err.message).toBe("ReadableStream is locked"); + } + } finally { + server?.stop(); + } + }); + it("can deflate with and without headers #4478", async () => { let server: Server | null = null; try { @@ -77,7 +213,12 @@ describe("fetch() with streaming", () => { .listen(0); const address = server.address() as AddressInfo; - const url = `http://${address.address}:${address.port}`; + let url; + if (address.family == "IPv4") { + url = `http://${address.address}:${address.port}`; + } else { + url = `http://[${address.address}]:${address.port}`; + } async function getRequestLen(url: string) { const response = await fetch(url); const hasBody = response.body; diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts index aa44ee76a..3ed20345b 100644 --- a/test/js/web/fetch/fetch.test.ts +++ b/test/js/web/fetch/fetch.test.ts @@ -3,8 +3,10 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, beforeEach } from import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs"; import { mkfifo } from "mkfifo"; import { tmpdir } from "os"; +import { gzipSync } from "zlib"; import { join } from "path"; import { gc, withoutAggressiveGC, gcTick } from "harness"; +import net from "net"; const tmp_dir = mkdtempSync(join(realpathSync(tmpdir()), "fetch.test")); @@ -93,6 +95,30 @@ describe("fetch data urls", () => { expect(blob.type).toBe("text/plain;charset=utf-8"); expect(blob.text()).resolves.toBe("helloworld!"); }); + it("unstrict parsing of invalid URL characters", async () => { + var url = "data:application/json,{%7B%7D}"; + var res = await fetch(url); + expect(res.status).toBe(200); + expect(res.statusText).toBe("OK"); + expect(res.ok).toBe(true); + + var blob = await res.blob(); + expect(blob.size).toBe(4); + expect(blob.type).toBe("application/json;charset=utf-8"); + expect(blob.text()).resolves.toBe("{{}}"); + }); + it("unstrict parsing of double percent characters", async () => { + var url = "data:application/json,{%%7B%7D%%}%%"; + var res = await fetch(url); + expect(res.status).toBe(200); + expect(res.statusText).toBe("OK"); + expect(res.ok).toBe(true); + + var blob = await res.blob(); + expect(blob.size).toBe(9); + expect(blob.type).toBe("application/json;charset=utf-8"); + expect(blob.text()).resolves.toBe("{%{}%%}%%"); + }); it("data url (invalid)", async () => { var url = "data:Hello%2C%20World!"; expect(async () => { @@ -321,6 +347,27 @@ describe("Headers", () => { expect(headers.getAll("set-cookie")).toEqual(["foo=bar; Path=/; HttpOnly"]); }); + it("presence of content-encoding header(issue #5668)", async () => { + startServer({ + fetch(req) { + const content = gzipSync(JSON.stringify({ message: "Hello world" })); + return new Response(content, { + status: 200, + headers: { + "content-encoding": "gzip", + "content-type": "application/json", + }, + }); + }, + }); + const result = await fetch(`http://${server.hostname}:${server.port}/`); + const value = result.headers.get("content-encoding"); + const body = await result.json(); + expect(value).toBe("gzip"); + expect(body).toBeDefined(); + expect(body.message).toBe("Hello world"); + }); + it(".getSetCookie() with array", () => { const headers = new Headers([ ["content-length", "123"], @@ -1216,6 +1263,16 @@ describe("Request", () => { expect(req.signal.aborted).toBe(true); }); + it("copies method (#6144)", () => { + const request = new Request("http://localhost:1337/test", { + method: "POST", + }); + const new_req = new Request(request, { + body: JSON.stringify({ message: "Hello world" }), + }); + expect(new_req.method).toBe("POST"); + }); + it("cloned signal", async () => { gc(); const controller = new AbortController(); @@ -1391,3 +1448,371 @@ it("cloned response headers are independent after accessing", () => { cloned.headers.set("content-type", "text/plain"); expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8"); }); + +it("should work with http 100 continue", async () => { + let server: net.Server | undefined; + try { + server = net.createServer(socket => { + socket.on("data", data => { + const lines = data.toString().split("\r\n"); + for (const line of lines) { + if (line.length == 0) { + socket.write("HTTP/1.1 100 Continue\r\n\r\n"); + socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!"); + break; + } + } + }); + }); + + const { promise: start, resolve } = Promise.withResolvers(); + server.listen(8080, resolve); + + await start; + + const address = server.address() as net.AddressInfo; + const result = await fetch(`http://localhost:${address.port}`).then(r => r.text()); + expect(result).toBe("Hello, World!"); + } finally { + server?.close(); + } +}); + +it("should work with http 100 continue on the same buffer", async () => { + let server: net.Server | undefined; + try { + server = net.createServer(socket => { + socket.on("data", data => { + const lines = data.toString().split("\r\n"); + for (const line of lines) { + if (line.length == 0) { + socket.write( + "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!", + ); + break; + } + } + }); + }); + + const { promise: start, resolve } = Promise.withResolvers(); + server.listen(8080, resolve); + + await start; + + const address = server.address() as net.AddressInfo; + const result = await fetch(`http://localhost:${address.port}`).then(r => r.text()); + expect(result).toBe("Hello, World!"); + } finally { + server?.close(); + } +}); + +describe("should strip headers", () => { + it("status code 303", async () => { + const server = Bun.serve({ + port: 0, + async fetch(request: Request) { + if (request.url.endsWith("/redirect")) { + return new Response("hello", { + headers: { + ...request.headers, + "Location": "/redirected", + }, + status: 303, + }); + } + + return new Response("hello", { + headers: request.headers, + }); + }, + }); + + const { headers, url, redirected } = await fetch(`http://${server.hostname}:${server.port}/redirect`, { + method: "POST", + headers: { + "I-Am-Here": "yes", + "Content-Language": "This should be stripped", + }, + }); + + expect(headers.get("I-Am-Here")).toBe("yes"); + expect(headers.get("Content-Language")).toBeNull(); + expect(url).toEndWith("/redirected"); + expect(redirected).toBe(true); + server.stop(true); + }); + + it("cross-origin status code 302", async () => { + const server1 = Bun.serve({ + port: 0, + async fetch(request: Request) { + if (request.url.endsWith("/redirect")) { + return new Response("hello", { + headers: { + ...request.headers, + "Location": `http://${server2.hostname}:${server2.port}/redirected`, + }, + status: 302, + }); + } + + return new Response("hello", { + headers: request.headers, + }); + }, + }); + + const server2 = Bun.serve({ + port: 0, + async fetch(request: Request, server) { + if (request.url.endsWith("/redirect")) { + return new Response("hello", { + headers: { + ...request.headers, + "Location": `http://${server.hostname}:${server.port}/redirected`, + }, + status: 302, + }); + } + + return new Response("hello", { + headers: request.headers, + }); + }, + }); + + const { headers, url, redirected } = await fetch(`http://${server1.hostname}:${server1.port}/redirect`, { + method: "GET", + headers: { + "Authorization": "yes", + }, + }); + + expect(headers.get("Authorization")).toBeNull(); + expect(url).toEndWith("/redirected"); + expect(redirected).toBe(true); + server1.stop(true); + server2.stop(true); + }); +}); + +it("same-origin status code 302 should not strip headers", async () => { + const server = Bun.serve({ + port: 0, + async fetch(request: Request, server) { + if (request.url.endsWith("/redirect")) { + return new Response("hello", { + headers: { + ...request.headers, + "Location": `http://${server.hostname}:${server.port}/redirected`, + }, + status: 302, + }); + } + + return new Response("hello", { + headers: request.headers, + }); + }, + }); + + const { headers, url, redirected } = await fetch(`http://${server.hostname}:${server.port}/redirect`, { + method: "GET", + headers: { + "Authorization": "yes", + }, + }); + + expect(headers.get("Authorization")).toEqual("yes"); + expect(url).toEndWith("/redirected"); + expect(redirected).toBe(true); + server.stop(true); +}); + +describe("should handle relative location in the redirect, issue#5635", () => { + var server: Server; + beforeAll(async () => { + server = Bun.serve({ + port: 0, + async fetch(request: Request) { + return new Response("Not Found", { + status: 404, + }); + }, + }); + }); + afterAll(() => { + server.stop(true); + }); + + it.each([ + ["/a/b", "/c", "/c"], + ["/a/b", "c", "/a/c"], + ["/a/b", "/c/d", "/c/d"], + ["/a/b", "c/d", "/a/c/d"], + ["/a/b", "../c", "/c"], + ["/a/b", "../c/d", "/c/d"], + ["/a/b", "../../../c", "/c"], + // slash + ["/a/b/", "/c", "/c"], + ["/a/b/", "c", "/a/b/c"], + ["/a/b/", "/c/d", "/c/d"], + ["/a/b/", "c/d", "/a/b/c/d"], + ["/a/b/", "../c", "/a/c"], + ["/a/b/", "../c/d", "/a/c/d"], + ["/a/b/", "../../../c", "/c"], + ])("('%s', '%s')", async (pathname, location, expected) => { + server.reload({ + async fetch(request: Request) { + const url = new URL(request.url); + if (url.pathname == pathname) { + return new Response("redirecting", { + headers: { + "Location": location, + }, + status: 302, + }); + } else if (url.pathname == expected) { + return new Response("Fine."); + } + return new Response("Not Found", { + status: 404, + }); + }, + }); + + const resp = await fetch(`http://${server.hostname}:${server.port}${pathname}`); + expect(resp.redirected).toBe(true); + expect(new URL(resp.url).pathname).toStrictEqual(expected); + expect(resp.status).toBe(200); + expect(await resp.text()).toBe("Fine."); + }); +}); + +it("should throw RedirectURLTooLong when location is too long", async () => { + const server = Bun.serve({ + port: 0, + async fetch(request: Request) { + gc(); + const url = new URL(request.url); + if (url.pathname == "/redirect") { + return new Response("redirecting", { + headers: { + "Location": "B".repeat(8193), + }, + status: 302, + }); + } + return new Response("Not Found", { + status: 404, + }); + }, + }); + + let err = undefined; + try { + gc(); + const resp = await fetch(`http://${server.hostname}:${server.port}/redirect`); + } catch (error) { + gc(); + err = error; + } + expect(err).not.toBeUndefined(); + expect(err).toBeInstanceOf(Error); + expect(err.code).toStrictEqual("RedirectURLTooLong"); + server.stop(true); +}); + +it("304 not modified with missing content-length does not cause a request timeout", async () => { + const server = await Bun.listen({ + socket: { + open(socket) { + socket.write("HTTP/1.1 304 Not Modified\r\n\r\n"); + socket.flush(); + setTimeout(() => { + socket.end(); + }, 9999).unref(); + }, + data() {}, + close() {}, + }, + port: 0, + hostname: "localhost", + }); + + const response = await fetch(`http://${server.hostname}:${server.port}/`); + expect(response.status).toBe(304); + expect(await response.arrayBuffer()).toHaveLength(0); + server.stop(true); +}); + +it("304 not modified with missing content-length and connection close does not cause a request timeout", async () => { + const server = await Bun.listen({ + socket: { + open(socket) { + socket.write("HTTP/1.1 304 Not Modified\r\nConnection: close\r\n\r\n"); + socket.flush(); + setTimeout(() => { + socket.end(); + }, 9999).unref(); + }, + data() {}, + close() {}, + }, + port: 0, + hostname: "localhost", + }); + + const response = await fetch(`http://${server.hostname}:${server.port}/`); + expect(response.status).toBe(304); + expect(await response.arrayBuffer()).toHaveLength(0); + server.stop(true); +}); + +it("304 not modified with content-length 0 and connection close does not cause a request timeout", async () => { + const server = await Bun.listen({ + socket: { + open(socket) { + socket.write("HTTP/1.1 304 Not Modified\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"); + socket.flush(); + setTimeout(() => { + socket.end(); + }, 9999).unref(); + }, + data() {}, + close() {}, + }, + port: 0, + hostname: "localhost", + }); + + const response = await fetch(`http://${server.hostname}:${server.port}/`); + expect(response.status).toBe(304); + expect(await response.arrayBuffer()).toHaveLength(0); + server.stop(true); +}); + +it("304 not modified with 0 content-length does not cause a request timeout", async () => { + const server = await Bun.listen({ + socket: { + open(socket) { + socket.write("HTTP/1.1 304 Not Modified\r\nContent-Length: 0\r\n\r\n"); + socket.flush(); + setTimeout(() => { + socket.end(); + }, 9999).unref(); + }, + data() {}, + close() {}, + }, + port: 0, + hostname: "localhost", + }); + + const response = await fetch(`http://${server.hostname}:${server.port}/`); + expect(response.status).toBe(304); + expect(await response.arrayBuffer()).toHaveLength(0); + server.stop(true); +}); diff --git a/test/js/web/fetch/response.test.ts b/test/js/web/fetch/response.test.ts new file mode 100644 index 000000000..ddfcd70f7 --- /dev/null +++ b/test/js/web/fetch/response.test.ts @@ -0,0 +1,32 @@ +import { describe, test, expect } from "bun:test"; + +test("zero args returns an otherwise empty 200 response", () => { + const response = new Response(); + expect(response.status).toBe(200); + expect(response.statusText).toBe(""); +}); + +test("1-arg form returns a 200 response", () => { + const response = new Response("body text"); + + expect(response.status).toBe(200); + expect(response.statusText).toBe(""); +}); + +describe("2-arg form", () => { + test("can fill in status/statusText, and it works", () => { + const response = new Response("body text", { + status: 202, + statusText: "Accepted.", + }); + + expect(response.status).toBe(202); + expect(response.statusText).toBe("Accepted."); + }); + test('empty object continues to return 200/""', () => { + const response = new Response("body text", {}); + + expect(response.status).toBe(200); + expect(response.statusText).toBe(""); + }); +}); diff --git a/test/js/web/html/URLSearchParams.test.ts b/test/js/web/html/URLSearchParams.test.ts index a948af821..6de1e6be2 100644 --- a/test/js/web/html/URLSearchParams.test.ts +++ b/test/js/web/html/URLSearchParams.test.ts @@ -86,6 +86,8 @@ describe("URLSearchParams", () => { // @ts-expect-error expect(params.toJSON()).toEqual(props); + + expect(Array.from(params.keys())).toHaveLength(params.size); }); describe("non-standard extensions", () => { diff --git a/test/js/web/request/request.test.ts b/test/js/web/request/request.test.ts new file mode 100644 index 000000000..ac6008eb4 --- /dev/null +++ b/test/js/web/request/request.test.ts @@ -0,0 +1,37 @@ +import { test, expect } from "bun:test"; + +test("request can receive undefined signal", async () => { + const request = new Request("http://example.com/", { + method: "POST", + headers: { + "Content-Type": "text/bun;charset=utf-8", + }, + body: "bun", + signal: undefined, + }); + expect(request.method).toBe("POST"); + // @ts-ignore + const clone = new Request(request); + expect(clone.method).toBe("POST"); + expect(clone.headers.get("content-type")).toBe("text/bun;charset=utf-8"); + expect(await request.text()).toBe("bun"); + expect(await clone.text()).toBe("bun"); +}); + +test("request can receive null signal", async () => { + const request = new Request("http://example.com/", { + method: "POST", + headers: { + "Content-Type": "text/bun;charset=utf-8", + }, + body: "bun", + signal: null, + }); + expect(request.method).toBe("POST"); + // @ts-ignore + const clone = new Request(request); + expect(clone.method).toBe("POST"); + expect(clone.headers.get("content-type")).toBe("text/bun;charset=utf-8"); + expect(await request.text()).toBe("bun"); + expect(await clone.text()).toBe("bun"); +}); diff --git a/test/js/web/timers/performance.test.js b/test/js/web/timers/performance.test.js index dd50c4dc6..0d6cd577b 100644 --- a/test/js/web/timers/performance.test.js +++ b/test/js/web/timers/performance.test.js @@ -20,3 +20,28 @@ it("performance.now() should be monotonic", () => { it("performance.timeOrigin + performance.now() should be similar to Date.now()", () => { expect(Math.abs(performance.timeOrigin + performance.now() - Date.now()) < 1000).toBe(true); }); + +// https://github.com/oven-sh/bun/issues/5604 +it("performance.now() DOMJIT", () => { + // This test is very finnicky. + // It has to return true || return false to reproduce. Throwing an error doesn't work. + function run(start, prev) { + while (true) { + const current = performance.now(); + + if (Number.isNaN(current) || current < prev) { + return false; + } + + if (current - start > 200) { + return true; + } + prev = current; + } + } + + const start = performance.now(); + if (!run(start, start)) { + throw new Error("performance.now() is not monotonic"); + } +}); diff --git a/test/js/web/url/url.test.ts b/test/js/web/url/url.test.ts index 6ad691c1b..f19a132df 100644 --- a/test/js/web/url/url.test.ts +++ b/test/js/web/url/url.test.ts @@ -1,6 +1,19 @@ import { describe, it, expect } from "bun:test"; describe("url", () => { + it("URL throws", () => { + expect(() => new URL("")).toThrow('"" cannot be parsed as a URL'); + expect(() => new URL(" ")).toThrow('" " cannot be parsed as a URL'); + expect(() => new URL("boop", "http!/example.com")).toThrow( + '"boop" cannot be parsed as a URL against "http!/example.com"', + ); + + // redact + expect(() => new URL("boop", "https!!username:password@example.com")).toThrow( + '"boop" cannot be parsed as a URL against <redacted>', + ); + }); + it("should have correct origin and protocol", () => { var url = new URL("https://example.com"); expect(url.protocol).toBe("https:"); @@ -39,8 +52,7 @@ describe("url", () => { expect(url.protocol).toBe("mailto:"); expect(url.origin).toBe("null"); }); - it.skip("should work with blob urls", () => { - // TODO + it("blob urls", () => { var url = new URL("blob:https://example.com/1234-5678"); expect(url.protocol).toBe("blob:"); expect(url.origin).toBe("https://example.com"); @@ -59,6 +71,9 @@ describe("url", () => { url = new URL("blob:ws://example.com"); expect(url.protocol).toBe("blob:"); expect(url.origin).toBe("ws://example.com"); + url = new URL("blob:file:///folder/else/text.txt"); + expect(url.protocol).toBe("blob:"); + expect(url.origin).toBe("file://"); }); it("prints", () => { expect(Bun.inspect(new URL("https://example.com"))).toBe(`URL { @@ -165,4 +180,51 @@ describe("url", () => { expect(result.username).toBe(values.username); } }); + + describe("URL.canParse", () => { + ( + [ + { + "url": undefined, + "base": undefined, + "expected": false, + }, + { + "url": "a:b", + "base": undefined, + "expected": true, + }, + { + "url": undefined, + "base": "a:b", + "expected": false, + }, + { + "url": "a:/b", + "base": undefined, + "expected": true, + }, + { + "url": undefined, + "base": "a:/b", + "expected": true, + }, + { + "url": "https://test:test", + "base": undefined, + "expected": false, + }, + { + "url": "a", + "base": "https://b/", + "expected": true, + }, + ] as const + ).forEach(({ url, base, expected }) => { + it(`URL.canParse(${url}, ${base})`, () => { + // @ts-expect-error + expect(URL.canParse(url, base)).toBe(expected); + }); + }); + }); }); diff --git a/test/js/web/web-globals.test.js b/test/js/web/web-globals.test.js index 1a4d7b1d1..74b28c0b0 100644 --- a/test/js/web/web-globals.test.js +++ b/test/js/web/web-globals.test.js @@ -1,6 +1,6 @@ -import { unsafe } from "bun"; +import { spawn } from "bun"; import { expect, it, test } from "bun:test"; -import { withoutAggressiveGC } from "harness"; +import { bunEnv, bunExe, withoutAggressiveGC } from "harness"; test("exists", () => { expect(typeof URL !== "undefined").toBe(true); @@ -31,6 +31,7 @@ const globalSetters = [ for (const [Constructor, name, eventName, prop] of globalSetters) { test(`self.${name}`, () => { var called = false; + console.log("name", name); const callback = ({ [prop]: data }) => { expect(data).toBe("hello"); @@ -232,3 +233,39 @@ test("navigator", () => { expect(navigator.platform).toBe("Linux x86_64"); } }); + +test("confirm (yes)", async () => { + const proc = spawn({ + cmd: [bunExe(), require("path").join(import.meta.dir, "./confirm-fixture.js")], + stderr: "pipe", + stdin: "pipe", + stdout: "pipe", + env: bunEnv, + }); + + proc.stdin.write("Y"); + await proc.stdin.flush(); + + proc.stdin.write("\n"); + await proc.stdin.flush(); + + await proc.exited; + + expect(await new Response(proc.stderr).text()).toBe("Yes\n"); +}); + +test("confirm (no)", async () => { + const proc = spawn({ + cmd: [bunExe(), require("path").join(import.meta.dir, "./confirm-fixture.js")], + stderr: "pipe", + stdin: "pipe", + stdout: "pipe", + env: bunEnv, + }); + + proc.stdin.write("poask\n"); + await proc.stdin.flush(); + await proc.exited; + + expect(await new Response(proc.stderr).text()).toBe("No\n"); +}); diff --git a/test/js/web/worker.test.ts b/test/js/web/worker.test.ts index e1ab80487..8d7cf72d2 100644 --- a/test/js/web/worker.test.ts +++ b/test/js/web/worker.test.ts @@ -126,7 +126,7 @@ test("worker with event listeners doesnt close event loop", done => { }); }); -test("worker with event listeners doesnt close event loop 2", done => { +test("worker with event listeners doesn't close event loop 2", done => { const x = Bun.spawn({ cmd: [bunExe(), path.join(import.meta.dir, "many-messages-event-loop.mjs"), "worker-fixture-many-messages2.js"], env: bunEnv, |