feat: add existing cert detection and regenerate prompt to setup-tls

- Added existing cert detection at the top of setup_tls() in muxplex/cli.py
- After defining cert_path/key_path, checks if tls_cert and tls_key are both set in settings AND the cert file exists
- If so, calls get_cert_info() and prints 'TLS already configured (expires YYYY-MM-DD).'
- Prompts user with input('Regenerate? [y/N] ') with try/except for EOFError and KeyboardInterrupt (defaults to 'n')
- If user does not answer 'y' or 'yes', prints 'Keeping existing certificates.' and returns early
- Added two new tests in test_cli.py: test_setup_tls_prompts_when_certs_exist and test_setup_tls_regenerates_on_eof
- All 866 tests pass

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This commit is contained in:
Brian Krabach
2026-04-03 23:28:59 -07:00
parent 52d0021f06
commit aa1dadf834
2 changed files with 122 additions and 1 deletions
+18 -1
View File
@@ -718,19 +718,36 @@ def setup_tls(method: str = "auto") -> None:
Auto-detection chain (method='auto'): Tailscale → mkcert → self-signed.
Use --method to force a specific certificate source.
"""
from muxplex.settings import SETTINGS_PATH, patch_settings # noqa: PLC0415
from muxplex.settings import SETTINGS_PATH, load_settings, patch_settings # noqa: PLC0415
from muxplex.tls import ( # noqa: PLC0415
detect_mkcert,
detect_tailscale,
generate_mkcert,
generate_self_signed,
generate_tailscale,
get_cert_info,
)
config_dir = SETTINGS_PATH.parent
cert_path = config_dir / "muxplex.crt"
key_path = config_dir / "muxplex.key"
# Check for existing certificates and prompt before overwriting
_settings = load_settings()
_existing_cert = _settings.get("tls_cert", "")
_existing_key = _settings.get("tls_key", "")
if _existing_cert and _existing_key and Path(_existing_cert).exists():
_info = get_cert_info(_existing_cert)
if _info is not None:
print(f"TLS already configured (expires {str(_info['expires'])[:10]}).")
try:
_answer = input("Regenerate? [y/N] ")
except (EOFError, KeyboardInterrupt):
_answer = "n"
if _answer.lower() not in ("y", "yes"):
print("Keeping existing certificates.")
return
result = None
tailscale_info = None
+104
View File
@@ -1901,3 +1901,107 @@ def test_setup_tls_method_choices_expanded():
assert "mkcert" in help_text, (
f"Expected 'mkcert' in setup-tls --help output, got:\n{help_text}"
)
# ---------------------------------------------------------------------------
# task-6-existing-cert-regenerate-prompt: Existing cert detection & prompt
# ---------------------------------------------------------------------------
def test_setup_tls_prompts_when_certs_exist(tmp_path, monkeypatch, capsys):
"""setup_tls() prints 'already configured' and prompts when certs already exist.
When tls_cert/tls_key are set in settings and the cert file exists,
setup_tls() must inform the user and prompt before overwriting.
When the user answers 'n', it must keep existing certs and return early.
"""
import json
import muxplex.settings as settings_mod
import muxplex.tls as tls_mod
from muxplex.tls import generate_self_signed
from muxplex.cli import setup_tls
# Generate real self-signed cert in tmp_path
cert_path = tmp_path / "muxplex.crt"
key_path = tmp_path / "muxplex.key"
generate_self_signed(cert_path, key_path)
# Write settings pointing to the generated cert
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)
# Monkeypatch input to return 'n' (user declines regeneration)
monkeypatch.setattr("builtins.input", lambda prompt="": "n")
# Monkeypatch detection functions to isolate prompt behavior
monkeypatch.setattr(tls_mod, "detect_tailscale", lambda: None)
monkeypatch.setattr(tls_mod, "detect_mkcert", lambda: False)
setup_tls()
out = capsys.readouterr().out
out_lower = out.lower()
assert "already configured" in out_lower or "regenerate" in out_lower, (
f"Expected 'already configured' or 'regenerate' in output, got: {out!r}"
)
# User said 'n' — should keep existing certs and return early
assert "keeping" in out_lower, (
f"Expected 'keeping' in output (user declined regeneration), got: {out!r}"
)
# Must NOT proceed to generate new certs (no "TLS setup complete" message)
assert "tls setup complete" not in out_lower, (
f"setup_tls() must return early when user says 'n', got: {out!r}"
)
def test_setup_tls_regenerates_on_eof(tmp_path, monkeypatch, capsys):
"""setup_tls() handles EOFError from input() gracefully (non-interactive mode).
When running in a non-interactive environment (e.g. piped stdin), input()
raises EOFError. The function must treat this as 'n' (keep existing certs)
and return normally without crashing.
"""
import json
import muxplex.settings as settings_mod
import muxplex.tls as tls_mod
from muxplex.tls import generate_self_signed
from muxplex.cli import setup_tls
# Generate real self-signed cert in tmp_path
cert_path = tmp_path / "muxplex.crt"
key_path = tmp_path / "muxplex.key"
generate_self_signed(cert_path, key_path)
# Write settings pointing to the generated cert
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)
# Monkeypatch input to raise EOFError (non-interactive environment)
def raise_eof(prompt=""):
raise EOFError("non-interactive stdin")
monkeypatch.setattr("builtins.input", raise_eof)
# Monkeypatch detection functions to isolate behavior
monkeypatch.setattr(tls_mod, "detect_tailscale", lambda: None)
monkeypatch.setattr(tls_mod, "detect_mkcert", lambda: False)
# Must not crash — EOFError is caught and treated as 'n'
setup_tls() # No exception should propagate
out = capsys.readouterr().out
out_lower = out.lower()
# EOFError → default 'n' → keep existing certs
assert "keeping" in out_lower, (
f"Expected 'keeping' in output after EOFError (default 'n'), got: {out!r}"
)