radicle-reticulum/tests/test_identity.py

172 lines
6.2 KiB
Python

"""Tests for identity mapping."""
import tempfile
from pathlib import Path
import pytest
from radicle_reticulum.identity import (
RadicleIdentity,
_base58btc_encode,
_base58btc_decode,
)
class TestBase58:
"""Test base58btc encoding/decoding."""
def test_encode_decode_roundtrip(self):
"""Test that encode/decode are inverses."""
test_data = b"\x00\x01\x02\x03\x04\x05"
encoded = _base58btc_encode(test_data)
decoded = _base58btc_decode(encoded)
assert decoded == test_data
def test_encode_known_value(self):
"""Test encoding against known value."""
# "Hello" in base58btc
data = b"Hello"
encoded = _base58btc_encode(data)
assert encoded == "9Ajdvzr"
def test_preserves_leading_zeros(self):
"""Test that leading zeros are preserved."""
data = b"\x00\x00\x00test"
encoded = _base58btc_encode(data)
decoded = _base58btc_decode(encoded)
assert decoded == data
class TestRadicleIdentity:
"""Test RadicleIdentity class."""
def test_generate_creates_valid_identity(self):
"""Test that generate() creates a valid identity."""
identity = RadicleIdentity.generate()
assert identity.private_key is not None
assert identity.public_key is not None
assert identity.rns_identity is not None
assert len(identity.public_key_bytes) == 32
assert len(identity.rns_hash) == 16
def test_did_format(self):
"""Test that DID has correct format."""
identity = RadicleIdentity.generate()
did = identity.did
assert did.startswith("did:key:z6Mk")
assert identity.node_id == did
def test_did_roundtrip(self):
"""Test DID encoding/decoding roundtrip."""
identity = RadicleIdentity.generate()
original_did = identity.did
# Create identity from DID (public key only)
restored = RadicleIdentity.from_did(original_did)
assert restored.did == original_did
assert restored.public_key_bytes == identity.public_key_bytes
assert restored.private_key is None # DID import is public-only
assert restored.rns_identity is None # DID doesn't have X25519 key
def test_sign_and_verify(self):
"""Test signing and verification."""
identity = RadicleIdentity.generate()
message = b"test message"
signature = identity.sign(message)
assert len(signature) == 64 # Ed25519 signature size
assert identity.verify(signature, message)
assert not identity.verify(signature, b"wrong message")
def test_cannot_sign_without_private_key(self):
"""Test that signing fails without private key."""
identity = RadicleIdentity.generate()
public_only = RadicleIdentity.from_did(identity.did)
with pytest.raises(ValueError, match="Cannot sign without private key"):
public_only.sign(b"test")
def test_invalid_did_format(self):
"""Test that invalid DIDs are rejected."""
with pytest.raises(ValueError, match="Invalid DID format"):
RadicleIdentity.from_did("not:a:valid:did")
def test_rns_hash_is_stable(self):
"""Test that RNS hash is deterministic for same key."""
identity = RadicleIdentity.generate()
hash1 = identity.rns_hash_hex
# The hash should be consistent
assert identity.rns_hash_hex == hash1
assert len(hash1) == 32 # 16 bytes = 32 hex chars
def test_repr(self):
"""Test string representation."""
identity = RadicleIdentity.generate()
repr_str = repr(identity)
assert "RadicleIdentity" in repr_str
assert "with private key" in repr_str
public_only = RadicleIdentity.from_did(identity.did)
assert "public only" in repr(public_only)
class TestIdentityPersistence:
"""Test identity save/load."""
def test_save_and_load_roundtrip(self):
"""Saved identity reloads with same DID and RNS hash."""
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "identity"
original = RadicleIdentity.generate()
original.save(path)
loaded = RadicleIdentity.load(path)
assert loaded.did == original.did
assert loaded.rns_identity_hash_hex == original.rns_identity_hash_hex
assert loaded.private_key is not None
def test_save_creates_parent_dirs(self):
"""save() creates intermediate directories."""
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "subdir" / "nested" / "identity"
identity = RadicleIdentity.generate()
identity.save(path)
assert path.exists()
def test_load_missing_file_raises(self):
"""load() raises FileNotFoundError for missing path."""
with pytest.raises(FileNotFoundError):
RadicleIdentity.load("/nonexistent/path/identity")
def test_load_or_generate_creates_on_first_run(self):
"""load_or_generate() creates and saves a new identity when absent."""
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "identity"
assert not path.exists()
identity = RadicleIdentity.load_or_generate(path)
assert path.exists()
assert identity.did.startswith("did:key:z6Mk")
def test_load_or_generate_stable_across_calls(self):
"""load_or_generate() returns the same identity on subsequent calls."""
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "identity"
first = RadicleIdentity.load_or_generate(path)
second = RadicleIdentity.load_or_generate(path)
assert first.did == second.did
assert first.rns_identity_hash_hex == second.rns_identity_hash_hex
def test_save_public_only_raises(self):
"""save() raises ValueError for public-key-only identities."""
identity = RadicleIdentity.from_did(
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
)
with tempfile.TemporaryDirectory() as tmpdir:
with pytest.raises(ValueError, match="public-key-only"):
identity.save(Path(tmpdir) / "identity")