""" The export route: what it serves, what it refuses, and what it will not truncate. WHY THE CEILING IS TESTED AS HARD AS THE GEOMETRY ``/m/stl`` is reachable without authentication on a public name, and ``member_length_ft`` is an unbounded number. The response body grows with nothing but that parameter. The bound is the only thing standing between a short URL and a very large computation, so a bound that silently stopped applying would be worse than none -- it would be documented, believed, and absent. """ from __future__ import annotations import struct import pytest app = pytest.importorskip("mechcomp.web.app") stl = pytest.importorskip("mechcomp.stl") CEILING = app.DEFAULT_MAX_EXPORT_MM def export(ceiling=None, **overrides): return app.build_export("3x", "Y", dict(overrides), "", ceiling) # --------------------------------------------------------------------------- # It serves a file # --------------------------------------------------------------------------- def test_a_default_export_is_a_named_stl(): filename, data = export() assert filename.endswith(".stl") assert len(data) == 84 + 50 * struct.unpack(" 84 def test_a_member_over_the_ceiling_is_refused(): outcome = export(length_view="Full Length", member_length_ft=100) assert isinstance(outcome, app.ExportRefusal) assert outcome.status == 413 def test_the_refusal_names_the_limit_and_how_to_raise_it(): """ A refusal a person cannot act on is a wall. This one says the length, the limit, and the configuration key. """ outcome = export(length_view="Full Length", member_length_ft=100) assert "30480" in outcome.message assert "3048" in outcome.message assert "MECHCOMP_MAX_EXPORT_MM" in outcome.message def test_nothing_is_truncated_to_fit(): """ The failure this prevents: exporting a 3048 mm file for a 30480 mm request would ship a design record describing a different object -- the same class of silent wrongness the length_view control was added to stop, arriving by a different door. """ outcome = export(length_view="Full Length", member_length_ft=100) assert isinstance(outcome, app.ExportRefusal) assert "truncated" in outcome.message.lower() def test_the_ceiling_is_configurable(): """Raising it lets the same member through, which proves the gate is the ceiling rather than something else about a long member.""" assert isinstance(export(length_view="Full Length", member_length_ft=100), app.ExportRefusal) filename, data = export(ceiling=1e9, length_view="Full Length", member_length_ft=100) assert filename.endswith(".stl") def test_a_broken_ceiling_setting_falls_back_to_the_default(monkeypatch, tmp_path): """ A typo must not silently remove the bound. That is the failure where a limit exists in the documentation and nowhere else. """ empty = tmp_path / "none.env" empty.write_text("") for bad in ("", "abc", "0", "-5", " "): monkeypatch.setenv("MECHCOMP_MAX_EXPORT_MM", bad) assert app.max_export_mm(str(empty)) == CEILING monkeypatch.setenv("MECHCOMP_MAX_EXPORT_MM", "5000") assert app.max_export_mm(str(empty)) == 5000.0 # --------------------------------------------------------------------------- # Refusals are readable, not stack traces # --------------------------------------------------------------------------- def test_an_unknown_family_is_a_404(): outcome = app.build_export("9x", "Y", {}, "") assert isinstance(outcome, app.ExportRefusal) assert outcome.status == 404 def test_an_unknown_profile_is_not_a_500(): outcome = app.build_export("3x", "Nonexistent", {}, "") assert isinstance(outcome, app.ExportRefusal) assert outcome.status < 500 def test_a_rejected_profile_is_a_422_carrying_the_reason(): """ A rejection is the compiler working. The message names the parameter and the limit, as the reference did, and must survive to the caller. """ outcome = export(min_wall_mm=50) assert isinstance(outcome, app.ExportRefusal) assert outcome.status == 422 assert outcome.message assert "Traceback" not in outcome.message def test_a_buildable_member_is_not_refused(): """Positive control: the refusals are not simply refusing everything.""" assert not isinstance(export(), app.ExportRefusal) # --------------------------------------------------------------------------- # Authorship travels with the export # --------------------------------------------------------------------------- def test_an_author_reaches_the_record_without_moving_the_file_identity(): plain = app.build_export("3x", "Y", {}, "") signed = app.build_export("3x", "Y", {}, "someone@example.org") assert plain[0] == signed[0], "input_id must not move with the author" assert plain[1][:80] == signed[1][:80], "neither id may move"