"""Tests for ``muse auth show``. Covers: - ``muse auth show``: display identity detail including HD paths + AVAX address. Test categories --------------- - unit : _show_identity_detail helper with bare and HD entries - integration : CLI round-trips for show (via CliRunner + monkeypatch) - data-integrity : show reads HD fields correctly - performance : show completes under 5 s - security : show never reveals mnemonic - docstrings : public API has docstrings """ from __future__ import annotations import json import pathlib import time from unittest.mock import MagicMock import pytest from tests.cli_test_helper import CliRunner import muse.core.keypair as kp_mod runner = CliRunner() HUB = "https://localhost:1337" HOSTNAME = "localhost:1337" FAKE_MNEMONIC = ( "abandon abandon abandon abandon abandon abandon " "abandon abandon abandon abandon abandon about" ) FAKE_HD_PATH = "m/1075233755'/0'/0'/0'/0'/0'" FAKE_FINGERPRINT = "a" * 64 FAKE_HANDLE = "gabriel" # --------------------------------------------------------------------------- # Shared fixtures # --------------------------------------------------------------------------- def _patch_home(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path: """Redirect identity.toml and key paths to a temp directory.""" fake_home = tmp_path / "home" fake_muse = fake_home / ".muse" fake_keys = fake_muse / "keys" fake_keys.mkdir(parents=True) identity_file = fake_muse / "identity.toml" import muse.core.identity as id_mod monkeypatch.setattr(id_mod, "_IDENTITY_FILE", identity_file) monkeypatch.setattr(id_mod, "_IDENTITY_DIR", fake_muse) monkeypatch.setattr(kp_mod, "_KEYS_DIR", fake_keys) return fake_home def _write_bare_identity(fake_home: pathlib.Path, handle: str = FAKE_HANDLE) -> pathlib.Path: """Write a minimal identity entry (no HD fields) to identity.toml.""" import muse.core.identity as id_mod entry = { "type": "human", "handle": handle, "key_path": str(kp_mod._KEYS_DIR / f"{HOSTNAME}.pem"), "algorithm": "ed25519", "fingerprint": FAKE_FINGERPRINT, } from muse.core.identity import save_identity save_identity(f"http://{HOSTNAME}", entry) return id_mod._IDENTITY_FILE def _write_hd_identity( fake_home: pathlib.Path, monkeypatch: pytest.MonkeyPatch, handle: str = FAKE_HANDLE, ) -> pathlib.Path: """Write an HD identity entry with mnemonic stored in an in-memory keychain.""" _kc: dict[str, str] = {} monkeypatch.setattr("muse.core.keychain.is_available", lambda: True) monkeypatch.setattr("muse.core.keychain.store", lambda m: _kc.__setitem__("mnemonic", m)) monkeypatch.setattr("muse.core.keychain.load", lambda: _kc.get("mnemonic")) import muse.core.identity as id_mod entry = { "type": "human", "handle": handle, "key_path": str(kp_mod._KEYS_DIR / f"{HOSTNAME}.pem"), "algorithm": "ed25519", "fingerprint": FAKE_FINGERPRINT, "hd_path": FAKE_HD_PATH, } from muse.core.identity import save_identity save_identity(f"https://{HOSTNAME}", entry, mnemonic=FAKE_MNEMONIC) return id_mod._IDENTITY_FILE # --------------------------------------------------------------------------- # Unit: _show_identity_detail helper # --------------------------------------------------------------------------- class TestShowIdentityDetail: """Unit tests for the _show_identity_detail helper.""" def test_bare_entry_json(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """Bare entry (no hd_path) must contain required fields and no HD extras.""" _patch_home(monkeypatch, tmp_path) from muse.cli.commands.auth import _show_identity_detail entry = { "type": "human", "handle": "gabriel", "key_path": "/tmp/key.pem", "fingerprint": FAKE_FINGERPRINT, } _show_identity_detail(HOSTNAME, entry, json_output=True) out = json.loads(capsys.readouterr().out) assert out["hub"] == HOSTNAME assert out["handle"] == "gabriel" assert out["type"] == "human" assert out["fingerprint"] == FAKE_FINGERPRINT assert "hd_path" not in out assert "mnemonic_word_count" not in out assert "derived_paths" not in out def test_hd_entry_json_has_paths(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """HD entry JSON must include derived_paths with the four standard paths.""" _patch_home(monkeypatch, tmp_path) from muse.cli.commands.auth import _show_identity_detail entry = { "type": "human", "handle": "gabriel", "key_path": "/tmp/key.pem", "fingerprint": FAKE_FINGERPRINT, "mnemonic": FAKE_MNEMONIC, "hd_path": FAKE_HD_PATH, } _show_identity_detail(HOSTNAME, entry, json_output=True) out = json.loads(capsys.readouterr().out) assert out["hd_path"] == FAKE_HD_PATH assert out["mnemonic_word_count"] == 12 assert "derived_paths" in out dp = out["derived_paths"] assert "identity_msign" in dp assert "payments_mpay" in dp assert "avax_c_chain" in dp assert "agent_slot_0" in dp def test_hd_entry_has_avax_address(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """HD entry must include avax_c_chain_address derived from the mnemonic.""" _patch_home(monkeypatch, tmp_path) from muse.cli.commands.auth import _show_identity_detail entry = { "type": "human", "handle": "gabriel", "key_path": "/tmp/key.pem", "fingerprint": FAKE_FINGERPRINT, "mnemonic": FAKE_MNEMONIC, "hd_path": FAKE_HD_PATH, } _show_identity_detail(HOSTNAME, entry, json_output=True) out = json.loads(capsys.readouterr().out) assert "avax_c_chain_address" in out addr = out["avax_c_chain_address"] assert addr.startswith("0x") assert len(addr) == 42 def test_hd_derived_paths_format(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """All derived paths must start with 'm/' and use the Muse purpose constant.""" _patch_home(monkeypatch, tmp_path) from muse.cli.commands.auth import _show_identity_detail entry = { "type": "human", "handle": "gabriel", "key_path": "/tmp/key.pem", "fingerprint": FAKE_FINGERPRINT, "mnemonic": FAKE_MNEMONIC, "hd_path": FAKE_HD_PATH, } _show_identity_detail(HOSTNAME, entry, json_output=True) out = json.loads(capsys.readouterr().out) dp = out["derived_paths"] for name, path in dp.items(): if name != "avax_c_chain": assert path.startswith("m/1075233755'"), f"{name}: {path}" else: assert path == "m/44'/60'/0'/0/0" def test_bare_entry_stderr_human_readable(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """Bare entry human-readable output must appear on stderr, not stdout.""" _patch_home(monkeypatch, tmp_path) from muse.cli.commands.auth import _show_identity_detail entry = { "type": "human", "handle": "gabriel", "key_path": "/tmp/key.pem", "fingerprint": FAKE_FINGERPRINT, } _show_identity_detail(HOSTNAME, entry, json_output=False) captured = capsys.readouterr() assert captured.out == "" assert "gabriel" in captured.err # --------------------------------------------------------------------------- # Integration: CLI round-trips # --------------------------------------------------------------------------- class TestShowCLI: """Integration: ``muse auth show`` CLI round-trips.""" def test_show_bare_json(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """``muse auth show --hub … --json`` returns correct JSON for a bare identity.""" _patch_home(monkeypatch, tmp_path) _write_bare_identity(tmp_path) monkeypatch.setenv("MUSE_KEYCHAIN_BACKEND", "disabled") monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB) from muse.cli.commands.auth import run_show ns = MagicMock() ns.hub = HUB ns.json_output = True run_show(ns) out = json.loads(capsys.readouterr().out) assert out["handle"] == FAKE_HANDLE assert "key_source" not in out assert "hd_path" not in out def test_show_hd_json(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """``muse auth show --hub … --json`` returns HD fields for an HD identity.""" _patch_home(monkeypatch, tmp_path) _write_hd_identity(tmp_path, monkeypatch) monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB) from muse.cli.commands.auth import run_show ns = MagicMock() ns.hub = HUB ns.json_output = True run_show(ns) out = json.loads(capsys.readouterr().out) assert "key_source" not in out assert out["mnemonic_word_count"] == 12 assert "derived_paths" in out def test_show_no_identity_exits_1(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None: """``run_show`` exits with code 1 when no identity is stored.""" _patch_home(monkeypatch, tmp_path) monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB) from muse.cli.commands.auth import run_show ns = MagicMock() ns.hub = HUB ns.json_output = False with pytest.raises(SystemExit) as exc_info: run_show(ns) assert exc_info.value.code == 1 # --------------------------------------------------------------------------- # Data integrity: show reads correct fields # --------------------------------------------------------------------------- class TestDataIntegrity: """Data-integrity tests for show.""" def test_show_mnemonic_word_count_matches_24_words(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """show must report mnemonic_word_count = 24 for a 24-word mnemonic.""" _patch_home(monkeypatch, tmp_path) long_mnemonic = " ".join(["abandon"] * 23 + ["art"]) _kc: dict[str, str] = {} monkeypatch.setattr("muse.core.keychain.is_available", lambda: True) monkeypatch.setattr("muse.core.keychain.store", lambda m: _kc.__setitem__("mnemonic", m)) monkeypatch.setattr("muse.core.keychain.load", lambda: _kc.get("mnemonic")) from muse.core.identity import save_identity save_identity(f"https://{HOSTNAME}", { "type": "human", "handle": FAKE_HANDLE, "key_path": str(kp_mod._KEYS_DIR / f"{HOSTNAME}.pem"), "algorithm": "ed25519", "fingerprint": FAKE_FINGERPRINT, "hd_path": FAKE_HD_PATH, }, mnemonic=long_mnemonic) monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB) from muse.cli.commands.auth import run_show ns = MagicMock() ns.hub = HUB ns.json_output = True run_show(ns) out = json.loads(capsys.readouterr().out) assert out["mnemonic_word_count"] == 24 # --------------------------------------------------------------------------- # Performance: show completes within time limits # --------------------------------------------------------------------------- class TestPerformance: """show must be fast (< 5 s including crypto).""" def test_show_hd_latency(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """``run_show`` on an HD identity (with AVAX derivation) under 5 s.""" _patch_home(monkeypatch, tmp_path) _write_hd_identity(tmp_path, monkeypatch) monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB) from muse.cli.commands.auth import run_show ns = MagicMock() ns.hub = HUB ns.json_output = True start = time.perf_counter() run_show(ns) elapsed = time.perf_counter() - start assert elapsed < 5.0, f"Too slow: {elapsed:.2f} s" # --------------------------------------------------------------------------- # Security: mnemonic never exposed # --------------------------------------------------------------------------- class TestSecurity: """Security properties of show.""" def test_show_never_prints_mnemonic(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """``run_show`` must not emit the mnemonic to stdout or stderr.""" _patch_home(monkeypatch, tmp_path) _write_hd_identity(tmp_path, monkeypatch) monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB) from muse.cli.commands.auth import run_show ns = MagicMock() ns.hub = HUB ns.json_output = True run_show(ns) captured = capsys.readouterr() for word in FAKE_MNEMONIC.split(): out = json.loads(captured.out) assert word not in json.dumps(out).split(), f"word '{word}' found in JSON output" def test_show_json_has_no_mnemonic_key(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None: """JSON output must not contain a ``mnemonic`` key.""" _patch_home(monkeypatch, tmp_path) _write_hd_identity(tmp_path, monkeypatch) monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB) from muse.cli.commands.auth import run_show ns = MagicMock() ns.hub = HUB ns.json_output = True run_show(ns) out = json.loads(capsys.readouterr().out) assert "mnemonic" not in out # --------------------------------------------------------------------------- # Docstrings: public API coverage # --------------------------------------------------------------------------- class TestDocstrings: """All public functions must have docstrings.""" def test_run_show_docstring(self) -> None: from muse.cli.commands.auth import run_show assert run_show.__doc__ def test_show_identity_detail_docstring(self) -> None: from muse.cli.commands.auth import _show_identity_detail assert _show_identity_detail.__doc__