import { Blob as NodeBlob, File as NodeFile } from "node:buffer"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { closeBook, openEpub } from "../../src/epub/archive"; import { patchPackageMetadata, rebuildEpub } from "../../src/epub/export"; import { resolvePackageHref, validateEntryPath } from "../../src/epub/paths"; import { renderChapter, revokeRenderedChapter } from "../../src/epub/reader"; import type { EpubBook } from "../../src/epub/types"; import { createEpubFixture } from "./fixture"; let opened: EpubBook[] = []; beforeEach(() => { vi.stubGlobal("Blob", NodeBlob); vi.stubGlobal("File", NodeFile); }); afterEach(async () => { for (const book of opened) await closeBook(book); opened = []; vi.unstubAllGlobals(); }); describe("EPUB path policy", () => { it("resolves package-relative references and rejects traversal", () => { expect( resolvePackageHref("EPUB/text/chapter.xhtml", "../images/cover.png#art"), ).toBe("EPUB/images/cover.png"); expect(() => resolvePackageHref("chapter.xhtml", "../outside")).toThrow( /escapes/u, ); expect(() => validateEntryPath("../escape")).toThrow(/traversal/u); }); }); describe("EPUB package inspection", () => { it("opens metadata, navigation, spine, and bounded inventory", async () => { const book = await openEpub(await createEpubFixture()); opened.push(book); expect(book.package.metadata).toMatchObject({ title: "Fixture Book", language: "en", identifier: "urn:test:book", }); expect(book.package.navigation[0]).toMatchObject({ label: "Opening chapter", path: "EPUB/chapter.xhtml", }); expect(book.package.spine[0]?.item?.path).toBe("EPUB/chapter.xhtml"); expect(book.summary).toMatchObject({ mimetypeFirst: true, mimetypeStored: true, encryptedResources: false, }); expect(book.issues.filter((item) => item.severity === "error")).toEqual([]); }); it("reports active content and broken resources", async () => { const book = await openEpub( await createEpubFixture({ activeContent: true, brokenLink: true }), ); opened.push(book); expect(book.issues.map((item) => item.code)).toEqual( expect.arrayContaining(["active-content", "broken-link"]), ); }); }); describe("safe rendering and editing", () => { it("removes active content, disables links, and resolves local images", async () => { vi.stubGlobal("URL", { ...URL, createObjectURL: vi.fn(() => "blob:fixture-cover"), revokeObjectURL: vi.fn(), }); const book = await openEpub( await createEpubFixture({ activeContent: true }), ); opened.push(book); const chapter = await renderChapter(book, "EPUB/chapter.xhtml"); expect(chapter.html).not.toMatch(/ { const book = await openEpub(await createEpubFixture()); opened.push(book); const metadata = { ...book.package.metadata, title: "Updated title", creators: ["One", "Two"], subjects: ["Testing"], }; const xml = patchPackageMetadata(book.packageXml, metadata); expect(xml).toContain("Updated title"); expect(xml.match(/