import zipfile import yaml from conftest import PNG, Book, Doc, build_epub def test_minimal_epub2_with_ncx(convert): book = Book(version="2.0", ncx=True, docs=[Doc("ch1.xhtml", "
Hallo Welt.
", "Eins")]) run = convert(book) assert run.rc == 0 text = run.chapter("001") assert "# Erstes Kapitel" in text assert "Hallo Welt." in text assert run.report["epub"]["version"] == "2.0" assert run.report["epub"]["ncx"] == "OEBPS/toc.ncx" def test_minimal_epub3_with_nav_and_metadata(convert): book = Book( authors=["Anna A.", "Bert B."], docs=[Doc("a.xhtml", "x
"), Doc("b.xhtml", "y
")], ) run = convert(book) assert run.rc == 0 meta = yaml.safe_load((run.out / "metadata.yaml").read_text(encoding="utf-8")) assert meta["title"] == "Testbuch" assert meta["authors"] == ["Anna A.", "Bert B."] assert meta["language"] == "de" assert meta["identifier"] == "urn:isbn:9783161484100" assert meta["isbn"] == "9783161484100" assert meta["epub_version"] == "3.0" assert len(meta["source_sha256"]) == 64 assert "subtitle" not in meta # nothing invented assert run.report["epub"]["nav"] == "OEBPS/nav.xhtml" def test_spine_order_is_respected_not_file_names(convert): docs = [Doc("z.xhtml", "1
"), Doc("a.xhtml", "2
")] run = convert(Book(docs=docs)) names = [p.name for p in run.chapters()] assert names == ["001-zuerst.md", "002-danach.md"] def test_cover_image_is_extracted_first(convert): book = Book(docs=[Doc("a.xhtml", "x
")], media={"img/cover.png": PNG}, cover="img/cover.png") run = convert(book) meta = yaml.safe_load((run.out / "metadata.yaml").read_text(encoding="utf-8")) assert meta["cover_image"] == "media/images/0001-cover.png" assert (run.out / meta["cover_image"]).read_bytes() == PNG def test_missing_container_returns_exit_code_2(tmp_path, convert): path = tmp_path / "broken.epub" with zipfile.ZipFile(path, "w") as z: z.writestr("mimetype", "application/epub+zip") z.writestr("OEBPS/x.xhtml", "&xxe;
") run = convert(path) assert run.rc == 2 assert "container.xml" in run.stderr assert not run.out.exists() def test_not_a_zip_returns_exit_code_2(tmp_path, convert): path = tmp_path / "kaputt.epub" path.write_bytes(b"this is not a zip") run = convert(path) assert run.rc == 2 def test_broken_opf_spine_reference(convert): book = Book(docs=[Doc("a.xhtml", "
x
")], spine_extra='
x
")],
extra_entries={"META-INF/encryption.xml": b"""
x raus
')], extra_entries={"../../outside.txt": b"evil", "/absolute/path.txt": b"evil", "OEBPS/../../escape.xhtml": b"evil"}, ) run = convert(book) assert run.rc == 0 assert any(e["code"] == "unsafe-zip-path" for e in run.report["errors"]) assert not (tmp_path / "outside.txt").exists() assert not (tmp_path.parent / "outside.txt").exists() for f in run.out.rglob("*"): assert run.out.resolve() in f.resolve().parents assert convert(book, "--strict").rc == 2 def test_xml_external_entities_are_not_resolved(tmp_path, convert): secret = tmp_path / "secret.txt" secret.write_text("GEHEIM") body = "
&xxe;
" book = Book(docs=[Doc("a.xhtml", body)]) path = build_epub(tmp_path / "xxe.epub", book) # inject a DOCTYPE with an external entity into the chapter with zipfile.ZipFile(path) as z: entries = {n: z.read(n) for n in z.namelist()} entries["OEBPS/a.xhtml"] = ( f']>' '