141 lines
5.1 KiB
Python
141 lines
5.1 KiB
Python
#!/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()
|