Files
k7/tests/unit/test_cli_api_lifecycle.py
T
G d65ea43536 Release 0.2.0
See CHANGELOG.md for what shipped.
2026-08-12 00:01:43 +02:00

248 lines
10 KiB
Python

"""Unit tests for the Spec 10d ``k7 api ...`` sub-app + deprecation aliases.
Coverage:
- ``k7 api status / endpoint / enable / disable`` shell out to the right
kubectl commands and surface the standard error paths.
- ``k7 dev api rebuild`` is registered (hidden) and routes through the
rebuild path.
- The deprecated top-level commands (``start-api`` / ``stop-api`` /
``api-status`` / ``get-api-endpoint``) emit a stderr warning and
forward to the new home.
- ``k7 install --no-api`` passes ``k7_api_enabled=false`` into the
Ansible extra_vars (which is what the playbook actually keys off).
"""
from __future__ import annotations
import subprocess
from types import SimpleNamespace
from unittest.mock import patch
from typer.testing import CliRunner
from k7.cli.k7 import app
runner = CliRunner()
def _make_completed(stdout: str = "", stderr: str = "", returncode: int = 0) -> subprocess.CompletedProcess:
return subprocess.CompletedProcess(args=[], returncode=returncode, stdout=stdout, stderr=stderr)
# ---------------------------------------------------------------------------
# ``k7 api enable / disable``.
# ---------------------------------------------------------------------------
class TestApiEnableDisable:
def test_enable_calls_kubectl_scale_1(self):
with patch("k7.cli.k7.subprocess.run", return_value=_make_completed()) as run:
result = runner.invoke(app, ["api", "enable"])
assert result.exit_code == 0, result.output
# Find the scale call.
scale_calls = [c for c in run.call_args_list if "scale" in (c.args[0] if c.args else [])]
assert scale_calls, "expected at least one kubectl scale call"
cmd = scale_calls[0].args[0]
assert "deployment" in cmd
assert "k7-api" in cmd
assert "--replicas=1" in cmd
assert "enabled" in result.output.lower()
def test_disable_calls_kubectl_scale_0(self):
with patch("k7.cli.k7.subprocess.run", return_value=_make_completed()) as run:
result = runner.invoke(app, ["api", "disable"])
assert result.exit_code == 0, result.output
scale_calls = [c for c in run.call_args_list if "scale" in (c.args[0] if c.args else [])]
assert scale_calls
cmd = scale_calls[0].args[0]
assert "--replicas=0" in cmd
assert "disabled" in result.output.lower()
def test_enable_exits_nonzero_on_kubectl_failure(self):
with patch(
"k7.cli.k7.subprocess.run",
return_value=_make_completed(stderr="not found", returncode=1),
):
result = runner.invoke(app, ["api", "enable"])
assert result.exit_code != 0
assert "failed" in result.output.lower()
# ---------------------------------------------------------------------------
# ``k7 api status``.
# ---------------------------------------------------------------------------
class TestApiStatus:
def test_reports_running_when_replicas_ready(self):
# subprocess.run is called three times by api_status_cmd:
# 1) jsonpath get deployment → "1/1"
# 2) get pods -l app=k7-api → human-readable line
# 3) _read_api_endpoint -> get svc / get nodes (two calls inside)
calls = [
_make_completed(stdout="1/1"), # deployment readyReplicas/replicas
_make_completed(stdout="k7-api-abc 1/1 Running 0 1m k7-node-01"), # pods
_make_completed(stdout="31007"), # service nodePort
_make_completed(stdout='[{"type":"InternalIP","address":"10.0.0.1"}]'),
]
with patch("k7.cli.k7.subprocess.run", side_effect=calls):
result = runner.invoke(app, ["api", "status"])
assert result.exit_code == 0, result.output
assert "running" in result.output.lower()
assert "1/1" in result.output
def test_reports_disabled_when_replicas_zero(self):
calls = [_make_completed(stdout="0/0")]
# status_cmd may make more subprocess calls (pods/endpoint); pad with no-ops.
calls.extend(_make_completed(stdout="") for _ in range(5))
with patch("k7.cli.k7.subprocess.run", side_effect=calls):
result = runner.invoke(app, ["api", "status"])
assert result.exit_code == 0, result.output
assert "disabled" in result.output.lower() or "scaled to 0" in result.output.lower()
def test_reports_missing_when_deployment_absent(self):
with patch(
"k7.cli.k7.subprocess.run",
return_value=_make_completed(stderr="NotFound", returncode=1),
):
result = runner.invoke(app, ["api", "status"])
# status reports the problem but doesn't exit 1 — diagnostic only.
assert result.exit_code == 0
assert "not found" in result.output.lower()
assert "k7 install" in result.output
# ---------------------------------------------------------------------------
# ``k7 api endpoint``.
# ---------------------------------------------------------------------------
class TestApiEndpoint:
def test_prints_url_when_service_exists(self):
with patch("k7.cli.k7._read_api_endpoint", return_value="http://10.0.0.1:31007"):
result = runner.invoke(app, ["api", "endpoint"])
assert result.exit_code == 0, result.output
assert result.output.strip() == "http://10.0.0.1:31007"
def test_exits_nonzero_when_service_missing(self):
with patch("k7.cli.k7._read_api_endpoint", return_value=None):
result = runner.invoke(app, ["api", "endpoint"])
assert result.exit_code != 0
# ---------------------------------------------------------------------------
# Deprecated command aliases.
# ---------------------------------------------------------------------------
class TestDeprecatedAliases:
def test_api_status_alias_warns_and_forwards(self):
# Calls subprocess.run several times under the hood; first call returns
# something that lets api_status_cmd produce output.
seq = [_make_completed(stdout="1/1")] + [_make_completed(stdout="") for _ in range(5)]
with patch("k7.cli.k7.subprocess.run", side_effect=seq):
result = runner.invoke(app, ["api-status"])
assert result.exit_code == 0
combined = result.output + (result.stderr or "")
assert "deprecated" in combined.lower()
assert "k7 api status" in combined
def test_get_api_endpoint_alias_warns_and_forwards(self):
with patch("k7.cli.k7._read_api_endpoint", return_value="http://x:31007"):
result = runner.invoke(app, ["get-api-endpoint"])
assert result.exit_code == 0
combined = result.output + (result.stderr or "")
assert "deprecated" in combined.lower()
assert "k7 api endpoint" in combined
assert "http://x:31007" in result.output
def test_stop_api_alias_warns_and_forwards_to_disable(self):
with patch("k7.cli.k7.subprocess.run", return_value=_make_completed()) as run:
result = runner.invoke(app, ["stop-api"])
assert result.exit_code == 0
combined = result.output + (result.stderr or "")
assert "deprecated" in combined.lower()
assert "k7 api disable" in combined
# The scale-to-0 call still went through.
scale_calls = [c for c in run.call_args_list if "--replicas=0" in (c.args[0] if c.args else [])]
assert scale_calls
def test_start_api_alias_warns_and_forwards_to_dev_rebuild(self):
# Avoid actually building Docker images: stub the build helper.
with (
patch("k7.cli.k7._build_and_import_api_image", return_value=False),
patch("k7.cli.k7.subprocess.run", return_value=_make_completed()),
):
result = runner.invoke(app, ["start-api"])
combined = result.output + (result.stderr or "")
assert "deprecated" in combined.lower()
assert "k7 dev api rebuild" in combined
# ---------------------------------------------------------------------------
# ``k7 install --no-api`` passes the right extra var.
# ---------------------------------------------------------------------------
class TestInstallNoApi:
def test_install_no_api_passes_k7_api_enabled_false(self):
seen: dict = {}
def _capture(*args, **kwargs):
# ``K7Core.install_node`` is sync (it shells out to ansible-playbook).
seen["kwargs"] = kwargs
return SimpleNamespace(success=True, message="ok", error="")
with (
patch("k7.cli.k7.K7Core") as core_cls,
patch("k7.cli.k7._build_default_inventory", return_value="[k7_servers]\nhost ansible_host=1.2.3.4"),
):
core_cls.return_value.install_node = _capture
result = runner.invoke(app, ["install", "--no-api", "host"])
assert result.exit_code == 0, result.output
extra_vars = seen["kwargs"]["extra_vars"]
assert extra_vars.get("k7_api_enabled") == "false"
def test_install_without_flag_passes_k7_api_enabled_true(self):
seen: dict = {}
def _capture(*args, **kwargs):
seen["kwargs"] = kwargs
return SimpleNamespace(success=True, message="ok", error="")
with (
patch("k7.cli.k7.K7Core") as core_cls,
patch("k7.cli.k7._build_default_inventory", return_value="[k7_servers]\nhost ansible_host=1.2.3.4"),
):
core_cls.return_value.install_node = _capture
result = runner.invoke(app, ["install", "host"])
assert result.exit_code == 0, result.output
extra_vars = seen["kwargs"]["extra_vars"]
assert extra_vars.get("k7_api_enabled") == "true"
# ---------------------------------------------------------------------------
# ``k7 --help`` no longer lists the deprecated commands.
# ---------------------------------------------------------------------------
def test_top_level_help_hides_deprecated_commands():
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
for name in ("start-api", "stop-api", "api-status", "get-api-endpoint"):
assert name not in result.output, f"deprecated command {name!r} still appears in `k7 --help`"
def test_api_subapp_help_lists_new_commands():
result = runner.invoke(app, ["api", "--help"])
assert result.exit_code == 0
for name in ("status", "endpoint", "enable", "disable"):
assert name in result.output, f"missing `{name}` under `k7 api --help`"
def test_dev_api_subapp_help_lists_rebuild():
result = runner.invoke(app, ["dev", "api", "--help"])
assert result.exit_code == 0
assert "rebuild" in result.output