test_auth_show_public_key.py
python
sha256:0d3de0ee53b63667de053f3ad712184124142df86473b80939347b487a65d6af
fix(diff): bare path scopes the diff instead of failing as …
Sonnet 5
patch
5 minutes ago
| 1 | """muse#100 — `muse auth show --public-key`: recover an already-registered |
| 2 | public key without touching internal Python modules. |
| 3 | |
| 4 | `muse sign verify --public-key-b64` needs the raw base64url Ed25519 public |
| 5 | key (no `ed25519:` prefix). The only place that value was ever printed was |
| 6 | at keygen/register time -- `muse auth whoami`, `muse auth show`, and |
| 7 | `muse auth key list` all expose only the one-way fingerprint, which cannot |
| 8 | be reversed back to the public key bytes. |
| 9 | |
| 10 | Coverage: |
| 11 | - unit: _resolve_public_key computes the same (pub_b64, fingerprint) pair |
| 12 | `muse sign` itself uses, via the identical get_signing_identity() / |
| 13 | _public_key_info() resolution path -- not a reimplementation that could |
| 14 | silently drift from what `muse sign verify` actually expects. |
| 15 | - integration: `muse auth show --public-key` CLI round-trip, text and --json. |
| 16 | - error path: no identity registered for the hub -> clean error, not a crash. |
| 17 | """ |
| 18 | from __future__ import annotations |
| 19 | |
| 20 | import json |
| 21 | import pathlib |
| 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 | def _patch_home(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path: |
| 42 | fake_home = tmp_path / "home" |
| 43 | fake_muse = fake_home / ".muse" |
| 44 | fake_keys = fake_muse / "keys" |
| 45 | fake_keys.mkdir(parents=True) |
| 46 | identity_file = fake_muse / "identity.toml" |
| 47 | |
| 48 | import muse.core.identity as id_mod |
| 49 | monkeypatch.setattr(id_mod, "_IDENTITY_FILE", identity_file) |
| 50 | monkeypatch.setattr(id_mod, "_IDENTITY_DIR", fake_muse) |
| 51 | monkeypatch.setattr(kp_mod, "_KEYS_DIR", fake_keys) |
| 52 | return fake_home |
| 53 | |
| 54 | |
| 55 | def _write_hd_identity( |
| 56 | monkeypatch: pytest.MonkeyPatch, handle: str = FAKE_HANDLE |
| 57 | ) -> None: |
| 58 | """Write an HD identity entry with mnemonic stored in an in-memory keychain |
| 59 | -- the real resolution path (resolve_signing_identity / get_signing_identity) |
| 60 | derives the private key from this mnemonic at call time, exactly as it |
| 61 | does for `muse sign`.""" |
| 62 | _kc: dict[str, str] = {} |
| 63 | monkeypatch.setattr("muse.core.keychain.is_available", lambda: True) |
| 64 | monkeypatch.setattr("muse.core.keychain.store", lambda m: _kc.__setitem__("mnemonic", m)) |
| 65 | monkeypatch.setattr("muse.core.keychain.load", lambda: _kc.get("mnemonic")) |
| 66 | |
| 67 | entry = { |
| 68 | "type": "human", |
| 69 | "handle": handle, |
| 70 | "key_path": str(kp_mod._KEYS_DIR / f"{HOSTNAME}.pem"), |
| 71 | "algorithm": "ed25519", |
| 72 | "fingerprint": FAKE_FINGERPRINT, |
| 73 | "hd_path": FAKE_HD_PATH, |
| 74 | } |
| 75 | from muse.core.identity import save_identity |
| 76 | save_identity(f"https://{HOSTNAME}", entry, mnemonic=FAKE_MNEMONIC) |
| 77 | |
| 78 | |
| 79 | # --------------------------------------------------------------------------- |
| 80 | # Unit: the resolution path matches muse sign's own public-key derivation |
| 81 | # --------------------------------------------------------------------------- |
| 82 | |
| 83 | |
| 84 | def test_auth_show_public_key_matches_sign_commands_own_derivation( |
| 85 | monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path |
| 86 | ) -> None: |
| 87 | """The public key `muse auth show --public-key` prints must be byte-identical |
| 88 | to what `muse sign`'s own _public_key_info derives for the same identity -- |
| 89 | not a parallel reimplementation that could silently diverge.""" |
| 90 | _patch_home(monkeypatch, tmp_path) |
| 91 | _write_hd_identity(monkeypatch) |
| 92 | |
| 93 | from muse.cli.config import get_signing_identity |
| 94 | from muse.cli.commands.sign import _public_key_info |
| 95 | |
| 96 | signing = get_signing_identity(remote_url=HUB) |
| 97 | assert signing is not None |
| 98 | expected_pub_b64, expected_fingerprint = _public_key_info(signing) |
| 99 | |
| 100 | from muse.cli.commands.auth import _resolve_public_key |
| 101 | pub_b64, fingerprint, handle = _resolve_public_key(HUB, agent_id=None) |
| 102 | |
| 103 | assert pub_b64 == expected_pub_b64 |
| 104 | assert fingerprint == expected_fingerprint |
| 105 | assert handle == FAKE_HANDLE |
| 106 | assert not pub_b64.startswith("ed25519:"), ( |
| 107 | "must be the raw base64url muse sign verify --public-key-b64 expects, " |
| 108 | "not the ed25519:-prefixed canonical form" |
| 109 | ) |
| 110 | |
| 111 | |
| 112 | def test_auth_show_public_key_no_identity_returns_none( |
| 113 | monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path |
| 114 | ) -> None: |
| 115 | _patch_home(monkeypatch, tmp_path) |
| 116 | from muse.cli.commands.auth import _resolve_public_key |
| 117 | assert _resolve_public_key(HUB, agent_id=None) is None |
| 118 | |
| 119 | |
| 120 | # --------------------------------------------------------------------------- |
| 121 | # Integration: CLI round-trip |
| 122 | # --------------------------------------------------------------------------- |
| 123 | |
| 124 | |
| 125 | def test_cli_auth_show_public_key_text( |
| 126 | monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str] |
| 127 | ) -> None: |
| 128 | _patch_home(monkeypatch, tmp_path) |
| 129 | _write_hd_identity(monkeypatch) |
| 130 | |
| 131 | result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key"]) |
| 132 | assert result.exit_code == 0, result.output |
| 133 | printed = result.output.strip() |
| 134 | assert printed |
| 135 | assert not printed.startswith("ed25519:") |
| 136 | # Must be usable verbatim as `muse sign verify --public-key-b64 <this>`. |
| 137 | assert " " not in printed |
| 138 | assert "\n" not in printed |
| 139 | |
| 140 | |
| 141 | def test_cli_auth_show_public_key_json( |
| 142 | monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path |
| 143 | ) -> None: |
| 144 | _patch_home(monkeypatch, tmp_path) |
| 145 | _write_hd_identity(monkeypatch) |
| 146 | |
| 147 | result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key", "--json"]) |
| 148 | assert result.exit_code == 0, result.output |
| 149 | out = json.loads(result.output) |
| 150 | assert out["public_key_b64"] |
| 151 | assert not out["public_key_b64"].startswith("ed25519:") |
| 152 | # Computed live from the derived public key, not echoed from the (possibly |
| 153 | # stale) stored identity.toml fingerprint field -- intentionally does NOT |
| 154 | # equal FAKE_FINGERPRINT, which is a placeholder set at identity-write time. |
| 155 | assert out["fingerprint"].startswith("sha256:") |
| 156 | assert out["handle"] == FAKE_HANDLE |
| 157 | assert out["hub"] == HOSTNAME |
| 158 | |
| 159 | |
| 160 | def test_cli_auth_show_public_key_no_identity_clean_error( |
| 161 | monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path |
| 162 | ) -> None: |
| 163 | """No crash, no traceback -- a clean, structured error.""" |
| 164 | _patch_home(monkeypatch, tmp_path) |
| 165 | |
| 166 | result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key", "--json"]) |
| 167 | assert result.exit_code != 0 |
| 168 | assert "Traceback" not in result.output |
| 169 | assert "Traceback" not in result.stderr |
| 170 | out = json.loads(result.output) |
| 171 | assert "error" in out |
| 172 | |
| 173 | |
| 174 | def test_cli_auth_show_public_key_roundtrips_through_sign_verify( |
| 175 | monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path |
| 176 | ) -> None: |
| 177 | """End-to-end acceptance criterion: the printed value must be exactly what |
| 178 | `muse sign verify --public-key-b64` accepts for a header signed by the |
| 179 | same identity.""" |
| 180 | _patch_home(monkeypatch, tmp_path) |
| 181 | _write_hd_identity(monkeypatch) |
| 182 | |
| 183 | show_result = runner.invoke(None, ["auth", "show", "--hub", HUB, "--public-key"]) |
| 184 | assert show_result.exit_code == 0, show_result.output |
| 185 | pub_b64 = show_result.output.strip() |
| 186 | |
| 187 | from muse.core.msign import build_msign_header, verify_msign_header |
| 188 | from muse.cli.config import get_signing_identity |
| 189 | |
| 190 | signing = get_signing_identity(remote_url=HUB) |
| 191 | assert signing is not None |
| 192 | url = f"{HUB}/x/y/push" |
| 193 | header = build_msign_header(signing, "POST", url, b"") |
| 194 | ok, reason = verify_msign_header(header, "POST", url, b"", pub_b64) |
| 195 | assert ok, reason |
File History
1 commit
sha256:0d3de0ee53b63667de053f3ad712184124142df86473b80939347b487a65d6af
fix(diff): bare path scopes the diff instead of failing as …
Sonnet 5
patch
5 minutes ago