120 lines
6.4 KiB
TypeScript
120 lines
6.4 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);
|
|
});
|
|
});
|
|
|
|
describe("link expiry", () => {
|
|
const now = Date.UTC(2026, 9, 15);
|
|
const hour = 3_600_000;
|
|
test("litterbox after its chosen time, our server after 30 days, catbox and direct never", async () => {
|
|
const { linkExpiry } = await import("../src/core/hosting/host");
|
|
expect(linkExpiry("litterbox", { litterboxTime: "1h" }, now)).toBe(now + hour);
|
|
expect(linkExpiry("litterbox", {}, now)).toBe(now + 72 * hour);
|
|
expect(linkExpiry("server", {}, now)).toBe(now + 30 * 24 * hour);
|
|
expect(linkExpiry("catbox", {}, now)).toBeNull();
|
|
expect(linkExpiry("direct", {}, now)).toBeNull();
|
|
});
|
|
});
|