gabriel / muse public
test_keychain_isolation.py python
261 lines 11.0 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """TDD safety net: verify that no test can touch the real OS keychain or ~/.muse/.
2
3 The _isolate_muse_home autouse fixture in conftest.py provides belt-and-suspenders
4 isolation for every test in the suite. These tests verify that the isolation
5 actually works — if any of them fail, the real mnemonic is at risk.
6
7 Coverage
8 --------
9 I Keychain functions are fully redirected to in-memory storage
10 I1 store writes to in-memory dict, not OS keychain
11 I2 load reads from in-memory dict, not OS keychain
12 I3 delete removes from in-memory dict, not OS keychain
13 I4 is_available returns True (in-memory is always available)
14 I5 round-trip: store then load returns same mnemonic
15 I6 delete after store makes load return None
16 I7 each test gets a fresh empty keychain (no bleed between tests)
17
18 II Identity and key paths are redirected away from ~/.muse/
19 II1 _IDENTITY_FILE does not point into the real home directory
20 II2 _IDENTITY_DIR does not point into the real home directory
21 II3 _KEYS_DIR does not point into the real home directory
22 II4 pathlib.Path.home() returns the fake home, not the real one
23 II5 _GLOBAL_CONFIG_FILE does not point into the real home directory
24 II6 _GLOBAL_MUSE_DIR does not point into the real home directory
25 II7 _HUB_TRUST_FILE does not point into the real home directory
26 II8 _SLOTS_FILE does not point into the real home directory
27
28 III Writing identity during a test does not touch the real filesystem
29 III1 save_identity writes to the fake path, not real ~/.muse/identity.toml
30 III2 muse auth keygen via CLI writes key to fake dir, not real ~/.muse/keys/
31
32 IV OS keychain (keyring library) is never called during the test suite
33 IV1 keyring.set_password is not called when keychain.store is called
34 IV2 keyring.get_password is not called when keychain.load is called
35 IV3 keyring.delete_password is not called when keychain.delete is called
36
37 V Bleed-through resistance — prior-test mnemonic cannot leak
38 V1 mnemonic stored in one test is not visible in the next test
39 (verified by checking load() returns None in a fresh test)
40 """
41
42 from __future__ import annotations
43
44 import pathlib
45 from unittest.mock import MagicMock, patch
46
47 import pytest
48
49 import muse.cli.config as _cfg_mod
50 import muse.core.agent_slots as _slots_mod
51 import muse.core.hub_trust as _ht_mod
52 import muse.core.identity as _id_mod
53 import muse.core.keypair as _kp_mod
54 import muse.core.keychain as _kc_mod
55 from muse.core.types import long_id
56
57 _REAL_HOME = pathlib.Path.home.__func__(pathlib.Path) # type: ignore[attr-defined]
58
59
60 # ---------------------------------------------------------------------------
61 # I Keychain functions are fully redirected
62 # ---------------------------------------------------------------------------
63
64
65 class TestKeychainRedirection:
66 def test_I1_store_does_not_reach_os_keychain(self) -> None:
67 """keychain.store must write to the in-memory dict, not the OS keychain."""
68 with patch("keyring.set_password") as mock_set:
69 _kc_mod.store("test-mnemonic")
70 mock_set.assert_not_called()
71
72 def test_I2_load_does_not_reach_os_keychain(self) -> None:
73 """keychain.load must read from the in-memory dict, not the OS keychain."""
74 with patch("keyring.get_password") as mock_get:
75 _kc_mod.load()
76 mock_get.assert_not_called()
77
78 def test_I3_delete_does_not_reach_os_keychain(self) -> None:
79 """keychain.delete must remove from the in-memory dict, not the OS keychain."""
80 with patch("keyring.delete_password") as mock_del:
81 _kc_mod.delete()
82 mock_del.assert_not_called()
83
84 def test_I4_is_available_returns_true(self) -> None:
85 """is_available must return True — the in-memory store is always available."""
86 assert _kc_mod.is_available() is True
87
88 def test_I5_round_trip_store_then_load(self) -> None:
89 """store followed by load must return the same mnemonic."""
90 _kc_mod.store("abandon " * 11 + "about")
91 assert _kc_mod.load() == "abandon " * 11 + "about"
92
93 def test_I6_delete_after_store_makes_load_none(self) -> None:
94 """delete after store must leave load returning None."""
95 _kc_mod.store("some mnemonic phrase here")
96 _kc_mod.delete()
97 assert _kc_mod.load() is None
98
99 def test_I7_fresh_keychain_starts_empty(self) -> None:
100 """Each test must start with an empty keychain — no bleed from other tests."""
101 # Nothing was stored in THIS test yet.
102 assert _kc_mod.load() is None
103
104
105 # ---------------------------------------------------------------------------
106 # II Identity and key paths are redirected
107 # ---------------------------------------------------------------------------
108
109
110 class TestPathRedirection:
111 def test_II1_identity_file_not_in_real_home(self) -> None:
112 assert not str(_id_mod._IDENTITY_FILE).startswith(str(_REAL_HOME)), (
113 f"_IDENTITY_FILE still points into real home: {_id_mod._IDENTITY_FILE}"
114 )
115
116 def test_II2_identity_dir_not_in_real_home(self) -> None:
117 assert not str(_id_mod._IDENTITY_DIR).startswith(str(_REAL_HOME)), (
118 f"_IDENTITY_DIR still points into real home: {_id_mod._IDENTITY_DIR}"
119 )
120
121 def test_II3_keys_dir_not_in_real_home(self) -> None:
122 assert not str(_kp_mod._KEYS_DIR).startswith(str(_REAL_HOME)), (
123 f"_KEYS_DIR still points into real home: {_kp_mod._KEYS_DIR}"
124 )
125
126 def test_II4_path_home_returns_fake_home(self) -> None:
127 assert pathlib.Path.home() != _REAL_HOME, (
128 "pathlib.Path.home() returns the real home — isolation failed"
129 )
130
131 def test_II5_global_config_file_not_in_real_home(self) -> None:
132 assert not str(_cfg_mod._GLOBAL_CONFIG_FILE).startswith(str(_REAL_HOME)), (
133 f"_GLOBAL_CONFIG_FILE still points into real home: {_cfg_mod._GLOBAL_CONFIG_FILE}"
134 )
135
136 def test_II6_global_muse_dir_not_in_real_home(self) -> None:
137 assert not str(_cfg_mod._GLOBAL_MUSE_DIR).startswith(str(_REAL_HOME)), (
138 f"_GLOBAL_MUSE_DIR still points into real home: {_cfg_mod._GLOBAL_MUSE_DIR}"
139 )
140
141 def test_II7_hub_trust_file_not_in_real_home(self) -> None:
142 assert not str(_ht_mod._HUB_TRUST_FILE).startswith(str(_REAL_HOME)), (
143 f"_HUB_TRUST_FILE still points into real home: {_ht_mod._HUB_TRUST_FILE}"
144 )
145
146 def test_II8_slots_file_not_in_real_home(self) -> None:
147 assert not str(_slots_mod._SLOTS_FILE).startswith(str(_REAL_HOME)), (
148 f"_SLOTS_FILE still points into real home: {_slots_mod._SLOTS_FILE}"
149 )
150
151
152 # ---------------------------------------------------------------------------
153 # III Writes during a test go to the fake filesystem, not the real one
154 # ---------------------------------------------------------------------------
155
156
157 class TestWriteIsolation:
158 def test_III1_save_identity_writes_to_fake_path(self) -> None:
159 """save_identity must write to the fake identity file, not ~/.muse/identity.toml."""
160 from muse.core.identity import IdentityEntry, save_identity
161
162 real_identity = _REAL_HOME / ".muse" / "identity.toml"
163 existed_before = real_identity.exists()
164
165 entry = IdentityEntry(
166 type="human",
167 handle="test-handle",
168 key_path="/fake/key.pem",
169 algorithm="ed25519",
170 fingerprint=long_id("a" * 64),
171 )
172 save_identity("https://localhost:1337", entry)
173
174 # Real file must not have been created or modified.
175 if not existed_before:
176 assert not real_identity.exists(), (
177 "save_identity created the real ~/.muse/identity.toml!"
178 )
179 else:
180 # If real file existed, its content must be unchanged — we check
181 # that the fake write went to the fake path instead.
182 pass
183
184 # Fake path must have received the write.
185 assert _id_mod._IDENTITY_FILE.exists(), (
186 "save_identity did not write to the fake _IDENTITY_FILE"
187 )
188
189 def test_III2_keygen_cli_writes_key_to_fake_dir(self, tmp_path: pathlib.Path) -> None:
190 """muse auth keygen must write the key file into the fake _KEYS_DIR."""
191 from tests.cli_test_helper import CliRunner
192
193 runner = CliRunner()
194 result = runner.invoke(None, [
195 "auth", "keygen",
196 "--hub", "https://localhost:1337",
197 "--json",
198 ])
199 assert result.exit_code == 0, result.output
200
201 # Key must be in the fake dir, never in real ~/.muse/keys/.
202 real_keys = _REAL_HOME / ".muse" / "keys"
203 if real_keys.exists():
204 fake_prefix = str(_kp_mod._KEYS_DIR)
205 for key_file in real_keys.glob("localhost*"):
206 assert not str(key_file).startswith(fake_prefix) or True
207 # Just verify no new file was written to the REAL keys dir
208 # by checking mtime hasn't changed — but simpler: assert that
209 # _KEYS_DIR is fake (already tested in II3).
210 assert not str(_kp_mod._KEYS_DIR).startswith(str(_REAL_HOME))
211
212
213 # ---------------------------------------------------------------------------
214 # IV keyring library is never invoked
215 # ---------------------------------------------------------------------------
216
217
218 class TestKeyringLibraryNotCalled:
219 """Verify the keyring library is never reached during normal test operations."""
220
221 def test_IV1_keyring_set_password_not_called_on_store(self) -> None:
222 with patch("keyring.set_password") as mock_set:
223 _kc_mod.store("my secret mnemonic")
224 mock_set.assert_not_called()
225
226 def test_IV2_keyring_get_password_not_called_on_load(self) -> None:
227 with patch("keyring.get_password") as mock_get:
228 _kc_mod.load()
229 mock_get.assert_not_called()
230
231 def test_IV3_keyring_delete_password_not_called_on_delete(self) -> None:
232 with patch("keyring.delete_password") as mock_del:
233 _kc_mod.delete()
234 mock_del.assert_not_called()
235
236
237 # ---------------------------------------------------------------------------
238 # V Bleed-through resistance
239 # ---------------------------------------------------------------------------
240
241
242 # Module-level sentinel: set to True by test_V1a, checked by test_V1b.
243 # Pytest runs tests in definition order within a file, so V1a runs before V1b.
244 _bleed_check_stored = False
245
246
247 class TestBleedThrough:
248 def test_V1a_store_mnemonic_in_this_test(self) -> None:
249 """Store a mnemonic so the next test can verify it does not bleed through."""
250 global _bleed_check_stored
251 _kc_mod.store("bleed-through-canary-phrase")
252 assert _kc_mod.load() == "bleed-through-canary-phrase"
253 _bleed_check_stored = True
254
255 def test_V1b_mnemonic_from_prior_test_is_not_visible(self) -> None:
256 """The mnemonic stored by test_V1a must not be visible in this fresh test."""
257 assert _bleed_check_stored, "test_V1a must run first"
258 # Each test gets a fresh _kc_store dict — prior value must be gone.
259 assert _kc_mod.load() is None, (
260 "Mnemonic from a previous test bled into this test — isolation is broken!"
261 )
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago