Files
NODEDC_MISSION_CORE/tests/test_cli.py
T

562 lines
18 KiB
Python

from __future__ import annotations
import json
import subprocess
import sys
from http.client import BadStatusLine
from pathlib import Path
from types import SimpleNamespace
from typing import Any
import pytest
from typer.testing import CliRunner
from k1link.device_plugins.xgrids_k1 import cli
from k1link.device_plugins.xgrids_k1.cli import app
runner = CliRunner()
class _FakeServeLease:
def __init__(self) -> None:
self.active = False
def __enter__(self) -> _FakeServeLease:
assert self.active is False
self.active = True
return self
def __exit__(self, *_: object) -> None:
assert self.active is True
self.active = False
def _install_fake_serve_lease(
monkeypatch: Any,
*,
error: BaseException | None = None,
) -> _FakeServeLease:
lease = _FakeServeLease()
def acquire(_: Path) -> _FakeServeLease:
if error is not None:
raise error
return lease
monkeypatch.setattr(cli, "_acquire_mission_core_serve_lease", acquire)
return lease
def test_help() -> None:
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "evidence-led" in result.stdout
def test_doctor_json() -> None:
result = runner.invoke(app, ["doctor", "--json"])
assert result.exit_code == 0
payload = json.loads(result.stdout)
assert payload["k1link_version"] == "0.1.0"
assert isinstance(payload["tools"], list)
assert isinstance(payload["network"], dict)
assert any(item["name"] == "tcpdump" for item in payload["tools"])
def test_serve_resolves_frontend_from_repository_root(monkeypatch: Any) -> None:
captured: dict[str, object] = {}
lease = _install_fake_serve_lease(monkeypatch)
def fake_run(application: str, **kwargs: object) -> None:
assert lease.active is True
captured.update({"application": application, **kwargs})
monkeypatch.setattr(cli.uvicorn, "run", fake_run)
monkeypatch.setattr(cli, "_local_server_status", lambda _: "free")
result = runner.invoke(app, ["serve"])
assert result.exit_code == 0
assert captured == {
"application": "k1link.web.app:app",
"host": "127.0.0.1",
"port": 8000,
"log_level": "info",
"access_log": True,
"timeout_graceful_shutdown": 10,
}
assert lease.active is False
def test_serve_releases_singleton_lease_when_uvicorn_fails(monkeypatch: Any) -> None:
lease = _install_fake_serve_lease(monkeypatch)
monkeypatch.setattr(cli, "_local_server_status", lambda _: "free")
def failed_run(*_: object, **__: object) -> None:
assert lease.active is True
raise RuntimeError("startup failed")
monkeypatch.setattr(cli.uvicorn, "run", failed_run)
result = runner.invoke(app, ["serve"])
assert result.exit_code == 1
assert isinstance(result.exception, RuntimeError)
assert lease.active is False
def test_serve_reuses_an_existing_mission_core_backend(monkeypatch: Any) -> None:
lease = _install_fake_serve_lease(monkeypatch)
monkeypatch.setattr(cli, "_local_server_status", lambda _: "mission-core")
def unexpected_run(*_: object, **__: object) -> None:
raise AssertionError("a second backend must not be started")
monkeypatch.setattr(cli.uvicorn, "run", unexpected_run)
result = runner.invoke(app, ["serve"])
assert result.exit_code == 0
assert "уже запущен" in result.stdout
assert "второй процесс не создан" in result.stdout
assert lease.active is False
def test_serve_rejects_an_unrelated_loopback_listener(monkeypatch: Any) -> None:
lease = _install_fake_serve_lease(monkeypatch)
monkeypatch.setattr(cli, "_local_server_status", lambda _: "occupied")
def unexpected_run(*_: object, **__: object) -> None:
raise AssertionError("an occupied port must fail before Uvicorn startup")
monkeypatch.setattr(cli.uvicorn, "run", unexpected_run)
result = runner.invoke(app, ["serve"])
assert result.exit_code == 2
assert "уже занят" in result.stdout
assert "Mission Core не стал создавать второй backend" in result.stdout
assert "--port" not in result.stdout
assert lease.active is False
def test_serve_rejects_noncanonical_port_before_lock_or_health(monkeypatch: Any) -> None:
def unexpected_call(*_: object, **__: object) -> None:
raise AssertionError("noncanonical port must fail before lock or health inspection")
monkeypatch.setattr(cli, "_acquire_mission_core_serve_lease", unexpected_call)
monkeypatch.setattr(cli, "_local_server_status", unexpected_call)
monkeypatch.setattr(cli.uvicorn, "run", unexpected_call)
result = runner.invoke(app, ["serve", "--port", "8001"])
assert result.exit_code == 2
assert "только на каноническом порту 8000" in result.stdout
assert "Другой локальный backend не создан" in result.stdout
def test_serve_busy_lock_reuses_confirmed_mission_core(monkeypatch: Any) -> None:
_install_fake_serve_lease(
monkeypatch,
error=cli._MissionCoreServeLeaseUnavailable("busy"),
)
monkeypatch.setattr(cli, "_local_server_status", lambda _: "mission-core")
def unexpected_run(*_: object, **__: object) -> None:
raise AssertionError("busy singleton lease must not start Uvicorn")
monkeypatch.setattr(cli.uvicorn, "run", unexpected_run)
result = runner.invoke(app, ["serve"])
assert result.exit_code == 0
assert "уже запущен" in result.stdout
assert "второй процесс не создан" in result.stdout
def test_serve_busy_lock_reports_in_progress_when_health_is_not_listening(
monkeypatch: Any,
) -> None:
_install_fake_serve_lease(
monkeypatch,
error=cli._MissionCoreServeLeaseUnavailable("busy"),
)
monkeypatch.setattr(cli, "_local_server_status", lambda _: "free")
def unexpected_run(*_: object, **__: object) -> None:
raise AssertionError("busy singleton lease must not start Uvicorn")
monkeypatch.setattr(cli.uvicorn, "run", unexpected_run)
result = runner.invoke(app, ["serve"])
assert result.exit_code == 2
assert "уже запускается или завершает работу" in result.stdout
assert "Второй backend не создан" in result.stdout
def test_serve_busy_lock_rejects_unconfirmed_health_listener(monkeypatch: Any) -> None:
_install_fake_serve_lease(
monkeypatch,
error=cli._MissionCoreServeLeaseUnavailable("busy"),
)
monkeypatch.setattr(cli, "_local_server_status", lambda _: "occupied")
def unexpected_run(*_: object, **__: object) -> None:
raise AssertionError("busy singleton lease must not start Uvicorn")
monkeypatch.setattr(cli.uvicorn, "run", unexpected_run)
result = runner.invoke(app, ["serve"])
assert result.exit_code == 2
assert "канонический health endpoint" in result.stdout
assert "подтверждён" in result.stdout
assert "Второй backend не создан" in result.stdout
def test_serve_lock_integrity_error_fails_before_health_or_uvicorn(monkeypatch: Any) -> None:
_install_fake_serve_lease(
monkeypatch,
error=cli._MissionCoreServeLeaseError("unsafe lock"),
)
def unexpected_call(*_: object, **__: object) -> None:
raise AssertionError("an untrusted lock must block health and Uvicorn")
monkeypatch.setattr(cli, "_local_server_status", unexpected_call)
monkeypatch.setattr(cli.uvicorn, "run", unexpected_call)
result = runner.invoke(app, ["serve"])
assert result.exit_code == 2
assert "безопасно получить блокировку" in result.stdout
assert "Второй backend не создан" in result.stdout
def test_serve_lock_is_stable_private_and_cross_process(tmp_path: Path) -> None:
expected_path = tmp_path / ".runtime" / "mission-core" / ".serve.lock"
with cli._MissionCoreServeLease.acquire(tmp_path) as first:
assert first.path == expected_path
first_identity = (expected_path.stat().st_dev, expected_path.stat().st_ino)
assert expected_path.stat().st_mode & 0o777 == 0o600
assert expected_path.parent.stat().st_mode & 0o777 == 0o700
child = subprocess.run(
[
sys.executable,
"-c",
(
"import sys; from pathlib import Path; "
"from k1link.device_plugins.xgrids_k1.cli import "
"_MissionCoreServeLease, _MissionCoreServeLeaseUnavailable; "
"root=Path(sys.argv[1]); "
"\ntry: lease=_MissionCoreServeLease.acquire(root)"
"\nexcept _MissionCoreServeLeaseUnavailable: raise SystemExit(0)"
"\nelse: lease.release(); raise SystemExit(1)"
),
str(tmp_path),
],
check=False,
capture_output=True,
text=True,
timeout=10,
)
assert child.returncode == 0, child.stderr
assert expected_path.is_file()
with cli._MissionCoreServeLease.acquire(tmp_path) as second:
assert (second.path.stat().st_dev, second.path.stat().st_ino) == first_identity
def test_serve_lock_rejects_nonprivate_lock_directory(tmp_path: Path) -> None:
lock_dir = tmp_path / ".runtime" / "mission-core"
lock_dir.mkdir(mode=0o755, parents=True)
lock_dir.chmod(0o755)
with pytest.raises(cli._MissionCoreServeLeaseError, match="not private"):
cli._MissionCoreServeLease.acquire(tmp_path)
def test_serve_lock_rejects_symlinked_lock_directory(tmp_path: Path) -> None:
runtime_dir = tmp_path / ".runtime"
runtime_dir.mkdir()
redirected = tmp_path / "redirected"
redirected.mkdir(mode=0o700)
(runtime_dir / "mission-core").symlink_to(redirected, target_is_directory=True)
with pytest.raises(cli._MissionCoreServeLeaseError, match="not private"):
cli._MissionCoreServeLease.acquire(tmp_path)
class _FakeHealthResponse:
def __init__(self, *, status: int = 200, body: bytes = b"") -> None:
self.status = status
self.body = body
def read(self, limit: int) -> bytes:
assert limit == 16_385
return self.body[:limit]
class _FakeHealthConnection:
def __init__(
self,
*,
response: _FakeHealthResponse | None = None,
error: BaseException | None = None,
) -> None:
self.response = response
self.error = error
self.request_call: tuple[str, str, dict[str, str]] | None = None
self.closed = False
def request(self, method: str, path: str, *, headers: dict[str, str]) -> None:
self.request_call = (method, path, headers)
if self.error is not None:
raise self.error
def getresponse(self) -> _FakeHealthResponse:
assert self.response is not None
return self.response
def close(self) -> None:
self.closed = True
def _install_fake_health_connection(
monkeypatch: Any,
connection: _FakeHealthConnection,
) -> None:
def fake_connection(host: str, port: int, *, timeout: float) -> _FakeHealthConnection:
assert (host, port, timeout) == ("127.0.0.1", 8000, 0.75)
return connection
monkeypatch.setattr(cli.http.client, "HTTPConnection", fake_connection)
def test_local_server_status_recognizes_exact_mission_core_health(monkeypatch: Any) -> None:
connection = _FakeHealthConnection(
response=_FakeHealthResponse(
body=json.dumps({"service": "mission-core-control-plane"}).encode("utf-8")
)
)
_install_fake_health_connection(monkeypatch, connection)
assert cli._local_server_status(8000) == "mission-core"
assert connection.request_call == (
"GET",
"/api/health",
{"Accept": "application/json", "Connection": "close"},
)
assert connection.closed is True
def test_local_server_status_rejects_a_different_health_identity(monkeypatch: Any) -> None:
connection = _FakeHealthConnection(
response=_FakeHealthResponse(body=json.dumps({"service": "other"}).encode("utf-8"))
)
_install_fake_health_connection(monkeypatch, connection)
assert cli._local_server_status(8000) == "occupied"
assert connection.closed is True
def test_local_server_status_only_treats_explicit_refusal_as_free(monkeypatch: Any) -> None:
connection = _FakeHealthConnection(error=ConnectionRefusedError())
_install_fake_health_connection(monkeypatch, connection)
assert cli._local_server_status(8000) == "free"
assert connection.closed is True
def test_local_server_status_fails_closed_on_listener_timeout(monkeypatch: Any) -> None:
connection = _FakeHealthConnection(error=TimeoutError())
_install_fake_health_connection(monkeypatch, connection)
assert cli._local_server_status(8000) == "occupied"
assert connection.closed is True
def test_local_server_status_fails_closed_on_non_http_listener(monkeypatch: Any) -> None:
connection = _FakeHealthConnection(error=BadStatusLine("not HTTP"))
_install_fake_health_connection(monkeypatch, connection)
assert cli._local_server_status(8000) == "occupied"
assert connection.closed is True
def test_local_server_status_rejects_oversized_health_payload(monkeypatch: Any) -> None:
connection = _FakeHealthConnection(
response=_FakeHealthResponse(body=b" " * 16_385)
)
_install_fake_health_connection(monkeypatch, connection)
assert cli._local_server_status(8000) == "occupied"
assert connection.closed is True
def test_authority_provision_requires_explicit_reviewed_value_confirmation() -> None:
result = runner.invoke(app, ["authority", "provision"])
assert result.exit_code == 2
assert "provisioning not confirmed" in result.stdout
def test_authority_provision_never_accepts_the_secret_as_a_cli_value(monkeypatch: Any) -> None:
calls = 0
class FakeSnapshot:
service = "fixed-service"
account = "fixed-account"
class FakeProvisioner:
def provision_interactively(self) -> FakeSnapshot:
nonlocal calls
calls += 1
return FakeSnapshot()
monkeypatch.setattr(
"k1link.device_plugins.xgrids_k1.cli.MacOSKeychainApplicationAuthorityProvisioner",
FakeProvisioner,
)
result = runner.invoke(
app,
["authority", "provision", "--confirm-reviewed-authority"],
)
assert result.exit_code == 0
assert calls == 1
assert "Keychain item validated" in result.stdout
assert "No K1 command was sent" in " ".join(result.stdout.split())
def test_mqtt_capture_requires_owned_device_confirmation(tmp_path: Path) -> None:
result = runner.invoke(
app,
[
"net",
"mqtt-capture",
"--host",
"192.168.1.20",
"--out",
str(tmp_path / "capture"),
],
)
assert result.exit_code == 2
assert "ownership not confirmed" in result.stdout
assert not (tmp_path / "capture").exists()
def test_mqtt_capture_cli_uses_bounded_read_only_capture(
monkeypatch: Any,
tmp_path: Path,
) -> None:
captured: dict[str, object] = {}
def fake_capture(host: str, out: Path, **kwargs: object) -> dict[str, object]:
on_ready = kwargs.pop("on_ready")
assert callable(on_ready)
on_ready()
captured.update({"host": host, "out": out, **kwargs})
return {
"stop_reason": "duration_elapsed",
"message_count": 2,
"payload_bytes": 128,
}
monkeypatch.setattr("k1link.device_plugins.xgrids_k1.cli.capture_mqtt", fake_capture)
out = tmp_path / "capture"
result = runner.invoke(
app,
[
"net",
"mqtt-capture",
"--host",
"10.0.0.42",
"--out",
str(out),
"--duration",
"5",
"--max-message-bytes",
"1024",
"--confirm-owned-device",
],
)
assert result.exit_code == 0
assert captured == {
"host": "10.0.0.42",
"out": out,
"port": 1883,
"duration_seconds": 5.0,
"max_message_bytes": 1024,
}
assert "messages: 2" in result.stdout
assert "subscriptions active" in result.stdout
@pytest.mark.parametrize("ledger_status", ["resolved", "corrupt"])
def test_cli_wifi_configure_blocks_retired_or_untrusted_audit_before_credentials_and_ble(
monkeypatch: Any,
tmp_path: Path,
ledger_status: str,
) -> None:
repository_root = tmp_path / "repository"
repository_root.mkdir()
monkeypatch.setenv("MISSIONCORE_DATA_DIR", str(tmp_path / "private-data"))
monkeypatch.setattr(cli, "_configure_ble_process_lease", lambda: repository_root)
retired_transport_ref = "F89438FA-55ED-85AD-EED7-734AC84746D8"
record = (
SimpleNamespace(
operator_retirements=(
SimpleNamespace(
retirement_id="retirement-cli-blocked-target",
retired_transport_ref=retired_transport_ref,
),
),
operator_reconciliation_reopens=(),
)
if ledger_status == "resolved"
else None
)
monkeypatch.setattr(
cli,
"PhysicalCommandLedger",
lambda _root: SimpleNamespace(
snapshot=lambda: SimpleNamespace(status=ledger_status, record=record)
),
)
calls: list[str] = []
monkeypatch.setattr(
cli,
"prompt_wifi_credentials",
lambda: calls.append("credentials") or ("ssid", "secret"),
)
async def forbidden_provision(*_args: object, **_kwargs: object) -> dict[str, object]:
calls.append("ble-write")
raise AssertionError("retired CLI target must not reach BLE")
monkeypatch.setattr(cli, "provision_wifi_once", forbidden_provision)
result = runner.invoke(
app,
[
"ble",
"wifi-configure",
"--device",
retired_transport_ref.lower(),
"--out",
str(tmp_path / "wifi-result.json"),
"--profile",
cli.PROFILE_ID,
"--confirm-write",
],
)
assert result.exit_code == 2
assert "blocked before credential or device access" in result.stdout
assert calls == []
assert not (tmp_path / "wifi-result.json").exists()