gabriel / muse public

test_auth_show_migrate.py file-level

at sha256:7 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:5 fix: muse code add unconditionally wrote EMPTY_DIR_OID to the object st… · gabriel · Oct 24, 2026
1 """Tests for ``muse auth show``.
2
3 Covers:
4 - ``muse auth show``: display identity detail including HD paths + AVAX address.
5
6 Test categories
7 ---------------
8 - unit : _show_identity_detail helper with bare and HD entries
9 - integration : CLI round-trips for show (via CliRunner + monkeypatch)
10 - data-integrity : show reads HD fields correctly
11 - performance : show completes under 5 s
12 - security : show never reveals mnemonic
13 - docstrings : public API has docstrings
14 """
15
16 from __future__ import annotations
17
18 import json
19 import pathlib
20 import time
21 from unittest.mock import MagicMock
22
23 import pytest
24 from tests.cli_test_helper import CliRunner
25
26 import muse.core.keypair as kp_mod
27
28 runner = CliRunner()
29
30 HUB = "https://localhost:1337"
31 HOSTNAME = "localhost:1337"
32 FAKE_MNEMONIC = (
33 "abandon abandon abandon abandon abandon abandon "
34 "abandon abandon abandon abandon abandon about"
35 )
36 FAKE_HD_PATH = "m/1075233755'/0'/0'/0'/0'/0'"
37 FAKE_FINGERPRINT = "a" * 64
38 FAKE_HANDLE = "gabriel"
39
40
41 # ---------------------------------------------------------------------------
42 # Shared fixtures
43 # ---------------------------------------------------------------------------
44
45
46 def _patch_home(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path:
47 """Redirect identity.toml and key paths to a temp directory."""
48 fake_home = tmp_path / "home"
49 fake_muse = fake_home / ".muse"
50 fake_keys = fake_muse / "keys"
51 fake_keys.mkdir(parents=True)
52 identity_file = fake_muse / "identity.toml"
53
54 import muse.core.identity as id_mod
55 monkeypatch.setattr(id_mod, "_IDENTITY_FILE", identity_file)
56 monkeypatch.setattr(id_mod, "_IDENTITY_DIR", fake_muse)
57 monkeypatch.setattr(kp_mod, "_KEYS_DIR", fake_keys)
58 return fake_home
59
60
61 def _write_bare_identity(fake_home: pathlib.Path, handle: str = FAKE_HANDLE) -> pathlib.Path:
62 """Write a minimal identity entry (no HD fields) to identity.toml."""
63 import muse.core.identity as id_mod
64 entry = {
65 "type": "human",
66 "handle": handle,
67 "key_path": str(kp_mod._KEYS_DIR / f"{HOSTNAME}.pem"),
68 "algorithm": "ed25519",
69 "fingerprint": FAKE_FINGERPRINT,
70 }
71 from muse.core.identity import save_identity
72 save_identity(f"http://{HOSTNAME}", entry)
73 return id_mod._IDENTITY_FILE
74
75
76 def _write_hd_identity(
77 fake_home: pathlib.Path,
78 monkeypatch: pytest.MonkeyPatch,
79 handle: str = FAKE_HANDLE,
80 ) -> pathlib.Path:
81 """Write an HD identity entry with mnemonic stored in an in-memory keychain."""
82 _kc: dict[str, str] = {}
83 monkeypatch.setattr("muse.core.keychain.is_available", lambda: True)
84 monkeypatch.setattr("muse.core.keychain.store",
85 lambda m: _kc.__setitem__("mnemonic", m))
86 monkeypatch.setattr("muse.core.keychain.load", lambda: _kc.get("mnemonic"))
87
88 import muse.core.identity as id_mod
89 entry = {
90 "type": "human",
91 "handle": handle,
92 "key_path": str(kp_mod._KEYS_DIR / f"{HOSTNAME}.pem"),
93 "algorithm": "ed25519",
94 "fingerprint": FAKE_FINGERPRINT,
95 "hd_path": FAKE_HD_PATH,
96 }
97 from muse.core.identity import save_identity
98 save_identity(f"https://{HOSTNAME}", entry, mnemonic=FAKE_MNEMONIC)
99 return id_mod._IDENTITY_FILE
100
101
102 # ---------------------------------------------------------------------------
103 # Unit: _show_identity_detail helper
104 # ---------------------------------------------------------------------------
105
106
107 class TestShowIdentityDetail:
108 """Unit tests for the _show_identity_detail helper."""
109
110 def test_bare_entry_json(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
111 """Bare entry (no hd_path) must contain required fields and no HD extras."""
112 _patch_home(monkeypatch, tmp_path)
113 from muse.cli.commands.auth import _show_identity_detail
114
115 entry = {
116 "type": "human",
117 "handle": "gabriel",
118 "key_path": "/tmp/key.pem",
119 "fingerprint": FAKE_FINGERPRINT,
120 }
121 _show_identity_detail(HOSTNAME, entry, json_output=True)
122 out = json.loads(capsys.readouterr().out)
123
124 assert out["hub"] == HOSTNAME
125 assert out["handle"] == "gabriel"
126 assert out["type"] == "human"
127 assert out["fingerprint"] == FAKE_FINGERPRINT
128 assert "hd_path" not in out
129 assert "mnemonic_word_count" not in out
130 assert "derived_paths" not in out
131
132 def test_hd_entry_json_has_paths(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
133 """HD entry JSON must include derived_paths with the four standard paths."""
134 _patch_home(monkeypatch, tmp_path)
135 from muse.cli.commands.auth import _show_identity_detail
136
137 entry = {
138 "type": "human",
139 "handle": "gabriel",
140 "key_path": "/tmp/key.pem",
141 "fingerprint": FAKE_FINGERPRINT,
142 "mnemonic": FAKE_MNEMONIC,
143 "hd_path": FAKE_HD_PATH,
144 }
145 _show_identity_detail(HOSTNAME, entry, json_output=True)
146 out = json.loads(capsys.readouterr().out)
147
148 assert out["hd_path"] == FAKE_HD_PATH
149 assert out["mnemonic_word_count"] == 12
150 assert "derived_paths" in out
151 dp = out["derived_paths"]
152 assert "identity_msign" in dp
153 assert "payments_mpay" in dp
154 assert "avax_c_chain" in dp
155 assert "agent_slot_0" in dp
156
157 def test_hd_entry_has_avax_address(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
158 """HD entry must include avax_c_chain_address derived from the mnemonic."""
159 _patch_home(monkeypatch, tmp_path)
160 from muse.cli.commands.auth import _show_identity_detail
161
162 entry = {
163 "type": "human",
164 "handle": "gabriel",
165 "key_path": "/tmp/key.pem",
166 "fingerprint": FAKE_FINGERPRINT,
167 "mnemonic": FAKE_MNEMONIC,
168 "hd_path": FAKE_HD_PATH,
169 }
170 _show_identity_detail(HOSTNAME, entry, json_output=True)
171 out = json.loads(capsys.readouterr().out)
172
173 assert "avax_c_chain_address" in out
174 addr = out["avax_c_chain_address"]
175 assert addr.startswith("0x")
176 assert len(addr) == 42
177
178 def test_hd_derived_paths_format(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
179 """All derived paths must start with 'm/' and use the Muse purpose constant."""
180 _patch_home(monkeypatch, tmp_path)
181 from muse.cli.commands.auth import _show_identity_detail
182
183 entry = {
184 "type": "human", "handle": "gabriel", "key_path": "/tmp/key.pem",
185 "fingerprint": FAKE_FINGERPRINT,
186 "mnemonic": FAKE_MNEMONIC, "hd_path": FAKE_HD_PATH,
187 }
188 _show_identity_detail(HOSTNAME, entry, json_output=True)
189 out = json.loads(capsys.readouterr().out)
190
191 dp = out["derived_paths"]
192 for name, path in dp.items():
193 if name != "avax_c_chain":
194 assert path.startswith("m/1075233755'"), f"{name}: {path}"
195 else:
196 assert path == "m/44'/60'/0'/0/0"
197
198 def test_bare_entry_stderr_human_readable(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
199 """Bare entry human-readable output must appear on stderr, not stdout."""
200 _patch_home(monkeypatch, tmp_path)
201 from muse.cli.commands.auth import _show_identity_detail
202
203 entry = {
204 "type": "human", "handle": "gabriel",
205 "key_path": "/tmp/key.pem", "fingerprint": FAKE_FINGERPRINT,
206 }
207 _show_identity_detail(HOSTNAME, entry, json_output=False)
208 captured = capsys.readouterr()
209 assert captured.out == ""
210 assert "gabriel" in captured.err
211
212
213 # ---------------------------------------------------------------------------
214 # Integration: CLI round-trips
215 # ---------------------------------------------------------------------------
216
217
218 class TestShowCLI:
219 """Integration: ``muse auth show`` CLI round-trips."""
220
221 def test_show_bare_json(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
222 """``muse auth show --hub … --json`` returns correct JSON for a bare identity."""
223 _patch_home(monkeypatch, tmp_path)
224 _write_bare_identity(tmp_path)
225 monkeypatch.setenv("MUSE_KEYCHAIN_BACKEND", "disabled")
226 monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB)
227
228 from muse.cli.commands.auth import run_show
229 ns = MagicMock()
230 ns.hub = HUB
231 ns.public_key = False
232 ns.agent_id = None
233 ns.json_output = True
234 run_show(ns)
235
236 out = json.loads(capsys.readouterr().out)
237 assert out["handle"] == FAKE_HANDLE
238 assert "key_source" not in out
239 assert "hd_path" not in out
240
241 def test_show_hd_json(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
242 """``muse auth show --hub … --json`` returns HD fields for an HD identity."""
243 _patch_home(monkeypatch, tmp_path)
244 _write_hd_identity(tmp_path, monkeypatch)
245 monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB)
246
247 from muse.cli.commands.auth import run_show
248 ns = MagicMock()
249 ns.hub = HUB
250 ns.public_key = False
251 ns.agent_id = None
252 ns.json_output = True
253 run_show(ns)
254
255 out = json.loads(capsys.readouterr().out)
256 assert "key_source" not in out
257 assert out["mnemonic_word_count"] == 12
258 assert "derived_paths" in out
259
260 def test_show_no_identity_exits_1(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None:
261 """``run_show`` exits with code 1 when no identity is stored."""
262 _patch_home(monkeypatch, tmp_path)
263 monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB)
264
265 from muse.cli.commands.auth import run_show
266 ns = MagicMock()
267 ns.hub = HUB
268 ns.public_key = False
269 ns.agent_id = None
270 ns.json_output = False
271
272 with pytest.raises(SystemExit) as exc_info:
273 run_show(ns)
274 assert exc_info.value.code == 1
275
276
277 # ---------------------------------------------------------------------------
278 # Data integrity: show reads correct fields
279 # ---------------------------------------------------------------------------
280
281
282 class TestDataIntegrity:
283 """Data-integrity tests for show."""
284
285 def test_show_mnemonic_word_count_matches_24_words(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
286 """show must report mnemonic_word_count = 24 for a 24-word mnemonic."""
287 _patch_home(monkeypatch, tmp_path)
288
289 long_mnemonic = " ".join(["abandon"] * 23 + ["art"])
290 _kc: dict[str, str] = {}
291 monkeypatch.setattr("muse.core.keychain.is_available", lambda: True)
292 monkeypatch.setattr("muse.core.keychain.store",
293 lambda m: _kc.__setitem__("mnemonic", m))
294 monkeypatch.setattr("muse.core.keychain.load", lambda: _kc.get("mnemonic"))
295
296 from muse.core.identity import save_identity
297 save_identity(f"https://{HOSTNAME}", {
298 "type": "human", "handle": FAKE_HANDLE,
299 "key_path": str(kp_mod._KEYS_DIR / f"{HOSTNAME}.pem"),
300 "algorithm": "ed25519", "fingerprint": FAKE_FINGERPRINT,
301 "hd_path": FAKE_HD_PATH,
302 }, mnemonic=long_mnemonic)
303
304 monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB)
305
306 from muse.cli.commands.auth import run_show
307 ns = MagicMock()
308 ns.hub = HUB
309 ns.public_key = False
310 ns.agent_id = None
311 ns.json_output = True
312 run_show(ns)
313
314 out = json.loads(capsys.readouterr().out)
315 assert out["mnemonic_word_count"] == 24
316
317
318 # ---------------------------------------------------------------------------
319 # Performance: show completes within time limits
320 # ---------------------------------------------------------------------------
321
322
323 class TestPerformance:
324 """show must be fast (< 5 s including crypto)."""
325
326 def test_show_hd_latency(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
327 """``run_show`` on an HD identity (with AVAX derivation) under 5 s."""
328 _patch_home(monkeypatch, tmp_path)
329 _write_hd_identity(tmp_path, monkeypatch)
330 monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB)
331
332 from muse.cli.commands.auth import run_show
333 ns = MagicMock()
334 ns.hub = HUB
335 ns.public_key = False
336 ns.agent_id = None
337 ns.json_output = True
338
339 start = time.perf_counter()
340 run_show(ns)
341 elapsed = time.perf_counter() - start
342 assert elapsed < 5.0, f"Too slow: {elapsed:.2f} s"
343
344
345 # ---------------------------------------------------------------------------
346 # Security: mnemonic never exposed
347 # ---------------------------------------------------------------------------
348
349
350 class TestSecurity:
351 """Security properties of show."""
352
353 def test_show_never_prints_mnemonic(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
354 """``run_show`` must not emit the mnemonic to stdout or stderr."""
355 _patch_home(monkeypatch, tmp_path)
356 _write_hd_identity(tmp_path, monkeypatch)
357 monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB)
358
359 from muse.cli.commands.auth import run_show
360 ns = MagicMock()
361 ns.hub = HUB
362 ns.public_key = False
363 ns.agent_id = None
364 ns.json_output = True
365 run_show(ns)
366
367 captured = capsys.readouterr()
368 for word in FAKE_MNEMONIC.split():
369 out = json.loads(captured.out)
370 assert word not in json.dumps(out).split(), f"word '{word}' found in JSON output"
371
372 def test_show_json_has_no_mnemonic_key(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
373 """JSON output must not contain a ``mnemonic`` key."""
374 _patch_home(monkeypatch, tmp_path)
375 _write_hd_identity(tmp_path, monkeypatch)
376 monkeypatch.setattr("muse.cli.commands.auth._resolve_hub", lambda h, r=None: HUB)
377
378 from muse.cli.commands.auth import run_show
379 ns = MagicMock()
380 ns.hub = HUB
381 ns.public_key = False
382 ns.agent_id = None
383 ns.json_output = True
384 run_show(ns)
385
386 out = json.loads(capsys.readouterr().out)
387 assert "mnemonic" not in out
388
389
390 # ---------------------------------------------------------------------------
391 # Docstrings: public API coverage
392 # ---------------------------------------------------------------------------
393
394
395 class TestDocstrings:
396 """All public functions must have docstrings."""
397
398 def test_run_show_docstring(self) -> None:
399 from muse.cli.commands.auth import run_show
400 assert run_show.__doc__
401
402 def test_show_identity_detail_docstring(self) -> None:
403 from muse.cli.commands.auth import _show_identity_detail
404 assert _show_identity_detail.__doc__