aboutsummaryrefslogtreecommitdiff
path: root/test/bun.js/FormData.test.ts
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2023-02-13 00:50:15 -0800
committerGravatar GitHub <noreply@github.com> 2023-02-13 00:50:15 -0800
commitaa0762e4660bb17b86890b923368e5a0dc8daf7b (patch)
treea134621368f9def9a85473e90a6189afb956b457 /test/bun.js/FormData.test.ts
parentcdbc620104b939f7112fa613ca192e5fe6e02a7d (diff)
downloadbun-aa0762e4660bb17b86890b923368e5a0dc8daf7b.tar.gz
bun-aa0762e4660bb17b86890b923368e5a0dc8daf7b.tar.zst
bun-aa0762e4660bb17b86890b923368e5a0dc8daf7b.zip
Implement `FormData` (#2051)
* Backport std::forward change * Implement `FormData` * Fix io_darwin headers issue * Implement `Blob` support in FormData * Add test for file upload * Fix bug with Blob not reading Content-Type * Finish implementing FormData * Add FormData to types --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Diffstat (limited to 'test/bun.js/FormData.test.ts')
-rw-r--r--test/bun.js/FormData.test.ts385
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"]);
+ });
+ });
+});