"""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")