"""Tests for fd guard rails on --passphrase-fd and --mnemonic-fd. Passing fd 0 (stdin), 1 (stdout), or 2 (stderr) to either flag is always a mistake and can be destructive: --passphrase-fd 1 → os.read(1, …) reads from stdout, os.close(1) kills it --passphrase-fd 2 → os.read(2, …) reads from stderr, os.close(2) kills it --mnemonic-fd 0 → os.fdopen(0) wraps stdin; fine in isolation but wrong --mnemonic-fd 1 → os.fdopen(1, "r") on a write-only fd — undefined --mnemonic-fd 2 → same Both _resolve_passphrase and _read_mnemonic_securely must reject fd < 3 with a clear error before touching the descriptor. Coverage -------- I --passphrase-fd rejects stdin / stdout / stderr I1 --passphrase-fd 0 exits non-zero I2 --passphrase-fd 1 exits non-zero I3 --passphrase-fd 2 exits non-zero II --mnemonic-fd rejects stdin / stdout / stderr II1 recover --mnemonic-fd 0 exits non-zero II2 recover --mnemonic-fd 1 exits non-zero II3 recover --mnemonic-fd 2 exits non-zero III Valid fds (>= 3) still work III1 --passphrase-fd with a real pipe fd succeeds III2 --mnemonic-fd with a real pipe fd succeeds """ from __future__ import annotations import os import pathlib import pytest from tests.cli_test_helper import CliRunner from muse.core import keypair as kp_module from muse.core import identity as id_module runner = CliRunner() _HUB = "https://localhost:1337" _MNEMONIC = ( "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon about" ) @pytest.fixture() def isolated(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path: 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") return fake_home @pytest.fixture() def fixed_mnemonic(monkeypatch: pytest.MonkeyPatch) -> str: from muse.core import bip39 as bip39_mod monkeypatch.setattr(bip39_mod, "generate_mnemonic", lambda **kw: _MNEMONIC) return _MNEMONIC def _pipe(content: str) -> int: r, w = os.pipe() os.write(w, content.encode()) os.close(w) return r # --------------------------------------------------------------------------- # I --passphrase-fd rejects stdin / stdout / stderr # --------------------------------------------------------------------------- class TestPassphraseFdGuard: def test_I1_passphrase_fd_0_rejected( self, isolated: pathlib.Path, fixed_mnemonic: str ) -> None: """I1: --passphrase-fd 0 must be rejected before touching the fd.""" r = runner.invoke(None, ["auth", "keygen", "--hub", _HUB, "--passphrase-fd", "0"]) assert r.exit_code != 0, "passphrase-fd 0 (stdin) must be rejected" def test_I2_passphrase_fd_1_rejected( self, isolated: pathlib.Path, fixed_mnemonic: str ) -> None: """I2: --passphrase-fd 1 must be rejected before os.read closes stdout.""" r = runner.invoke(None, ["auth", "keygen", "--hub", _HUB, "--passphrase-fd", "1"]) assert r.exit_code != 0, "passphrase-fd 1 (stdout) must be rejected" def test_I3_passphrase_fd_2_rejected( self, isolated: pathlib.Path, fixed_mnemonic: str ) -> None: """I3: --passphrase-fd 2 must be rejected before os.read closes stderr.""" r = runner.invoke(None, ["auth", "keygen", "--hub", _HUB, "--passphrase-fd", "2"]) assert r.exit_code != 0, "passphrase-fd 2 (stderr) must be rejected" # --------------------------------------------------------------------------- # II --mnemonic-fd rejects stdin / stdout / stderr # --------------------------------------------------------------------------- class TestMnemonicFdGuard: def test_II1_mnemonic_fd_0_rejected(self, isolated: pathlib.Path) -> None: """II1: --mnemonic-fd 0 must be rejected.""" r = runner.invoke( None, ["auth", "recover", "--hub", _HUB, "--force", "--mnemonic-fd", "0"], ) assert r.exit_code != 0, "mnemonic-fd 0 (stdin) must be rejected" def test_II2_mnemonic_fd_1_rejected(self, isolated: pathlib.Path) -> None: """II2: --mnemonic-fd 1 must be rejected.""" r = runner.invoke( None, ["auth", "recover", "--hub", _HUB, "--force", "--mnemonic-fd", "1"], ) assert r.exit_code != 0, "mnemonic-fd 1 (stdout) must be rejected" def test_II3_mnemonic_fd_2_rejected(self, isolated: pathlib.Path) -> None: """II3: --mnemonic-fd 2 must be rejected.""" r = runner.invoke( None, ["auth", "recover", "--hub", _HUB, "--force", "--mnemonic-fd", "2"], ) assert r.exit_code != 0, "mnemonic-fd 2 (stderr) must be rejected" # --------------------------------------------------------------------------- # III Valid fds (>= 3) still work # --------------------------------------------------------------------------- class TestValidFdStillWorks: def test_III1_passphrase_fd_valid_pipe_works( self, isolated: pathlib.Path, fixed_mnemonic: str ) -> None: """III1: a real pipe fd >= 3 is accepted.""" r = runner.invoke( None, ["auth", "keygen", "--hub", _HUB, "--json", "--passphrase-fd", str(_pipe("hunter2"))], ) assert r.exit_code == 0, r.output def test_III2_mnemonic_fd_valid_pipe_works( self, isolated: pathlib.Path, fixed_mnemonic: str ) -> None: """III2: a real mnemonic pipe fd >= 3 is accepted.""" _keygen_first = runner.invoke( None, ["auth", "keygen", "--hub", _HUB, "--json"] ) assert _keygen_first.exit_code == 0 r = runner.invoke( None, ["auth", "recover", "--hub", _HUB, "--force", "--json", "--mnemonic-fd", str(_pipe(_MNEMONIC))], ) assert r.exit_code == 0, r.output