#!/usr/bin/env python3 """ Smoke / E2E test for application-form PDF export (docxtpl + LibreOffice). Direct (no HTTP): exercises merge + conversion on this machine / container. cd be0 && python tools/e2e_application_form_pdf_export.py --direct --out /tmp/e2e-mau.pdf HTTP: needs FastAPI listening (--url is the API origin, no /api prefix in path). python tools/e2e_application_form_pdf_export.py --http http://127.0.0.1:4402 --out /tmp/e2e-mau.pdf Pure curl + jq (no Python on host; official JSON must be wrapped): cd /path/to/repo && jq -n --slurpfile o fe0/public/assets/bieu_mau_sang_kien_template.json \ '{officialBieuMau: $o[0]}' | curl -sfS -X POST http://127.0.0.1:4402/api/v1/docx/preview-application-form-pdf \ -H 'Content-Type: application/json' -o /tmp/e2e-mau.pdf && file /tmp/e2e-mau.pdf If the API returns HTTP 501, rebuild/run be0 with LibreOffice (see be0/Dockerfile: libreoffice-writer-nogui). Docker (stack up): docker compose exec be0 python tools/e2e_application_form_pdf_export.py --direct --out /tmp/e2e-mau.pdf Refresh bundled sample after template changes: cp ../fe0/public/assets/bieu_mau_sang_kien_template.json tools/e2e_sample_official_bieu_mau.json Exit 0 on success; non-zero on failure. from __future__ import annotations import argparse import json import sys from pathlib import Path from typing import Any, Dict BE0_ROOT = Path(__file__).resolve().parent.parent if str(BE0_ROOT) not in sys.path: sys.path.insert(0, str(BE0_ROOT)) def _repo_root() -> Path: return BE0_ROOT.parent def load_sample_official() -> Dict[str, Any]: """officialBieuMau-shaped JSON (same as Review « Xem lại »).""" candidates = [ Path(__file__).resolve().parent / "e2e_sample_official_bieu_mau.json", _repo_root() / "fe0/public/assets/bieu_mau_sang_kien_template.json", ] for p in candidates: if p.is_file(): with open(p, encoding="utf-8") as f: data = json.load(f) break else: raise FileNotFoundError( "No sample official JSON found. Expected tools/e2e_sample_official_bieu_mau.json " "or fe0/public/assets/bieu_mau_sang_kien_template.json next to be0/." ) # Visible markers in generated PDF/DOCX if isinstance(data.get("TRANG BÌA"), dict): data["TRANG BÌA"]["Tên sáng kiến (Tiếng Việt)"] = "E2E PDF export — tên sáng kiến kiểm thử" data["TRANG BÌA"]["Năm"] = "2026" return data def run_direct(out_path: Path | None) -> bytes: from src.be01.docx_to_pdf import convert_docx_bytes_to_pdf from src.be01.fill_application_form import fill_application_form_docx from src.be01.official_to_data_blank import official_to_data_blank official = load_sample_official() ctx = official_to_data_blank(official) docx = fill_application_form_docx(ctx) pdf = convert_docx_bytes_to_pdf(docx) if not pdf.startswith(b"%PDF"): raise RuntimeError("Output is not a PDF (missing %PDF header).") if out_path: out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_bytes(pdf) print(f"Wrote {len(pdf)} bytes → {out_path}") else: print(f"OK: generated PDF, {len(pdf)} bytes (no --out)") return pdf def run_http(base_url: str, out_path: Path | None) -> bytes: import urllib.error import urllib.request official = load_sample_official() url = base_url.rstrip("/") + "/api/v1/docx/preview-application-form-pdf" body = json.dumps({"officialBieuMau": official}).encode("utf-8") req = urllib.request.Request( url, data=body, method="POST", headers={"Content-Type": "application/json", "Accept": "application/pdf"}, ) try: with urllib.request.urlopen(req, timeout=180) as resp: raw = resp.read() except urllib.error.HTTPError as e: detail = e.read().decode("utf-8", errors="replace") raise SystemExit(f"HTTP {e.code}: {detail}") from e if not raw.startswith(b"%PDF"): raise SystemExit(f"Expected PDF, got {len(raw)} bytes, head={raw[:64]!r}") if out_path: out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_bytes(raw) print(f"Wrote {len(raw)} bytes → {out_path}") else: print(f"OK: HTTP PDF, {len(raw)} bytes") return raw def main() -> None: ap = argparse.ArgumentParser(description=__doc__) g = ap.add_mutually_exclusive_group(required=True) g.add_argument("--direct", action="store_true", help="Run docxtpl + LibreOffice in-process") g.add_argument("--http", metavar="BASE_URL", help="POST to FastAPI (e.g. http://127.0.0.1:4402)") ap.add_argument("--out", type=Path, metavar="FILE.pdf", help="Write PDF to this path") args = ap.parse_args() try: if args.direct: run_direct(args.out) else: run_http(args.http, args.out) except FileNotFoundError as e: print(f"FAIL: {e}", file=sys.stderr) sys.exit(2) except Exception as e: print(f"FAIL: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()