143 lines
7.3 KiB
TypeScript
143 lines
7.3 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { HostError } from "../src/core/hosting/host";
|
|
import { MangaBakaError } from "../src/core/mangabaka/client";
|
|
import type { SubmissionImage } from "../src/core/mangabaka/types";
|
|
import type { SourceSeries } from "../src/core/model";
|
|
import type { RunResult } from "../src/core/pipeline";
|
|
import { HOSTED_KEY, Session, memoryStore, type SessionDeps } from "../src/core/session";
|
|
import { webtoons } from "../src/core/sources/webtoons";
|
|
import { inspected } from "./helpers/covers";
|
|
import { FakeTransport, json, text } from "./helpers/fake-transport";
|
|
|
|
const series: SourceSeries = {
|
|
source: "webtoons", externalId: "95", title: "Tower of God", language: "en",
|
|
canonicalUrl: "https://www.webtoons.com/en/fantasy/tower-of-god/list?title_no=95", archiveUrls: [], mature: false, flags: {},
|
|
};
|
|
function result(): RunResult {
|
|
const poster = inspected({ candidate: { role: "poster", current: true } });
|
|
const landing = inspected({ shape: "tall", type: "other", candidate: { role: "poster", current: false, seenAt: "20230401000000" } });
|
|
const already = inspected({ candidate: { role: "square" }, shape: "square", type: "other" });
|
|
return {
|
|
seriesId: 9999, mbSeries: { id: 9999, title: "Tower of God", type: "manhwa", content_rating: "safe" }, source: webtoons, series,
|
|
linkMatches: true, fresh: [poster, landing], present: [{ cover: already, existing: { id: 8000, sha256: already.sha256, language: "en", type: "other" } }],
|
|
suggestedDefault: poster, warnings: [],
|
|
};
|
|
}
|
|
const deps = (transport: FakeTransport, over: Partial<SessionDeps> = {}): SessionDeps =>
|
|
({ transport, serverBase: null, catboxVia: "direct", store: memoryStore(), persistHostedUrls: false, ...over });
|
|
|
|
describe("review cards", () => {
|
|
test("landing-page art starts unselected; notes and rating filled in", () => {
|
|
const s = new Session(deps(new FakeTransport()));
|
|
s.load(result(), "suggestive");
|
|
expect(s.cards.map((c) => c.selected)).toEqual([true, false]);
|
|
expect(s.cards.map((c) => c.note)).toEqual(["WEBTOON poster (current)", "WEBTOON poster (landing-page art), seen 2023-04"]);
|
|
expect(s.cards.every((c) => c.contentRating === "suggestive")).toBe(true);
|
|
});
|
|
|
|
test("main cover: the suggested poster, unless it's unticked or deselected", () => {
|
|
const s = new Session(deps(new FakeTransport()));
|
|
const r = result();
|
|
s.load(r);
|
|
expect(s.defaultChoice()).toEqual({ kind: "new", sha256: r.suggestedDefault!.sha256 });
|
|
s.update(r.suggestedDefault!.sha256, { selected: false });
|
|
expect(s.defaultChoice()).toBeNull();
|
|
s.update(r.suggestedDefault!.sha256, { selected: true });
|
|
s.makeDefault = false;
|
|
expect(s.defaultChoice()).toBeNull();
|
|
});
|
|
|
|
test("main cover already on MangaBaka: that existing image is marked", () => {
|
|
const s = new Session(deps(new FakeTransport()));
|
|
const r = result();
|
|
s.load({ ...r, suggestedDefault: r.present[0]!.cover });
|
|
expect(s.defaultChoice()).toEqual({ kind: "existing", id: 8000 });
|
|
});
|
|
});
|
|
|
|
describe("rehost and manual entry", () => {
|
|
test("rehosted URLs are used in the copy-and-paste text and remembered (catbox only)", async () => {
|
|
const fake = new FakeTransport().on("https://catbox.moe/user/api.php", () => text("https://files.catbox.moe/p.jpg"));
|
|
const store = memoryStore();
|
|
const r = result();
|
|
const s = new Session(deps(fake, { store, persistHostedUrls: true }));
|
|
s.load(r);
|
|
const res = await s.rehost("catbox", {});
|
|
expect(res.failures).toEqual([]);
|
|
expect(s.cards[0]!.hostedUrl).toBe("https://files.catbox.moe/p.jpg");
|
|
expect(s.manualText()).toStartWith("https://files.catbox.moe/p.jpg\ntype: volume · language: en · rating: safe · main cover");
|
|
expect(JSON.parse(store.get(HOSTED_KEY)!)).toEqual({ [`catbox:${s.cards[0]!.cover.sha256}`]: "https://files.catbox.moe/p.jpg" });
|
|
|
|
const again = new Session(deps(fake, { store, persistHostedUrls: true }));
|
|
again.load(r);
|
|
await again.rehost("catbox", {});
|
|
expect(fake.calls).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
// Snapshot/preview/save shapes from the OpenAPI spec (not capturable without a token).
|
|
const old: SubmissionImage = {
|
|
id: 7193, series_id: 9999, work_id: null, url: "https://example.org/old.jpg", index: "0.1", index_numeric: 0.1,
|
|
language: "ko", type: "volume", note: "Season Cover 3", content_rating: "safe", is_default: true,
|
|
};
|
|
const SUB = "https://api.mangabaka.org/v0/my/submissions/series-images/9999";
|
|
const LITTERBOX = "https://litterbox.catbox.moe/resources/internals/api.php";
|
|
function mangabaka(saveStatuses: number[]) {
|
|
return new FakeTransport()
|
|
.on((u, i) => u === SUB && (i.method ?? "GET") === "GET", () => json({ status: 200, data: { data: { images: [old] }, version: 42 } }))
|
|
.on(`${SUB}/preview`, () => json({ status: 200, data: { has_changes: true, changes: [] } }))
|
|
.on((u, i) => u === SUB && i.method === "POST", () => {
|
|
const status = saveStatuses.shift() ?? 200;
|
|
return status === 200
|
|
? json({ status: 200, data: { submission_id: 77, status: "pending", changes: [] } })
|
|
: json({ status, message: "x" }, status);
|
|
});
|
|
}
|
|
|
|
describe("submit with a token", () => {
|
|
test("preview, then save for review with the whole set and one main cover", async () => {
|
|
const fake = mangabaka([]);
|
|
const s = new Session(deps(fake));
|
|
s.load(result());
|
|
const prepared = await s.preview("mb-token");
|
|
expect(prepared.seed.images.map((i) => [i.id, i.is_default])).toEqual([[7193, false], [null, true]]);
|
|
const outcome = await s.submit("mb-token", prepared, "direct", {});
|
|
expect(outcome).toMatchObject({ status: "saved", result: { submission_id: 77 } });
|
|
const save = JSON.parse(fake.calls.at(-1)!.init.body as string);
|
|
expect(save.save_mode).toBe("review");
|
|
expect(save.version).toBe(42);
|
|
expect(save.data.images).toHaveLength(2);
|
|
});
|
|
|
|
test("409 once: refetch and ask again; 409 twice: give up", async () => {
|
|
const fake = mangabaka([409, 409]);
|
|
const s = new Session(deps(fake));
|
|
s.load(result());
|
|
const first = await s.submit("mb-token", await s.preview("mb-token"), "direct", {});
|
|
expect(first.status).toBe("changed");
|
|
if (first.status !== "changed") throw new Error("unreachable");
|
|
await expect(s.submit("mb-token", first.prepared, "direct", {})).rejects.toBeInstanceOf(MangaBakaError);
|
|
});
|
|
|
|
test("a failed upload is left out and the user gets a fresh preview; nothing is saved yet", async () => {
|
|
let uploads = 0;
|
|
const fake = mangabaka([]).on(LITTERBOX, () => (++uploads === 1 ? text("https://litter.catbox.moe/ok.jpg") : text("error", 500)));
|
|
const s = new Session(deps(fake));
|
|
const r = result();
|
|
s.load(r);
|
|
s.update(r.fresh[1]!.sha256, { selected: true });
|
|
const outcome = await s.submit("mb-token", await s.preview("mb-token"), "litterbox", {});
|
|
expect(outcome.status).toBe("changed");
|
|
expect(s.cards[1]!.selected).toBe(false);
|
|
expect(fake.calls.some((c) => c.url === SUB && c.init.method === "POST")).toBe(false);
|
|
});
|
|
|
|
test("every upload failing aborts the action", async () => {
|
|
const fake = mangabaka([]).on(LITTERBOX, () => text("error", 500));
|
|
const s = new Session(deps(fake));
|
|
s.load(result());
|
|
await expect(s.submit("mb-token", await s.preview("mb-token"), "litterbox", {})).rejects.toBeInstanceOf(HostError);
|
|
expect(fake.calls.some((c) => c.url === SUB && c.init.method === "POST")).toBe(false);
|
|
});
|
|
});
|