|
|
|
@@ -7,12 +7,10 @@ import stat
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cli_module_importable():
|
|
|
|
|
"""muxplex.cli must be importable."""
|
|
|
|
|
from muxplex.cli import main # noqa: F401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_calls_serve_by_default():
|
|
|
|
|
"""Calling main() with no args must invoke serve() with None defaults (settings layer resolves)."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -24,7 +22,6 @@ def test_main_calls_serve_by_default():
|
|
|
|
|
host=None, port=None, auth=None, session_ttl=None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_passes_custom_host_and_port():
|
|
|
|
|
"""main() with --host/--port must forward them to serve(); unset flags are None."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -36,7 +33,6 @@ def test_main_passes_custom_host_and_port():
|
|
|
|
|
host="192.168.1.1", port=9000, auth=None, session_ttl=None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_default_host_is_localhost():
|
|
|
|
|
"""Default --host must be None (settings layer resolves to 127.0.0.1)."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -47,7 +43,6 @@ def test_main_default_host_is_localhost():
|
|
|
|
|
_, kwargs = mock_serve.call_args
|
|
|
|
|
assert kwargs["host"] is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_passes_auth_flag():
|
|
|
|
|
"""main() with --auth password must forward auth='password'; unset flags are None."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -59,7 +54,6 @@ def test_main_passes_auth_flag():
|
|
|
|
|
host=None, port=None, auth="password", session_ttl=None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_passes_session_ttl_flag():
|
|
|
|
|
"""main() with --session-ttl 3600 must forward session_ttl=3600; unset flags are None."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -71,27 +65,6 @@ def test_main_passes_session_ttl_flag():
|
|
|
|
|
host=None, port=None, auth=None, session_ttl=3600
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_install_service_subcommand():
|
|
|
|
|
"""main() with 'install-service' must invoke service_install() (new module)."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
|
|
|
|
|
|
with patch("muxplex.service.service_install") as mock_install:
|
|
|
|
|
with patch("sys.argv", ["muxplex", "install-service"]):
|
|
|
|
|
main()
|
|
|
|
|
mock_install.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_install_service_system_flag():
|
|
|
|
|
"""main() with 'install-service --system' still calls service_install() (--system ignored; new module auto-detects)."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
|
|
|
|
|
|
with patch("muxplex.service.service_install") as mock_install:
|
|
|
|
|
with patch("sys.argv", ["muxplex", "install-service", "--system"]):
|
|
|
|
|
main()
|
|
|
|
|
mock_install.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_show_password_prints_password_from_file(tmp_path, monkeypatch, capsys):
|
|
|
|
|
"""show_password() prints the password when MUXPLEX_AUTH=password and file exists."""
|
|
|
|
|
from muxplex.cli import show_password
|
|
|
|
@@ -111,7 +84,6 @@ def test_show_password_prints_password_from_file(tmp_path, monkeypatch, capsys):
|
|
|
|
|
captured = capsys.readouterr()
|
|
|
|
|
assert "my-test-password" in captured.out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_show_password_no_file(tmp_path, monkeypatch, capsys):
|
|
|
|
|
"""show_password() tells user no file found when in password mode with no file."""
|
|
|
|
|
from muxplex.cli import show_password
|
|
|
|
@@ -129,7 +101,6 @@ def test_show_password_no_file(tmp_path, monkeypatch, capsys):
|
|
|
|
|
output_lower = captured.out.lower()
|
|
|
|
|
assert "no password" in output_lower or "not found" in output_lower
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_show_password_pam_mode(monkeypatch, capsys):
|
|
|
|
|
"""show_password() reports PAM mode when pam_available() is True and not password mode."""
|
|
|
|
|
from muxplex.cli import show_password
|
|
|
|
@@ -142,7 +113,6 @@ def test_show_password_pam_mode(monkeypatch, capsys):
|
|
|
|
|
captured = capsys.readouterr()
|
|
|
|
|
assert "pam" in captured.out.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reset_secret_writes_new_secret(tmp_path, monkeypatch):
|
|
|
|
|
"""reset_secret() writes a new secret file with content longer than 20 chars."""
|
|
|
|
|
from muxplex.cli import reset_secret
|
|
|
|
@@ -158,7 +128,6 @@ def test_reset_secret_writes_new_secret(tmp_path, monkeypatch):
|
|
|
|
|
content = secret_path.read_text().strip()
|
|
|
|
|
assert len(content) > 20, f"Secret must be longer than 20 chars, got {len(content)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reset_secret_sets_0600_permissions(tmp_path, monkeypatch):
|
|
|
|
|
"""reset_secret() sets file permissions to 0o600."""
|
|
|
|
|
from muxplex.cli import reset_secret
|
|
|
|
@@ -173,7 +142,6 @@ def test_reset_secret_sets_0600_permissions(tmp_path, monkeypatch):
|
|
|
|
|
file_mode = stat.S_IMODE(secret_path.stat().st_mode)
|
|
|
|
|
assert file_mode == 0o600, f"Expected 0o600, got {oct(file_mode)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reset_secret_prints_warning(tmp_path, monkeypatch, capsys):
|
|
|
|
|
"""reset_secret() prints a warning that sessions are now invalid."""
|
|
|
|
|
from muxplex.cli import reset_secret
|
|
|
|
@@ -190,23 +158,6 @@ def test_reset_secret_prints_warning(tmp_path, monkeypatch, capsys):
|
|
|
|
|
f"Expected 'invalid' or 'warning' in output, got: {captured.out!r}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_install_service_deprecated_alias_prints_warning(capsys):
|
|
|
|
|
"""install-service must print deprecation warning and forward to service install."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
patch("sys.argv", ["muxplex", "install-service"]),
|
|
|
|
|
patch("muxplex.service.service_install") as mock_install,
|
|
|
|
|
):
|
|
|
|
|
main()
|
|
|
|
|
|
|
|
|
|
captured = capsys.readouterr()
|
|
|
|
|
assert "deprecated" in captured.err.lower()
|
|
|
|
|
assert "service install" in captured.err.lower()
|
|
|
|
|
mock_install.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_check_dependencies_exits_when_ttyd_missing(monkeypatch):
|
|
|
|
|
"""_check_dependencies() must sys.exit(1) when ttyd is not in PATH."""
|
|
|
|
|
import shutil
|
|
|
|
@@ -226,7 +177,6 @@ def test_check_dependencies_exits_when_ttyd_missing(monkeypatch):
|
|
|
|
|
_check_dependencies()
|
|
|
|
|
assert exc_info.value.code == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_check_dependencies_exits_when_tmux_missing(monkeypatch):
|
|
|
|
|
"""_check_dependencies() must sys.exit(1) when tmux is not in PATH."""
|
|
|
|
|
import shutil
|
|
|
|
@@ -246,7 +196,6 @@ def test_check_dependencies_exits_when_tmux_missing(monkeypatch):
|
|
|
|
|
_check_dependencies()
|
|
|
|
|
assert exc_info.value.code == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_check_dependencies_passes_when_all_present(monkeypatch):
|
|
|
|
|
"""_check_dependencies() must not raise when both tmux and ttyd are found."""
|
|
|
|
|
import shutil
|
|
|
|
@@ -257,7 +206,6 @@ def test_check_dependencies_passes_when_all_present(monkeypatch):
|
|
|
|
|
# Should not raise
|
|
|
|
|
_check_dependencies()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_check_dependencies_called_for_serve(monkeypatch):
|
|
|
|
|
"""main() must call _check_dependencies() when subcommand is serve."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -271,21 +219,6 @@ def test_main_check_dependencies_called_for_serve(monkeypatch):
|
|
|
|
|
|
|
|
|
|
assert len(calls) == 1, "_check_dependencies must be called once for serve"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_check_dependencies_not_called_for_install_service(monkeypatch):
|
|
|
|
|
"""main() must NOT call _check_dependencies() for install-service subcommand."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
|
|
|
|
|
|
calls = []
|
|
|
|
|
monkeypatch.setattr("muxplex.cli._check_dependencies", lambda: calls.append(True))
|
|
|
|
|
|
|
|
|
|
with patch("muxplex.service.service_install"):
|
|
|
|
|
with patch("sys.argv", ["muxplex", "install-service"]):
|
|
|
|
|
main()
|
|
|
|
|
|
|
|
|
|
assert len(calls) == 0, "_check_dependencies must NOT be called for install-service"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_dunder_main_calls_main():
|
|
|
|
|
"""python -m muxplex must call cli.main()."""
|
|
|
|
|
import importlib.util
|
|
|
|
@@ -298,12 +231,10 @@ def test_dunder_main_calls_main():
|
|
|
|
|
exec(Path(spec.origin).read_text()) # noqa: S102
|
|
|
|
|
mock_main.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# doctor() tests
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_doctor_shows_python_version(capsys):
|
|
|
|
|
"""doctor must show Python version."""
|
|
|
|
|
from muxplex.cli import doctor
|
|
|
|
@@ -312,7 +243,6 @@ def test_doctor_shows_python_version(capsys):
|
|
|
|
|
out = capsys.readouterr().out
|
|
|
|
|
assert "Python" in out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_doctor_checks_tmux(capsys, monkeypatch):
|
|
|
|
|
"""doctor must check for tmux."""
|
|
|
|
|
import subprocess
|
|
|
|
@@ -333,7 +263,6 @@ def test_doctor_checks_tmux(capsys, monkeypatch):
|
|
|
|
|
out = capsys.readouterr().out
|
|
|
|
|
assert "tmux" in out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_doctor_reports_missing_ttyd(capsys, monkeypatch):
|
|
|
|
|
"""doctor must report when ttyd is missing."""
|
|
|
|
|
from muxplex.cli import doctor
|
|
|
|
@@ -351,7 +280,6 @@ def test_doctor_reports_missing_ttyd(capsys, monkeypatch):
|
|
|
|
|
assert "ttyd" in out
|
|
|
|
|
assert "not found" in out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_doctor_shows_platform(capsys):
|
|
|
|
|
"""doctor must show platform info."""
|
|
|
|
|
from muxplex.cli import doctor
|
|
|
|
@@ -360,7 +288,6 @@ def test_doctor_shows_platform(capsys):
|
|
|
|
|
out = capsys.readouterr().out
|
|
|
|
|
assert "Platform" in out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_doctor_subcommand_registered():
|
|
|
|
|
"""doctor must be a valid subcommand in main() argparse."""
|
|
|
|
|
import io
|
|
|
|
@@ -378,7 +305,6 @@ def test_doctor_subcommand_registered():
|
|
|
|
|
help_text = buf.getvalue().lower()
|
|
|
|
|
assert "doctor" in help_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_dispatches_to_doctor(monkeypatch):
|
|
|
|
|
"""main() with 'doctor' subcommand must invoke doctor()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -393,12 +319,10 @@ def test_main_dispatches_to_doctor(monkeypatch):
|
|
|
|
|
"doctor() must be called once when 'doctor' subcommand is used"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# upgrade / update subcommand tests
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upgrade_subcommand_registered():
|
|
|
|
|
"""upgrade must be a valid subcommand."""
|
|
|
|
|
import io
|
|
|
|
@@ -416,7 +340,6 @@ def test_upgrade_subcommand_registered():
|
|
|
|
|
help_text = buf.getvalue().lower()
|
|
|
|
|
assert "upgrade" in help_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_alias_registered():
|
|
|
|
|
"""update must be a valid subcommand (alias for upgrade)."""
|
|
|
|
|
import io
|
|
|
|
@@ -434,7 +357,6 @@ def test_update_alias_registered():
|
|
|
|
|
help_text = buf.getvalue().lower()
|
|
|
|
|
assert "update" in help_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upgrade_calls_uv_tool_install(monkeypatch, capsys):
|
|
|
|
|
"""upgrade must attempt uv tool install when update is available."""
|
|
|
|
|
import subprocess
|
|
|
|
@@ -464,7 +386,6 @@ def test_upgrade_calls_uv_tool_install(monkeypatch, capsys):
|
|
|
|
|
uv_calls = [c for c in calls if isinstance(c, list) and "uv" in str(c)]
|
|
|
|
|
assert len(uv_calls) > 0, "upgrade must call uv tool install"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_dispatches_to_upgrade(monkeypatch):
|
|
|
|
|
"""main() with 'upgrade' subcommand must invoke upgrade()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -477,7 +398,6 @@ def test_main_dispatches_to_upgrade(monkeypatch):
|
|
|
|
|
|
|
|
|
|
assert len(calls) == 1, "upgrade() must be called once for 'upgrade' subcommand"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_dispatches_update_to_upgrade(monkeypatch):
|
|
|
|
|
"""main() with 'update' subcommand must also invoke upgrade()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -490,12 +410,10 @@ def test_main_dispatches_update_to_upgrade(monkeypatch):
|
|
|
|
|
|
|
|
|
|
assert len(calls) == 1, "upgrade() must be called once for 'update' subcommand"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Smart version-check tests (_get_install_info / _check_for_update)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_install_info_returns_dict():
|
|
|
|
|
"""_get_install_info must return a dict with all required keys."""
|
|
|
|
|
from muxplex.cli import _get_install_info
|
|
|
|
@@ -507,7 +425,6 @@ def test_get_install_info_returns_dict():
|
|
|
|
|
assert "url" in info
|
|
|
|
|
assert info["source"] in ("git", "editable", "pypi", "unknown")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_check_for_update_editable_returns_false():
|
|
|
|
|
"""Editable installs must never suggest an update."""
|
|
|
|
|
from muxplex.cli import _check_for_update
|
|
|
|
@@ -517,7 +434,6 @@ def test_check_for_update_editable_returns_false():
|
|
|
|
|
assert available is False
|
|
|
|
|
assert "editable" in msg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upgrade_force_skips_version_check(monkeypatch, capsys):
|
|
|
|
|
"""upgrade(force=True) must skip the version check and proceed to install."""
|
|
|
|
|
import subprocess
|
|
|
|
@@ -550,7 +466,6 @@ def test_upgrade_force_skips_version_check(monkeypatch, capsys):
|
|
|
|
|
uv_calls = [c for c in calls if isinstance(c, list) and "uv" in str(c)]
|
|
|
|
|
assert len(uv_calls) > 0, "upgrade(force=True) must still call uv tool install"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upgrade_already_up_to_date_skips_install(monkeypatch, capsys):
|
|
|
|
|
"""upgrade() must print 'up to date' and NOT call uv when version check says current."""
|
|
|
|
|
import subprocess
|
|
|
|
@@ -581,7 +496,6 @@ def test_upgrade_already_up_to_date_skips_install(monkeypatch, capsys):
|
|
|
|
|
uv_calls = [c for c in calls if isinstance(c, list) and "uv" in str(c)]
|
|
|
|
|
assert len(uv_calls) == 0, "uv must NOT be called when already up to date"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upgrade_force_flag_registered():
|
|
|
|
|
"""upgrade --force must be accepted by argparse without error."""
|
|
|
|
|
import io
|
|
|
|
@@ -599,12 +513,10 @@ def test_upgrade_force_flag_registered():
|
|
|
|
|
help_text = buf.getvalue()
|
|
|
|
|
assert "--force" in help_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# serve() settings.json integration tests
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_serve_reads_host_from_settings(tmp_path, monkeypatch):
|
|
|
|
|
"""serve(host=None) must use host from settings.json."""
|
|
|
|
|
settings_file = tmp_path / "settings.json"
|
|
|
|
@@ -626,7 +538,6 @@ def test_serve_reads_host_from_settings(tmp_path, monkeypatch):
|
|
|
|
|
assert len(calls) == 1
|
|
|
|
|
assert calls[0]["host"] == "192.168.0.1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_serve_cli_flag_overrides_settings(tmp_path, monkeypatch):
|
|
|
|
|
"""serve(host='10.0.0.1') must override settings.json host."""
|
|
|
|
|
settings_file = tmp_path / "settings.json"
|
|
|
|
@@ -648,7 +559,6 @@ def test_serve_cli_flag_overrides_settings(tmp_path, monkeypatch):
|
|
|
|
|
assert len(calls) == 1
|
|
|
|
|
assert calls[0]["host"] == "10.0.0.1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_serve_falls_back_to_default_when_no_settings_file(tmp_path, monkeypatch):
|
|
|
|
|
"""serve() with no settings file and no CLI flags uses hardcoded defaults."""
|
|
|
|
|
settings_file = tmp_path / "nonexistent_settings.json"
|
|
|
|
@@ -671,7 +581,6 @@ def test_serve_falls_back_to_default_when_no_settings_file(tmp_path, monkeypatch
|
|
|
|
|
assert calls[0]["host"] == "127.0.0.1"
|
|
|
|
|
assert calls[0]["port"] == 8088
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_serve_port_from_settings(tmp_path, monkeypatch):
|
|
|
|
|
"""serve(port=None) must use port from settings.json."""
|
|
|
|
|
settings_file = tmp_path / "settings.json"
|
|
|
|
@@ -693,7 +602,6 @@ def test_serve_port_from_settings(tmp_path, monkeypatch):
|
|
|
|
|
assert len(calls) == 1
|
|
|
|
|
assert calls[0]["port"] == 9999
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_serve_session_ttl_from_settings(tmp_path, monkeypatch):
|
|
|
|
|
"""serve(session_ttl=None) must use session_ttl from settings.json."""
|
|
|
|
|
settings_file = tmp_path / "settings.json"
|
|
|
|
@@ -710,7 +618,6 @@ def test_serve_session_ttl_from_settings(tmp_path, monkeypatch):
|
|
|
|
|
|
|
|
|
|
assert os.environ.get("MUXPLEX_SESSION_TTL") == "3600"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_serve_session_ttl_zero_is_valid(tmp_path, monkeypatch):
|
|
|
|
|
"""serve(session_ttl=0) must work — 0 means browser session, a valid value."""
|
|
|
|
|
settings_file = tmp_path / "settings.json"
|
|
|
|
@@ -727,13 +634,11 @@ def test_serve_session_ttl_zero_is_valid(tmp_path, monkeypatch):
|
|
|
|
|
|
|
|
|
|
assert os.environ.get("MUXPLEX_SESSION_TTL") == "0"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# argparse refactoring tests — None defaults, serve flags on both parsers,
|
|
|
|
|
# upgrade alias, install-service deprecation warning
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_passes_none_for_unset_flags():
|
|
|
|
|
"""main() with no flags passes None for host/port/auth/session_ttl to serve()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -745,7 +650,6 @@ def test_main_passes_none_for_unset_flags():
|
|
|
|
|
host=None, port=None, auth=None, session_ttl=None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_passes_explicit_host_only():
|
|
|
|
|
"""main() with --host 10.0.0.1 passes host='10.0.0.1', others as None."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -757,7 +661,6 @@ def test_main_passes_explicit_host_only():
|
|
|
|
|
host="10.0.0.1", port=None, auth=None, session_ttl=None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_main_serve_subcommand_accepts_flags():
|
|
|
|
|
"""'muxplex serve --host 10.0.0.1 --port 9000' passes values to serve()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -771,20 +674,6 @@ def test_main_serve_subcommand_accepts_flags():
|
|
|
|
|
host="10.0.0.1", port=9000, auth=None, session_ttl=None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_install_service_subcommand_prints_deprecation_warning(capsys):
|
|
|
|
|
"""'muxplex install-service' must print a deprecation warning to stderr."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
|
|
|
|
|
|
with patch("muxplex.service.service_install"):
|
|
|
|
|
with patch("sys.argv", ["muxplex", "install-service"]):
|
|
|
|
|
main()
|
|
|
|
|
|
|
|
|
|
captured = capsys.readouterr()
|
|
|
|
|
assert "deprecated" in captured.err.lower()
|
|
|
|
|
assert "muxplex service install" in captured.err
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_help_shows_single_upgrade_line():
|
|
|
|
|
"""Help output shows 'upgrade (update)' alias notation, not two separate subcommand entries."""
|
|
|
|
|
import io
|
|
|
|
@@ -807,7 +696,6 @@ def test_help_shows_single_upgrade_line():
|
|
|
|
|
f"Got help text:\n{help_text}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_doctor_shows_serve_config(tmp_path, monkeypatch, capsys):
|
|
|
|
|
"""doctor() must show the current serve config (host, port, auth)."""
|
|
|
|
|
import json
|
|
|
|
@@ -829,12 +717,10 @@ def test_doctor_shows_serve_config(tmp_path, monkeypatch, capsys):
|
|
|
|
|
assert "9999" in out
|
|
|
|
|
assert "password" in out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# service subcommand dispatch tests
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_service_install_dispatches():
|
|
|
|
|
"""muxplex service install must call service_install()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -844,7 +730,6 @@ def test_service_install_dispatches():
|
|
|
|
|
main()
|
|
|
|
|
mock_fn.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_service_uninstall_dispatches():
|
|
|
|
|
"""muxplex service uninstall must call service_uninstall()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -854,7 +739,6 @@ def test_service_uninstall_dispatches():
|
|
|
|
|
main()
|
|
|
|
|
mock_fn.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_service_start_dispatches():
|
|
|
|
|
"""muxplex service start must call service_start()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -864,7 +748,6 @@ def test_service_start_dispatches():
|
|
|
|
|
main()
|
|
|
|
|
mock_fn.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_service_stop_dispatches():
|
|
|
|
|
"""muxplex service stop must call service_stop()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -874,7 +757,6 @@ def test_service_stop_dispatches():
|
|
|
|
|
main()
|
|
|
|
|
mock_fn.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_service_restart_dispatches():
|
|
|
|
|
"""muxplex service restart must call service_restart()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -884,7 +766,6 @@ def test_service_restart_dispatches():
|
|
|
|
|
main()
|
|
|
|
|
mock_fn.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_service_status_dispatches():
|
|
|
|
|
"""muxplex service status must call service_status()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -894,7 +775,6 @@ def test_service_status_dispatches():
|
|
|
|
|
main()
|
|
|
|
|
mock_fn.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_service_logs_dispatches():
|
|
|
|
|
"""muxplex service logs must call service_logs()."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
@@ -904,20 +784,6 @@ def test_service_logs_dispatches():
|
|
|
|
|
main()
|
|
|
|
|
mock_fn.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_install_service_deprecated_calls_service_install(capsys):
|
|
|
|
|
"""'muxplex install-service' deprecated alias must call service_install() and print deprecation warning."""
|
|
|
|
|
from muxplex.cli import main
|
|
|
|
|
|
|
|
|
|
with patch("muxplex.service.service_install") as mock_service_install:
|
|
|
|
|
with patch("sys.argv", ["muxplex", "install-service"]):
|
|
|
|
|
main()
|
|
|
|
|
|
|
|
|
|
mock_service_install.assert_called_once()
|
|
|
|
|
captured = capsys.readouterr()
|
|
|
|
|
assert "deprecated" in captured.err.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_service_subcommand_in_help():
|
|
|
|
|
"""'service' must appear in muxplex --help output."""
|
|
|
|
|
import io
|
|
|
|
@@ -935,21 +801,10 @@ def test_service_subcommand_in_help():
|
|
|
|
|
help_text = buf.getvalue().lower()
|
|
|
|
|
assert "service" in help_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# task-6: Verify old install_service/launchd/systemd removed from cli.py
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_old_install_service_removed_from_cli():
|
|
|
|
|
"""install_service must no longer exist in muxplex.cli (moved to muxplex.service)."""
|
|
|
|
|
import muxplex.cli as cli_mod
|
|
|
|
|
|
|
|
|
|
assert not hasattr(cli_mod, "install_service"), (
|
|
|
|
|
"install_service should be removed from cli.py; use muxplex.service.service_install"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_old_install_launchd_removed_from_cli():
|
|
|
|
|
"""_install_launchd must no longer exist in muxplex.cli (moved to muxplex.service)."""
|
|
|
|
|
import muxplex.cli as cli_mod
|
|
|
|
@@ -958,7 +813,6 @@ def test_old_install_launchd_removed_from_cli():
|
|
|
|
|
"_install_launchd should be removed from cli.py; functionality is in muxplex.service"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_old_install_systemd_removed_from_cli():
|
|
|
|
|
"""_install_systemd must no longer exist in muxplex.cli (moved to muxplex.service)."""
|
|
|
|
|
import muxplex.cli as cli_mod
|
|
|
|
@@ -967,7 +821,6 @@ def test_old_install_systemd_removed_from_cli():
|
|
|
|
|
"_install_systemd should be removed from cli.py; functionality is in muxplex.service"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upgrade_uses_service_module_install(monkeypatch, capsys):
|
|
|
|
|
"""upgrade() must call muxplex.service.service_install instead of cli.install_service."""
|
|
|
|
|
import subprocess
|
|
|
|
|