From 2e81996719d25d884c4192ca24aefc8b8332d677 Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 23 Apr 2026 04:54:46 -0500 Subject: [PATCH] feat(tests): add property-based tests for URL normalization and interface config parsing to improve robustness --- .../backend/test_http_url_guard_hypothesis.py | 43 +++++++++++++++++++ ...test_interface_config_parser_hypothesis.py | 30 +++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/backend/test_http_url_guard_hypothesis.py create mode 100644 tests/backend/test_interface_config_parser_hypothesis.py diff --git a/tests/backend/test_http_url_guard_hypothesis.py b/tests/backend/test_http_url_guard_hypothesis.py new file mode 100644 index 0000000..1bd03f1 --- /dev/null +++ b/tests/backend/test_http_url_guard_hypothesis.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: 0BSD + +"""Property tests for outbound loopback URL normalization.""" + +from urllib.parse import urlparse + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from meshchatx.src.backend.http_url_guard import ( + UnsafeOutboundUrlError, + normalize_loopback_http_service_base, +) + + +@settings(max_examples=200, deadline=None) +@given(s=st.text(max_size=4000)) +def test_normalize_loopback_only_raises_safe_error_or_returns_origin(s): + try: + out = normalize_loopback_http_service_base(s) + except UnsafeOutboundUrlError: + return + except Exception as e: + raise AssertionError(f"unexpected exception: {type(e).__name__}: {e}") from e + + assert isinstance(out, str) + parsed = urlparse(out) + assert parsed.scheme in ("http", "https") + assert parsed.netloc + assert "@" not in parsed.netloc + host = (parsed.hostname or "").lower().strip("[]") + assert host in ("127.0.0.1", "localhost", "::1") + assert parsed.path in ("", "/") + assert parsed.query == "" + assert parsed.fragment == "" + + +@settings(max_examples=80, deadline=None) +@given(s=st.one_of(st.none(), st.binary(min_size=0, max_size=256))) +def test_normalize_loopback_rejects_non_string(s): + with pytest.raises(UnsafeOutboundUrlError): + normalize_loopback_http_service_base(s) diff --git a/tests/backend/test_interface_config_parser_hypothesis.py b/tests/backend/test_interface_config_parser_hypothesis.py new file mode 100644 index 0000000..1bd5076 --- /dev/null +++ b/tests/backend/test_interface_config_parser_hypothesis.py @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: 0BSD + +"""Property tests for Reticulum-style interface config parsing.""" + +from hypothesis import given, settings +from hypothesis import strategies as st + +from meshchatx.src.backend.interface_config_parser import InterfaceConfigParser + + +@settings(max_examples=150, deadline=None) +@given(text=st.text(max_size=8000)) +def test_interface_config_parse_never_raises(text): + result = InterfaceConfigParser.parse(text) + assert isinstance(result, list) + for iface in result: + assert isinstance(iface, dict) + assert "name" in iface + assert isinstance(iface["name"], str) + + +@settings(max_examples=120, deadline=None) +@given(lines=st.lists(st.text(max_size=400), max_size=120)) +def test_interface_config_parse_joined_lines_never_raises(lines): + text = "\n".join(lines) + result = InterfaceConfigParser.parse(text) + assert isinstance(result, list) + for iface in result: + assert isinstance(iface, dict) + assert "name" in iface