aps-agent/tests/golden/test_dxf_api.py

238 lines
9.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import os
import re
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from server.aps_domain.drawing_dxf import inspect_dxf, render_dxf_preview
from server.gateway.app import create_app
from tests.auth_provider import install_test_auth
from tests.external_data import external_dir
_DEFAULT_SAMPLE = external_dir("RUIYANG_DEMO_DIR", "ruiyang") / "5060102101-001-e(1).dxf"
_FORBIDDEN_SVG = re.compile(r"<(?:script|foreignObject)\b|(?:href|xlink:href)\s*=|\bon\w+\s*=", re.IGNORECASE)
def _sample_dxf() -> Path:
try:
import ezdxf # noqa: F401
except ImportError as exc:
pytest.skip(f"DXF 运行依赖未完整安装:{exc}; 需要 ezdxf 及其 fontTools 依赖")
path = Path(os.environ.get("APS_DXF_SAMPLE", str(_DEFAULT_SAMPLE)))
if not path.is_file():
pytest.skip(f"真实 DXF 样例不存在:{path};可通过 APS_DXF_SAMPLE 指定")
return path
def test_real_dxf_metadata_entities_texts_and_dimensions_contract():
parsed = inspect_dxf(_sample_dxf())
drawing = parsed["drawing"]
assert drawing["metadata"]["acadVersion"].startswith("AC")
assert drawing["modelspaceEntityCount"] >= 9_000
assert drawing["layerCount"] > 0
assert drawing["bbox"]["width"] > 0
assert drawing["bbox"]["height"] > 0
assert sum(parsed["entityStatistics"].values()) == drawing["modelspaceEntityCount"]
assert parsed["entityStatistics"].get("LINE", 0) > 0
assert parsed["entityStatistics"].get("ARC", 0) > 0
assert len(parsed["texts"]) >= 10
assert all(row["text"] and row["evidence"].get("entityHandle") for row in parsed["texts"][:10])
assert len(parsed["dimensions"]) >= 50
assert any(row.get("measurement") is not None for row in parsed["dimensions"])
assert any(row["field"] == "drawingNumber" and row["value"] == "5060102101-001"
for row in parsed["fieldCandidates"])
def test_real_dxf_svg_preview_is_standalone_and_safe():
svg = render_dxf_preview(_sample_dxf())
assert svg.startswith("<svg ")
assert svg.endswith("</svg>")
assert "viewBox=" in svg
assert "sourceSha256=" in svg
assert "data-handle=" in svg
assert len(svg.encode("utf-8")) <= 2_000_000
assert not _FORBIDDEN_SVG.search(svg), "SVG 不得包含脚本、事件处理器或外部引用"
def test_drawing_http_api_contract_when_gateway_routes_are_merged(monkeypatch):
sample = _sample_dxf()
monkeypatch.setenv("APS_AUTH_ENABLED", "0")
monkeypatch.setenv("APS_DRAWING_ALLOWED_ROOTS", str(sample.parent))
app = create_app()
paths = app.openapi().get("paths", {})
required = {"/api/drawings/inspect", "/api/drawings/{drawing_id}", "/api/drawings/{drawing_id}/preview"}
missing = sorted(required - set(paths))
if missing:
pytest.skip(f"依赖未合并:Drawing HTTP API 缺少 {missing}")
client = TestClient(app)
response = client.post("/api/drawings/inspect", json={"path": str(sample)})
assert response.status_code == 200, response.text
data = response.json()
parsed = data.get("parsed") or data.get("drawing") or data
asset = parsed["asset"]
assert asset["kind"] == "DXF"
assert parsed["drawing"]["drawingNumber"] == "5060102101-001"
drawing_id = asset["id"]
detail = client.get(f"/api/drawings/{drawing_id}")
assert detail.status_code == 200, detail.text
preview = client.get(f"/api/drawings/{drawing_id}/preview")
assert preview.status_code == 200, preview.text
assert "<svg" in preview.text
assert not _FORBIDDEN_SVG.search(preview.text)
def _auth_client(monkeypatch: pytest.MonkeyPatch, tenant: str) -> TestClient:
install_test_auth(monkeypatch, tenant)
client = TestClient(create_app())
login = client.post("/api/auth/login", json={"username": "planner", "password": "test"})
assert login.status_code == 200, login.text
return client
def _create_project(client: TestClient, name: str, work_dir: Path) -> str:
created = client.post("/api/projects", json={"name": name, "workDir": str(work_dir)})
assert created.status_code == 200, created.text
return created.json()["project"]["id"]
def test_project_upload_accepts_dxf_and_rejects_unknown_type(monkeypatch, tmp_path: Path):
client = _auth_client(monkeypatch, "tenant-dxf-upload")
pid = _create_project(client, "DXF 上传验收", tmp_path)
ok = client.post(
f"/api/projects/{pid}/files/upload",
files=[("files", ("E2E-001-a.dxf", b"0\nSECTION\n2\nHEADER\n0\nENDSEC\n0\nEOF\n", "application/octet-stream"))],
)
assert ok.status_code == 200, ok.text
assert ok.json()["saved"] == ["E2E-001-a.dxf"]
assert (tmp_path / "E2E-001-a.dxf").is_file()
bad = client.post(
f"/api/projects/{pid}/files/upload",
files=[("files", ("说明.docx", b"not a drawing", "application/octet-stream"))],
)
assert bad.status_code == 400
detail = bad.json()["detail"]
assert "仅支持" in detail
assert ".dxf" in detail
def test_upload_size_limit_returns_readable_error(monkeypatch, tmp_path: Path):
import server.gateway.app as gateway_module
client = _auth_client(monkeypatch, "tenant-dxf-upload-limit")
monkeypatch.setattr(gateway_module, "PROJECT_UPLOAD_MAX_BYTES", 32)
pid = _create_project(client, "超限验收", tmp_path)
original = b"existing-valid-drawing"
(tmp_path / "big.dxf").write_bytes(original)
resp = client.post(
f"/api/projects/{pid}/files/upload",
files=[("files", ("big.dxf", b"x" * 64, "application/octet-stream"))],
)
assert resp.status_code == 200, resp.text
errors = resp.json().get("errors") or []
assert errors
assert "超过" in errors[0]["error"]
assert (tmp_path / "big.dxf").read_bytes() == original
assert not list(tmp_path.glob(".*.upload"))
def test_drawing_inspect_errors_are_readable_chinese(monkeypatch, tmp_path: Path):
client = _auth_client(monkeypatch, "tenant-dxf-errors")
allowed = tmp_path / "allowed"
allowed.mkdir()
monkeypatch.setenv("APS_DRAWING_ALLOWED_ROOTS", str(allowed))
outside = tmp_path / "outside.dxf"
outside.write_bytes(b"0\nSECTION\n2\nHEADER\n0\nENDSEC\n0\nEOF\n")
denied = client.post("/api/drawings/inspect", json={"path": str(outside)})
assert denied.status_code == 403
assert "不在项目工程目录" in denied.json()["detail"]
wrong = tmp_path / "drawing.txt"
wrong.write_text("not a drawing", encoding="utf-8")
bad = client.post("/api/drawings/inspect", json={"path": str(wrong)})
assert bad.status_code == 400
assert "仅允许读取存在的 DXF" in bad.json()["detail"]
def test_project_drawing_files_listing_returns_dxf_paths(monkeypatch, tmp_path: Path):
client = _auth_client(monkeypatch, "tenant-dxf-files")
pid = _create_project(client, "图纸清单验收", tmp_path)
(tmp_path / "A-001-a.dxf").write_bytes(b"0\nSECTION\n2\nHEADER\n0\nENDSEC\n0\nEOF\n")
(tmp_path / "订单.csv").write_text("订单号,数量\nSO-1,1\n", encoding="utf-8")
resp = client.get(f"/api/projects/{pid}/drawing-files")
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["count"] == 1
assert body["files"][0]["name"] == "A-001-a.dxf"
assert body["files"][0]["path"] == "A-001-a.dxf"
assert body["workDir"] is None
def test_drawing_inspect_is_strictly_scoped_to_requested_project(monkeypatch, tmp_path: Path):
client = _auth_client(monkeypatch, "tenant-dxf-project-scope")
first_root = tmp_path / "first"
second_root = tmp_path / "second"
first_root.mkdir()
second_root.mkdir()
first_pid = _create_project(client, "图纸项目 A", first_root)
second_pid = _create_project(client, "图纸项目 B", second_root)
drawing = first_root / "A-001-a.dxf"
drawing.write_bytes(b"0\nSECTION\n2\nHEADER\n0\nENDSEC\n0\nEOF\n")
denied = client.post(
"/api/drawings/inspect",
json={"projectId": second_pid, "path": str(drawing)},
)
assert denied.status_code == 403
allowed = client.post(
"/api/drawings/inspect",
json={"projectId": first_pid, "path": drawing.name},
)
assert allowed.status_code == 200, allowed.text
response_body = allowed.json()
parsed = response_body["drawing"]
assert "sourcePath" not in response_body["parsed"]["asset"]
assert parsed["filePath"] is None
assert all(row["status"] == "PENDING_REVIEW" for row in parsed["candidates"])
assert all(row["status"] == "PENDING_REVIEW" for row in parsed["fields"])
assert all(isinstance(row["reviewRequired"], bool) for row in parsed["fields"])
listed = client.get(f"/api/projects/{first_pid}/drawings")
assert listed.status_code == 200
assert listed.json()["drawings"][0]["projectId"] == first_pid
def test_project_drawing_scope_cannot_be_widened_by_global_roots(monkeypatch, tmp_path: Path):
client = _auth_client(monkeypatch, "tenant-dxf-global-root-scope")
first_root = tmp_path / "first"
second_root = tmp_path / "second"
first_root.mkdir()
second_root.mkdir()
first_pid = _create_project(client, "全局根项目 A", first_root)
second_pid = _create_project(client, "全局根项目 B", second_root)
drawing = first_root / "A-002-a.dxf"
drawing.write_bytes(b"0\nSECTION\n2\nHEADER\n0\nENDSEC\n0\nEOF\n")
monkeypatch.setenv("APS_DRAWING_ALLOWED_ROOTS", str(first_root))
denied = client.post(
"/api/drawings/inspect",
json={"projectId": second_pid, "path": str(drawing)},
)
assert denied.status_code == 403
allowed = client.post(
"/api/drawings/inspect",
json={"projectId": first_pid, "path": drawing.name},
)
assert allowed.status_code == 200, allowed.text