From c2c916077e3e5f4c68a2344771d8bfc4c3e47d10 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Fri, 3 Apr 2026 22:13:39 -0700 Subject: [PATCH] test: add edge case tests for TLS certificate generation and SSL serve behavior --- muxplex/tests/test_cli.py | 33 +++++++++++++++++++++++ muxplex/tests/test_tls.py | 56 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/muxplex/tests/test_cli.py b/muxplex/tests/test_cli.py index f189cf5..9b6c198 100644 --- a/muxplex/tests/test_cli.py +++ b/muxplex/tests/test_cli.py @@ -1634,3 +1634,36 @@ def test_doctor_shows_tls_clipboard_warning(tmp_path, monkeypatch, capsys): assert "clipboard" in out_lower or "https" in out_lower, ( f"Expected 'clipboard' or 'https' in doctor TLS-disabled output, got: {out!r}" ) + + +# --------------------------------------------------------------------------- +# task-7: Edge case tests for serve() TLS behavior +# --------------------------------------------------------------------------- + + +def test_serve_warns_when_only_cert_set(tmp_path, monkeypatch, capsys): + """serve() must NOT enable SSL when tls_cert is set but tls_key is empty string.""" + import muxplex.cli as cli_mod + + # Create a real cert file so tls_cert path check passes the "file exists" guard + cert_file = tmp_path / "server.crt" + cert_file.write_text("fake cert content") + + settings_file = tmp_path / "settings.json" + monkeypatch.setattr("muxplex.settings.SETTINGS_PATH", settings_file) + + uvicorn_calls = [] + + def fake_run(*args, **kwargs): + uvicorn_calls.append(kwargs) + + with patch("uvicorn.run", fake_run): + with patch.dict("sys.modules", {"muxplex.main": MagicMock()}): + cli_mod.serve(tls_cert=str(cert_file), tls_key="") + + assert len(uvicorn_calls) == 1 + kwargs = uvicorn_calls[0] + assert "ssl_certfile" not in kwargs, ( + "serve() must NOT pass ssl_certfile to uvicorn when tls_key is empty string — " + "SSL requires both cert and key" + ) diff --git a/muxplex/tests/test_tls.py b/muxplex/tests/test_tls.py index 6fc0632..a3f3d3b 100644 --- a/muxplex/tests/test_tls.py +++ b/muxplex/tests/test_tls.py @@ -156,3 +156,59 @@ def test_get_cert_info_returns_none_for_missing_file(tmp_path): assert result is None, ( f"get_cert_info() must return None for missing file, got: {result!r}" ) + + +# --------------------------------------------------------------------------- +# 10–12. Edge case tests +# --------------------------------------------------------------------------- + + +def test_generate_self_signed_with_custom_hostnames(tmp_path): + """generate_self_signed() with custom hostnames must include all of them in result['hostnames'].""" + from muxplex.tls import generate_self_signed + + cert_path = tmp_path / "cert.pem" + key_path = tmp_path / "key.pem" + custom_hostnames = ["mybox.local", "mybox.tailnet.ts.net"] + + result = generate_self_signed(cert_path, key_path, hostnames=custom_hostnames) + + assert isinstance(result, dict), "generate_self_signed() must return a dict" + assert isinstance(result.get("hostnames"), list), "result['hostnames'] must be a list" + assert "mybox.local" in result["hostnames"], ( + f"'mybox.local' must be in result['hostnames'], got: {result['hostnames']!r}" + ) + assert "mybox.tailnet.ts.net" in result["hostnames"], ( + f"'mybox.tailnet.ts.net' must be in result['hostnames'], got: {result['hostnames']!r}" + ) + + +def test_get_cert_info_hostnames_include_ip(tmp_path): + """get_cert_info() must include '127.0.0.1' in hostnames (from IP SANs added by generate_self_signed).""" + from muxplex.tls import generate_self_signed, get_cert_info + + cert_path = tmp_path / "cert.pem" + key_path = tmp_path / "key.pem" + generate_self_signed(cert_path, key_path, hostnames=["localhost"]) + + info = get_cert_info(cert_path) + + assert info is not None, "get_cert_info() must not return None for a valid cert" + assert "hostnames" in info, "get_cert_info() result must have 'hostnames' key" + assert "127.0.0.1" in info["hostnames"], ( + f"'127.0.0.1' must be in info['hostnames'] (IP SANs), got: {info['hostnames']!r}" + ) + + +def test_get_cert_info_returns_none_for_corrupt_file(tmp_path): + """get_cert_info() must return None for a file containing invalid PEM data.""" + from muxplex.tls import get_cert_info + + corrupt_pem = tmp_path / "corrupt.pem" + corrupt_pem.write_text("THIS IS NOT A CERTIFICATE") + + result = get_cert_info(corrupt_pem) + + assert result is None, ( + f"get_cert_info() must return None for corrupt PEM file, got: {result!r}" + )