Files
bun.sh/test/js/web/fetch/headers.undici.test.ts
2024-09-03 21:32:52 -07:00

686 lines
18 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { once } from "node:events";
import { createServer } from "node:http";
describe("Headers initialization", () => {
test("allows undefined", () => {
expect(() => new Headers()).not.toThrow();
});
describe("with array of header entries", () => {
test("fails on invalid array-based init", () => {
expect(() => new Headers([["undici", "fetch"], ["fetch"]])).toThrow(TypeError);
expect(() => new Headers(["undici", "fetch", "fetch"])).toThrow(TypeError);
expect(() => new Headers([0, 1, 2])).toThrow(TypeError);
});
test("allows even length init", () => {
const init = [
["undici", "fetch"],
["fetch", "undici"],
];
expect(() => new Headers(init)).not.toThrow();
});
test("fails for event flattened init", () => {
const init = ["undici", "fetch", "fetch", "undici"];
expect(() => new Headers(init)).toThrow(TypeError);
});
});
test("with object of header entries", () => {
const init = {
undici: "fetch",
fetch: "undici",
};
expect(() => new Headers(init)).not.toThrow();
});
test("fails silently if a boxed primitive object is passed", () => {
/* eslint-disable no-new-wrappers */
expect(() => new Headers(new Number())).not.toThrow();
expect(() => new Headers(new Boolean())).not.toThrow();
expect(() => new Headers(new String())).not.toThrow();
/* eslint-enable no-new-wrappers */
});
test("fails if primitive is passed", () => {
const expectedTypeError = TypeError;
expect(() => new Headers(1)).toThrow(expectedTypeError);
expect(() => new Headers("1")).toThrow(expectedTypeError);
});
test("allows some weird stuff (because of webidl)", () => {
expect(() => {
new Headers(function () {}); // eslint-disable-line no-new
}).not.toThrow();
expect(() => {
new Headers(Function); // eslint-disable-line no-new
}).not.toThrow();
});
test("allows a myriad of header values to be passed", () => {
// Headers constructor uses Headers.append
expect(() => {
new Headers([
["a", ["b", "c"]],
["d", ["e", "f"]],
]);
}).not.toThrow();
expect(() => new Headers([["key", null]])).not.toThrow(); // allow null values
expect(() => new Headers([["key"]])).toThrow();
expect(() => new Headers([["key", "value", "value2"]])).toThrow();
});
test("accepts headers as objects with array values", () => {
const headers = new Headers({
c: "5",
b: ["3", "4"],
a: ["1", "2"],
});
expect([...headers.entries()]).toEqual([
["a", "1,2"],
["b", "3,4"],
["c", "5"],
]);
});
});
describe("Headers append", () => {
test("adds valid header entry to instance", () => {
const headers = new Headers();
const name = "undici";
const value = "fetch";
expect(() => headers.append(name, value)).not.toThrow();
expect(headers.get(name)).toBe(value);
});
test("adds valid header to existing entry", () => {
const headers = new Headers();
const name = "undici";
const value1 = "fetch1";
const value2 = "fetch2";
const value3 = "fetch3";
headers.append(name, value1);
expect(headers.get(name)).toBe(value1);
expect(() => headers.append(name, value2)).not.toThrow();
expect(() => headers.append(name, value3)).not.toThrow();
expect(headers.get(name)).toEqual([value1, value2, value3].join(", "));
});
test("throws on invalid entry", () => {
const headers = new Headers();
expect(() => headers.append()).toThrow();
expect(() => headers.append("undici")).toThrow();
expect(() => headers.append("invalid @ header ? name", "valid value")).toThrow();
});
});
describe("Headers delete", () => {
test("deletes valid header entry from instance", () => {
const headers = new Headers();
const name = "undici";
const value = "fetch";
headers.append(name, value);
expect(headers.get(name)).toBe(value);
expect(() => headers.delete(name)).not.toThrow();
expect(headers.get(name)).toBeNull();
});
test("does not mutate internal list when no match is found", () => {
const headers = new Headers();
const name = "undici";
const value = "fetch";
headers.append(name, value);
expect(headers.get(name)).toBe(value);
expect(() => headers.delete("not-undici")).not.toThrow();
expect(headers.get(name)).toBe(value);
});
test("throws on invalid entry", () => {
const headers = new Headers();
expect(() => headers.delete()).toThrow();
expect(() => headers.delete("invalid @ header ? name")).toThrow();
});
// https://github.com/nodejs/undici/issues/2429
test("`Headers#delete` returns undefined", () => {
const headers = new Headers({ test: "test" });
expect(headers.delete("test")).toBeUndefined();
expect(headers.delete("test2")).toBeUndefined();
});
});
describe("Headers get", () => {
test("returns null if not found in instance", () => {
const headers = new Headers();
headers.append("undici", "fetch");
expect(headers.get("not-undici")).toBeNull();
});
test("returns header values from valid header name", () => {
const headers = new Headers();
const name = "undici";
const value1 = "fetch1";
const value2 = "fetch2";
headers.append(name, value1);
expect(headers.get(name)).toBe(value1);
headers.append(name, value2);
expect(headers.get(name)).toEqual([value1, value2].join(", "));
});
test("throws on invalid entry", () => {
const headers = new Headers();
expect(() => headers.get()).toThrow();
expect(() => headers.get("invalid @ header ? name")).toThrow();
});
});
describe("Headers has", () => {
test("returns boolean existence for a header name", () => {
const headers = new Headers();
const name = "undici";
headers.append("not-undici", "fetch");
expect(headers.has(name)).toBe(false);
headers.append(name, "fetch");
expect(headers.has(name)).toBe(true);
});
test("throws on invalid entry", () => {
const headers = new Headers();
expect(() => headers.has()).toThrow();
expect(() => headers.has("invalid @ header ? name")).toThrow();
});
});
describe("Headers set", async () => {
test("sets valid header entry to instance", () => {
const headers = new Headers();
const name = "undici";
const value = "fetch";
headers.append("not-undici", "fetch");
expect(() => headers.set(name, value)).not.toThrow();
expect(headers.get(name)).toBe(value);
});
test("overwrites existing entry", () => {
const headers = new Headers();
const name = "undici";
const value1 = "fetch1";
const value2 = "fetch2";
expect(() => headers.set(name, value1)).not.toThrow();
expect(headers.get(name)).toBe(value1);
expect(() => headers.set(name, value2)).not.toThrow();
expect(headers.get(name)).toBe(value2);
});
test("allows setting a myriad of values", () => {
const headers = new Headers();
expect(() => headers.set("a", ["b", "c"])).not.toThrow();
expect(() => headers.set("b", null)).not.toThrow();
expect(() => headers.set("c")).toThrow();
expect(() => headers.set("c", "d", "e")).not.toThrow();
});
test("throws on invalid entry", () => {
const headers = new Headers();
expect(() => headers.set()).toThrow();
expect(() => headers.set("undici")).toThrow();
expect(() => headers.set("invalid @ header ? name", "valid value")).toThrow();
});
// https://github.com/nodejs/undici/issues/2431
test("`Headers#set` returns undefined", () => {
const headers = new Headers();
expect(headers.set("a", "b")).toBeUndefined();
expect(headers.set("c", "d") instanceof Map).toBe(false);
});
});
describe("Headers forEach", async () => {
const headers = new Headers([
["a", "b"],
["c", "d"],
]);
test("standard", () => {
expect(typeof headers.forEach).toBe("function");
headers.forEach((value, key, headerInstance) => {
expect(value === "b" || value === "d").toBeTrue();
expect(key === "a" || key === "c").toBeTrue();
expect(headers).toBe(headerInstance);
});
});
test("with thisArg", () => {
const thisArg = { a: Math.random() };
headers.forEach(function () {
expect(this).toBe(thisArg);
}, thisArg);
});
});
describe("Headers as Iterable", () => {
test("should freeze values while iterating", () => {
const init = [
["foo", "123"],
["bar", "456"],
];
const expected = [
["foo", "123"],
["x-x-bar", "456"],
];
const headers = new Headers(init);
for (const [key, val] of headers) {
headers.delete(key);
headers.set(`x-${key}`, val);
}
expect([...headers]).toEqual(expected);
});
test("returns combined and sorted entries using .forEach()", () => {
const init = [
["a", "1"],
["b", "2"],
["c", "3"],
["abc", "4"],
["b", "5"],
];
const expected = [
["a", "1"],
["abc", "4"],
["b", "2, 5"],
["c", "3"],
];
const headers = new Headers(init);
const that = {};
let i = 0;
headers.forEach(function (value, key, _headers) {
expect(expected[i++]).toEqual([key, value]);
expect(this).toBe(that);
}, that);
});
test("returns combined and sorted entries using .entries()", () => {
const init = [
["a", "1"],
["b", "2"],
["c", "3"],
["abc", "4"],
["b", "5"],
];
const expected = [
["a", "1"],
["abc", "4"],
["b", "2, 5"],
["c", "3"],
];
const headers = new Headers(init);
let i = 0;
for (const header of headers.entries()) {
expect(header).toEqual(expected[i++]);
}
});
test("returns combined and sorted keys using .keys()", () => {
const init = [
["a", "1"],
["b", "2"],
["c", "3"],
["abc", "4"],
["b", "5"],
];
const expected = ["a", "abc", "b", "c"];
const headers = new Headers(init);
let i = 0;
for (const key of headers.keys()) {
expect(key).toEqual(expected[i++]);
}
});
test("returns combined and sorted values using .values()", () => {
const init = [
["a", "1"],
["b", "2"],
["c", "3"],
["abc", "4"],
["b", "5"],
];
const expected = ["1", "4", "2, 5", "3"];
const headers = new Headers(init);
let i = 0;
for (const value of headers.values()) {
expect(value).toEqual(expected[i++]);
}
});
test("returns combined and sorted entries using for...of loop", () => {
const init = [
["a", "1"],
["b", "2"],
["c", "3"],
["abc", "4"],
["b", "5"],
["d", ["6", "7"]],
];
const expected = [
["a", "1"],
["abc", "4"],
["b", "2, 5"],
["c", "3"],
["d", "6,7"],
];
let i = 0;
for (const header of new Headers(init)) {
expect(header).toEqual(expected[i++]);
}
});
test("validate append ordering", () => {
const headers = new Headers([
["b", "2"],
["c", "3"],
["e", "5"],
]);
headers.append("d", "4");
headers.append("a", "1");
headers.append("f", "6");
headers.append("c", "7");
headers.append("abc", "8");
const expected = [
...new Map([
["a", "1"],
["abc", "8"],
["b", "2"],
["c", "3, 7"],
["d", "4"],
["e", "5"],
["f", "6"],
]),
];
expect([...headers]).toEqual(expected);
});
test("always use the same prototype Iterator", () => {
const HeadersIteratorNext = Function.call.bind(new Headers()[Symbol.iterator]().next);
const init = [
["a", "1"],
["b", "2"],
];
const headers = new Headers(init);
const iterator = headers[Symbol.iterator]();
expect(HeadersIteratorNext(iterator)).toEqual({ value: init[0], done: false });
expect(HeadersIteratorNext(iterator)).toEqual({ value: init[1], done: false });
expect(HeadersIteratorNext(iterator)).toEqual({ value: undefined, done: true });
});
});
test("arg validation", () => {
const headers = new Headers();
// constructor
expect(() => {
// eslint-disable-next-line
new Headers(0);
}).toThrow(TypeError);
// get [Symbol.toStringTag]
expect(() => {
Object.prototype.toString.call(Headers.prototype);
}).not.toThrow();
// toString
expect(() => {
Headers.prototype.toString.call(null);
}).not.toThrow();
// append
expect(() => {
Headers.prototype.append.call(null);
}).toThrow(TypeError);
expect(() => {
headers.append();
}).toThrow(TypeError);
// delete
expect(() => {
Headers.prototype.delete.call(null);
}).toThrow(TypeError);
expect(() => {
headers.delete();
}).toThrow(TypeError);
// get
expect(() => {
Headers.prototype.get.call(null);
}).toThrow(TypeError);
expect(() => {
headers.get();
}).toThrow(TypeError);
// has
expect(() => {
Headers.prototype.has.call(null);
}).toThrow(TypeError);
expect(() => {
headers.has();
}).toThrow(TypeError);
// set
expect(() => {
Headers.prototype.set.call(null);
}).toThrow(TypeError);
expect(() => {
headers.set();
}).toThrow(TypeError);
// forEach
expect(() => {
Headers.prototype.forEach.call(null);
}).toThrow(TypeError);
expect(() => {
headers.forEach();
}).toThrow(TypeError);
expect(() => {
headers.forEach(1);
}).toThrow(TypeError);
// inspect
expect(() => {
Headers.prototype[Symbol.for("nodejs.util.inspect.custom")].call(null);
}).toThrow(TypeError);
});
describe("function signature verification", async () => {
test("function length", () => {
expect(Headers.prototype.append.length, 2);
expect(Headers.prototype.constructor.length, 0);
expect(Headers.prototype.delete.length, 1);
expect(Headers.prototype.entries.length, 0);
expect(Headers.prototype.forEach.length, 1);
expect(Headers.prototype.get.length, 1);
expect(Headers.prototype.has.length, 1);
expect(Headers.prototype.keys.length, 0);
expect(Headers.prototype.set.length, 2);
expect(Headers.prototype.values.length, 0);
expect(Headers.prototype[Symbol.iterator].length, 0);
expect(Headers.prototype.toString.length, 0);
});
test("function equality", () => {
expect(Headers.prototype.entries, Headers.prototype[Symbol.iterator]);
expect(Headers.prototype.toString, Object.prototype.toString);
});
test("toString and Symbol.toStringTag", () => {
expect(Object.prototype.toString.call(Headers.prototype)).toBe("[object Headers]");
expect(Headers.prototype[Symbol.toStringTag]).toBe("Headers");
expect(Headers.prototype.toString.call(null)).toBe("[object Null]");
});
});
test("various init paths of Headers", () => {
const h1 = new Headers();
const h2 = new Headers({});
const h3 = new Headers(undefined);
expect([...h1.entries()].length).toBe(0);
expect([...h2.entries()].length).toBe(0);
expect([...h3.entries()].length).toBe(0);
});
test("invalid headers", () => {
expect(() => new Headers({ "abcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+-.^_`|~": "test" })).not.toThrow();
const chars = '"(),/:;<=>?@[\\]{}'.split("");
for (const char of chars) {
expect(() => new Headers({ [char]: "test" })).toThrow(TypeError);
}
for (const byte of ["\r", "\n", "\t", " ", String.fromCharCode(128), ""]) {
expect(() => {
new Headers().set(byte, "test");
}).toThrow(TypeError);
}
for (const byte of ["\0", "\r", "\n"]) {
expect(() => {
new Headers().set("a", `a${byte}b`);
}).toThrow(TypeError);
}
expect(() => {
new Headers().set("a", "\r");
}).not.toThrow(TypeError);
expect(() => {
new Headers().set("a", "\n");
}).not.toThrow(TypeError);
expect(() => {
new Headers().set("a", Symbol("symbol"));
}).toThrow(TypeError);
});
test("headers that might cause a ReDoS", () => {
expect(() => {
// This test will time out if the ReDoS attack is successful.
const headers = new Headers();
const attack = "a" + "\t".repeat(500_000) + "\ta";
headers.append("fhqwhgads", attack);
}).not.toThrow(TypeError);
});
describe("Headers.prototype.getSetCookie", () => {
test("Mutating the returned list does not affect the set-cookie list", () => {
const h = new Headers([
["set-cookie", "a=b"],
["set-cookie", "c=d"],
]);
const old = h.getSetCookie();
h.getSetCookie().push("oh=no");
const now = h.getSetCookie();
expect(old).toEqual(now);
});
// https://github.com/nodejs/undici/issues/1935
test("When Headers are cloned, so are the cookies (single entry)", async () => {
await using server = createServer((req, res) => {
res.setHeader("Set-Cookie", "test=onetwo");
res.end("Hello World!");
}).listen(0);
await once(server, "listening");
const res = await fetch(`http://localhost:${server.address().port}`);
const entries = Object.fromEntries(res.headers.entries());
expect(res.headers.getSetCookie()).toEqual(["test=onetwo"]);
expect("set-cookie" in entries).toBeTrue();
});
test("When Headers are cloned, so are the cookies (multiple entries)", async () => {
await using server = createServer((req, res) => {
res.setHeader("Set-Cookie", ["test=onetwo", "test=onetwothree"]);
res.end("Hello World!");
}).listen(0);
await once(server, "listening");
const res = await fetch(`http://localhost:${server.address().port}`);
const entries = Object.fromEntries(res.headers.entries());
expect(res.headers.getSetCookie()).toEqual(["test=onetwo", "test=onetwothree"]);
expect("set-cookie" in entries).toBeTrue();
});
test("When Headers are cloned, so are the cookies (Headers constructor)", () => {
const headers = new Headers([
["set-cookie", "a"],
["set-cookie", "b"],
]);
expect([...headers]).toEqual([...new Headers(headers)]);
});
});
test("When the value is updated, update the cache", () => {
const expected = [
["a", "a"],
["b", "b"],
["c", "c"],
];
const headers = new Headers(expected);
expect([...headers]).toEqual(expected);
headers.append("d", "d");
expect([...headers]).toEqual([...expected, ["d", "d"]]);
});
test("Symbol.iterator is only accessed once", () => {
let called = 0;
const dict = new Proxy(
{},
{
get() {
called++;
return function* () {};
},
},
);
new Headers(dict); // eslint-disable-line no-new
expect(called).toBe(1);
});
test("Invalid Symbol.iterators", () => {
expect(() => new Headers({ [Symbol.iterator]: null })).toThrow(TypeError);
expect(() => new Headers({ [Symbol.iterator]: undefined })).toThrow(TypeError);
});