"""Tests for the ``muse auth security-check`` hub-scoping gate (musehub#221). Check 5 (``hub_scoped``) verifies the checked identity's ``hd_path`` has a hub segment. Unlike check 4 (fingerprint-matches-mnemonic), which honestly verifies against whichever scheme is actually on file, this check deliberately fails on a pre-#221 identity — ``security-check`` doubles as the hub-scoping migration gate, not just PEM/keychain hygiene. Coverage -------- HS-1 hub_scoped=True and ok=True for a real (post-#221) keygen identity. HS-2 hub_scoped=False and ok=False for a pre-#221 (six-level) identity. HS-3 fingerprint_matches_mnemonic still reports honestly (True) for a pre-#221 identity even though ok=False overall — check 4 and check 5 are independent facts, not conflated into one. HS-4 Text-mode output names the remediation command for a failing check. HS-5 hub_scoped is null when no identity can be checked at all. """ from __future__ import annotations import json import pathlib import pytest from tests.cli_test_helper import CliRunner _HUB = "https://localhost:1337" _HOSTNAME = "localhost:1337" _MNEMONIC = ( "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon about" ) runner = CliRunner() def _patch_env(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path: import muse.core.keypair as kp_module import muse.core.identity as id_module fake_home = tmp_path / "home" fake_home.mkdir(parents=True, exist_ok=True) monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home)) monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys") monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse") monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml") _kc: dict[str, str] = {} monkeypatch.setattr("muse.core.keychain.is_available", lambda: True) monkeypatch.setattr("muse.core.keychain.load", lambda: _kc.get("mnemonic")) monkeypatch.setattr("muse.core.keychain.store", lambda m: _kc.__setitem__("mnemonic", m)) monkeypatch.setattr("muse.core.keychain.delete", lambda: _kc.pop("mnemonic", None)) monkeypatch.setattr("muse.cli.commands.auth._stderr_isatty", lambda: False) return fake_home def _save_legacy_identity(mnemonic: str = _MNEMONIC) -> str: """Write a pre-#221 (six-level, hub=None) identity and return its fingerprint.""" from muse.core.identity import save_identity from muse.core.bip39 import mnemonic_to_seed from muse.core.keypair import derive_hd_public_info seed = mnemonic_to_seed(mnemonic) _, fp = derive_hd_public_info(seed) # hub=None -> legacy six-level path save_identity(_HUB, { "type": "human", "handle": "gabriel", "algorithm": "ed25519", "fingerprint": fp, "hd_path": "m/1075233755'/1660078172'/0'/0'/0'/0'", }, mnemonic=mnemonic) return fp class TestHubScopedTrue: def test_HS_1_real_keygen_is_hub_scoped_and_ok( self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: _patch_env(monkeypatch, tmp_path) runner.invoke(None, ["auth", "keygen", "--hub", _HUB]) result = runner.invoke(None, ["auth", "security-check", "--hub", _HUB, "--json"]) payload = json.loads(result.output.splitlines()[0]) assert payload["hub_scoped"] is True assert payload["ok"] is True assert result.exit_code == 0 class TestHubScopedFalse: def test_HS_2_legacy_identity_is_not_hub_scoped_and_fails( self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: _patch_env(monkeypatch, tmp_path) _save_legacy_identity() result = runner.invoke(None, ["auth", "security-check", "--hub", _HUB, "--json"]) payload = json.loads(result.output.splitlines()[0]) assert payload["hub_scoped"] is False assert payload["ok"] is False assert result.exit_code != 0 def test_HS_3_fingerprint_check_is_independently_honest( self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: """A legacy identity's stored fingerprint still genuinely matches its own (legacy-scheme) mnemonic derivation — check 4 doesn't lie just because check 5 is failing.""" _patch_env(monkeypatch, tmp_path) _save_legacy_identity() result = runner.invoke(None, ["auth", "security-check", "--hub", _HUB, "--json"]) payload = json.loads(result.output.splitlines()[0]) assert payload["fingerprint_matches_mnemonic"] is True assert payload["hub_scoped"] is False assert payload["ok"] is False def test_HS_4_text_output_names_migrate_command( self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: _patch_env(monkeypatch, tmp_path) _save_legacy_identity() result = runner.invoke(None, ["auth", "security-check", "--hub", _HUB]) assert "muse migrate hub-scoping" in result.stderr class TestHubScopedNull: def test_HS_5_no_identity_yields_null( self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> None: _patch_env(monkeypatch, tmp_path) result = runner.invoke(None, ["auth", "security-check", "--hub", _HUB, "--json"]) payload = json.loads(result.output.splitlines()[0]) assert payload["hub_scoped"] is None assert payload["ok"] is False