"""Pure-function test helpers for the EPUB -> PDF converter suite. No pytest fixtures here: every function takes its dependencies explicitly so the helpers stay unit-testable and reusable. """ import io import time import zipfile from pypdf import PdfReader TERMINAL_STATUSES = ("done", "error") def wait_for_job(client, job_id, timeout=90.0): """Poll GET /api/jobs/ until a terminal status; return snapshot.""" deadline = time.time() + timeout last = None while time.time() < deadline: resp = client.get(f"/api/jobs/{job_id}") assert resp.status_code == 200, f"job lookup failed: {resp.status_code}" last = resp.get_json() if last["status"] in TERMINAL_STATUSES: return last time.sleep(0.05) raise TimeoutError(f"job {job_id} not terminal after {timeout}s: {last}") def download_bytes(client, filename): """GET /download/ and return the raw body (asserts 200).""" resp = client.get(f"/download/{filename}") assert resp.status_code == 200, ( f"download failed for {filename}: {resp.status_code}" ) return resp.data def pdf_text(data_bytes): """Extract all text from a PDF byte string (pypdf).""" reader = PdfReader(io.BytesIO(data_bytes)) return "\n".join((page.extract_text() or "") for page in reader.pages) def pdf_page_count(data_bytes): return len(PdfReader(io.BytesIO(data_bytes)).pages) def pdf_metadata_title(data_bytes): reader = PdfReader(io.BytesIO(data_bytes)) meta = reader.metadata return meta.get("/Title") if meta else None def make_zip_without_container(): """A valid zip that is not an EPUB (no META-INF/container.xml).""" buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("notes.txt", "hello, not an epub") return buf.getvalue() _CONTAINER_XML = ( '' '' '' ) def make_epub_zip(items, spine_ids, title="Synthetic", creator="Synth"): """Build a minimal EPUB byte string from (id, href, media_type, props) items.""" manifest = "".join( f'" for i, h, m, p in items ) refs = "".join(f'' for sid in spine_ids) opf = ( '' '' '' f"{title}{creator}" "" f"{manifest}" f"{refs}" "" ) buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("META-INF/container.xml", _CONTAINER_XML) zf.writestr("OEBPS/book.opf", opf) return buf.getvalue() def build_epub_tree(base, *, items=None, spine_ids=None, pdir=None): """Materialize a minimal on-disk EPUB structure for parse_opf tests.""" items = items or [ ("ch1", "chapter1.xhtml", "application/xhtml+xml", ""), ("ch2", "chapter2.xhtml", "application/xhtml+xml", ""), ("img1", "img.png", "image/png", ""), ] spine_ids = ( list(spine_ids) if spine_ids is not None else [i for i, h, m, p in items if "html" in m] ) pdir_attr = f' page-progression-direction="{pdir}"' if pdir else "" manifest = "".join( f'" for i, h, m, p in items ) refs = "".join(f'' for sid in spine_ids) opf = ( '' f'' '' "Tree Book" "Tree Author" "en" "" f"{manifest}" f"{refs}" "" ) (base / "META-INF").mkdir(parents=True, exist_ok=True) (base / "META-INF" / "container.xml").write_text(_CONTAINER_XML, encoding="utf-8") (base / "OEBPS").mkdir(parents=True, exist_ok=True) (base / "OEBPS" / "book.opf").write_text(opf, encoding="utf-8") for i, h, m, p in items: if "html" in m: (base / "OEBPS" / h).write_text( '' "

chapter body

", encoding="utf-8", ) return base