gabriel / muse public
test_security_check_hub_scoped.py python
133 lines 5.4 KB
Raw
sha256:08c083095bcaffb4c43ce947668fac93bf5d261e13d321776170c4019dd1d77d fix: migration must never update identity.toml on a failed … Sonnet 5 minor ⚠ breaking 19 hours ago
1 """Tests for the ``muse auth security-check`` hub-scoping gate (musehub#221).
2
3 Check 5 (``hub_scoped``) verifies the checked identity's ``hd_path`` has a
4 hub segment. Unlike check 4 (fingerprint-matches-mnemonic), which honestly
5 verifies against whichever scheme is actually on file, this check
6 deliberately fails on a pre-#221 identity — ``security-check`` doubles as
7 the hub-scoping migration gate, not just PEM/keychain hygiene.
8
9 Coverage
10 --------
11 HS-1 hub_scoped=True and ok=True for a real (post-#221) keygen identity.
12 HS-2 hub_scoped=False and ok=False for a pre-#221 (six-level) identity.
13 HS-3 fingerprint_matches_mnemonic still reports honestly (True) for a
14 pre-#221 identity even though ok=False overall — check 4 and check 5
15 are independent facts, not conflated into one.
16 HS-4 Text-mode output names the remediation command for a failing check.
17 HS-5 hub_scoped is null when no identity can be checked at all.
18 """
19
20 from __future__ import annotations
21
22 import json
23 import pathlib
24
25 import pytest
26
27 from tests.cli_test_helper import CliRunner
28
29 _HUB = "https://localhost:1337"
30 _HOSTNAME = "localhost:1337"
31 _MNEMONIC = (
32 "abandon abandon abandon abandon abandon abandon abandon abandon "
33 "abandon abandon abandon about"
34 )
35
36 runner = CliRunner()
37
38
39 def _patch_env(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path:
40 import muse.core.keypair as kp_module
41 import muse.core.identity as id_module
42
43 fake_home = tmp_path / "home"
44 fake_home.mkdir(parents=True, exist_ok=True)
45 monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home))
46 monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys")
47 monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse")
48 monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml")
49
50 _kc: dict[str, str] = {}
51 monkeypatch.setattr("muse.core.keychain.is_available", lambda: True)
52 monkeypatch.setattr("muse.core.keychain.load", lambda: _kc.get("mnemonic"))
53 monkeypatch.setattr("muse.core.keychain.store", lambda m: _kc.__setitem__("mnemonic", m))
54 monkeypatch.setattr("muse.core.keychain.delete", lambda: _kc.pop("mnemonic", None))
55 monkeypatch.setattr("muse.cli.commands.auth._stderr_isatty", lambda: False)
56 return fake_home
57
58
59 def _save_legacy_identity(mnemonic: str = _MNEMONIC) -> str:
60 """Write a pre-#221 (six-level, hub=None) identity and return its fingerprint."""
61 from muse.core.identity import save_identity
62 from muse.core.bip39 import mnemonic_to_seed
63 from muse.core.keypair import derive_hd_public_info
64
65 seed = mnemonic_to_seed(mnemonic)
66 _, fp = derive_hd_public_info(seed) # hub=None -> legacy six-level path
67 save_identity(_HUB, {
68 "type": "human",
69 "handle": "gabriel",
70 "algorithm": "ed25519",
71 "fingerprint": fp,
72 "hd_path": "m/1075233755'/1660078172'/0'/0'/0'/0'",
73 }, mnemonic=mnemonic)
74 return fp
75
76
77 class TestHubScopedTrue:
78 def test_HS_1_real_keygen_is_hub_scoped_and_ok(
79 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
80 ) -> None:
81 _patch_env(monkeypatch, tmp_path)
82 runner.invoke(None, ["auth", "keygen", "--hub", _HUB])
83 result = runner.invoke(None, ["auth", "security-check", "--hub", _HUB, "--json"])
84 payload = json.loads(result.output.splitlines()[0])
85 assert payload["hub_scoped"] is True
86 assert payload["ok"] is True
87 assert result.exit_code == 0
88
89
90 class TestHubScopedFalse:
91 def test_HS_2_legacy_identity_is_not_hub_scoped_and_fails(
92 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
93 ) -> None:
94 _patch_env(monkeypatch, tmp_path)
95 _save_legacy_identity()
96 result = runner.invoke(None, ["auth", "security-check", "--hub", _HUB, "--json"])
97 payload = json.loads(result.output.splitlines()[0])
98 assert payload["hub_scoped"] is False
99 assert payload["ok"] is False
100 assert result.exit_code != 0
101
102 def test_HS_3_fingerprint_check_is_independently_honest(
103 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
104 ) -> None:
105 """A legacy identity's stored fingerprint still genuinely matches its
106 own (legacy-scheme) mnemonic derivation — check 4 doesn't lie just
107 because check 5 is failing."""
108 _patch_env(monkeypatch, tmp_path)
109 _save_legacy_identity()
110 result = runner.invoke(None, ["auth", "security-check", "--hub", _HUB, "--json"])
111 payload = json.loads(result.output.splitlines()[0])
112 assert payload["fingerprint_matches_mnemonic"] is True
113 assert payload["hub_scoped"] is False
114 assert payload["ok"] is False
115
116 def test_HS_4_text_output_names_migrate_command(
117 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
118 ) -> None:
119 _patch_env(monkeypatch, tmp_path)
120 _save_legacy_identity()
121 result = runner.invoke(None, ["auth", "security-check", "--hub", _HUB])
122 assert "muse migrate hub-scoping" in result.stderr
123
124
125 class TestHubScopedNull:
126 def test_HS_5_no_identity_yields_null(
127 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
128 ) -> None:
129 _patch_env(monkeypatch, tmp_path)
130 result = runner.invoke(None, ["auth", "security-check", "--hub", _HUB, "--json"])
131 payload = json.loads(result.output.splitlines()[0])
132 assert payload["hub_scoped"] is None
133 assert payload["ok"] is False
File History 1 commit
sha256:08c083095bcaffb4c43ce947668fac93bf5d261e13d321776170c4019dd1d77d fix: migration must never update identity.toml on a failed … Sonnet 5 minor 19 hours ago