Files
mangabaka-seeder/test/tapas-source.test.ts
T
Lucas WintherandClaude Opus 5.5 187e85fcaa feat(core): Tapas source (comics and novels), registered
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 <[email protected]>
2026-10-15 11:56:03 +02:00

98 lines
6.2 KiB
TypeScript

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