Files
mangabaka-seeder/test/gm.test.ts
T

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");
});