74 lines
3.7 KiB
TypeScript
74 lines
3.7 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { TOOL_USER_AGENT, TransportError } from "../src/core/transport";
|
|
import { gmStore, gmTransport, parseHeaders, type GmRequest } from "../src/userscript/gm";
|
|
import { userscriptBanner } from "../scripts/userscript-banner";
|
|
|
|
function fakeGm(respond: (req: GmRequest) => void) {
|
|
const requests: GmRequest[] = [];
|
|
const gm = (req: GmRequest) => {
|
|
requests.push(req);
|
|
queueMicrotask(() => respond(req));
|
|
return { abort: () => req.onabort() };
|
|
};
|
|
return { gm, requests };
|
|
}
|
|
|
|
describe("gmTransport", () => {
|
|
test("wraps GM_xmlhttpRequest into a Response", async () => {
|
|
const body = new TextEncoder().encode("<html>").buffer;
|
|
const { gm, requests } = fakeGm((r) =>
|
|
r.onload({ status: 200, statusText: "OK", responseHeaders: "Content-Type: text/html\r\nX-Two: a: b\r\n", response: body }));
|
|
const res = await gmTransport(gm).fetch("https://www.webtoons.com/-/-/-/list?title_no=95");
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers.get("content-type")).toBe("text/html");
|
|
expect(await res.text()).toBe("<html>");
|
|
expect(requests[0]).toMatchObject({ method: "GET", responseType: "arraybuffer", anonymous: true });
|
|
expect(requests[0]!.headers!["user-agent"]).toMatch(/^Mozilla/);
|
|
});
|
|
|
|
test("Wayback gets the honest User-Agent", async () => {
|
|
const { gm, requests } = fakeGm((r) => r.onload({ status: 200, statusText: "OK", responseHeaders: "", response: new ArrayBuffer(0) }));
|
|
await gmTransport(gm).fetch("https://web.archive.org/cdx/search/cdx?url=x");
|
|
expect(requests[0]!.headers!["user-agent"]).toBe(TOOL_USER_AGENT);
|
|
});
|
|
|
|
test("network errors and timeouts reject with TransportError; odd statuses become 502", async () => {
|
|
await expect(gmTransport(fakeGm((r) => r.onerror({})).gm).fetch("https://x/")).rejects.toBeInstanceOf(TransportError);
|
|
await expect(gmTransport(fakeGm((r) => r.ontimeout()).gm).fetch("https://x/")).rejects.toBeInstanceOf(TransportError);
|
|
const res = await gmTransport(fakeGm((r) => r.onload({ status: 0, statusText: "", responseHeaders: "", response: null })).gm).fetch("https://x/");
|
|
expect(res.status).toBe(502);
|
|
});
|
|
|
|
test("an abort signal aborts the request", async () => {
|
|
const controller = new AbortController();
|
|
const { gm } = fakeGm(() => controller.abort());
|
|
await expect(gmTransport(gm).fetch("https://x/", { signal: controller.signal })).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
test("parseHeaders keeps colons inside values", () => {
|
|
expect(parseHeaders("A: 1\r\nB: x: y\r\nbroken\r\n").get("b")).toBe("x: y");
|
|
});
|
|
|
|
test("gmStore maps to GM_getValue / GM_setValue / GM_deleteValue", () => {
|
|
const values = new Map<string, string>();
|
|
const store = gmStore((k) => values.get(k), (k, v) => void values.set(k, v), (k) => void values.delete(k));
|
|
store.set("a", "1");
|
|
expect(store.get("a")).toBe("1");
|
|
store.remove("a");
|
|
expect(store.get("a")).toBeNull();
|
|
});
|
|
|
|
test("banner: matches mangabaka.org, grants GM APIs, connects to every host it needs", () => {
|
|
const banner = userscriptBanner("0.1.0", "https://mbseed.example.org");
|
|
expect(banner).toStartWith("// ==UserScript==\n");
|
|
expect(banner).toContain("// @version 0.1.0\n");
|
|
expect(banner).toContain("// @match https://mangabaka.org/*\n");
|
|
for (const grant of ["GM_xmlhttpRequest", "GM_getValue", "GM_setValue", "GM_deleteValue"]) expect(banner).toContain(`// @grant ${grant}\n`);
|
|
for (const host of ["api.mangabaka.org", "webtoons.com", "pstatic.net", "tapas.io", "d30womf5coomej.cloudfront.net", "web.archive.org", "catbox.moe", "mbseed.example.org"]) {
|
|
expect(banner).toContain(`// @connect ${host}\n`);
|
|
}
|
|
expect(banner).toEndWith("// ==/UserScript==\n\n");
|
|
expect(userscriptBanner("0.1.0")).not.toContain("mbseed.example.org");
|
|
});
|