diff --git a/muxplex/cli.py b/muxplex/cli.py index 4072063..f7476cb 100644 --- a/muxplex/cli.py +++ b/muxplex/cli.py @@ -342,6 +342,37 @@ def doctor() -> None: f" (auth={cfg['auth']}, ttl={cfg['session_ttl']}s)" ) + # TLS status + tls_cert = cfg.get("tls_cert", "") + tls_key = cfg.get("tls_key", "") + if tls_cert and tls_key: + from datetime import datetime, timezone # noqa: PLC0415 + + from muxplex.tls import get_cert_info # noqa: PLC0415 + + cert_info = get_cert_info(tls_cert) + if cert_info is not None: + expires = cert_info["expires"] + # Ensure timezone-aware for comparison + if expires.tzinfo is None: + expires = expires.replace(tzinfo=timezone.utc) + now = datetime.now(timezone.utc) + if expires < now: + days_ago = (now - expires).days + print( + f" {warn_mark} TLS: WARNING \u2014 cert expired {days_ago} days ago." + " Run muxplex setup-tls to renew" + ) + else: + expiry_str = expires.strftime("%Y-%m-%d") + print(f" {ok_mark} TLS: enabled (cert expires {expiry_str})") + else: + print(f" {warn_mark} TLS: configured but cert not readable ({tls_cert})") + else: + print( + f" {warn_mark} TLS: disabled (clipboard requires HTTPS on non-localhost)" + ) + # Auth status pw_path = get_password_path() if pam_available(): diff --git a/muxplex/tests/test_cli.py b/muxplex/tests/test_cli.py index ee5f3cc..f189cf5 100644 --- a/muxplex/tests/test_cli.py +++ b/muxplex/tests/test_cli.py @@ -1561,3 +1561,76 @@ def test_serve_subcommand_accepts_tls_flags(): tls_cert="/path/cert.pem", tls_key="/path/key.pem", ) + + +# --------------------------------------------------------------------------- +# task-6-doctor-tls: TLS status section in doctor() +# --------------------------------------------------------------------------- + + +def test_doctor_shows_tls_disabled(tmp_path, monkeypatch, capsys): + """doctor() shows TLS disabled when no TLS configured.""" + import muxplex.settings as settings_mod + + settings_file = tmp_path / "settings.json" + # No tls_cert/tls_key — just use empty settings + settings_file.write_text("{}") + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file) + + from muxplex.cli import doctor + + doctor() + + out = capsys.readouterr().out + out_lower = out.lower() + assert "tls" in out_lower, f"Expected 'tls' in doctor output, got: {out!r}" + assert "disabled" in out_lower, ( + f"Expected 'disabled' in doctor output, got: {out!r}" + ) + + +def test_doctor_shows_tls_enabled(tmp_path, monkeypatch, capsys): + """doctor() shows TLS enabled when valid certs are configured.""" + import json + + import muxplex.settings as settings_mod + from muxplex.tls import generate_self_signed + + # Generate real self-signed certs in tmp_path + cert_path = tmp_path / "muxplex.crt" + key_path = tmp_path / "muxplex.key" + generate_self_signed(cert_path, key_path) + + settings_file = tmp_path / "settings.json" + settings_file.write_text( + json.dumps({"tls_cert": str(cert_path), "tls_key": str(key_path)}) + ) + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file) + + from muxplex.cli import doctor + + doctor() + + out = capsys.readouterr().out + out_lower = out.lower() + assert "tls" in out_lower, f"Expected 'tls' in doctor output, got: {out!r}" + assert "enabled" in out_lower, f"Expected 'enabled' in doctor output, got: {out!r}" + + +def test_doctor_shows_tls_clipboard_warning(tmp_path, monkeypatch, capsys): + """doctor() mentions clipboard or https when TLS is disabled.""" + import muxplex.settings as settings_mod + + settings_file = tmp_path / "settings.json" + settings_file.write_text("{}") + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file) + + from muxplex.cli import doctor + + doctor() + + out = capsys.readouterr().out + out_lower = out.lower() + assert "clipboard" in out_lower or "https" in out_lower, ( + f"Expected 'clipboard' or 'https' in doctor TLS-disabled output, got: {out!r}" + )