fix: mock _check_dependencies in CLI tests for CI (ttyd not in Ubuntu repos)
11 tests calling main() failed in GitHub Actions because ttyd is not available in standard Ubuntu packages. _check_dependencies() calls sys.exit(1) when ttyd is missing. Fixed by adding a mock_check_deps pytest fixture and applying it to all 11 affected tests. Affected tests: - test_main_calls_serve_by_default - test_main_passes_custom_host_and_port - test_main_default_host_is_localhost - test_main_passes_auth_flag - test_main_passes_session_ttl_flag - test_main_passes_none_for_unset_flags - test_main_passes_explicit_host_only - test_main_serve_subcommand_accepts_flags - test_main_passes_tls_cert_and_key_flags - test_main_passes_none_for_unset_tls_flags - test_serve_subcommand_accepts_tls_flags
This commit is contained in:
+73
-20
@@ -7,13 +7,26 @@ import stat
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_check_deps(monkeypatch):
|
||||||
|
"""No-op _check_dependencies so tests that call main() for serve work without ttyd installed.
|
||||||
|
|
||||||
|
ttyd is not available in standard Ubuntu repos (used by GitHub Actions CI runners).
|
||||||
|
Tests that exercise the serve path of main() should use this fixture to avoid
|
||||||
|
SystemExit(1) when ttyd is absent from the test environment.
|
||||||
|
"""
|
||||||
|
monkeypatch.setattr("muxplex.cli._check_dependencies", lambda: None)
|
||||||
|
|
||||||
|
|
||||||
def test_cli_module_importable():
|
def test_cli_module_importable():
|
||||||
"""muxplex.cli must be importable."""
|
"""muxplex.cli must be importable."""
|
||||||
from muxplex.cli import main # noqa: F401
|
from muxplex.cli import main # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
def test_main_calls_serve_by_default():
|
def test_main_calls_serve_by_default(mock_check_deps):
|
||||||
"""Calling main() with no args must invoke serve() with None defaults (settings layer resolves)."""
|
"""Calling main() with no args must invoke serve() with None defaults (settings layer resolves)."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
@@ -30,7 +43,7 @@ def test_main_calls_serve_by_default():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_main_passes_custom_host_and_port():
|
def test_main_passes_custom_host_and_port(mock_check_deps):
|
||||||
"""main() with --host/--port must forward them to serve(); unset flags are None."""
|
"""main() with --host/--port must forward them to serve(); unset flags are None."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
@@ -47,7 +60,7 @@ def test_main_passes_custom_host_and_port():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_main_default_host_is_localhost():
|
def test_main_default_host_is_localhost(mock_check_deps):
|
||||||
"""Default --host must be None (settings layer resolves to 127.0.0.1)."""
|
"""Default --host must be None (settings layer resolves to 127.0.0.1)."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
@@ -58,7 +71,7 @@ def test_main_default_host_is_localhost():
|
|||||||
assert kwargs["host"] is None
|
assert kwargs["host"] is None
|
||||||
|
|
||||||
|
|
||||||
def test_main_passes_auth_flag():
|
def test_main_passes_auth_flag(mock_check_deps):
|
||||||
"""main() with --auth password must forward auth='password'; unset flags are None."""
|
"""main() with --auth password must forward auth='password'; unset flags are None."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
@@ -75,7 +88,7 @@ def test_main_passes_auth_flag():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_main_passes_session_ttl_flag():
|
def test_main_passes_session_ttl_flag(mock_check_deps):
|
||||||
"""main() with --session-ttl 3600 must forward session_ttl=3600; unset flags are None."""
|
"""main() with --session-ttl 3600 must forward session_ttl=3600; unset flags are None."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
@@ -704,7 +717,7 @@ def test_serve_session_ttl_zero_is_valid(tmp_path, monkeypatch):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_main_passes_none_for_unset_flags():
|
def test_main_passes_none_for_unset_flags(mock_check_deps):
|
||||||
"""main() with no flags passes None for host/port/auth/session_ttl/tls_cert/tls_key to serve()."""
|
"""main() with no flags passes None for host/port/auth/session_ttl/tls_cert/tls_key to serve()."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
@@ -721,7 +734,7 @@ def test_main_passes_none_for_unset_flags():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_main_passes_explicit_host_only():
|
def test_main_passes_explicit_host_only(mock_check_deps):
|
||||||
"""main() with --host 10.0.0.1 passes host='10.0.0.1', others as None."""
|
"""main() with --host 10.0.0.1 passes host='10.0.0.1', others as None."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
@@ -738,7 +751,7 @@ def test_main_passes_explicit_host_only():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_main_serve_subcommand_accepts_flags():
|
def test_main_serve_subcommand_accepts_flags(mock_check_deps):
|
||||||
"""'muxplex serve --host 10.0.0.1 --port 9000' passes values to serve()."""
|
"""'muxplex serve --host 10.0.0.1 --port 9000' passes values to serve()."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
@@ -1414,7 +1427,7 @@ def test_serve_prints_http_url_when_no_tls(tmp_path, monkeypatch, capsys):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_main_passes_tls_cert_and_key_flags():
|
def test_main_passes_tls_cert_and_key_flags(mock_check_deps):
|
||||||
"""main() with --tls-cert and --tls-key must forward exact paths to serve()."""
|
"""main() with --tls-cert and --tls-key must forward exact paths to serve()."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
@@ -1434,7 +1447,7 @@ def test_main_passes_tls_cert_and_key_flags():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_main_passes_none_for_unset_tls_flags():
|
def test_main_passes_none_for_unset_tls_flags(mock_check_deps):
|
||||||
"""main() with no TLS flags must call serve() with tls_cert=None and tls_key=None."""
|
"""main() with no TLS flags must call serve() with tls_cert=None and tls_key=None."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
@@ -1536,7 +1549,7 @@ def test_setup_tls_selfsigned_creates_certs(tmp_path, monkeypatch, capsys):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_serve_subcommand_accepts_tls_flags():
|
def test_serve_subcommand_accepts_tls_flags(mock_check_deps):
|
||||||
"""'muxplex serve --tls-cert ... --tls-key ...' must forward both paths to serve()."""
|
"""'muxplex serve --tls-cert ... --tls-key ...' must forward both paths to serve()."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
@@ -2015,14 +2028,18 @@ def test_setup_tls_regenerates_on_eof(tmp_path, monkeypatch, capsys):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_doctor_tls_nudge_shows_run_command_on_network_host(capsys, tmp_path, monkeypatch):
|
def test_doctor_tls_nudge_shows_run_command_on_network_host(
|
||||||
|
capsys, tmp_path, monkeypatch
|
||||||
|
):
|
||||||
"""doctor must show 'Run: muxplex setup-tls' when host is network and TLS disabled."""
|
"""doctor must show 'Run: muxplex setup-tls' when host is network and TLS disabled."""
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import muxplex.settings as settings_mod
|
import muxplex.settings as settings_mod
|
||||||
|
|
||||||
settings_file = tmp_path / "settings.json"
|
settings_file = tmp_path / "settings.json"
|
||||||
settings_file.write_text(json.dumps({"host": "0.0.0.0", "tls_cert": "", "tls_key": ""}))
|
settings_file.write_text(
|
||||||
|
json.dumps({"host": "0.0.0.0", "tls_cert": "", "tls_key": ""})
|
||||||
|
)
|
||||||
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file)
|
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file)
|
||||||
|
|
||||||
from muxplex.cli import doctor
|
from muxplex.cli import doctor
|
||||||
@@ -2042,7 +2059,9 @@ def test_doctor_tls_nudge_hidden_on_localhost(capsys, tmp_path, monkeypatch):
|
|||||||
import muxplex.settings as settings_mod
|
import muxplex.settings as settings_mod
|
||||||
|
|
||||||
settings_file = tmp_path / "settings.json"
|
settings_file = tmp_path / "settings.json"
|
||||||
settings_file.write_text(json.dumps({"host": "127.0.0.1", "tls_cert": "", "tls_key": ""}))
|
settings_file.write_text(
|
||||||
|
json.dumps({"host": "127.0.0.1", "tls_cert": "", "tls_key": ""})
|
||||||
|
)
|
||||||
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file)
|
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file)
|
||||||
|
|
||||||
from muxplex.cli import doctor
|
from muxplex.cli import doctor
|
||||||
@@ -2063,6 +2082,7 @@ def test_doctor_tls_nudge_hidden_on_localhost(capsys, tmp_path, monkeypatch):
|
|||||||
def test_pyproject_has_authors():
|
def test_pyproject_has_authors():
|
||||||
"""pyproject.toml must declare at least one author with name and email."""
|
"""pyproject.toml must declare at least one author with name and email."""
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
||||||
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
||||||
data = tomllib.loads(pyproject.read_text())
|
data = tomllib.loads(pyproject.read_text())
|
||||||
authors = data["project"].get("authors", [])
|
authors = data["project"].get("authors", [])
|
||||||
@@ -2074,6 +2094,7 @@ def test_pyproject_has_authors():
|
|||||||
def test_pyproject_has_classifiers():
|
def test_pyproject_has_classifiers():
|
||||||
"""pyproject.toml must declare at least 3 classifiers including License and Python version."""
|
"""pyproject.toml must declare at least 3 classifiers including License and Python version."""
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
||||||
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
||||||
data = tomllib.loads(pyproject.read_text())
|
data = tomllib.loads(pyproject.read_text())
|
||||||
classifiers = data["project"].get("classifiers", [])
|
classifiers = data["project"].get("classifiers", [])
|
||||||
@@ -2086,6 +2107,7 @@ def test_pyproject_has_classifiers():
|
|||||||
def test_pyproject_has_keywords():
|
def test_pyproject_has_keywords():
|
||||||
"""pyproject.toml must declare at least 3 keywords for PyPI discoverability."""
|
"""pyproject.toml must declare at least 3 keywords for PyPI discoverability."""
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
||||||
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
||||||
data = tomllib.loads(pyproject.read_text())
|
data = tomllib.loads(pyproject.read_text())
|
||||||
keywords = data["project"].get("keywords", [])
|
keywords = data["project"].get("keywords", [])
|
||||||
@@ -2101,18 +2123,31 @@ def test_upgrade_pypi_install_uses_package_name(monkeypatch, capsys):
|
|||||||
"""upgrade() for PyPI installs must use 'muxplex' not git+https URL."""
|
"""upgrade() for PyPI installs must use 'muxplex' not git+https URL."""
|
||||||
import subprocess
|
import subprocess
|
||||||
import muxplex.cli as cli_mod
|
import muxplex.cli as cli_mod
|
||||||
|
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
def mock_run(cmd, **kwargs):
|
def mock_run(cmd, **kwargs):
|
||||||
calls.append(cmd)
|
calls.append(cmd)
|
||||||
return type("R", (), {"returncode": 0, "stdout": "", "stderr": ""})()
|
return type("R", (), {"returncode": 0, "stdout": "", "stderr": ""})()
|
||||||
|
|
||||||
monkeypatch.setattr(subprocess, "run", mock_run)
|
monkeypatch.setattr(subprocess, "run", mock_run)
|
||||||
monkeypatch.setattr(shutil, "which", lambda name: f"/usr/bin/{name}")
|
monkeypatch.setattr(shutil, "which", lambda name: f"/usr/bin/{name}")
|
||||||
monkeypatch.setattr(cli_mod, "doctor", lambda: None)
|
monkeypatch.setattr(cli_mod, "doctor", lambda: None)
|
||||||
monkeypatch.setattr(cli_mod, "_get_install_info", lambda: {"source": "pypi", "version": "0.1.0", "commit": None, "url": None})
|
monkeypatch.setattr(
|
||||||
monkeypatch.setattr(cli_mod, "_check_for_update", lambda info: (True, "update available (v0.1.0 → v0.2.0)"))
|
cli_mod,
|
||||||
|
"_get_install_info",
|
||||||
|
lambda: {"source": "pypi", "version": "0.1.0", "commit": None, "url": None},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
cli_mod,
|
||||||
|
"_check_for_update",
|
||||||
|
lambda info: (True, "update available (v0.1.0 → v0.2.0)"),
|
||||||
|
)
|
||||||
with patch("muxplex.service.service_install", lambda: None):
|
with patch("muxplex.service.service_install", lambda: None):
|
||||||
cli_mod.upgrade()
|
cli_mod.upgrade()
|
||||||
uv_calls = [c for c in calls if isinstance(c, list) and "tool" in c and "install" in c]
|
uv_calls = [
|
||||||
|
c for c in calls if isinstance(c, list) and "tool" in c and "install" in c
|
||||||
|
]
|
||||||
assert len(uv_calls) > 0
|
assert len(uv_calls) > 0
|
||||||
install_cmd = uv_calls[0]
|
install_cmd = uv_calls[0]
|
||||||
assert "muxplex" in install_cmd
|
assert "muxplex" in install_cmd
|
||||||
@@ -2123,18 +2158,36 @@ def test_upgrade_git_install_uses_git_url(monkeypatch, capsys):
|
|||||||
"""upgrade() for git installs must still use git+https URL."""
|
"""upgrade() for git installs must still use git+https URL."""
|
||||||
import subprocess
|
import subprocess
|
||||||
import muxplex.cli as cli_mod
|
import muxplex.cli as cli_mod
|
||||||
|
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
def mock_run(cmd, **kwargs):
|
def mock_run(cmd, **kwargs):
|
||||||
calls.append(cmd)
|
calls.append(cmd)
|
||||||
return type("R", (), {"returncode": 0, "stdout": "", "stderr": ""})()
|
return type("R", (), {"returncode": 0, "stdout": "", "stderr": ""})()
|
||||||
|
|
||||||
monkeypatch.setattr(subprocess, "run", mock_run)
|
monkeypatch.setattr(subprocess, "run", mock_run)
|
||||||
monkeypatch.setattr(shutil, "which", lambda name: f"/usr/bin/{name}")
|
monkeypatch.setattr(shutil, "which", lambda name: f"/usr/bin/{name}")
|
||||||
monkeypatch.setattr(cli_mod, "doctor", lambda: None)
|
monkeypatch.setattr(cli_mod, "doctor", lambda: None)
|
||||||
monkeypatch.setattr(cli_mod, "_get_install_info", lambda: {"source": "git", "version": "0.1.0", "commit": "abc12345", "url": "https://github.com/bkrabach/muxplex"})
|
monkeypatch.setattr(
|
||||||
monkeypatch.setattr(cli_mod, "_check_for_update", lambda info: (True, "update available (abc12345 → def67890)"))
|
cli_mod,
|
||||||
|
"_get_install_info",
|
||||||
|
lambda: {
|
||||||
|
"source": "git",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"commit": "abc12345",
|
||||||
|
"url": "https://github.com/bkrabach/muxplex",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
cli_mod,
|
||||||
|
"_check_for_update",
|
||||||
|
lambda info: (True, "update available (abc12345 → def67890)"),
|
||||||
|
)
|
||||||
with patch("muxplex.service.service_install", lambda: None):
|
with patch("muxplex.service.service_install", lambda: None):
|
||||||
cli_mod.upgrade()
|
cli_mod.upgrade()
|
||||||
uv_calls = [c for c in calls if isinstance(c, list) and "tool" in c and "install" in c]
|
uv_calls = [
|
||||||
|
c for c in calls if isinstance(c, list) and "tool" in c and "install" in c
|
||||||
|
]
|
||||||
assert len(uv_calls) > 0
|
assert len(uv_calls) > 0
|
||||||
install_cmd = uv_calls[0]
|
install_cmd = uv_calls[0]
|
||||||
assert any("git+" in str(arg) for arg in install_cmd)
|
assert any("git+" in str(arg) for arg in install_cmd)
|
||||||
|
|||||||
Reference in New Issue
Block a user