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 => ({ 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(); 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); }); });