"""Integration tests for /engine/trace and /engine/simplify endpoints.""" import json import cv2 import numpy as np import pytest from fastapi.testclient import TestClient from main import app client = TestClient(app) def _make_test_png(width: int = 100, height: int = 100) -> bytes: """Create a simple test PNG with a white rectangle on black background.""" img = np.zeros((height, width, 3), dtype=np.uint8) cv2.rectangle(img, (20, 20), (80, 80), (255, 255, 255), -1) ok, buf = cv2.imencode(".png", img) assert ok return buf.tobytes() def _make_test_svg() -> str: """Create a simple SVG with a rectangular path for simplify tests.""" return ( '' '' "" ) def _make_complex_svg() -> str: """Create an SVG with many intermediate points (suitable for RDP reduction).""" # A path with extra collinear intermediate points that RDP can remove points = " ".join( f"L {x},{10}" for x in range(11, 91) ) return ( '' f'' "" ) @pytest.fixture def test_png() -> bytes: return _make_test_png() @pytest.fixture def test_svg() -> bytes: return _make_test_svg().encode("utf-8") @pytest.fixture def complex_svg() -> bytes: return _make_complex_svg().encode("utf-8") # ----------------------------------------------------------------------- # /engine/trace — SVG output (default) # ----------------------------------------------------------------------- class TestTraceEndpointSVG: """Tests for /engine/trace with SVG output format.""" def test_basic_trace(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "potrace"}, ) assert resp.status_code == 200 body = resp.json() assert body["format"] == "svg" assert "= 1 def test_custom_params(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={ "mode": "potrace", "params": json.dumps({"turdsize": 5, "alphamax": 0.5}), }, ) assert resp.status_code == 200 assert "= 1 p = paths[0] assert "commands" in p assert "properties" in p assert p["commands"][0]["type"] == "M" # starts with MoveTo def test_json_vtracer(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "vtracer", "output_format": "json"}, ) assert resp.status_code == 200 assert resp.json()["format"] == "json" # ----------------------------------------------------------------------- # /engine/trace — DXF output # ----------------------------------------------------------------------- class TestTraceEndpointDXF: """Tests for /engine/trace with DXF output format.""" def test_dxf_output(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "potrace", "output_format": "dxf"}, ) assert resp.status_code == 200 assert resp.headers["content-type"] == "application/dxf" assert "Content-Disposition" in resp.headers assert len(resp.content) > 0 def test_dxf_metadata_header(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "potrace", "output_format": "dxf"}, ) meta_header = resp.headers.get("X-Kerf-Metadata") assert meta_header is not None meta = json.loads(meta_header) assert meta["format"] == "dxf" assert "path_count" in meta assert "node_count_total" in meta def test_dxf_is_valid_ac1015(self, test_png): """DXF output should be parseable by ezdxf as AC1015.""" import io import ezdxf resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "potrace", "output_format": "dxf"}, ) stream = io.StringIO(resp.content.decode("utf-8")) doc = ezdxf.read(stream) assert doc.dxfversion == "AC1015" msp = doc.modelspace() entities = list(msp) assert len(entities) >= 1 def test_dxf_vtracer(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "vtracer", "output_format": "dxf"}, ) assert resp.status_code == 200 assert resp.headers["content-type"] == "application/dxf" # ----------------------------------------------------------------------- # /engine/trace — Validation # ----------------------------------------------------------------------- class TestTraceEndpointValidation: """Tests for input validation on /engine/trace.""" def test_invalid_mode(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "invalid"}, ) assert resp.status_code == 422 def test_unsupported_output_format(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "potrace", "output_format": "pdf"}, ) assert resp.status_code == 422 def test_invalid_params_json(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "potrace", "params": "not-json"}, ) assert resp.status_code == 422 def test_empty_file(self): resp = client.post( "/engine/trace", files={"file": ("empty.png", b"", "image/png")}, data={"mode": "potrace"}, ) assert resp.status_code == 422 def test_corrupt_image(self): resp = client.post( "/engine/trace", files={"file": ("bad.png", b"not an image", "image/png")}, data={"mode": "potrace"}, ) assert resp.status_code == 422 def test_preset_ignored(self, test_png): """Preset param is accepted but ignored for now.""" resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "potrace", "preset": "logo"}, ) assert resp.status_code == 200 # ----------------------------------------------------------------------- # /engine/trace — VTracer mode # ----------------------------------------------------------------------- class TestTraceEndpointVtracer: """Tests for /engine/trace with mode=vtracer.""" def test_basic_trace(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "vtracer"}, ) assert resp.status_code == 200 body = resp.json() assert body["format"] == "svg" def test_metadata_present(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={"mode": "vtracer"}, ) body = resp.json() meta = body["metadata"] assert isinstance(meta["path_count"], int) assert isinstance(meta["processing_ms"], float) def test_custom_params(self, test_png): resp = client.post( "/engine/trace", files={"file": ("test.png", test_png, "image/png")}, data={ "mode": "vtracer", "params": json.dumps({"filter_speckle": 10, "corner_threshold": 90}), }, ) assert resp.status_code == 200 # ----------------------------------------------------------------------- # /engine/simplify — SVG output # ----------------------------------------------------------------------- class TestSimplifyEndpointSVG: """Tests for /engine/simplify with SVG output format.""" def test_basic_simplify(self, test_svg): resp = client.post( "/engine/simplify", files={"file": ("test.svg", test_svg, "image/svg+xml")}, data={"epsilon": "1.0"}, ) assert resp.status_code == 200 body = resp.json() assert body["format"] == "svg" assert "= 1 assert paths[0]["commands"][0]["type"] == "M" # ----------------------------------------------------------------------- # /engine/simplify — DXF output # ----------------------------------------------------------------------- class TestSimplifyEndpointDXF: """Tests for /engine/simplify with DXF output format.""" def test_dxf_output(self, test_svg): resp = client.post( "/engine/simplify", files={"file": ("test.svg", test_svg, "image/svg+xml")}, data={"epsilon": "1.0", "output_format": "dxf"}, ) assert resp.status_code == 200 assert resp.headers["content-type"] == "application/dxf" assert len(resp.content) > 0 def test_dxf_metadata_header(self, test_svg): resp = client.post( "/engine/simplify", files={"file": ("test.svg", test_svg, "image/svg+xml")}, data={"epsilon": "1.0", "output_format": "dxf"}, ) meta = json.loads(resp.headers["X-Kerf-Metadata"]) assert meta["format"] == "dxf" assert isinstance(meta["path_count"], int) def test_dxf_is_valid(self, test_svg): """DXF output from simplify should be parseable.""" import io import ezdxf resp = client.post( "/engine/simplify", files={"file": ("test.svg", test_svg, "image/svg+xml")}, data={"epsilon": "1.0", "output_format": "dxf"}, ) stream = io.StringIO(resp.content.decode("utf-8")) doc = ezdxf.read(stream) assert doc.dxfversion == "AC1015" # ----------------------------------------------------------------------- # /engine/simplify — Validation # ----------------------------------------------------------------------- class TestSimplifyEndpointValidation: """Tests for input validation on /engine/simplify.""" def test_empty_file(self): resp = client.post( "/engine/simplify", files={"file": ("empty.svg", b"", "image/svg+xml")}, data={"epsilon": "1.0"}, ) assert resp.status_code == 422 def test_not_svg(self): resp = client.post( "/engine/simplify", files={"file": ("test.txt", b"hello world", "text/plain")}, data={"epsilon": "1.0"}, ) assert resp.status_code == 422 def test_invalid_output_format(self, test_svg): resp = client.post( "/engine/simplify", files={"file": ("test.svg", test_svg, "image/svg+xml")}, data={"epsilon": "1.0", "output_format": "pdf"}, ) assert resp.status_code == 422 def test_default_epsilon(self, test_svg): """Epsilon defaults to 1.0 when not specified.""" resp = client.post( "/engine/simplify", files={"file": ("test.svg", test_svg, "image/svg+xml")}, ) assert resp.status_code == 200 assert resp.json()["format"] == "svg" def test_binary_file_rejected(self): """Binary (non-UTF-8) file should be rejected.""" resp = client.post( "/engine/simplify", files={"file": ("test.svg", b"\x80\x81\x82\xff", "image/svg+xml")}, data={"epsilon": "1.0"}, ) assert resp.status_code == 422