# ============================================================ # PDF 图纸解析黄金测试(R71.1 扩展) # 覆盖:解析契约、PNG 预览、上传/清单、HTTP API、批量解析。 # ============================================================ from __future__ import annotations from pathlib import Path from fastapi.testclient import TestClient from server.aps_domain.drawing_dxf import build_drawing_master_candidates from server.aps_domain.drawing_pdf import inspect_pdf, render_pdf_preview from server.gateway.app import create_app from tests.auth_provider import install_test_auth def _make_pdf(path: Path, *, drawing_no: str = "5060102101-001-A", extra_lines: tuple[str, ...] = ()) -> Path: lines = (drawing_no,) + tuple(extra_lines) content = "BT /F1 10 Tf 72 760 Td 14 TL\n" + "".join(f"({line}) Tj T*\n" for line in lines) + "ET" stream = content.encode("latin-1") objects = [ b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>", b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>", b"<< /Length " + str(len(stream)).encode("ascii") + b" >>\nstream\n" + stream + b"\nendstream", b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", ] body = bytearray(b"%PDF-1.4\n") offsets: list[int] = [] for idx, obj in enumerate(objects, start=1): offsets.append(len(body)) body += f"{idx} 0 obj\n".encode("ascii") + obj + b"\nendobj\n" xref_pos = len(body) body += f"xref\n0 {len(objects) + 1}\n".encode("ascii") body += b"0000000000 65535 f \n" for offset in offsets: body += f"{offset:010d} 00000 n \n".encode("ascii") body += f"trailer\n<< /Size {len(objects) + 1} /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n".encode("ascii") path.write_bytes(body) return path def test_inspect_pdf_contract_and_review_candidates(tmp_path: Path): pdf = _make_pdf( tmp_path / "5060102101-001-A.pdf", extra_lines=("DRAWN BY: ENGINEER", "SCALE 1:1", "MATERIAL: STEEL"), ) parsed = inspect_pdf(pdf) assert parsed["asset"]["kind"] == "PDF" assert len(parsed["asset"]["sha256"]) == 64 assert parsed["drawing"]["drawingNumber"] == "5060102101-001" assert parsed["drawing"]["revision"] == "a" assert parsed["drawing"]["pageCount"] == 1 assert any("DRAWN BY" in row["text"] for row in parsed["texts"]) assert any( row["field"] == "drawingNumber" and row["value"] == "5060102101-001" and row["confidence"] >= 0.9 for row in parsed["fieldCandidates"] ) candidates = build_drawing_master_candidates(parsed) assert candidates["status"] == "PENDING_REVIEW" assert candidates["reviewRequired"] is True assert candidates["materials"][0]["code"] == "5060102101-001" assert candidates["materials"][0]["reviewRequired"] is True assert candidates["prohibitedAutoFill"] def test_render_pdf_preview_returns_png_data_url(tmp_path: Path): pdf = _make_pdf(tmp_path / "A-002-a.pdf") preview = render_pdf_preview(pdf) assert preview["mimeType"] == "image/png" assert preview["dataUrl"].startswith("data:image/png;base64,") assert len(preview["dataUrl"]) > 100 assert preview["width"] > 0 and preview["height"] > 0 def test_project_upload_accepts_pdf_and_lists_it(monkeypatch, tmp_path: Path): import server.gateway.app as gateway_module assert ".pdf" in gateway_module.PROJECT_UPLOAD_EXTS install_test_auth(monkeypatch, "tenant-pdf-upload") client = TestClient(create_app()) login = client.post("/api/auth/login", json={"username": "planner", "password": "test"}) assert login.status_code == 200, login.text created = client.post("/api/projects", json={"name": "PDF 上传验收", "workDir": str(tmp_path)}) assert created.status_code == 200, created.text pid = created.json()["project"]["id"] pdf_bytes = _make_pdf(tmp_path / "B-001-a.pdf").read_bytes() resp = client.post( f"/api/projects/{pid}/files/upload", files=[("files", ("B-001-a.pdf", pdf_bytes, "application/pdf"))], ) assert resp.status_code == 200, resp.text assert resp.json()["saved"] == ["B-001-a.pdf"] listing = client.get(f"/api/projects/{pid}/drawing-files") assert listing.status_code == 200, listing.text names = [row["name"] for row in listing.json()["files"]] assert "B-001-a.pdf" in names def test_drawing_http_api_inspect_pdf_flow(monkeypatch, tmp_path: Path): install_test_auth(monkeypatch, "tenant-pdf-api") client = TestClient(create_app()) login = client.post("/api/auth/login", json={"username": "planner", "password": "test"}) assert login.status_code == 200, login.text created = client.post("/api/projects", json={"name": "PDF 解析验收", "workDir": str(tmp_path)}) assert created.status_code == 200, created.text pid = created.json()["project"]["id"] pdf = _make_pdf(tmp_path / "C-001-a.pdf", extra_lines=("MATERIAL: STEEL", "SCALE 1:1")) resp = client.post("/api/drawings/inspect", json={"path": str(pdf), "projectId": pid}) assert resp.status_code == 200, resp.text data = resp.json() parsed = data.get("parsed") or data.get("drawing") or data assert parsed["asset"]["kind"] == "PDF" drawing_id = parsed["asset"]["id"] detail = client.get(f"/api/drawings/{drawing_id}") assert detail.status_code == 200, detail.text assert detail.json()["format"] == "PDF" preview = client.get(f"/api/drawings/{drawing_id}/preview") assert preview.status_code == 200, preview.text assert preview.json()["mimeType"] == "image/png" def test_batch_inspect_accepts_multiple_pdf(monkeypatch, tmp_path: Path): install_test_auth(monkeypatch, "tenant-pdf-batch") client = TestClient(create_app()) login = client.post("/api/auth/login", json={"username": "planner", "password": "test"}) assert login.status_code == 200, login.text created = client.post("/api/projects", json={"name": "PDF 批量验收", "workDir": str(tmp_path)}) assert created.status_code == 200, created.text _make_pdf(tmp_path / "D-001-a.pdf") _make_pdf(tmp_path / "D-002-b.pdf") resp = client.post("/api/drawings/inspect-batch", json={"directory": str(tmp_path)}) assert resp.status_code == 200, resp.text body = resp.json() assert body["count"] == 2 assert {row["fileName"] for row in body["drawings"]} == {"D-001-a.pdf", "D-002-b.pdf"}