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

107 lines
5.8 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { catbox } from "../src/core/hosting/catbox";
import { direct } from "../src/core/hosting/direct";
import { HostError, type HostContext, type HostImage } from "../src/core/hosting/host";
import { litterbox } from "../src/core/hosting/litterbox";
import { HOSTS } from "../src/core/hosting/registry";
import { rehostAll } from "../src/core/hosting/rehost";
import { serverHost } from "../src/core/hosting/server";
import { FakeTransport, json, text } from "./helpers/fake-transport";
const image = (n = 1, live = true): HostImage => ({
sourceUrl: `https://swebtoon-phinf.pstatic.net/x/${n}.jpg`, live, bytes: new Uint8Array([n, 2, 3]),
sha256: String(n).repeat(64).slice(0, 64), contentType: "image/jpeg", ext: "jpg",
});
const ctx = (transport: FakeTransport, over: Partial<HostContext> = {}): HostContext =>
({ transport, serverBase: "https://mbseed.example.org", catboxVia: "direct", options: {}, ...over });
const LITTERBOX = "https://litterbox.catbox.moe/resources/internals/api.php";
const CATBOX = "https://catbox.moe/user/api.php";
const UPLOAD = "https://mbseed.example.org/api/upload";
test("lifetimes shown next to each host", () => {
expect(HOSTS.server.lifetime({})).toBe("30 days");
expect(HOSTS.litterbox.lifetime({})).toBe("72 hours");
expect(HOSTS.litterbox.lifetime({ litterboxTime: "12h" })).toBe("12 hours");
expect(HOSTS.catbox.lifetime({})).toBe("permanent");
expect(HOSTS.direct.lifetime({})).toBe("as long as the source keeps it");
});
test("direct: the live source URL; refuses archive-only images", async () => {
const fake = new FakeTransport();
expect(await direct.upload(image(), ctx(fake))).toEqual({ url: image().sourceUrl });
await expect(direct.upload(image(1, false), ctx(fake))).rejects.toBeInstanceOf(HostError);
expect(fake.calls).toHaveLength(0);
});
describe("litterbox", () => {
test("multipart upload of the checked bytes, 72h by default", async () => {
const fake = new FakeTransport().on(LITTERBOX, () => text("https://litter.catbox.moe/abc123.jpg\n"));
expect(await litterbox.upload(image(), ctx(fake))).toEqual({ url: "https://litter.catbox.moe/abc123.jpg" });
const form = fake.calls[0]!.init.body as FormData;
expect(fake.calls[0]!.init.method).toBe("POST");
expect(form.get("reqtype")).toBe("fileupload");
expect(form.get("time")).toBe("72h");
expect((form.get("fileToUpload") as File).size).toBe(3);
});
test("an error body is a HostError", async () => {
const fake = new FakeTransport().on(LITTERBOX, () => text("File too large", 413));
await expect(litterbox.upload(image(), ctx(fake))).rejects.toThrow(/File too large/);
});
});
describe("catbox", () => {
test("direct (userscript): multipart with the userhash", async () => {
const fake = new FakeTransport().on(CATBOX, () => text("https://files.catbox.moe/xyz.jpg"));
const url = await catbox.upload(image(), ctx(fake, { options: { catboxUserhash: "abc123" } }));
expect(url).toEqual({ url: "https://files.catbox.moe/xyz.jpg" });
expect((fake.calls[0]!.init.body as FormData).get("userhash")).toBe("abc123");
});
test("via our server (page): sends the source URL, never bytes; checks the sha256", async () => {
const fake = new FakeTransport().on(UPLOAD, () => json({ url: "https://files.catbox.moe/xyz.jpg", sha256: image().sha256 }));
await catbox.upload(image(), ctx(fake, { catboxVia: "server", options: { catboxUserhash: "abc123" } }));
expect(JSON.parse(fake.calls[0]!.init.body as string)).toEqual({ url: image().sourceUrl, host: "catbox", userhash: "abc123" });
});
});
describe("server storage", () => {
test("posts the source URL with host store", async () => {
const fake = new FakeTransport().on(UPLOAD, () =>
json({ url: `https://mbseed.example.org/covers/${image().sha256}.jpg`, sha256: image().sha256 }));
expect((await serverHost.upload(image(), ctx(fake))).url).toContain("/covers/");
expect(JSON.parse(fake.calls[0]!.init.body as string)).toEqual({ url: image().sourceUrl, host: "store" });
});
test("rejects different bytes", async () => {
const fake = new FakeTransport().on(UPLOAD, () => json({ url: "https://x/covers/y.jpg", sha256: "0".repeat(64) }));
await expect(serverHost.upload(image(), ctx(fake))).rejects.toThrow(/different bytes/);
});
test("storage full is HostError 507", async () => {
const fake = new FakeTransport().on(UPLOAD, () => json({ error: "server storage is full" }, 507));
await expect(serverHost.upload(image(), ctx(fake))).rejects.toMatchObject({ status: 507 });
});
test("no server configured", async () => {
await expect(serverHost.upload(image(), ctx(new FakeTransport(), { serverBase: null }))).rejects.toBeInstanceOf(HostError);
});
});
describe("rehostAll", () => {
test("failures are per image; the cache avoids uploading twice", async () => {
let n = 0;
const fake = new FakeTransport().on(LITTERBOX, () =>
++n === 2 ? text("error", 500) : text(`https://litter.catbox.moe/${n}.jpg`));
const cache = new Map<string, string>();
const first = await rehostAll([image(1), image(2), image(3)], litterbox, ctx(fake), cache);
expect([...first.hosted.keys()]).toEqual([image(1).sha256, image(3).sha256]);
expect(first.failures).toEqual([{ sha256: image(2).sha256, message: expect.stringContaining("litterbox") }]);
expect(first.allFailed).toBe(false);
await rehostAll([image(1)], litterbox, ctx(fake), cache);
expect(fake.calls).toHaveLength(3);
});
test("a full store stops the remaining uploads", async () => {
const fake = new FakeTransport().on(UPLOAD, () => json({ error: "server storage is full" }, 507));
const res = await rehostAll([image(1), image(2)], serverHost, ctx(fake), new Map());
expect(fake.calls).toHaveLength(1);
expect(res.failures).toHaveLength(2);
expect(res.allFailed).toBe(true);
});
});