host.controller_web_editor.tests.test_export
Tests for the PNG/PDF export endpoint.
Generated by Claude
The renderer itself lives in host/controller_config/print_render.py;
these tests cover the web-side wiring: parameter validation, the path
allowlist, and that the returned bytes have the correct file signature
and MIME type. Rendering quality is exercised by the Tkinter tool.
1"""Tests for the PNG/PDF export endpoint. 2 3# Generated by Claude 4 5The renderer itself lives in ``host/controller_config/print_render.py``; 6these tests cover the web-side wiring: parameter validation, the path 7allowlist, and that the returned bytes have the correct file signature 8and MIME type. Rendering quality is exercised by the Tkinter tool. 9""" 10 11import os 12import sys 13 14import pytest 15 16 17_REPO_ROOT = os.path.abspath( 18 os.path.join(os.path.dirname(__file__), "..", "..", "..")) 19if _REPO_ROOT not in sys.path: 20 sys.path.insert(0, _REPO_ROOT) 21 22 23from host.controller_web_editor.paths import safe_resolve # noqa: E402 24from host.controller_web_editor.routes.export import ( # noqa: E402 25 ExportRequestError, 26 export_from_yaml, 27) 28 29# Skip the whole module when the renderer's optional native deps aren't 30# available (cairosvg → libcairo). These tests only assert wiring, not 31# the Tkinter tool's existing rendering coverage. 32PIL = pytest.importorskip("PIL") 33pytest.importorskip("yaml") 34 35 36_DATA_INPUTS = os.path.join(_REPO_ROOT, "data", "inputs") 37_CONFIG = os.path.join(_DATA_INPUTS, "controller.yaml") 38 39 40PNG_SIG = b"\x89PNG\r\n\x1a\n" 41PDF_SIG = b"%PDF-" 42 43 44@pytest.fixture(scope="module") 45def yaml_path(): 46 if not os.path.isfile(_CONFIG): 47 pytest.skip(f"sample config not found: {_CONFIG}") 48 return safe_resolve("data/inputs/controller.yaml") 49 50 51@pytest.fixture 52def single_controller_yaml(tmp_path): 53 """Sample YAML trimmed to one controller — landscape PNG requires 54 one page, so multi-controller fixtures can't be used for that case. 55 """ 56 from utils.controller.config_io import load_config, save_config 57 58 full = load_config(_CONFIG) 59 # Keep only the first controller; drop the rest. 60 ports = sorted(full.controllers.keys()) 61 if len(ports) > 1: 62 for p in ports[1:]: 63 del full.controllers[p] 64 dst = tmp_path / "single.yaml" 65 save_config(full, dst) 66 return dst 67 68 69class TestParameterValidation: 70 def test_rejects_unknown_orientation(self, yaml_path): 71 with pytest.raises(ExportRequestError): 72 export_from_yaml(yaml_path, "sideways", "pdf") 73 74 def test_rejects_unknown_format(self, yaml_path): 75 with pytest.raises(ExportRequestError): 76 export_from_yaml(yaml_path, "landscape", "gif") 77 78 def test_format_normalizes_dot_prefix(self, yaml_path): 79 # "format=.pdf" sometimes appears in query strings; renderer 80 # should strip the leading dot. 81 data, mime, _ = export_from_yaml(yaml_path, "landscape", ".pdf") 82 assert mime == "application/pdf" 83 assert data.startswith(PDF_SIG) 84 85 86class TestRenderBytes: 87 def test_landscape_pdf(self, yaml_path): 88 data, mime, name = export_from_yaml( 89 yaml_path, "landscape", "pdf", hide_unassigned=False) 90 assert mime == "application/pdf" 91 assert data.startswith(PDF_SIG) 92 assert name.endswith("_landscape.pdf") 93 assert len(data) > 1000 # sanity: PDFs aren't tiny 94 95 def test_landscape_png_single_controller(self, single_controller_yaml): 96 # PNG only works with a single page (one controller in landscape, 97 # up to two in portrait), so use the trimmed fixture. 98 data, mime, name = export_from_yaml( 99 single_controller_yaml, "landscape", "png", 100 hide_unassigned=False) 101 assert mime == "image/png" 102 assert data.startswith(PNG_SIG) 103 assert name.endswith("_landscape.png") 104 105 def test_landscape_png_rejects_multi_controller(self, yaml_path): 106 # Sample config has two controllers; landscape PNG can't fit 107 # both on a single page. Renderer should raise so the client 108 # gets a clear 400 instead of a truncated file. 109 with pytest.raises(ValueError, match="single page"): 110 export_from_yaml(yaml_path, "landscape", "png") 111 112 def test_portrait_pdf(self, yaml_path): 113 data, mime, _ = export_from_yaml( 114 yaml_path, "portrait", "pdf", hide_unassigned=True) 115 assert mime == "application/pdf" 116 assert data.startswith(PDF_SIG) 117 118 def test_filename_uses_yaml_stem(self, yaml_path): 119 _, _, name = export_from_yaml(yaml_path, "landscape", "pdf") 120 assert name.startswith("controller_") # from controller.yaml
PNG_SIG =
b'\x89PNG\r\n\x1a\n'
PDF_SIG =
b'%PDF-'
@pytest.fixture(scope='module')
def
yaml_path():
@pytest.fixture
def
single_controller_yaml(tmp_path):
52@pytest.fixture 53def single_controller_yaml(tmp_path): 54 """Sample YAML trimmed to one controller — landscape PNG requires 55 one page, so multi-controller fixtures can't be used for that case. 56 """ 57 from utils.controller.config_io import load_config, save_config 58 59 full = load_config(_CONFIG) 60 # Keep only the first controller; drop the rest. 61 ports = sorted(full.controllers.keys()) 62 if len(ports) > 1: 63 for p in ports[1:]: 64 del full.controllers[p] 65 dst = tmp_path / "single.yaml" 66 save_config(full, dst) 67 return dst
Sample YAML trimmed to one controller — landscape PNG requires one page, so multi-controller fixtures can't be used for that case.
class
TestParameterValidation:
70class TestParameterValidation: 71 def test_rejects_unknown_orientation(self, yaml_path): 72 with pytest.raises(ExportRequestError): 73 export_from_yaml(yaml_path, "sideways", "pdf") 74 75 def test_rejects_unknown_format(self, yaml_path): 76 with pytest.raises(ExportRequestError): 77 export_from_yaml(yaml_path, "landscape", "gif") 78 79 def test_format_normalizes_dot_prefix(self, yaml_path): 80 # "format=.pdf" sometimes appears in query strings; renderer 81 # should strip the leading dot. 82 data, mime, _ = export_from_yaml(yaml_path, "landscape", ".pdf") 83 assert mime == "application/pdf" 84 assert data.startswith(PDF_SIG)
class
TestRenderBytes:
87class TestRenderBytes: 88 def test_landscape_pdf(self, yaml_path): 89 data, mime, name = export_from_yaml( 90 yaml_path, "landscape", "pdf", hide_unassigned=False) 91 assert mime == "application/pdf" 92 assert data.startswith(PDF_SIG) 93 assert name.endswith("_landscape.pdf") 94 assert len(data) > 1000 # sanity: PDFs aren't tiny 95 96 def test_landscape_png_single_controller(self, single_controller_yaml): 97 # PNG only works with a single page (one controller in landscape, 98 # up to two in portrait), so use the trimmed fixture. 99 data, mime, name = export_from_yaml( 100 single_controller_yaml, "landscape", "png", 101 hide_unassigned=False) 102 assert mime == "image/png" 103 assert data.startswith(PNG_SIG) 104 assert name.endswith("_landscape.png") 105 106 def test_landscape_png_rejects_multi_controller(self, yaml_path): 107 # Sample config has two controllers; landscape PNG can't fit 108 # both on a single page. Renderer should raise so the client 109 # gets a clear 400 instead of a truncated file. 110 with pytest.raises(ValueError, match="single page"): 111 export_from_yaml(yaml_path, "landscape", "png") 112 113 def test_portrait_pdf(self, yaml_path): 114 data, mime, _ = export_from_yaml( 115 yaml_path, "portrait", "pdf", hide_unassigned=True) 116 assert mime == "application/pdf" 117 assert data.startswith(PDF_SIG) 118 119 def test_filename_uses_yaml_stem(self, yaml_path): 120 _, _, name = export_from_yaml(yaml_path, "landscape", "pdf") 121 assert name.startswith("controller_") # from controller.yaml
def
test_landscape_pdf(self, yaml_path):
88 def test_landscape_pdf(self, yaml_path): 89 data, mime, name = export_from_yaml( 90 yaml_path, "landscape", "pdf", hide_unassigned=False) 91 assert mime == "application/pdf" 92 assert data.startswith(PDF_SIG) 93 assert name.endswith("_landscape.pdf") 94 assert len(data) > 1000 # sanity: PDFs aren't tiny
def
test_landscape_png_single_controller(self, single_controller_yaml):
96 def test_landscape_png_single_controller(self, single_controller_yaml): 97 # PNG only works with a single page (one controller in landscape, 98 # up to two in portrait), so use the trimmed fixture. 99 data, mime, name = export_from_yaml( 100 single_controller_yaml, "landscape", "png", 101 hide_unassigned=False) 102 assert mime == "image/png" 103 assert data.startswith(PNG_SIG) 104 assert name.endswith("_landscape.png")
def
test_landscape_png_rejects_multi_controller(self, yaml_path):
106 def test_landscape_png_rejects_multi_controller(self, yaml_path): 107 # Sample config has two controllers; landscape PNG can't fit 108 # both on a single page. Renderer should raise so the client 109 # gets a clear 400 instead of a truncated file. 110 with pytest.raises(ValueError, match="single page"): 111 export_from_yaml(yaml_path, "landscape", "png")