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

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