import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import AdmZip from "adm-zip";
import { afterEach, describe, expect, it } from "vitest";
import { ArchiveReadingSourceAdapter } from "./archive-reading-source.adapter.js";
const directories: string[] = [];
afterEach(() => {
for (const directory of directories.splice(0)) rmSync(directory, { recursive: true, force: true });
});
describe("ArchiveReadingSourceAdapter", () => {
it("builds an ordered EPUB spine and serves only opaque resource ids", async () => {
const directory = mkdtempSync(join(tmpdir(), "readabook-reading-"));
directories.push(directory);
const filePath = join(directory, "book.epub");
const zip = new AdmZip();
zip.addFile("META-INF/container.xml", Buffer.from(`

First
')); zip.addFile("OPS/chapter-2.xhtml", Buffer.from("Second
")); zip.addFile("OPS/images/cover.jpg", Buffer.from([1, 2, 3])); zip.addFile("OPS/styles/book.css", Buffer.from("body { color: black; }")); zip.writeZip(filePath); const stat = await import("node:fs").then(({ statSync }) => statSync(filePath)); const source = new ArchiveReadingSourceAdapter(); const book = { id: 7, format: "epub", filePath, fileSize: stat.size, fileMtime: stat.mtime.toISOString() }; const manifest = await source.manifest(book); expect(manifest.spine).toHaveLength(2); expect(manifest).toMatchObject({ containerPath: "META-INF/container.xml", opfPath: "OPS/package.opf", basePath: "OPS/", revision: expect.any(String) }); expect(JSON.stringify(manifest)).not.toContain(filePath); expect(JSON.stringify(manifest)).not.toContain(`/books/${book.id}/file`); expect(manifest.resources).toHaveLength(6); expect(manifest.resources.map((resource) => resource.resourceId)).not.toContain("OPS/chapter-1.xhtml"); expect(manifest.resources).toEqual(expect.arrayContaining([ expect.objectContaining({ href: "META-INF/container.xml", mediaType: "application/xml", size: expect.any(Number), url: expect.stringContaining("/reading-resources/") }), expect.objectContaining({ href: "OPS/package.opf", mediaType: "application/oebps-package+xml" }), expect.objectContaining({ href: "OPS/images/cover.jpg", mediaType: "image/jpeg", size: 3 }) ])); expect(manifest.spine[0]).toMatchObject({ index: 0, href: "OPS/chapter-1.xhtml", mediaType: "application/xhtml+xml", size: expect.any(Number) }); const firstResource = manifest.resources.find((resource) => resource.href === "OPS/chapter-1.xhtml"); const coverResource = manifest.resources.find((resource) => resource.href === "OPS/images/cover.jpg"); expect(firstResource?.references).toContain(coverResource?.resourceId); const chapter = await source.resource(book, manifest.spine[0].resourceId); expect(chapter.contentType).toBe("application/xhtml+xml"); expect(chapter.data?.toString()).toContain("First
"); const container = manifest.resources.find((resource) => resource.href === manifest.containerPath); expect((await source.resource(book, container!.resourceId)).data?.toString()).toContain("container"); }); it("exposes a PDF as a range-capable original without leaking its path in the manifest", async () => { const directory = mkdtempSync(join(tmpdir(), "readabook-reading-")); directories.push(directory); const filePath = join(directory, "secret-name.pdf"); writeFileSync(filePath, "%PDF-test"); const source = new ArchiveReadingSourceAdapter(); const book = { id: 8, format: "pdf", filePath, fileSize: 9, fileMtime: "2026-01-01T00:00:00.000Z" }; const manifest = await source.manifest(book); const resource = await source.resource(book, "original"); expect(JSON.stringify(manifest)).not.toContain(filePath); expect(manifest).toMatchObject({ format: "pdf", spine: [expect.objectContaining({ resourceId: "original" })], capabilities: { byteRanges: true, lightweightPages: false } }); expect(resource).toMatchObject({ filePath, contentType: "application/pdf", supportsRanges: true }); }); });