import { expect, test } from "bun:test"; import { waybackHistory } from "../src/core/history/wayback"; import { runCovers } from "../src/core/pipeline"; import { allowedFetchHosts, allowedImageHosts, findSource } from "../src/core/sources/registry"; import { SourceError } from "../src/core/sources/source"; import { tapas } from "../src/core/sources/tapas"; import { FakeTransport, json, text } from "./helpers/fake-transport"; import { fixtureJson, fixtureText } from "./helpers/fixtures"; const INFO = "https://tapas.io/series/villains-are-destined-to-die/info"; const comic = async () => text(await fixtureText("tapas/villains-info.html")); test("registered: Tapas links find the Tapas source; its hosts are allowlisted", () => { expect(findSource(new URL("https://tapas.io/series/villains-are-destined-to-die"))?.id).toBe("tapas"); expect(findSource(new URL("https://www.webtoons.com/en/fantasy/tower-of-god/list?title_no=95"))?.id).toBe("webtoons"); for (const host of ["tapas.io", "us-a.tapas.io", "d30womf5coomej.cloudfront.net"]) expect(allowedFetchHosts().has(host)).toBe(true); for (const host of ["us-a.tapas.io", "d30womf5coomej.cloudfront.net"]) expect(allowedImageHosts().has(host)).toBe(true); }); test("resolve reads the info page; currentCovers reuses it", async () => { const fake = new FakeTransport().on(INFO, comic); const series = await tapas.resolve(new URL("https://tapas.io/series/villains-are-destined-to-die"), { transport: fake }); expect(series).toEqual({ source: "tapas", externalId: "186414", title: "Villains Are Destined to Die", language: "en", canonicalUrl: INFO, archiveUrls: ["tapas.io/series/villains-are-destined-to-die"], mature: false, flags: { novel: false, ageGated: false }, }); const covers = await tapas.currentCovers(series, { transport: fake }); expect(fake.calls.length).toBe(1); expect(covers.map((c) => [c.role, c.current])).toEqual([["poster", true], ["banner", true]]); }); test("an id link: the page (reached by redirect) must be that series", async () => { const ok = new FakeTransport().on("https://tapas.io/series/186414/info", comic); expect((await tapas.resolve(new URL("https://tapas.io/series/186414"), { transport: ok })).canonicalUrl).toBe(INFO); const wrong = new FakeTransport().on("https://tapas.io/series/999/info", comic); await expect(tapas.resolve(new URL("https://tapas.io/series/999"), { transport: wrong })).rejects.toBeInstanceOf(SourceError); }); test("an episode link: the reader page names the series, then its info page", async () => { const fake = new FakeTransport() .on("https://tapas.io/episode/1901076", async () => text(await fixtureText("tapas/villains-episode-viewer.html"))) .on("https://tapas.io/series/186414/info", comic); const series = await tapas.resolve(new URL("https://tapas.io/episode/1901076"), { transport: fake }); expect(series.externalId).toBe("186414"); expect(fake.urls()).toEqual(["https://tapas.io/episode/1901076", "https://tapas.io/series/186414/info"]); }); test("novels are series too", async () => { const fake = new FakeTransport().on("https://tapas.io/series/villains-are-destined-to-die-novel/info", async () => text(await fixtureText("tapas/villains-novel-info.html"))); const series = await tapas.resolve(new URL("https://tapas.io/series/villains-are-destined-to-die-novel"), { transport: fake }); expect(series.flags).toEqual({ novel: true, ageGated: false }); expect(series.externalId).toBe("266599"); }); test("a series behind the age check: mature, no current covers, history still possible", async () => { const fake = new FakeTransport().on("https://tapas.io/series/286112/info", async () => text(await fixtureText("tapas/verify-age-dog-and-bird.html"))); const series = await tapas.resolve(new URL("https://tapas.io/series/286112"), { transport: fake }); expect(series).toMatchObject({ externalId: "286112", title: "Dog and bird mature", mature: true, canonicalUrl: "https://tapas.io/series/dog-and-bird-mature/info", archiveUrls: ["tapas.io/series/dog-and-bird-mature"], flags: { ageGated: true }, }); expect(await tapas.currentCovers(series, { transport: fake })).toEqual([]); }); test("a page that isn't there", async () => { const fake = new FakeTransport().on(() => true, () => text("gone", 404)); await expect(tapas.resolve(new URL("https://tapas.io/series/nope"), { transport: fake })).rejects.toThrow(/HTTP 404/); }); test("Wayback history keeps the comic's captures, not the novel's", async () => { const fake = new FakeTransport().on(INFO, comic) .on((u) => u.startsWith("https://web.archive.org/cdx/"), async () => text(await fixtureText("wayback/tapas-villains-cdx.txt"))) .on((u) => u.includes("id_/"), async () => text(await fixtureText("wayback/tapas-villains-2020.html"))); const series = await tapas.resolve(new URL(INFO), { transport: fake }); const found = await waybackHistory(tapas, series, { every: 12, transport: fake, warn: () => {} }); const snapshots = fake.urls().filter((u) => u.includes("id_/")); expect(snapshots.length).toBeGreaterThan(3); expect(snapshots.some((u) => u.includes("-novel"))).toBe(false); expect(found.find((c) => c.role === "poster")).toMatchObject({ url: "https://us-a.tapas.io/sa/c6/8eca19f0-84e2-4d86-bc5e-c2d7ffa6fd1c.jpg", archivedUrl: expect.stringContaining("id_/https://d30womf5coomej.cloudfront.net/sa/c6/8eca19f0-84e2-4d86-bc5e-c2d7ffa6fd1c_z.jpg"), }); }); test("the run says why an age-checked series has no current covers", async () => { const API = "https://api.mangabaka.org"; const fake = 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((u) => u.startsWith(`${API}/v1/series/9999/images`), () => json({ status: 200, data: [], pagination: { next: null } })) .on("https://tapas.io/series/286112/info", async () => text(await fixtureText("tapas/verify-age-dog-and-bird.html"))); const r = await runCovers({ mangabakaLink: "https://mangabaka.org/manhwa/9999/x", sourceLink: "https://tapas.io/series/286112", history: null }, { transport: fake }); expect(r.fresh).toEqual([]); expect(r.warnings.some((w) => /signed-in adults/.test(w))).toBe(true); });