diff options
Diffstat (limited to 'test/bun.js/FormData.test.ts')
-rw-r--r-- | test/bun.js/FormData.test.ts | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/test/bun.js/FormData.test.ts b/test/bun.js/FormData.test.ts new file mode 100644 index 000000000..b9d0f0856 --- /dev/null +++ b/test/bun.js/FormData.test.ts @@ -0,0 +1,385 @@ +import { afterAll, beforeAll, describe, expect, it, test } from "bun:test"; +import fs, { chmodSync, unlinkSync } from "fs"; +import { mkfifo } from "mkfifo"; +import { gc, withoutAggressiveGC } from "./gc"; + +describe("FormData", () => { + it("should be able to append a string", () => { + const formData = new FormData(); + formData.append("foo", "bar"); + expect(formData.get("foo")).toBe("bar"); + expect(formData.getAll("foo")[0]).toBe("bar"); + }); + + it("should be able to append a Blob", async () => { + const formData = new FormData(); + formData.append("foo", new Blob(["bar"])); + expect(await formData.get("foo").text()).toBe("bar"); + expect(formData.getAll("foo")[0] instanceof Blob).toBe(true); + }); + + it("should be able to set a Blob", async () => { + const formData = new FormData(); + formData.set("foo", new Blob(["bar"])); + expect(await formData.get("foo").text()).toBe("bar"); + expect(formData.getAll("foo")[0] instanceof Blob).toBe(true); + }); + + it("should be able to set a string", async () => { + const formData = new FormData(); + formData.set("foo", "bar"); + expect(formData.get("foo")).toBe("bar"); + expect(formData.getAll("foo")[0]).toBe("bar"); + }); + + const multipartFormDataFixturesRawBody = [ + { + name: "simple", + body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo--\r\n', + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + expected: { + foo: "bar", + }, + }, + { + name: "simple with trailing CRLF", + body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo--\r\n\r\n', + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + expected: { + foo: "bar", + }, + }, + { + name: "simple with trailing CRLF and extra CRLF", + body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo--\r\n\r\n\r\n', + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + expected: { + foo: "bar", + }, + }, + { + name: "advanced", + body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo\r\nContent-Disposition: form-data; name="baz"\r\n\r\nqux\r\n--foo--\r\n', + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + expected: { + foo: "bar", + baz: "qux", + }, + }, + { + name: "advanced with multiple values", + body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbaz\r\n--foo--\r\n', + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + expected: { + foo: ["bar", "baz"], + }, + }, + { + name: "advanced with multiple values and trailing CRLF", + body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbaz\r\n--foo--\r\n\r\n', + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + expected: { + foo: ["bar", "baz"], + }, + }, + { + name: "extremely advanced", + body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo\r\nContent-Disposition: form-data; name="baz"\r\n\r\nqux\r\n--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbaz\r\n--foo--\r\n', + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + expected: { + foo: ["bar", "baz"], + baz: "qux", + }, + }, + { + name: "with name and filename", + body: '--foo\r\nContent-Disposition: form-data; name="foo"; filename="bar"\r\n\r\nbaz\r\n--foo--\r\n', + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + expected: { + foo: new Blob(["baz"]), + }, + }, + { + name: "with name and filename and trailing CRLF", + body: '--foo\r\nContent-Disposition: form-data; name="foo"; filename="bar"\r\n\r\nbaz\r\n--foo--\r\n\r\n', + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + expected: { + foo: new Blob(["baz"]), + }, + }, + ]; + + for (const { name, body, headers, expected: expected_ } of multipartFormDataFixturesRawBody) { + const Class = [Response, Request] as const; + for (const C of Class) { + it(`should parse multipart/form-data (${name}) with ${C.name}`, async () => { + const response = C === Response ? new Response(body, { headers }) : new Request({ headers, body }); + const formData = await response.formData(); + expect(formData instanceof FormData).toBe(true); + const entry = {}; + const expected = Object.assign({}, expected_); + + for (const key of formData.keys()) { + const values = formData.getAll(key); + if (values.length > 1) { + entry[key] = values; + } else { + entry[key] = values[0]; + if (entry[key] instanceof Blob) { + expect(expected[key] instanceof Blob).toBe(true); + + entry[key] = await entry[key].text(); + expected[key] = await expected[key].text(); + } else { + expect(typeof entry[key]).toBe(typeof expected[key]); + expect(expected[key] instanceof Blob).toBe(false); + } + } + } + + expect(entry).toEqual(expected); + }); + + it(`should roundtrip multipart/form-data (${name}) with ${C.name}`, async () => { + const response = C === Response ? new Response(body, { headers }) : new Request({ headers, body }); + const formData = await response.formData(); + expect(formData instanceof FormData).toBe(true); + + const request = await new Response(formData).formData(); + expect(request instanceof FormData).toBe(true); + + const aKeys = Array.from(formData.keys()); + const bKeys = Array.from(request.keys()); + expect(aKeys).toEqual(bKeys); + + for (const key of aKeys) { + const aValues = formData.getAll(key); + const bValues = request.getAll(key); + for (let i = 0; i < aValues.length; i++) { + const a = aValues[i]; + const b = bValues[i]; + if (a instanceof Blob) { + expect(b instanceof Blob).toBe(true); + expect(await a.text()).toBe(await b.text()); + } else { + expect(a).toBe(b); + } + } + } + + // Test that it also works with Blob. + const c = await new Blob([body], { type: headers["Content-Type"] }).formData(); + expect(c instanceof FormData).toBe(true); + const cKeys = Array.from(c.keys()); + expect(cKeys).toEqual(bKeys); + for (const key of cKeys) { + const cValues = c.getAll(key); + const bValues = request.getAll(key); + for (let i = 0; i < cValues.length; i++) { + const c = cValues[i]; + const b = bValues[i]; + if (c instanceof Blob) { + expect(b instanceof Blob).toBe(true); + expect(await c.text()).toBe(await b.text()); + } else { + expect(c).toBe(b); + } + } + } + }); + } + } + + it("should throw on missing final boundary", async () => { + const response = new Response('-foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n', { + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + }); + try { + await response.formData(); + throw "should have thrown"; + } catch (e) { + expect(typeof e.message).toBe("string"); + } + }); + + it("should throw on bad boundary", async () => { + const response = new Response('foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n', { + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + }); + try { + await response.formData(); + throw "should have thrown"; + } catch (e) { + expect(typeof e.message).toBe("string"); + } + }); + + it("should throw on bad header", async () => { + const response = new Response('foo\r\nContent-Disposition: form-data; name"foo"\r\n\r\nbar\r\n', { + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + }); + try { + await response.formData(); + throw "should have thrown"; + } catch (e) { + expect(typeof e.message).toBe("string"); + } + }); + + it("file upload on HTTP server (receive)", async () => { + const server = Bun.serve({ + port: 4021, + development: false, + async fetch(req) { + const formData = await req.formData(); + return new Response(formData.get("foo")); + }, + }); + + const reqBody = new Request(`http://${server.hostname}:${server.port}`, { + body: '--foo\r\nContent-Disposition: form-data; name="foo"; filename="bar"\r\n\r\nbaz\r\n--foo--\r\n\r\n', + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + method: "POST", + }); + + const res = await fetch(reqBody); + const body = await res.text(); + expect(body).toBe("baz"); + server.stop(true); + }); + + it("file send on HTTP server (receive)", async () => { + const server = Bun.serve({ + port: 4022, + development: false, + async fetch(req) { + const formData = await req.formData(); + return new Response(formData); + }, + }); + + const reqBody = new Request(`http://${server.hostname}:${server.port}`, { + body: '--foo\r\nContent-Disposition: form-data; name="foo"; filename="bar"\r\n\r\nbaz\r\n--foo--\r\n\r\n', + headers: { + "Content-Type": "multipart/form-data; boundary=foo", + }, + method: "POST", + }); + + const res = await fetch(reqBody); + const body = await res.formData(); + expect(await (body.get("foo") as Blob).text()).toBe("baz"); + server.stop(true); + }); + + describe("Bun.file support", () => { + describe("roundtrip", () => { + const path = import.meta.dir + "/form-data-fixture.txt"; + for (const C of [Request, Response]) { + it(`with ${C.name}`, async () => { + await Bun.write(path, "foo!"); + const formData = new FormData(); + formData.append("foo", Bun.file(path)); + const response = C === Response ? new Response(formData) : new Request({ body: formData }); + const formData2 = await response.formData(); + expect(formData2 instanceof FormData).toBe(true); + expect(formData2.get("foo") instanceof Blob).toBe(true); + expect(await (formData2.get("foo") as Blob).text()).toBe("foo!"); + }); + } + }); + + it("doesnt crash when file is missing", async () => { + const formData = new FormData(); + formData.append("foo", Bun.file("missing")); + expect(() => new Response(formData)).toThrow(); + }); + }); + + describe("URLEncoded", () => { + test("should parse URL encoded", async () => { + const response = new Response("foo=bar&baz=qux", { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + const formData = await response.formData(); + expect(formData instanceof FormData).toBe(true); + expect(formData.get("foo")).toBe("bar"); + expect(formData.get("baz")).toBe("qux"); + }); + + test("should parse URL encoded with charset", async () => { + const response = new Response("foo=bar&baz=qux", { + headers: { + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + }, + }); + const formData = await response.formData(); + expect(formData instanceof FormData).toBe(true); + expect(formData.get("foo")).toBe("bar"); + expect(formData.get("baz")).toBe("qux"); + }); + + test("should parse URL encoded with charset and space", async () => { + const response = new Response("foo=bar&baz=qux+quux", { + headers: { + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + }, + }); + const formData = await response.formData(); + expect(formData instanceof FormData).toBe(true); + expect(formData.get("foo")).toBe("bar"); + expect(formData.get("baz")).toBe("qux quux"); + }); + + test("should parse URL encoded with charset and plus", async () => { + const response = new Response("foo=bar&baz=qux+quux", { + headers: { + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + }, + }); + const formData = await response.formData(); + expect(formData instanceof FormData).toBe(true); + expect(formData.get("foo")).toBe("bar"); + expect(formData.get("baz")).toBe("qux quux"); + }); + + it("should handle multiple values", async () => { + const response = new Response("foo=bar&foo=baz", { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + const formData = await response.formData(); + expect(formData instanceof FormData).toBe(true); + expect(formData.getAll("foo")).toEqual(["bar", "baz"]); + }); + }); +}); |