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

111 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from "bun:test";
import {
ImageError, inspectAll, inspectCandidate, probeImage, readCapped, sha256Hex, shapeOf, typeForShape,
} from "../src/core/images";
import type { CoverCandidate } from "../src/core/model";
import { FakeTransport, bytes, text } from "./helpers/fake-transport";
import { fixtureBytes } from "./helpers/fixtures";
describe("probeImage", () => {
test("real JPEG and PNG fixtures", async () => {
expect(probeImage(await fixtureBytes("images/poster-95.jpg")))
.toEqual({ width: 480, height: 623, contentType: "image/jpeg", ext: "jpg" });
expect(probeImage(await fixtureBytes("images/foreground-95.png")))
.toEqual({ width: 1200, height: 240, contentType: "image/png", ext: "png" });
expect(probeImage(await fixtureBytes("images/landing-2023.jpg")))
.toMatchObject({ width: 720, height: 1230 });
});
// Synthetic headers: these test the format decoders, they are not captured service responses.
test("GIF and WebP (VP8X) headers", () => {
const gif = new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x2c, 0x01, 0xc8, 0x00]);
expect(probeImage(gif)).toEqual({ width: 300, height: 200, contentType: "image/gif", ext: "gif" });
const webp = new Uint8Array(30);
webp.set([...new TextEncoder().encode("RIFF")], 0);
webp.set([...new TextEncoder().encode("WEBPVP8X")], 8);
webp.set([0x2b, 0x01, 0x00, 0xc7, 0x00, 0x00], 24); // 299+1 × 199+1
expect(probeImage(webp)).toEqual({ width: 300, height: 200, contentType: "image/webp", ext: "webp" });
});
test("HTML error pages and empty bodies are not images", () => {
expect(probeImage(new TextEncoder().encode("<!doctype html><title>404</title>"))).toBeNull();
expect(probeImage(new Uint8Array())).toBeNull();
});
});
test.each([
[480, 623, "portrait", "volume"],
[720, 1230, "tall", "other"],
[510, 510, "square", "other"],
[240, 254, "square", "other"],
[1920, 321, "wide", "banner"],
[1200, 240, "wide", "banner"],
[1000, 700, "other", "other"],
] as const)("%i×%i is %s → %s", (w, h, shape, type) => {
expect(shapeOf(w, h)).toBe(shape);
expect(typeForShape(shapeOf(w, h))).toBe(type);
});
test("sha256Hex", async () => {
expect(await sha256Hex(new TextEncoder().encode("abc")))
.toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
});
describe("readCapped", () => {
test("refuses a declared size over the cap", async () => {
const res = new Response("x", { headers: { "content-length": "999999" } });
await expect(readCapped(res, 1000)).rejects.toBeInstanceOf(ImageError);
});
test("refuses a stream that grows over the cap", async () => {
const stream = new ReadableStream({ start(c) { c.enqueue(new Uint8Array(600)); c.enqueue(new Uint8Array(600)); c.close(); } });
await expect(readCapped(new Response(stream), 1000)).rejects.toBeInstanceOf(ImageError);
});
test("returns the bytes under the cap", async () => {
expect((await readCapped(new Response(new Uint8Array(10)), 1000)).byteLength).toBe(10);
});
});
describe("inspectCandidate", () => {
const live = "https://swebtoon-phinf.pstatic.net/a/95.jpg";
const archived = "https://web.archive.org/web/20150312154616id_/http://swebtoon.phinf.naver.net/a/95.jpg";
const candidate: CoverCandidate = { url: live, role: "poster", current: false, seenAt: "20150312154616", archivedUrl: archived };
test("live image: measured, hashed, classified", async () => {
const poster = await fixtureBytes("images/poster-95.jpg");
const fake = new FakeTransport().on(live, () => bytes(poster));
const c = await inspectCandidate(candidate, fake);
expect(c).toMatchObject({ fetchedFrom: live, live: true, width: 480, height: 623, ext: "jpg", shape: "portrait", type: "volume" });
expect(c.sha256).toBe(await sha256Hex(poster));
});
test("falls back to the archived copy when the live URL is gone or not an image", async () => {
const poster = await fixtureBytes("images/poster-95.jpg");
for (const gone of [() => text("nope", 404), () => text("<html>image not found</html>")]) {
const fake = new FakeTransport().on(live, gone).on(archived, () => bytes(poster));
const c = await inspectCandidate(candidate, fake);
expect(c).toMatchObject({ fetchedFrom: archived, live: false });
}
});
test("throws ImageError naming every URL tried", async () => {
const fake = new FakeTransport().on(() => true, () => text("nope", 404));
await expect(inspectCandidate(candidate, fake)).rejects.toThrow(/95\.jpg.*404.*404/);
});
test("inspectAll drops failures with a warning and reports progress", async () => {
const poster = await fixtureBytes("images/poster-95.jpg");
const fake = new FakeTransport().on(live, () => bytes(poster)).on(() => true, () => text("nope", 404));
const warnings: string[] = [];
const progress: string[] = [];
const out = await inspectAll(
[candidate, { url: "https://swebtoon-phinf.pstatic.net/missing.jpg", role: "square", current: true }],
fake,
{ warn: (m) => warnings.push(m), onProgress: (d, t) => progress.push(`${d}/${t}`) },
);
expect(out).toHaveLength(1);
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain("missing.jpg");
expect(progress).toEqual(["1/2", "2/2"]);
});
});