Series by slug, id or episode link; portrait original and banner; age-checked mature series resolve with a warning and no current covers; Wayback history keeps only this series' captures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
6.0 KiB
TypeScript
95 lines
6.0 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
import { sha256Hex } from "../src/core/images";
|
||
import { LinkError } from "../src/core/links";
|
||
import { runCovers, type Stage } from "../src/core/pipeline";
|
||
import { FakeTransport, bytes, json, text } from "./helpers/fake-transport";
|
||
import { fixtureBytes, fixtureJson, fixtureText } from "./helpers/fixtures";
|
||
|
||
const API = "https://api.mangabaka.org";
|
||
const P = "https://swebtoon-phinf.pstatic.net";
|
||
const POSTER = `${P}/20250204_46/17386458444547o1b2_JPEG/95.jpg`;
|
||
const FOREGROUND = `${P}/20230324_64/1679612177711Smf0u_PNG/6TowerOfGod_landingpage_desktop_fg.png`;
|
||
|
||
/** Tower of God end to end on captured fixtures. Images other than three captured ones 404 (and are dropped). */
|
||
async function towerOfGod() {
|
||
const [p2015, p2023] = [await fixtureText("wayback/tower-of-god-2015.html"), await fixtureText("wayback/tower-of-god-2023.html")];
|
||
return new FakeTransport()
|
||
.on(`${API}/v1/series/9999`, async () => json(await fixtureJson("mangabaka/series-9999.json")))
|
||
.on(`${API}/v1/series/9999/links?limit=100`, async () => json(await fixtureJson("mangabaka/links-9999.json")))
|
||
.on(/\/v1\/series\/9999\/images\?.*page=2/, async () => json(await fixtureJson("mangabaka/images-9999-page2.json")))
|
||
.on(`${API}/v1/series/9999/images?limit=50`, async () => json(await fixtureJson("mangabaka/images-9999-page1.json")))
|
||
.on("https://www.webtoons.com/-/-/-/list?title_no=95", async () => text(await fixtureText("webtoons/tower-of-god.html")))
|
||
.on((u) => u.startsWith("https://web.archive.org/cdx/") && u.includes("www.webtoons.com"),
|
||
async () => text(await fixtureText("wayback/tower-of-god-cdx-www.txt")))
|
||
.on((u) => u.startsWith("https://web.archive.org/cdx/"), async () => text(await fixtureText("wayback/tower-of-god-cdx-m.txt")))
|
||
.on((u) => /\/web\/20(1\d)\d+id_\//.test(u) && !/\.(jpe?g|png)$/i.test(u), () => text(p2015))
|
||
.on((u) => /\/web\/20(2\d)\d+id_\//.test(u) && !/\.(jpe?g|png)$/i.test(u), () => text(p2023))
|
||
.on(POSTER, async () => bytes(await fixtureBytes("images/poster-95.jpg")))
|
||
.on(FOREGROUND, async () => bytes(await fixtureBytes("images/foreground-95.png"), "image/png"))
|
||
// The captured 2023-01 snapshot still shows the 2021 landing art; serve the captured 720×1230 landing image for it.
|
||
.on((u) => u.endsWith("1Tower-of-God-Mobile-Landing-Page.jpg"), async () => bytes(await fixtureBytes("images/landing-2023.jpg")))
|
||
.on(() => true, () => text("not found", 404));
|
||
}
|
||
|
||
describe("runCovers on Tower of God", () => {
|
||
test("current + yearly history: checked, deduplicated, compared, default suggested", async () => {
|
||
const fake = await towerOfGod();
|
||
const stages: Stage[] = [];
|
||
const r = await runCovers(
|
||
{ mangabakaLink: "https://mangabaka.org/manhwa/9999/Tower-of-God", sourceLink: "https://www.webtoons.com/en/fantasy/tower-of-god/list?title_no=95", history: 12 },
|
||
{ transport: fake },
|
||
{ onStage: (s) => stages.push(s) },
|
||
);
|
||
expect(stages).toEqual(["series", "source", "history", "inspect", "compare"]);
|
||
expect(r.seriesId).toBe(9999);
|
||
expect(r.mbSeries.title).toBe("Tower of God");
|
||
expect(r.series.language).toBe("en");
|
||
expect(r.linkMatches).toBe(true);
|
||
|
||
const all = [...r.fresh, ...r.present.map((p) => p.cover)];
|
||
const poster = all.find((c) => c.candidate.url === POSTER)!;
|
||
expect(poster).toMatchObject({ live: true, shape: "portrait", type: "volume", candidate: { current: true, role: "poster" } });
|
||
expect(r.suggestedDefault?.sha256).toBe(await sha256Hex(await fixtureBytes("images/poster-95.jpg")));
|
||
expect(all.find((c) => c.candidate.url.endsWith("1Tower-of-God-Mobile-Landing-Page.jpg"))).toMatchObject({ shape: "tall", candidate: { current: false } });
|
||
expect(all.find((c) => c.candidate.url === FOREGROUND)!.candidate.current).toBe(true);
|
||
expect(new Set(all.map((c) => c.sha256)).size).toBe(all.length);
|
||
expect(r.warnings.some((w) => w.startsWith("Could not download"))).toBe(true);
|
||
expect(fake.calls.some((c) => c.init.headers?.["x-api-key"])).toBe(false);
|
||
});
|
||
|
||
test("history off: Wayback is never asked", async () => {
|
||
const fake = await towerOfGod();
|
||
await runCovers({ mangabakaLink: "9999", sourceLink: "https://www.webtoons.com/-/-/-/list?title_no=95", history: null }, { transport: fake });
|
||
expect(fake.urls().some((u) => u.includes("web.archive.org"))).toBe(false);
|
||
});
|
||
|
||
test("'skip the rest of the history' keeps going with what was found", async () => {
|
||
const fake = await towerOfGod();
|
||
const stop = new AbortController();
|
||
const r = await runCovers(
|
||
{ mangabakaLink: "9999", sourceLink: "https://www.webtoons.com/-/-/-/list?title_no=95", history: 1 },
|
||
{ transport: fake },
|
||
{ stopHistory: stop.signal, onProgress: (stage) => stage === "history" && stop.abort() },
|
||
);
|
||
const snapshots = fake.urls().filter((u) => /id_\/http:\/\/(www|m)\.webtoons\.com/.test(u));
|
||
expect(snapshots).toHaveLength(1);
|
||
expect([...r.fresh, ...r.present.map((p) => p.cover)].some((c) => c.candidate.url === POSTER)).toBe(true);
|
||
});
|
||
|
||
test("warns when MangaBaka has no link to the Webtoons series", async () => {
|
||
const fake = await towerOfGod();
|
||
const r = await runCovers({ mangabakaLink: "9999", sourceLink: "https://www.webtoons.com/-/-/-/list?title_no=95", history: null }, {
|
||
transport: { fetch: (u, i) => (u.includes("/links") ? Promise.resolve(json({ status: 200, data: [], pagination: { next: null } })) : fake.fetch(u, i)) },
|
||
});
|
||
expect(r.linkMatches).toBe(false);
|
||
expect(r.warnings[0]).toMatch(/has no link to this WEBTOON series/);
|
||
});
|
||
});
|
||
|
||
test("unsupported source link is a LinkError naming the supported sources", async () => {
|
||
await expect(runCovers({ mangabakaLink: "9999", sourceLink: "https://www.lezhin.com/en/comic/x", history: null }, { transport: new FakeTransport() }))
|
||
.rejects.toThrow(/Supported: WEBTOON, Tapas/);
|
||
await expect(runCovers({ mangabakaLink: "nope", sourceLink: "https://tapas.io/series/x", history: null }, { transport: new FakeTransport() }))
|
||
.rejects.toBeInstanceOf(LinkError);
|
||
});
|