137 lines
4.6 KiB
Python
137 lines
4.6 KiB
Python
import sys
|
|
import os
|
|
import json
|
|
import pytest
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
# Add src to path at the very top
|
|
TEST_DIR = Path(__file__).parent
|
|
SRC_DIR = TEST_DIR.parent / "src"
|
|
sys.path.insert(0, str(SRC_DIR))
|
|
|
|
import pyhdwallet
|
|
from bip_utils import Bip39SeedGenerator
|
|
|
|
DATA_DIR = TEST_DIR / "data"
|
|
VECTORS_FILE = TEST_DIR / "vectors.json"
|
|
|
|
@pytest.fixture
|
|
def vectors():
|
|
if not VECTORS_FILE.exists():
|
|
pytest.fail("tests/vectors.json missing. Run tests/bootstrap_vectors.py first.")
|
|
with open(VECTORS_FILE, "r") as f:
|
|
return json.load(f)
|
|
|
|
@pytest.fixture
|
|
def recipient_key_content():
|
|
path = DATA_DIR / "recipient.asc"
|
|
if not path.exists():
|
|
pytest.fail("tests/data/recipient.asc missing.")
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
def test_pgp_fingerprint_calculation(vectors, recipient_key_content):
|
|
"""
|
|
Verifies that pgp_fingerprint computes the expected fingerprint
|
|
for the stored recipient key.
|
|
"""
|
|
expected = vectors["pgp"]["expected_fingerprint"]
|
|
actual = pyhdwallet.pgp_fingerprint(recipient_key_content)
|
|
assert actual == expected, "Fingerprint calculation logic has changed!"
|
|
|
|
def test_pgp_require_fingerprint_match(vectors):
|
|
"""
|
|
Verifies the safety check require_fingerprint_match enforces exact matches.
|
|
"""
|
|
expected = vectors["pgp"]["expected_fingerprint"]
|
|
|
|
# Should not raise
|
|
pyhdwallet.require_fingerprint_match(expected, expected, "test")
|
|
|
|
# Should raise on mismatch
|
|
wrong = expected.replace("A", "B") if "A" in expected else expected.replace("0", "1")
|
|
with pytest.raises(ValueError, match="test: PGP fingerprint mismatch"):
|
|
pyhdwallet.require_fingerprint_match(wrong, expected, "test")
|
|
|
|
def test_bip39_seed_derivation(vectors):
|
|
"""
|
|
Verifies that mnemonics convert to seeds exactly as they did at bootstrap time.
|
|
"""
|
|
for case in vectors["bip39"]:
|
|
mnemonic = case["mnemonic"]
|
|
passphrase = case["passphrase"]
|
|
expected_hex = case["expected_seed_hex"]
|
|
|
|
# Verify internal Bip39SeedGenerator usage matches expected hex
|
|
actual_bytes = Bip39SeedGenerator(mnemonic).Generate(passphrase)
|
|
assert actual_bytes.hex() == expected_hex
|
|
|
|
def test_address_derivation_integrity(vectors):
|
|
"""
|
|
Verifies derive_all produces the exact same addresses for supported chains.
|
|
"""
|
|
for case in vectors["bip39"]:
|
|
seed_hex = case["expected_seed_hex"]
|
|
seed_bytes = bytes.fromhex(seed_hex)
|
|
|
|
for profile, expected_addresses in case["derived_addresses"].items():
|
|
# Infer count from ethereum addresses in the expected data
|
|
# (Ethereum always generates `count` addresses, unlike Solana which depends on profile)
|
|
count = len(expected_addresses.get("ethereum", []))
|
|
|
|
result = pyhdwallet.derive_all(
|
|
seed_bytes,
|
|
chains=["ethereum", "bitcoin", "solana"],
|
|
count=count,
|
|
sol_profile=profile,
|
|
export_private=False
|
|
)
|
|
assert result["addresses"] == expected_addresses, f"Mismatch for profile {profile}"
|
|
|
|
def test_cli_recover_smoke(tmp_path, vectors):
|
|
"""
|
|
Runs the CLI in a subprocess to verify end-to-end wiring
|
|
without network (recover mode).
|
|
"""
|
|
vector = vectors["bip39"][0]
|
|
mnemonic = vector["mnemonic"]
|
|
expected_fp = vectors["pgp"]["expected_fingerprint"]
|
|
recipient_file = DATA_DIR / "recipient.asc"
|
|
|
|
# 1. Successful Recovery
|
|
cmd = [
|
|
sys.executable, "src/pyhdwallet.py", "recover",
|
|
"--mnemonic-stdin",
|
|
"--pgp-pubkey-file", str(recipient_file),
|
|
"--expected-fingerprint", expected_fp,
|
|
"--file",
|
|
"--wallet-location", str(tmp_path),
|
|
"--off-screen",
|
|
"--zip-password-mode", "auto"
|
|
]
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
input=mnemonic.encode("utf-8"),
|
|
capture_output=True
|
|
)
|
|
|
|
assert result.returncode == 0, f"CLI failed: {result.stderr.decode()}"
|
|
zips = list(tmp_path.glob("*.zip"))
|
|
assert len(zips) == 1, "Expected exactly one zip file output"
|
|
|
|
# 2. Failed Recovery (Wrong Fingerprint)
|
|
wrong_fp = expected_fp.replace("A", "B")
|
|
cmd_fail = list(cmd)
|
|
cmd_fail[cmd_fail.index(expected_fp)] = wrong_fp
|
|
|
|
result_fail = subprocess.run(
|
|
cmd_fail,
|
|
input=mnemonic.encode("utf-8"),
|
|
capture_output=True
|
|
)
|
|
|
|
assert result_fail.returncode != 0
|
|
assert b"fingerprint mismatch" in result_fail.stderr or b"fingerprint mismatch" in result_fail.stdout
|