diff --git a/crates/iota-sdk-crypto/src/multisig.rs b/crates/iota-sdk-crypto/src/multisig.rs index c1f56e22a6..cb395946c3 100644 --- a/crates/iota-sdk-crypto/src/multisig.rs +++ b/crates/iota-sdk-crypto/src/multisig.rs @@ -404,3 +404,191 @@ fn multisig_pubkey_and_signature_from_user_signature( _ => Err(SignatureError::from_source("unknown signature scheme")), } } + +#[cfg(test)] +mod tests { + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + + // --- BitmapIndices --- + + #[test] + fn bitmap_indices_empty_bitmap() { + let indices: Vec = BitmapIndices::new(0).collect(); + assert!(indices.is_empty(), "empty bitmap should yield no indices"); + } + + #[test] + fn bitmap_indices_single_bit() { + // 0b0001 = bit 0 set + let indices: Vec = BitmapIndices::new(1).collect(); + assert_eq!(indices, vec![0]); + } + + #[test] + fn bitmap_indices_multiple_bits() { + // 22 = 0b10110 => bits 1, 2, 4 are set + let indices: Vec = BitmapIndices::new(22).collect(); + assert_eq!(indices, vec![1, 2, 4]); + } + + #[test] + fn bitmap_indices_all_low_bits() { + // 0b1111 = 15 => bits 0, 1, 2, 3 + let indices: Vec = BitmapIndices::new(0b1111).collect(); + assert_eq!(indices, vec![0, 1, 2, 3]); + } + + #[test] + fn bitmap_indices_high_bit() { + // bit 15 set (highest for u16) + let indices: Vec = BitmapIndices::new(1 << 15).collect(); + assert_eq!(indices, vec![15]); + } + + #[test] + fn bitmap_indices_non_contiguous() { + // 0b101010101 = bits 0, 2, 4, 6, 8 + let indices: Vec = BitmapIndices::new(0b101010101).collect(); + assert_eq!(indices, vec![0, 2, 4, 6, 8]); + } + + #[test] + fn bitmap_indices_all_bits_set() { + let indices: Vec = BitmapIndices::new(u16::MAX).collect(); + let expected: Vec = (0..16).collect(); + assert_eq!(indices, expected); + } + + // --- MultisigVerifier / UserSignatureVerifier construction --- + + #[test] + fn multisig_verifier_default() { + let v = MultisigVerifier::new(); + let v2 = MultisigVerifier::default(); + assert_eq!(v, v2, "new() and default() should produce identical verifiers"); + } + + #[test] + fn user_signature_verifier_default() { + let v = UserSignatureVerifier::new(); + let v2 = UserSignatureVerifier::default(); + assert_eq!(v, v2); + } + + #[cfg(feature = "ed25519")] + #[test] + fn test_multisig_verifier_e2e_ed25519() { + use rand::rngs::OsRng; + use crate::ed25519::Ed25519PrivateKey; + use crate::Signer; // trait + use iota_types::{ + MultisigMember, MultisigMemberPublicKey, MultisigCommittee, + MultisigMemberSignature, MultisigAggregatedSignature + }; + + let msg = b"multisig test message"; + + // 1. Generate keys + let sk1 = Ed25519PrivateKey::generate(&mut OsRng); + let pk1 = sk1.public_key(); + let sk2 = Ed25519PrivateKey::generate(&mut OsRng); + let pk2 = sk2.public_key(); + let sk3 = Ed25519PrivateKey::generate(&mut OsRng); + let pk3 = sk3.public_key(); + + // 2. Create Committee (threshold 2) + let m1 = MultisigMember::new(MultisigMemberPublicKey::Ed25519(pk1), 1); + let m2 = MultisigMember::new(MultisigMemberPublicKey::Ed25519(pk2), 1); + let m3 = MultisigMember::new(MultisigMemberPublicKey::Ed25519(pk3), 1); + + let committee = MultisigCommittee::new(vec![m1, m2, m3], 2); + + // 3. Sign + let sig1: iota_types::Ed25519Signature = sk1.try_sign(msg).unwrap(); + let sig2: iota_types::Ed25519Signature = sk2.try_sign(msg).unwrap(); + + // 4. Create Aggregated Signature + // Signers are at indices 0 and 1. + let signatures = vec![ + MultisigMemberSignature::Ed25519(sig1), + MultisigMemberSignature::Ed25519(sig2), + ]; + // Bitmap: indices 0 (bit0) and 1 (bit1) -> 0b11 = 3 + let bitmap = 3; + + let agg_sig = MultisigAggregatedSignature::new(committee.clone(), signatures, bitmap); + + // 5. Verify + let verifier = MultisigVerifier::new(); + assert!(verifier.verify(msg, &agg_sig).is_ok()); + + // 6. Test Failures + + // Bad Message + assert!(verifier.verify(b"bad msg", &agg_sig).is_err()); + + // Insufficient Weight (remove sig2) + // Indices: 0. Bitmap: 1. + let partial_sigs = vec![ + MultisigMemberSignature::Ed25519(sk1.try_sign(msg).unwrap()), + ]; + let partial_agg = MultisigAggregatedSignature::new(committee.clone(), partial_sigs, 1); + assert!(verifier.verify(msg, &partial_agg).is_err()); // Weight 1 < 2 + + // Mismatched Bitmap (Bitmap says 2 signatures, we provide 1) + // Bitmap 3 (2 bits), Sig vector len 1. + let bad_bitmap_sig = MultisigAggregatedSignature::new(committee.clone(), vec![MultisigMemberSignature::Ed25519(sk1.try_sign(msg).unwrap())], 3); + assert!(verifier.verify(msg, &bad_bitmap_sig).is_err()); + + // Invalid Member Signature (Signed by random key) + let sk_random = Ed25519PrivateKey::generate(&mut OsRng); + let sig_random: iota_types::Ed25519Signature = sk_random.try_sign(msg).unwrap(); + let invalid_sigs = vec![ + MultisigMemberSignature::Ed25519(sk1.try_sign(msg).unwrap()), + MultisigMemberSignature::Ed25519(sig_random), // Index 1 expects pk2, but we give sig from random + ]; + let invalid_agg = MultisigAggregatedSignature::new(committee.clone(), invalid_sigs, 3); + assert!(verifier.verify(msg, &invalid_agg).is_err()); + } + + #[cfg(feature = "ed25519")] + #[test] + fn test_multisig_aggregator_workflow() { + use rand::rngs::OsRng; + use crate::ed25519::Ed25519PrivateKey; + use crate::Signer; + use iota_types::{ + MultisigMember, MultisigMemberPublicKey, MultisigCommittee, + UserSignature // UserSignature enum used by aggregator + }; + + let msg = b"aggregator test"; + let sk1 = Ed25519PrivateKey::generate(&mut OsRng); + let sk2 = Ed25519PrivateKey::generate(&mut OsRng); + + let m1 = MultisigMember::new(MultisigMemberPublicKey::Ed25519(sk1.public_key()), 1); + let m2 = MultisigMember::new(MultisigMemberPublicKey::Ed25519(sk2.public_key()), 1); + let committee = MultisigCommittee::new(vec![m1, m2], 2); + + let personal_msg = iota_types::PersonalMessage(msg.into()); + + let mut aggregator = MultisigAggregator::new_with_message(committee, &personal_msg); + + let digest = personal_msg.signing_digest(); + let sig1_user: UserSignature = sk1.try_sign(digest.as_ref()).unwrap(); + let sig2_user: UserSignature = sk2.try_sign(digest.as_ref()).unwrap(); + + aggregator.add_signature(sig1_user).unwrap(); + assert!(aggregator.finish().is_err()); // Weight 1 < 2 + + aggregator.add_signature(sig2_user).unwrap(); + let agg_sig = aggregator.finish().unwrap(); + + // Verify + let verifier = MultisigVerifier::new(); + assert!(verifier.verify(digest.as_ref(), &agg_sig).is_ok()); + } +} diff --git a/crates/iota-sdk-types/src/checkpoint.rs b/crates/iota-sdk-types/src/checkpoint.rs index bce0c49d2a..a5dafa42a9 100644 --- a/crates/iota-sdk-types/src/checkpoint.rs +++ b/crates/iota-sdk-types/src/checkpoint.rs @@ -682,3 +682,30 @@ mod serialization { } } } + +#[cfg(test)] +mod coverage_tests { + use super::*; + use crate::Digest; + + #[test] + fn checkpoint_transaction_info_basic() { + let tx_digest = Digest::new([1; 32]); + let fx_digest = Digest::new([2; 32]); + let info = CheckpointTransactionInfo { + transaction: tx_digest, + effects: fx_digest, + signatures: vec![], + }; + + assert_eq!(info.transaction, tx_digest); + assert_eq!(info.effects, fx_digest); + assert_eq!(info.signatures.len(), 0); + + let clone = info.clone(); + assert_eq!(info, clone); + + let debug = format!("{:?}", info); + assert!(debug.contains("CheckpointTransactionInfo")); + } +} diff --git a/crates/iota-sdk-types/src/crypto/bls12381.rs b/crates/iota-sdk-types/src/crypto/bls12381.rs index 509b50076a..b544510cd1 100644 --- a/crates/iota-sdk-types/src/crypto/bls12381.rs +++ b/crates/iota-sdk-types/src/crypto/bls12381.rs @@ -222,3 +222,64 @@ impl std::fmt::Debug for Bls12381Signature { .finish() } } + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn test_bls12381_public_key_roundtrip() { + let bytes = [1u8; 96]; + let pk = Bls12381PublicKey::new(bytes); + + assert_eq!(pk.into_inner(), bytes); + assert_eq!(pk.inner(), &bytes); + assert_eq!(pk.as_bytes(), &bytes); + assert_eq!(AsRef::<[u8]>::as_ref(&pk), &bytes); + } + + #[test] + fn test_bls12381_public_key_display_debug() { + let bytes = [1u8; 96]; + let pk = Bls12381PublicKey::new(bytes); + + // Display should be base64 + let s = pk.to_string(); + assert!(!s.is_empty()); + let pk_from_str = Bls12381PublicKey::from_str(&s).unwrap(); + assert_eq!(pk, pk_from_str); + + // Debug + let debug = format!("{:?}", pk); + assert!(debug.contains("Bls12381PublicKey")); + assert!(debug.contains(&s)); + } + + #[test] + fn test_bls12381_signature_roundtrip() { + let bytes = [2u8; 48]; + let sig = Bls12381Signature::new(bytes); + + assert_eq!(sig.into_inner(), bytes); + assert_eq!(sig.inner(), &bytes); + assert_eq!(sig.as_bytes(), &bytes); + + let sig2 = Bls12381Signature::from_bytes(&bytes).unwrap(); + assert_eq!(sig, sig2); + } + + #[cfg(feature = "serde")] + #[test] + fn test_bls12381_serde() { + let pk = Bls12381PublicKey::new([1u8; 96]); + let json = serde_json::to_string(&pk).unwrap(); + let pk2: Bls12381PublicKey = serde_json::from_str(&json).unwrap(); + assert_eq!(pk, pk2); + + let sig = Bls12381Signature::new([2u8; 48]); + let json_sig = serde_json::to_string(&sig).unwrap(); + let sig2: Bls12381Signature = serde_json::from_str(&json_sig).unwrap(); + assert_eq!(sig, sig2); + } +} diff --git a/crates/iota-sdk-types/src/crypto/ed25519.rs b/crates/iota-sdk-types/src/crypto/ed25519.rs index a61d43f941..c5776a2713 100644 --- a/crates/iota-sdk-types/src/crypto/ed25519.rs +++ b/crates/iota-sdk-types/src/crypto/ed25519.rs @@ -228,3 +228,68 @@ impl std::fmt::Debug for Ed25519Signature { .finish() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::{PublicKeyExt, SignatureScheme}; + use std::str::FromStr; + + #[test] + fn test_ed25519_public_key_roundtrip() { + let bytes = [1u8; 32]; + let pk = Ed25519PublicKey::new(bytes); + + assert_eq!(pk.into_inner(), bytes); + assert_eq!(pk.inner(), &bytes); + assert_eq!(pk.as_bytes(), &bytes); + assert_eq!(AsRef::<[u8]>::as_ref(&pk), &bytes); + + // Scheme + assert_eq!(pk.scheme(), SignatureScheme::Ed25519); + } + + #[test] + fn test_ed25519_public_key_display_debug() { + let bytes = [1u8; 32]; + let pk = Ed25519PublicKey::new(bytes); + + // Display should be base64 + let s = pk.to_string(); + assert!(!s.is_empty()); + let pk_from_str = Ed25519PublicKey::from_str(&s).unwrap(); + assert_eq!(pk, pk_from_str); + + // Debug + let debug = format!("{:?}", pk); + assert!(debug.contains("Ed25519PublicKey")); + assert!(debug.contains(&s)); + } + + #[test] + fn test_ed25519_signature_roundtrip() { + let bytes = [2u8; 64]; + let sig = Ed25519Signature::new(bytes); + + assert_eq!(sig.into_inner(), bytes); + assert_eq!(sig.inner(), &bytes); + assert_eq!(sig.as_bytes(), &bytes); + + let sig2 = Ed25519Signature::from_bytes(&bytes).unwrap(); + assert_eq!(sig, sig2); + } + + #[cfg(feature = "serde")] + #[test] + fn test_ed25519_serde() { + let pk = Ed25519PublicKey::new([1u8; 32]); + let json = serde_json::to_string(&pk).unwrap(); + let pk2: Ed25519PublicKey = serde_json::from_str(&json).unwrap(); + assert_eq!(pk, pk2); + + let sig = Ed25519Signature::new([2u8; 64]); + let json_sig = serde_json::to_string(&sig).unwrap(); + let sig2: Ed25519Signature = serde_json::from_str(&json_sig).unwrap(); + assert_eq!(sig, sig2); + } +} diff --git a/crates/iota-sdk-types/src/crypto/intent.rs b/crates/iota-sdk-types/src/crypto/intent.rs index 47e279fb67..3d77b889d8 100644 --- a/crates/iota-sdk-types/src/crypto/intent.rs +++ b/crates/iota-sdk-types/src/crypto/intent.rs @@ -311,3 +311,81 @@ pub enum HashingIntentScope { #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct PersonalMessage<'a>(pub std::borrow::Cow<'a, [u8]>); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_intent_creation_accessors() { + let intent = Intent::new( + IntentScope::TransactionData, + IntentVersion::V0, + IntentAppId::Iota, + ); + assert_eq!(intent.scope(), IntentScope::TransactionData); + assert_eq!(intent.version(), IntentVersion::V0); + assert_eq!(intent.app_id(), IntentAppId::Iota); + } + + #[test] + fn test_intent_predefined_constructors() { + let tx = Intent::iota_transaction(); + assert_eq!(tx.scope, IntentScope::TransactionData); + assert_eq!(tx.app_id, IntentAppId::Iota); + + let pm = Intent::personal_message(); + assert_eq!(pm.scope, IntentScope::PersonalMessage); + + // iota_app generic + let app = Intent::iota_app(IntentScope::CheckpointSummary); + assert_eq!(app.scope, IntentScope::CheckpointSummary); + assert_eq!(app.app_id, IntentAppId::Iota); + + // consensus_app generic + let con = Intent::consensus_app(IntentScope::ConsensusBlock); + assert_eq!(con.scope, IntentScope::ConsensusBlock); + assert_eq!(con.app_id, IntentAppId::Consensus); + } + + #[test] + fn test_intent_serialization() { + let intent = Intent::iota_transaction(); + // 0, 0, 0 + let bytes = intent.to_bytes(); + assert_eq!(bytes, [0, 0, 0]); + + #[cfg(feature = "serde")] + { + let decoded = Intent::from_bytes(&bytes).unwrap(); + assert_eq!(decoded, intent); + + // From hex string + let s = "0x000000"; + let parsed: Intent = s.parse().unwrap(); + assert_eq!(parsed, intent); + + // Without prefix + let s = "000000"; + let parsed: Intent = s.parse().unwrap(); + assert_eq!(parsed, intent); + } + } + + #[cfg(feature = "serde")] + #[test] + fn test_intent_parsing_errors() { + // Wrong length + assert!(matches!(Intent::from_bytes(&[0, 0]), Err(IntentError::Bytes))); + + // Invalid values (if enum check works) + // IntentScope is repr(u8), but TryFrom impl uses bcs::from_bytes. + // bcs::from_bytes will fail if u8 value is not a valid variant. + // Valid variants are 0..9. + // Let's try 255. + // We can't construct invalid Intent easily via safe code, but from_bytes takes &[u8]. + let bytes = [255, 0, 0]; + // Note: IntentError::Scope is expected + assert!(matches!(Intent::from_bytes(&bytes), Err(IntentError::Scope))); + } +} diff --git a/crates/iota-sdk-types/src/crypto/secp256k1.rs b/crates/iota-sdk-types/src/crypto/secp256k1.rs index 11bbe13b6b..a128321946 100644 --- a/crates/iota-sdk-types/src/crypto/secp256k1.rs +++ b/crates/iota-sdk-types/src/crypto/secp256k1.rs @@ -230,3 +230,68 @@ impl std::fmt::Debug for Secp256k1Signature { .finish() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::{PublicKeyExt, SignatureScheme}; + use std::str::FromStr; + + #[test] + fn test_secp256k1_public_key_roundtrip() { + let bytes = [1u8; 33]; + let pk = Secp256k1PublicKey::new(bytes); + + assert_eq!(pk.into_inner(), bytes); + assert_eq!(pk.inner(), &bytes); + assert_eq!(pk.as_bytes(), &bytes); + assert_eq!(AsRef::<[u8]>::as_ref(&pk), &bytes); + + // Scheme + assert_eq!(pk.scheme(), SignatureScheme::Secp256k1); + } + + #[test] + fn test_secp256k1_public_key_display_debug() { + let bytes = [1u8; 33]; + let pk = Secp256k1PublicKey::new(bytes); + + // Display should be base64 + let s = pk.to_string(); + assert!(!s.is_empty()); + let pk_from_str = Secp256k1PublicKey::from_str(&s).unwrap(); + assert_eq!(pk, pk_from_str); + + // Debug + let debug = format!("{:?}", pk); + assert!(debug.contains("Secp256k1PublicKey")); + assert!(debug.contains(&s)); + } + + #[test] + fn test_secp256k1_signature_roundtrip() { + let bytes = [2u8; 64]; + let sig = Secp256k1Signature::new(bytes); + + assert_eq!(sig.into_inner(), bytes); + assert_eq!(sig.inner(), &bytes); + assert_eq!(sig.as_bytes(), &bytes); + + let sig2 = Secp256k1Signature::from_bytes(&bytes).unwrap(); + assert_eq!(sig, sig2); + } + + #[cfg(feature = "serde")] + #[test] + fn test_secp256k1_serde() { + let pk = Secp256k1PublicKey::new([1u8; 33]); + let json = serde_json::to_string(&pk).unwrap(); + let pk2: Secp256k1PublicKey = serde_json::from_str(&json).unwrap(); + assert_eq!(pk, pk2); + + let sig = Secp256k1Signature::new([2u8; 64]); + let json_sig = serde_json::to_string(&sig).unwrap(); + let sig2: Secp256k1Signature = serde_json::from_str(&json_sig).unwrap(); + assert_eq!(sig, sig2); + } +} diff --git a/crates/iota-sdk-types/src/crypto/secp256r1.rs b/crates/iota-sdk-types/src/crypto/secp256r1.rs index 2fbac5b361..43085f0afb 100644 --- a/crates/iota-sdk-types/src/crypto/secp256r1.rs +++ b/crates/iota-sdk-types/src/crypto/secp256r1.rs @@ -230,3 +230,68 @@ impl std::fmt::Debug for Secp256r1Signature { .finish() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::{PublicKeyExt, SignatureScheme}; + use std::str::FromStr; + + #[test] + fn test_secp256r1_public_key_roundtrip() { + let bytes = [1u8; 33]; + let pk = Secp256r1PublicKey::new(bytes); + + assert_eq!(pk.into_inner(), bytes); + assert_eq!(pk.inner(), &bytes); + assert_eq!(pk.as_bytes(), &bytes); + assert_eq!(AsRef::<[u8]>::as_ref(&pk), &bytes); + + // Scheme + assert_eq!(pk.scheme(), SignatureScheme::Secp256r1); + } + + #[test] + fn test_secp256r1_public_key_display_debug() { + let bytes = [1u8; 33]; + let pk = Secp256r1PublicKey::new(bytes); + + // Display should be base64 + let s = pk.to_string(); + assert!(!s.is_empty()); + let pk_from_str = Secp256r1PublicKey::from_str(&s).unwrap(); + assert_eq!(pk, pk_from_str); + + // Debug + let debug = format!("{:?}", pk); + assert!(debug.contains("Secp256r1PublicKey")); + assert!(debug.contains(&s)); + } + + #[test] + fn test_secp256r1_signature_roundtrip() { + let bytes = [2u8; 64]; + let sig = Secp256r1Signature::new(bytes); + + assert_eq!(sig.into_inner(), bytes); + assert_eq!(sig.inner(), &bytes); + assert_eq!(sig.as_bytes(), &bytes); + + let sig2 = Secp256r1Signature::from_bytes(&bytes).unwrap(); + assert_eq!(sig, sig2); + } + + #[cfg(feature = "serde")] + #[test] + fn test_secp256r1_serde() { + let pk = Secp256r1PublicKey::new([1u8; 33]); + let json = serde_json::to_string(&pk).unwrap(); + let pk2: Secp256r1PublicKey = serde_json::from_str(&json).unwrap(); + assert_eq!(pk, pk2); + + let sig = Secp256r1Signature::new([2u8; 64]); + let json_sig = serde_json::to_string(&sig).unwrap(); + let sig2: Secp256r1Signature = serde_json::from_str(&json_sig).unwrap(); + assert_eq!(sig, sig2); + } +} diff --git a/crates/iota-sdk-types/src/crypto/signature.rs b/crates/iota-sdk-types/src/crypto/signature.rs index b10ec37565..d5b884ccdb 100644 --- a/crates/iota-sdk-types/src/crypto/signature.rs +++ b/crates/iota-sdk-types/src/crypto/signature.rs @@ -983,3 +983,171 @@ mod serialization { } } } + +#[cfg(test)] +mod tests { + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + + // --- SignatureScheme from_byte --- + + #[test] + fn signature_scheme_from_byte_all_valid() { + let expected = [ + (0x00, SignatureScheme::Ed25519), + (0x01, SignatureScheme::Secp256k1), + (0x02, SignatureScheme::Secp256r1), + (0x03, SignatureScheme::Multisig), + (0x04, SignatureScheme::Bls12381), + (0x05, SignatureScheme::ZkLoginAuthenticator), + (0x06, SignatureScheme::PasskeyAuthenticator), + (0x07, SignatureScheme::MoveAuthenticator), + ]; + for (byte, scheme) in expected { + assert_eq!(SignatureScheme::from_byte(byte).unwrap(), scheme); + } + } + + #[test] + fn signature_scheme_from_byte_invalid() { + assert!(SignatureScheme::from_byte(0x08).is_err()); + assert!(SignatureScheme::from_byte(0xFF).is_err()); + } + + #[test] + fn signature_scheme_to_u8_roundtrip() { + let schemes = [ + SignatureScheme::Ed25519, + SignatureScheme::Secp256k1, + SignatureScheme::Secp256r1, + SignatureScheme::Multisig, + SignatureScheme::Bls12381, + SignatureScheme::ZkLoginAuthenticator, + SignatureScheme::PasskeyAuthenticator, + SignatureScheme::MoveAuthenticator, + ]; + for scheme in schemes { + let byte = scheme.to_u8(); + assert_eq!(SignatureScheme::from_byte(byte).unwrap(), scheme); + } + } + + #[test] + fn signature_scheme_display() { + assert_eq!(SignatureScheme::Ed25519.to_string(), "ed25519"); + assert_eq!(SignatureScheme::Secp256k1.to_string(), "secp256k1"); + assert_eq!(SignatureScheme::Secp256r1.to_string(), "secp256r1"); + assert_eq!(SignatureScheme::Multisig.to_string(), "multisig"); + assert_eq!(SignatureScheme::Bls12381.to_string(), "bls12381"); + } + + // --- SignatureScheme is_* methods --- + + #[test] + fn signature_scheme_is_methods() { + assert!(SignatureScheme::Ed25519.is_ed25519()); + assert!(!SignatureScheme::Ed25519.is_secp256k1()); + assert!(SignatureScheme::Secp256k1.is_secp256k1()); + assert!(SignatureScheme::Secp256r1.is_secp256r1()); + assert!(SignatureScheme::Multisig.is_multisig()); + assert!(SignatureScheme::Bls12381.is_bls12381()); + assert!(SignatureScheme::ZkLoginAuthenticator.is_zk_login_authenticator()); + assert!(SignatureScheme::PasskeyAuthenticator.is_passkey_authenticator()); + assert!(SignatureScheme::MoveAuthenticator.is_move_authenticator()); + } + + // --- InvalidSignatureScheme Display --- + + #[test] + fn invalid_signature_scheme_display() { + let err = InvalidSignatureScheme(0xFF); + assert_eq!(err.to_string(), "invalid signature scheme: ff"); + } + + // --- SimpleSignature --- + + #[test] + fn simple_signature_scheme_method() { + let ed = SimpleSignature::Ed25519 { + signature: Ed25519Signature::new([0; Ed25519Signature::LENGTH]), + public_key: Ed25519PublicKey::new([0; Ed25519PublicKey::LENGTH]), + }; + assert_eq!(ed.scheme(), SignatureScheme::Ed25519); + + let k1 = SimpleSignature::Secp256k1 { + signature: Secp256k1Signature::new([0; Secp256k1Signature::LENGTH]), + public_key: Secp256k1PublicKey::new([0; Secp256k1PublicKey::LENGTH]), + }; + assert_eq!(k1.scheme(), SignatureScheme::Secp256k1); + + let r1 = SimpleSignature::Secp256r1 { + signature: Secp256r1Signature::new([0; Secp256r1Signature::LENGTH]), + public_key: Secp256r1PublicKey::new([0; Secp256r1PublicKey::LENGTH]), + }; + assert_eq!(r1.scheme(), SignatureScheme::Secp256r1); + } + + #[test] + fn simple_signature_is_methods() { + let ed = SimpleSignature::Ed25519 { + signature: Ed25519Signature::new([0; Ed25519Signature::LENGTH]), + public_key: Ed25519PublicKey::new([0; Ed25519PublicKey::LENGTH]), + }; + assert!(ed.is_ed25519()); + assert!(!ed.is_secp256k1()); + assert!(!ed.is_secp256r1()); + } + + #[test] + fn simple_signature_ed25519_accessors() { + let sig = Ed25519Signature::new([0xAA; Ed25519Signature::LENGTH]); + let pk = Ed25519PublicKey::new([0xBB; Ed25519PublicKey::LENGTH]); + let ss = SimpleSignature::Ed25519 { + signature: sig, + public_key: pk, + }; + assert!(ss.as_ed25519_sig_opt().is_some()); + assert!(ss.as_ed25519_pub_key_opt().is_some()); + assert!(ss.as_secp256k1_sig_opt().is_none()); + assert!(ss.as_secp256r1_sig_opt().is_none()); + } + + #[test] + fn simple_signature_into_ed25519() { + let sig = Ed25519Signature::new([0xAA; Ed25519Signature::LENGTH]); + let pk = Ed25519PublicKey::new([0xBB; Ed25519PublicKey::LENGTH]); + let ss = SimpleSignature::Ed25519 { + signature: sig, + public_key: pk, + }; + let (s, p) = ss.into_ed25519(); + assert_eq!(s, sig); + assert_eq!(p, pk); + } + + #[test] + fn simple_signature_into_secp256k1_returns_none_for_ed25519() { + let ss = SimpleSignature::Ed25519 { + signature: Ed25519Signature::new([0; Ed25519Signature::LENGTH]), + public_key: Ed25519PublicKey::new([0; Ed25519PublicKey::LENGTH]), + }; + assert!(ss.into_secp256k1_opt().is_none()); + } + + // --- UserSignature scheme dispatching --- + + #[test] + fn user_signature_simple_scheme() { + let ed = SimpleSignature::Ed25519 { + signature: Ed25519Signature::new([0; Ed25519Signature::LENGTH]), + public_key: Ed25519PublicKey::new([0; Ed25519PublicKey::LENGTH]), + }; + let us = UserSignature::Simple(ed); + assert_eq!(us.scheme(), SignatureScheme::Ed25519); + assert!(us.is_simple()); + assert!(!us.is_multisig()); + assert!(!us.is_zklogin_authenticator()); + } +} diff --git a/crates/iota-sdk-types/src/digest.rs b/crates/iota-sdk-types/src/digest.rs index 7a459f83da..aaf9ad937f 100644 --- a/crates/iota-sdk-types/src/digest.rs +++ b/crates/iota-sdk-types/src/digest.rs @@ -238,6 +238,132 @@ mod tests { assert_eq!(digest, d); } + // --- Construction --- + + #[test] + fn new_and_inner_roundtrip() { + let bytes = [0xABu8; 32]; + let digest = Digest::new(bytes); + assert_eq!(*digest.inner(), bytes); + assert_eq!(digest.into_inner(), bytes); + } + + #[test] + fn zero_constant_is_all_zeros() { + assert_eq!(Digest::ZERO, Digest::new([0u8; 32])); + assert_eq!(Digest::ZERO.as_bytes(), &[0u8; 32]); + } + + // --- from_bytes --- + + #[test] + fn from_bytes_valid_32_bytes() { + let bytes = vec![0xFFu8; 32]; + let digest = Digest::from_bytes(&bytes).unwrap(); + assert_eq!(digest.as_bytes(), &bytes[..]); + } + + #[test] + fn from_bytes_invalid_length_too_short() { + let bytes = vec![0u8; 31]; + let result = Digest::from_bytes(&bytes); + assert!(result.is_err()); + } + + #[test] + fn from_bytes_invalid_length_too_long() { + let bytes = vec![0u8; 33]; + let result = Digest::from_bytes(&bytes); + assert!(result.is_err()); + } + + // --- from_base58 --- + + #[test] + fn from_base58_invalid_string() { + // "0OIl" contains characters invalid in Base58 + let result = Digest::from_base58("0OIl"); + assert!(result.is_err()); + } + + #[test] + fn from_base58_too_long() { + // A valid Base58 string that decodes to more than 32 bytes + let long_b58 = "1111111111111111111111111111111111111111111111111111111111111111111111"; + let result = Digest::from_base58(long_b58); + assert!(result.is_err()); + } + + // --- Display, Debug, LowerHex formatting --- + + #[test] + fn lower_hex_without_prefix() { + let digest = Digest::new([0x01; 32]); + let hex = format!("{:x}", digest); + assert_eq!(hex.len(), 64, "hex without prefix should be 64 chars"); + assert!(!hex.starts_with("0x")); + assert!(hex.chars().all(|c| c.is_ascii_hexdigit())); + } + + #[test] + fn lower_hex_with_alternate_prefix() { + let digest = Digest::new([0x01; 32]); + let hex = format!("{:#x}", digest); + assert!( + hex.starts_with("0x"), + "alternate hex should have 0x prefix" + ); + assert_eq!(hex.len(), 66); + } + + #[test] + fn debug_format_includes_display() { + let digest = Digest::new([0x01; 32]); + let debug = format!("{:?}", digest); + let display = format!("{}", digest); + // Debug format wraps the display inside Digest("...") + assert!( + debug.contains(&display), + "Debug format should contain the Display string" + ); + assert!(debug.starts_with("Digest(")); + } + + // --- Conversions --- + + #[test] + fn from_byte_array() { + let bytes = [0xCD; 32]; + let digest = Digest::from(bytes); + assert_eq!(digest, Digest::new(bytes)); + } + + #[test] + fn into_byte_array() { + let bytes = [0xEF; 32]; + let digest = Digest::new(bytes); + let out: [u8; 32] = digest.into(); + assert_eq!(out, bytes); + } + + #[test] + fn as_ref_returns_slice() { + let bytes = [0x99; 32]; + let digest = Digest::new(bytes); + let slice: &[u8] = digest.as_ref(); + assert_eq!(slice, &bytes[..]); + } + + #[test] + fn as_ref_returns_array() { + let bytes = [0x88; 32]; + let digest = Digest::new(bytes); + let arr: &[u8; 32] = digest.as_ref(); + assert_eq!(arr, &bytes); + } + + // --- Existing tests below --- + #[test] fn test_lexical_order() { fn digest_from_str(s: &str) -> Digest { diff --git a/crates/iota-sdk-types/src/effects/mod.rs b/crates/iota-sdk-types/src/effects/mod.rs index 4abeb02be7..eb22e55ca9 100644 --- a/crates/iota-sdk-types/src/effects/mod.rs +++ b/crates/iota-sdk-types/src/effects/mod.rs @@ -162,3 +162,56 @@ mod serialization { } } } + +#[cfg(test)] +mod tests_accessors { + use super::*; + use crate::{ + Digest, GasCostSummary, + execution_status::ExecutionStatus, + }; + + fn create_v1_effects() -> TransactionEffectsV1 { + TransactionEffectsV1 { + status: ExecutionStatus::Success, + epoch: 10, + gas_used: GasCostSummary { + computation_cost: 100, + computation_cost_burned: 50, + storage_cost: 200, + storage_rebate: 50, + non_refundable_storage_fee: 10, + }, + transaction_digest: Digest::new([1; 32]), + gas_object_index: Some(0), + events_digest: None, + dependencies: vec![], + lamport_version: 5, + changed_objects: vec![], + unchanged_shared_objects: vec![], + auxiliary_data_digest: None, + } + } + + #[test] + fn test_accessors() { + let v1 = create_v1_effects(); + let effects = TransactionEffects::V1(Box::new(v1.clone())); + + assert_eq!(effects.status(), &ExecutionStatus::Success); + assert_eq!(effects.epoch(), 10); + + let gas = effects.gas_summary(); + assert_eq!(gas.computation_cost, 100); + assert_eq!(gas.storage_cost, 200); + + // Test as_v1 + let v1_ref = effects.as_v1(); + assert_eq!(v1_ref.epoch, 10); + + // Test into_v1 + let v1_owned = effects.into_v1(); + assert_eq!(v1_owned.epoch, 10); + assert_eq!(v1_owned, v1); + } +} diff --git a/crates/iota-sdk-types/src/effects/v1.rs b/crates/iota-sdk-types/src/effects/v1.rs index 0be15e0589..545adeecbc 100644 --- a/crates/iota-sdk-types/src/effects/v1.rs +++ b/crates/iota-sdk-types/src/effects/v1.rs @@ -897,3 +897,126 @@ mod serialization { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::Digest; + use crate::object::Owner; + use crate::Address; + + // Mock helpers + fn mock_digest() -> Digest { + Digest::new([1u8; 32]) + } + + fn mock_owner() -> Owner { + Owner::Address(Address::ZERO) + } + + #[test] + fn test_object_in_accessors() { + let digest = mock_digest(); + let owner = mock_owner(); + let version = 10; + + let obj = ObjectIn::Data { version, digest, owner }; + + assert_eq!(obj.version(), version); + assert_eq!(obj.digest(), digest); + assert_eq!(obj.owner(), owner); + + assert_eq!(obj.version_opt(), Some(version)); + assert_eq!(obj.digest_opt(), Some(digest)); + assert_eq!(obj.owner_opt(), Some(owner)); + + let missing = ObjectIn::Missing; + assert!(missing.version_opt().is_none()); + assert!(missing.digest_opt().is_none()); + assert!(missing.owner_opt().is_none()); + } + + #[test] + #[should_panic(expected = "object does not exist")] + fn test_object_in_panic_version() { + ObjectIn::Missing.version(); + } + + #[test] + fn test_object_out_accessors() { + let digest = mock_digest(); + let owner = mock_owner(); + let version = 20; + + // ObjectWrite + let obj = ObjectOut::ObjectWrite { digest, owner }; + assert_eq!(obj.object_digest(), digest); + assert_eq!(obj.object_owner(), owner); + assert!(obj.package_version_opt().is_none()); + + // PackageWrite + let pkg = ObjectOut::PackageWrite { version, digest }; + assert_eq!(pkg.package_version(), version); + assert_eq!(pkg.package_digest(), digest); + assert!(pkg.object_owner_opt().is_none()); + } + + #[cfg(feature = "serde")] + #[test] + fn test_serialization_object_in() { + let digest = mock_digest(); + let owner = mock_owner(); + let version = 10; + + let obj_in = ObjectIn::Data { version, digest, owner }; + + // JSON (Human Readable) + let json = serde_json::to_string(&obj_in).unwrap(); + // Verify structure (snake_case state) + assert!(json.contains("\"state\":\"data\"")); + let de: ObjectIn = serde_json::from_str(&json).unwrap(); + assert_eq!(obj_in, de); + + // BCS (Binary) + let bytes = bcs::to_bytes(&obj_in).unwrap(); + let de_bin: ObjectIn = bcs::from_bytes(&bytes).unwrap(); + assert_eq!(obj_in, de_bin); + } + + #[cfg(feature = "serde")] + #[test] + fn test_serialization_object_out() { + let digest = mock_digest(); + let owner = mock_owner(); + // let version = 20; + + let obj_out = ObjectOut::ObjectWrite { digest, owner }; + + let json = serde_json::to_string(&obj_out).unwrap(); + assert!(json.contains("\"state\":\"object_write\"")); + let de: ObjectOut = serde_json::from_str(&json).unwrap(); + assert_eq!(obj_out, de); + + let bytes = bcs::to_bytes(&obj_out).unwrap(); + let de_bin: ObjectOut = bcs::from_bytes(&bytes).unwrap(); + assert_eq!(obj_out, de_bin); + } + + #[cfg(feature = "serde")] + #[test] + fn test_serialization_unchanged_shared() { + let digest = mock_digest(); + let version = 30; + + let kind = UnchangedSharedKind::ReadOnlyRoot { version, digest }; + + let json = serde_json::to_string(&kind).unwrap(); + assert!(json.contains("\"kind\":\"read_only_root\"")); + let de: UnchangedSharedKind = serde_json::from_str(&json).unwrap(); + assert_eq!(kind, de); + + let bytes = bcs::to_bytes(&kind).unwrap(); + let de_bin: UnchangedSharedKind = bcs::from_bytes(&bytes).unwrap(); + assert_eq!(kind, de_bin); + } +} diff --git a/crates/iota-sdk-types/src/execution_status.rs b/crates/iota-sdk-types/src/execution_status.rs index 8cbe5e3cce..26d7640407 100644 --- a/crates/iota-sdk-types/src/execution_status.rs +++ b/crates/iota-sdk-types/src/execution_status.rs @@ -1670,3 +1670,319 @@ mod serialization { } } } + +#[cfg(test)] +mod tests { + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + + // --- ExecutionStatus --- + + #[test] + fn execution_status_success() { + let status = ExecutionStatus::Success; + assert!(status.is_success()); + assert!(!status.is_failure()); + assert!(status.error().is_none()); + assert!(status.error_command().is_none()); + } + + #[test] + fn execution_status_failure_with_command() { + let status = ExecutionStatus::Failure { + error: ExecutionError::InsufficientGas, + command: Some(3), + }; + assert!(!status.is_success()); + assert!(status.is_failure()); + assert_eq!(status.error(), Some(&ExecutionError::InsufficientGas)); + assert_eq!(status.error_command(), Some(3)); + } + + #[test] + fn execution_status_failure_without_command() { + let status = ExecutionStatus::Failure { + error: ExecutionError::InvalidGasObject, + command: None, + }; + assert!(status.is_failure()); + assert!(status.error().is_some()); + assert!(status.error_command().is_none()); + } + + // --- ExecutionError is_* methods --- + + #[test] + fn execution_error_is_methods() { + assert!(ExecutionError::InsufficientGas.is_insufficient_gas()); + assert!(!ExecutionError::InsufficientGas.is_invalid_gas_object()); + + assert!(ExecutionError::InvalidGasObject.is_invalid_gas_object()); + assert!(ExecutionError::InvariantViolation.is_invariant_violation()); + assert!(ExecutionError::FeatureNotYetSupported.is_feature_not_yet_supported()); + assert!(ExecutionError::InsufficientCoinBalance.is_insufficient_coin_balance()); + assert!(ExecutionError::CoinBalanceOverflow.is_coin_balance_overflow()); + assert!(ExecutionError::PublishErrorNonZeroAddress.is_publish_error_non_zero_address()); + assert!(ExecutionError::IotaMoveVerificationError.is_iota_move_verification_error()); + assert!( + ExecutionError::VmVerificationOrDeserializationError + .is_vm_verification_or_deserialization_error() + ); + assert!(ExecutionError::VmInvariantViolation.is_vm_invariant_violation()); + assert!(ExecutionError::FunctionNotFound.is_function_not_found()); + assert!(ExecutionError::ArityMismatch.is_arity_mismatch()); + assert!(ExecutionError::TypeArityMismatch.is_type_arity_mismatch()); + assert!(ExecutionError::NonEntryFunctionInvoked.is_non_entry_function_invoked()); + assert!(ExecutionError::InvalidTransferObject.is_invalid_transfer_object()); + assert!(ExecutionError::CertificateDenied.is_certificate_denied()); + assert!( + ExecutionError::SharedObjectOperationNotAllowed + .is_shared_object_operation_not_allowed() + ); + assert!(ExecutionError::InputObjectDeleted.is_input_object_deleted()); + assert!( + ExecutionError::ExecutionCancelledDueToRandomnessUnavailable + .is_execution_cancelled_due_to_randomness_unavailable() + ); + } + + #[test] + fn execution_error_with_data_variants() { + let err = ExecutionError::ObjectTooBig { + object_size: 100, + max_object_size: 50, + }; + assert!(err.is_object_too_big()); + assert!(!err.is_package_too_big()); + + let err = ExecutionError::MoveAbort { + location: MoveLocation { + package: ObjectId::ZERO, + module: "test".parse().unwrap(), + function: 1, + instruction: 2, + function_name: None, + }, + code: 42, + }; + assert!(err.is_move_abort()); + } + + // --- CommandArgumentError is_* methods --- + + #[test] + fn command_argument_error_is_methods() { + assert!(CommandArgumentError::TypeMismatch.is_type_mismatch()); + assert!(!CommandArgumentError::TypeMismatch.is_invalid_bcs_bytes()); + assert!(CommandArgumentError::InvalidBcsBytes.is_invalid_bcs_bytes()); + assert!( + CommandArgumentError::InvalidUsageOfPureArgument.is_invalid_usage_of_pure_argument() + ); + assert!(CommandArgumentError::InvalidGasCoinUsage.is_invalid_gas_coin_usage()); + assert!(CommandArgumentError::InvalidValueUsage.is_invalid_value_usage()); + assert!(CommandArgumentError::InvalidObjectByValue.is_invalid_object_by_value()); + assert!(CommandArgumentError::InvalidObjectByMutRef.is_invalid_object_by_mut_ref()); + assert!( + CommandArgumentError::SharedObjectOperationNotAllowed + .is_shared_object_operation_not_allowed() + ); + } + + // --- TypeArgumentError --- + + #[test] + fn type_argument_error_is_methods() { + assert!(TypeArgumentError::TypeNotFound.is_type_not_found()); + assert!(!TypeArgumentError::TypeNotFound.is_constraint_not_satisfied()); + assert!(TypeArgumentError::ConstraintNotSatisfied.is_constraint_not_satisfied()); + } + + // --- PackageUpgradeError --- + + #[test] + fn package_upgrade_error_is_methods() { + assert!(PackageUpgradeError::IncompatibleUpgrade.is_incompatible_upgrade()); + assert!(!PackageUpgradeError::IncompatibleUpgrade.is_not_a_package()); + let err = PackageUpgradeError::UnknownUpgradePolicy { policy: 99 }; + assert!(err.is_unknown_upgrade_policy()); + } +} + +#[cfg(test)] +#[cfg(test)] +mod serialization_tests { + use super::*; + use serde_json::json; + + #[test] + fn test_execution_status_success_roundtrip() { + let status = ExecutionStatus::Success; + + // JSON (Human Readable) + let json = serde_json::to_value(&status).unwrap(); + // Verify the custom serialization format + assert_eq!(json, json!({ "success": true })); + + // Roundtrip back + let decoded: ExecutionStatus = serde_json::from_value(json).unwrap(); + assert_eq!(decoded, status); + + // BCS (Binary) + let bcs = bcs::to_bytes(&status).unwrap(); + // BinaryExecutionStatus::Success is variant 0 + assert_eq!(bcs, vec![0]); + let decoded: ExecutionStatus = bcs::from_bytes(&bcs).unwrap(); + assert_eq!(decoded, status); + } + + #[test] + fn test_execution_status_failure_roundtrip() { + let error = ExecutionError::InsufficientGas; + let status = ExecutionStatus::Failure { + error: error.clone(), + command: Some(1), + }; + + // JSON (Human Readable) + let json = serde_json::to_value(&status).unwrap(); + // Readable uses "status": { "error": ..., "command": ... } and "success": false + assert_eq!(json["success"], false); + assert_eq!(json["status"]["error"]["error"], "insufficient_gas"); + assert_eq!(json["status"]["command"], 1); + + let decoded: ExecutionStatus = serde_json::from_value(json).unwrap(); + assert_eq!(decoded, status); + + // BCS (Binary) + let bcs = bcs::to_bytes(&status).unwrap(); + // BinaryExecutionStatus::Failure is variant 1 + // Followed by ExecutionError::InsufficientGas (variant 0) + // Followed by Option (1 = 0x01, value 1u64 = 0x0100000000000000) + let expected_bcs = vec![ + 1, // Failure variant + 0, // InsufficientGas variant + 1, // Option::Some + 1, 0, 0, 0, 0, 0, 0, 0 // 1u64 + ]; + assert_eq!(bcs, expected_bcs); + + let decoded: ExecutionStatus = bcs::from_bytes(&bcs).unwrap(); + assert_eq!(decoded, status); + } + + #[test] + fn test_execution_status_failure_no_command() { + let error = ExecutionError::InvalidGasObject; + let status = ExecutionStatus::Failure { + error: error.clone(), + command: None, + }; + + // JSON + let json = serde_json::to_value(&status).unwrap(); + assert_eq!(json["success"], false); + assert_eq!(json["status"]["error"]["error"], "invalid_gas_object"); + assert!(json["status"].get("command").is_none()); + + let decoded: ExecutionStatus = serde_json::from_value(json).unwrap(); + assert_eq!(decoded, status); + + // Accessors + assert_eq!(status.error(), Some(&error)); + assert_eq!(status.error_command(), None); + } + + #[test] + fn test_execution_error_complex_variants() { + // Test AddressDeniedForCoin + let addr = Address::new([7u8; 32]); + let coin_type = "0x2::sui::SUI".to_string(); + let error = ExecutionError::AddressDeniedForCoin { + address: addr, + coin_type: coin_type.clone() + }; + + let json = serde_json::to_value(&error).unwrap(); + assert_eq!(json["error"], "address_denied_for_coin"); + assert_eq!(json["address"], addr.to_string()); + assert_eq!(json["coin_type"], coin_type); + + let decoded: ExecutionError = serde_json::from_value(json).unwrap(); + assert_eq!(decoded, error); + + // Test ObjectTooBig with ReadableDisplay (u64 as string) + let error = ExecutionError::ObjectTooBig { + object_size: 100, + max_object_size: 50, + }; + let json = serde_json::to_value(&error).unwrap(); + assert_eq!(json["error"], "object_too_big"); + assert_eq!(json["object_size"], "100"); + assert_eq!(json["max_object_size"], "50"); + + let decoded: ExecutionError = serde_json::from_value(json).unwrap(); + assert_eq!(decoded, error); + } + + #[test] + fn test_move_location_serialization() { + let pkg_id = ObjectId::new([2u8; 32]); + let module = Identifier::new("coin").unwrap(); + let func_name = Identifier::new("transfer").unwrap(); + + let location = MoveLocation { + package: pkg_id, + module: module.clone(), + function: 1, + instruction: 42, + function_name: Some(func_name.clone()), + }; + + let json = serde_json::to_value(&location).unwrap(); + assert_eq!(json["package"], pkg_id.to_string()); + assert_eq!(json["module"], "coin"); + assert_eq!(json["function"], 1); + assert_eq!(json["instruction"], 42); + assert_eq!(json["functionName"], "transfer"); + + let decoded: MoveLocation = serde_json::from_value(json).unwrap(); + assert_eq!(decoded, location); + } + + #[test] + fn test_command_argument_error() { + let arg_error = CommandArgumentError::IndexOutOfBounds { index: 5 }; + let iter_error = ExecutionError::CommandArgumentError { + argument: 2, + kind: arg_error.clone() + }; + + let json = serde_json::to_value(&iter_error).unwrap(); + assert_eq!(json["error"], "command_argument_error"); + assert_eq!(json["argument"], 2); + assert_eq!(json["kind"]["kind"], "index_out_of_bounds"); + assert_eq!(json["kind"]["index"], 5); + + let decoded: ExecutionError = serde_json::from_value(json).unwrap(); + assert_eq!(decoded, iter_error); + } + + #[test] + fn test_package_upgrade_error() { + let digest = Digest::new([9u8; 32]); + let upgrade_error = PackageUpgradeError::DigestDoesNotMatch { digest }; + let iter_error = ExecutionError::PackageUpgradeError { + kind: upgrade_error.clone() + }; + + let json = serde_json::to_value(&iter_error).unwrap(); + assert_eq!(json["error"], "package_upgrade_error"); + assert_eq!(json["kind"]["kind"], "digest_does_not_match"); + assert_eq!(json["kind"]["digest"], digest.to_string()); + + let decoded: ExecutionError = serde_json::from_value(json).unwrap(); + assert_eq!(decoded, iter_error); + } +} diff --git a/crates/iota-sdk-types/src/framework.rs b/crates/iota-sdk-types/src/framework.rs index 7f0d381959..5cbd726fce 100644 --- a/crates/iota-sdk-types/src/framework.rs +++ b/crates/iota-sdk-types/src/framework.rs @@ -75,3 +75,167 @@ impl std::fmt::Display for CoinFromObjectError { } impl std::error::Error for CoinFromObjectError {} + +#[cfg(test)] +mod tests { + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + + #[test] + fn coin_from_object_error_display_not_a_coin() { + let err = CoinFromObjectError::NotACoin; + assert_eq!(err.to_string(), "not a coin"); + } + + #[test] + fn coin_from_object_error_display_invalid_content_length() { + let err = CoinFromObjectError::InvalidContentLength; + assert_eq!(err.to_string(), "invalid content length"); + } + + #[test] + fn coin_from_object_error_is_methods() { + let not_a_coin = CoinFromObjectError::NotACoin; + assert!(not_a_coin.is_not_a_coin()); + assert!(!not_a_coin.is_invalid_content_length()); + + let invalid_len = CoinFromObjectError::InvalidContentLength; + assert!(!invalid_len.is_not_a_coin()); + assert!(invalid_len.is_invalid_content_length()); + } + + #[test] + fn coin_from_object_error_equality() { + assert_eq!(CoinFromObjectError::NotACoin, CoinFromObjectError::NotACoin); + assert_ne!( + CoinFromObjectError::NotACoin, + CoinFromObjectError::InvalidContentLength + ); + } + + #[test] + fn coin_from_object_error_clone() { + let err = CoinFromObjectError::InvalidContentLength; + let cloned = err; + assert_eq!(err, cloned); + } + + #[test] + fn test_coin_accessors() { + let id = ObjectId::new([1; 32]); + let coin_type = TypeTag::U64; // Just a dummy type + let coin = Coin { + coin_type: coin_type.clone(), + id, + balance: 1000, + }; + + assert_eq!(coin.coin_type(), &coin_type); + assert_eq!(coin.id(), &id); + assert_eq!(coin.balance(), 1000); + } + + #[test] + fn test_coin_try_from_object_success() { + use crate::{Object, ObjectData, Owner, Digest, MoveStruct, StructTag}; + + let id_bytes = [1u8; 32]; + let balance = 500u64; + let mut contents = Vec::new(); + contents.extend_from_slice(&id_bytes); + contents.extend_from_slice(&balance.to_le_bytes()); + + let coin_type_tag = TypeTag::U64; + let struct_tag = StructTag::new_coin(coin_type_tag.clone()); + + let move_struct = MoveStruct { + type_: struct_tag, + version: 1, + contents, + }; + + let object = Object::new( + ObjectData::Struct(move_struct), + Owner::Immutable, // Owner doesn't matter strictly for parsing + Digest::new([0; 32]), + 0, + ); + + let coin = Coin::try_from_object(&object).expect("Should parse coin"); + assert_eq!(coin.id(), &ObjectId::new(id_bytes)); + assert_eq!(coin.balance(), balance); + assert_eq!(coin.coin_type(), &coin_type_tag); + } + + #[test] + fn test_coin_try_from_object_not_a_coin() { + use crate::{Object, ObjectData, Owner, Digest, MoveStruct, StructTag}; + + // Struct that is NOT a coin + let struct_tag = StructTag::new( + crate::Address::ZERO, + crate::Identifier::new("foo").unwrap(), + crate::Identifier::new("Bar").unwrap(), + vec![], + ); + + let move_struct = MoveStruct { + type_: struct_tag, + version: 1, + contents: vec![], + }; + + let object = Object::new( + ObjectData::Struct(move_struct), + Owner::Immutable, + Digest::new([0; 32]), + 0, + ); + + let err = Coin::try_from_object(&object).unwrap_err(); + assert_eq!(err, CoinFromObjectError::NotACoin); + + // Package object + let object_pkg = Object::new( + ObjectData::Package(crate::MovePackage { + id: ObjectId::ZERO, + version: 1, + modules: std::collections::BTreeMap::new(), + type_origin_table: vec![], + linkage_table: std::collections::BTreeMap::new(), + }), + Owner::Immutable, + Digest::new([0; 32]), + 0, + ); + let err_pkg = Coin::try_from_object(&object_pkg).unwrap_err(); + assert_eq!(err_pkg, CoinFromObjectError::NotACoin); + } + + #[test] + fn test_coin_try_from_object_invalid_length() { + use crate::{Object, ObjectData, Owner, Digest, MoveStruct, StructTag}; + + let coin_type_tag = TypeTag::U64; + let struct_tag = StructTag::new_coin(coin_type_tag); + + // Contents too short + let move_struct = MoveStruct { + type_: struct_tag, + version: 1, + contents: vec![0u8; 10], // Needs 32 + 8 = 40 + }; + + let object = Object::new( + ObjectData::Struct(move_struct), + Owner::Immutable, + Digest::new([0; 32]), + 0, + ); + + let err = Coin::try_from_object(&object).unwrap_err(); + assert_eq!(err, CoinFromObjectError::InvalidContentLength); + } +} diff --git a/crates/iota-sdk-types/src/gas.rs b/crates/iota-sdk-types/src/gas.rs index a8bf1c93bb..9a520e4a9e 100644 --- a/crates/iota-sdk-types/src/gas.rs +++ b/crates/iota-sdk-types/src/gas.rs @@ -150,4 +150,64 @@ mod tests { println!("{}", serde_json::to_string(&actual).unwrap()); println!("{:?}", bcs::to_bytes(&actual).unwrap()); } + + #[test] + fn constructor_sets_all_fields() { + let summary = GasCostSummary::new(100, 80, 50, 30, 10); + assert_eq!(summary.computation_cost, 100); + assert_eq!(summary.computation_cost_burned, 80); + assert_eq!(summary.storage_cost, 50); + assert_eq!(summary.storage_rebate, 30); + assert_eq!(summary.non_refundable_storage_fee, 10); + } + + #[test] + fn gas_used_is_sum_of_computation_and_storage() { + let summary = GasCostSummary::new(100, 80, 50, 30, 10); + assert_eq!(summary.gas_used(), 150, "gas_used = computation + storage"); + } + + #[test] + fn net_gas_usage_positive_when_cost_exceeds_rebate() { + let summary = GasCostSummary::new(100, 80, 50, 30, 10); + // 150 - 30 = 120 + assert_eq!(summary.net_gas_usage(), 120); + } + + #[test] + fn net_gas_usage_negative_when_rebate_exceeds_cost() { + let summary = GasCostSummary::new(10, 5, 20, 100, 5); + // (10+20) - 100 = -70 + assert_eq!(summary.net_gas_usage(), -70); + } + + #[test] + fn net_gas_usage_zero_when_balanced() { + let summary = GasCostSummary::new(50, 30, 50, 100, 10); + // (50+50) - 100 = 0 + assert_eq!(summary.net_gas_usage(), 0); + } + + #[test] + fn default_is_all_zeros() { + let summary = GasCostSummary::default(); + assert_eq!(summary.computation_cost, 0); + assert_eq!(summary.computation_cost_burned, 0); + assert_eq!(summary.storage_cost, 0); + assert_eq!(summary.storage_rebate, 0); + assert_eq!(summary.non_refundable_storage_fee, 0); + assert_eq!(summary.gas_used(), 0); + assert_eq!(summary.net_gas_usage(), 0); + } + + #[test] + fn display_contains_all_field_names() { + let summary = GasCostSummary::new(1, 2, 3, 4, 5); + let display = summary.to_string(); + assert!(display.contains("computation_cost: 1")); + assert!(display.contains("computation_cost_burned: 2")); + assert!(display.contains("storage_cost: 3")); + assert!(display.contains("storage_rebate: 4")); + assert!(display.contains("non_refundable_storage_fee: 5")); + } } diff --git a/crates/iota-sdk-types/src/hash.rs b/crates/iota-sdk-types/src/hash.rs index 32f6f680e4..32d863641c 100644 --- a/crates/iota-sdk-types/src/hash.rs +++ b/crates/iota-sdk-types/src/hash.rs @@ -514,8 +514,134 @@ impl crate::ObjectId { mod tests { use test_strategy::proptest; - use super::HashingIntent; - use crate::SignatureScheme; + use super::*; + use crate::{ + Address, Ed25519PublicKey, MultisigCommittee, MultisigMember, MultisigMemberPublicKey, + PasskeyPublicKey, Secp256k1PublicKey, Secp256r1PublicKey, SignatureScheme, + }; + + // --- Hasher Tests --- + + #[test] + fn hasher_basic_usage() { + let mut hasher = Hasher::new(); + hasher.update(b"hello"); + hasher.update(b" world"); + let digest = hasher.finalize(); + + // BLAKE2b-256("hello world") + // Calculated via external tool: 256c83b297114d201b30179f3f0ef0cace9783622da5974326b436178aeef610 + let expected = + hex::decode("256c83b297114d201b30179f3f0ef0cace9783622da5974326b436178aeef610") + .unwrap(); + assert_eq!(digest.into_inner().to_vec(), expected); + } + + #[test] + fn hasher_convenience_digest() { + let digest = Hasher::digest(b"hello world"); + let expected = + hex::decode("256c83b297114d201b30179f3f0ef0cace9783622da5974326b436178aeef610") + .unwrap(); + assert_eq!(digest.into_inner().to_vec(), expected); + } + + #[test] + fn hasher_write_trait() { + use std::io::Write; + let mut hasher = Hasher::new(); + hasher.write_all(b"hello world").unwrap(); + hasher.flush().unwrap(); + let digest = hasher.finalize(); + let expected = + hex::decode("256c83b297114d201b30179f3f0ef0cace9783622da5974326b436178aeef610") + .unwrap(); + assert_eq!(digest.into_inner().to_vec(), expected); + } + + // --- Address Derivation Tests --- + + #[test] + fn ed25519_address_derivation() { + let pk_bytes = [0xAA; 32]; + let pk = Ed25519PublicKey::new(pk_bytes); + + // Ed25519 address = Blake2b256(pk_bytes) (No prefix) + let mut hasher = Hasher::new(); + hasher.update(pk_bytes); + let expected_addr = Address::new(hasher.finalize().into_inner()); + + assert_eq!(pk.derive_address(), expected_addr); + } + + #[test] + fn secp256k1_address_derivation() { + let pk_bytes = [0xBB; 33]; + let pk = Secp256k1PublicKey::new(pk_bytes); + + // Secp256k1 address = Blake2b256(0x01 || pk_bytes) + let mut hasher = Hasher::new(); + hasher.update([0x01]); + hasher.update(pk_bytes); + let expected_addr = Address::new(hasher.finalize().into_inner()); + + assert_eq!(pk.derive_address(), expected_addr); + } + + #[test] + fn secp256r1_address_derivation() { + let pk_bytes = [0xCC; 33]; + let pk = Secp256r1PublicKey::new(pk_bytes); + + // Secp256r1 address = Blake2b256(0x02 || pk_bytes) + let mut hasher = Hasher::new(); + hasher.update([0x02]); + hasher.update(pk_bytes); + let expected_addr = Address::new(hasher.finalize().into_inner()); + + assert_eq!(pk.derive_address(), expected_addr); + } + + #[test] + fn passkey_address_derivation() { + let pk_bytes = [0xDD; 33]; + let inner = Secp256r1PublicKey::new(pk_bytes); + let pk = PasskeyPublicKey::new(inner); + + // Passkey address = Blake2b256(0x06 || pk_bytes) + let mut hasher = Hasher::new(); + hasher.update([0x06]); + hasher.update(pk_bytes); + let expected_addr = Address::new(hasher.finalize().into_inner()); + + assert_eq!(pk.derive_address(), expected_addr); + } + + #[test] + fn multisig_address_derivation() { + // Create a simple multisig committee with one member + let pk_bytes = [0xAA; 32]; + let pk = Ed25519PublicKey::new(pk_bytes); + let member = MultisigMember::new(MultisigMemberPublicKey::Ed25519(pk), 1); + let committee = MultisigCommittee::new(vec![member], 1); + + // Multisig address logic verification + let addr = committee.derive_address(); + + // Manual verification + let mut hasher = Hasher::new(); + hasher.update([0x03]); // Multisig Scheme + hasher.update(1u16.to_le_bytes()); // Threshold + // Member 1: Ed25519 has no prefix in write_into_hasher + hasher.update(pk_bytes); + hasher.update(1u8.to_le_bytes()); // Weight + + let expected = Address::new(hasher.finalize().into_inner()); + + assert_eq!(addr, expected); + } + + // --- HashingIntent Tests (Ported from existing) --- impl HashingIntent { fn from_byte(byte: u8) -> Result { @@ -541,4 +667,87 @@ mod tests { fn roundtrip_hashing_intent(intent: HashingIntent) { assert_eq!(Ok(intent), HashingIntent::from_byte(intent as u8)); } + + #[test] + fn zklogin_address_derivation() { + use crate::ZkLoginPublicIdentifier; + use crate::crypto::Bn254FieldElement; + + let iss = "https://accounts.google.com".to_string(); + let seed_bytes = [1u8; 32]; + let address_seed = Bn254FieldElement::new(seed_bytes); + // Note: New returns Option, but we know inputs are valid + let zk_id = ZkLoginPublicIdentifier::new(iss.clone(), address_seed).unwrap(); + + // Manual derivation + let mut hasher = Hasher::new(); + hasher.update([0x05]); // ZkLogin Scheme + hasher.update([iss.len() as u8]); + hasher.update(iss.as_bytes()); + hasher.update(seed_bytes); // Padded + let expected = Address::new(hasher.finalize().into_inner()); + + assert_eq!(zk_id.derive_address_padded(), expected); + } + + #[test] + fn zklogin_address_derivation_unpadded() { + use crate::ZkLoginPublicIdentifier; + use crate::crypto::Bn254FieldElement; + + let iss = "https://accounts.google.com".to_string(); + let mut seed_bytes = [0u8; 32]; + seed_bytes[31] = 1; // 0x00...01 + let address_seed = Bn254FieldElement::new(seed_bytes); + let zk_id = ZkLoginPublicIdentifier::new(iss.clone(), address_seed).unwrap(); + + // Manual derivation unpadded + let mut hasher = Hasher::new(); + hasher.update([0x05]); // ZkLogin Scheme + hasher.update([iss.len() as u8]); + hasher.update(iss.as_bytes()); + hasher.update([1]); // Unpadded 0x01 (last byte of seed) + let expected = Address::new(hasher.finalize().into_inner()); + + assert_eq!(zk_id.derive_address_unpadded(), expected); + } + + #[test] + fn object_id_derivation() { + use crate::ObjectId; + let digest = Digest::new([1u8; 32]); + let count: u64 = 0; + let id = ObjectId::derive_id(digest, count); + + let mut hasher = Hasher::new(); + hasher.update([0xf1]); // RegularObjectId (HashingIntent::RegularObjectId) + hasher.update(digest); + hasher.update(count.to_le_bytes()); + let expected = ObjectId::new(hasher.finalize().into_inner()); + + assert_eq!(id, expected); + } + + #[test] + fn object_id_derive_dynamic_child() { + use crate::ObjectId; + let parent = ObjectId::new([2u8; 32]); + // Use parse for TypeTag + use crate::TypeTag; + let type_tag: TypeTag = "0x0::test::Test".parse().unwrap(); + let key_bytes = b"verification_key"; + + let child_id = parent.derive_dynamic_child_id(&type_tag, key_bytes); + + let mut hasher = Hasher::new(); + hasher.update([0xf0]); // ChildObjectId (HashingIntent::ChildObjectId) + hasher.update(parent); + hasher.update((key_bytes.len() as u64).to_le_bytes()); + hasher.update(key_bytes); + bcs::serialize_into(&mut hasher, &type_tag).unwrap(); + let expected = ObjectId::new(hasher.finalize().into_inner()); + + assert_eq!(child_id, expected); + } + } diff --git a/crates/iota-sdk-types/src/iota_names/config.rs b/crates/iota-sdk-types/src/iota_names/config.rs index 59c516bca2..d26bfae373 100644 --- a/crates/iota-sdk-types/src/iota_names/config.rs +++ b/crates/iota-sdk-types/src/iota_names/config.rs @@ -116,3 +116,76 @@ impl IotaNamesConfig { ) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn test_config_devnet() { + let config = IotaNamesConfig::devnet(); + assert!(config.package_address.to_string().contains("b9d61")); + assert!(config.object_id.to_string().contains("07c59")); + } + + #[test] + fn test_config_testnet() { + let config = IotaNamesConfig::testnet(); + assert!(config.package_address.to_string().contains("7fff6")); + assert!(config.object_id.to_string().contains("7cab4")); + } + + #[test] + fn test_config_default() { + let config = IotaNamesConfig::default(); + // Default is currently Testnet + assert_eq!(config, IotaNamesConfig::testnet()); + } + + #[test] + fn test_config_new() { + let addr = Address::ZERO; + let id = ObjectId::ZERO; + let config = IotaNamesConfig::new(addr, id, addr, id, id); + assert_eq!(config.package_address, addr); + assert_eq!(config.object_id, id); + } + + #[test] + // Sequential execution implied as we modify env vars globally + fn test_config_from_env() { + let vars = [ + "IOTA_NAMES_PACKAGE_ADDRESS", + "IOTA_NAMES_OBJECT_ID", + "IOTA_NAMES_PAYMENTS_PACKAGE_ADDRESS", + "IOTA_NAMES_REGISTRY_ID", + "IOTA_NAMES_REVERSE_REGISTRY_ID" + ]; + + let old_values: Vec<_> = vars.iter().map(|&k| (k, std::env::var(k))).collect(); + + unsafe { + std::env::set_var("IOTA_NAMES_PACKAGE_ADDRESS", "0x1"); + std::env::set_var("IOTA_NAMES_OBJECT_ID", "0x2"); + std::env::set_var("IOTA_NAMES_PAYMENTS_PACKAGE_ADDRESS", "0x3"); + std::env::set_var("IOTA_NAMES_REGISTRY_ID", "0x4"); + std::env::set_var("IOTA_NAMES_REVERSE_REGISTRY_ID", "0x5"); + } + + let config = IotaNamesConfig::from_env().unwrap(); + assert_eq!(config.package_address, Address::from_str("0x1").unwrap()); + assert_eq!(config.object_id, ObjectId::from_str("0x2").unwrap()); + + // Restore + for (k, v) in old_values { + unsafe { + if let Ok(val) = v { + std::env::set_var(k, val); + } else { + std::env::remove_var(k); + } + } + } + } +} diff --git a/crates/iota-sdk-types/src/iota_names/mod.rs b/crates/iota-sdk-types/src/iota_names/mod.rs index 9c69c6cfbd..0a99db3fbf 100644 --- a/crates/iota-sdk-types/src/iota_names/mod.rs +++ b/crates/iota-sdk-types/src/iota_names/mod.rs @@ -127,3 +127,67 @@ impl IotaNamesNft for SubnameRegistration { self.id } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_name_registration() { + let id_bytes = [1u8; 32]; + let id = ObjectId::new(id_bytes); + let name: Name = "example.iota".parse().unwrap(); + let name_str = "example.iota".to_string(); + let timestamp = 1000; + + let reg = NameRegistration::new(id, name.clone(), name_str.clone(), timestamp); + + assert_eq!(reg.id(), id); + assert_eq!(reg.name(), &name); + assert_eq!(reg.name_str(), &name_str); + assert_eq!(reg.expiration_timestamp_ms(), timestamp); + + // Check trait constants + let type_tag = ::type_(Address::ZERO); + assert!(type_tag.to_string().contains("::name_registration::NameRegistration")); + } + + #[test] + fn test_subname_registration() { + let id = ObjectId::new([2u8; 32]); + let nft_id = ObjectId::new([3u8; 32]); + let name: Name = "sub.example.iota".parse().unwrap(); + let name_str = "sub.example.iota".to_string(); + let timestamp = 2000; + + let nft = NameRegistration::new(nft_id, name.clone(), name_str.clone(), timestamp); + let sub = SubnameRegistration::new(id, nft.clone()); + + assert_eq!(sub.id(), id); + assert_eq!(sub.into_inner(), nft.clone()); + + // Re-create for trait access + let sub = SubnameRegistration::new(id, nft); + assert_eq!(sub.name(), &name); + assert_eq!(sub.name_str(), &name_str); + assert_eq!(sub.expiration_timestamp_ms(), timestamp); + + let type_tag = ::type_(Address::ZERO); + assert!(type_tag.to_string().contains("::subname_registration::SubnameRegistration")); + } + + #[test] + fn test_expiration() { + let id = ObjectId::new([1u8; 32]); + let name: Name = "example.iota".parse().unwrap(); + + // Expired timestamp (1000ms after epoch) + let expired_reg = NameRegistration::new(id, name.clone(), "example.iota".into(), 1000); + assert!(expired_reg.has_expired()); + + // Future timestamp + let future_ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64 + 100000; + let active_reg = NameRegistration::new(id, name, "example.iota".into(), future_ts); + assert!(!active_reg.has_expired()); + } +} diff --git a/crates/iota-sdk-types/src/iota_names/registry.rs b/crates/iota-sdk-types/src/iota_names/registry.rs index fb6338cd4e..772da2bd82 100644 --- a/crates/iota-sdk-types/src/iota_names/registry.rs +++ b/crates/iota-sdk-types/src/iota_names/registry.rs @@ -189,4 +189,104 @@ mod tests { assert!(name.is_node_expired(system_time)); } + + #[test] + fn test_leaf_record() { + let record = NameRecord { + nft_id: ObjectId::new([1; 32]), + expiration_timestamp_ms: IOTA_NAMES_LEAF_EXPIRATION_TIMESTAMP, + target_address: Some(Address::new([2; 32])), + data: HashMap::new(), + }; + + assert!(record.is_leaf_record()); + + let mut record = record; + record.expiration_timestamp_ms = 123456789; + assert!(!record.is_leaf_record()); + } + + #[test] + fn test_valid_leaf_parent() { + let nft_id = ObjectId::new([1; 32]); + let parent = NameRecord { + nft_id, + expiration_timestamp_ms: 1000, + target_address: None, + data: HashMap::new(), + }; + + let valid_child = NameRecord { + nft_id, + expiration_timestamp_ms: IOTA_NAMES_LEAF_EXPIRATION_TIMESTAMP, + target_address: Some(Address::new([2; 32])), + data: HashMap::new(), + }; + + let invalid_child = NameRecord { + nft_id: ObjectId::new([3; 32]), + expiration_timestamp_ms: IOTA_NAMES_LEAF_EXPIRATION_TIMESTAMP, + target_address: Some(Address::new([4; 32])), + data: HashMap::new(), + }; + + assert!(parent.is_valid_leaf_parent(&valid_child)); + assert!(!parent.is_valid_leaf_parent(&invalid_child)); + } + + #[test] + fn test_expiration_time() { + // Need to be careful with timestamp math, so picking a safe small number + let timestamp_ms = 1_600_000_000_000; + let record = NameRecord { + nft_id: ObjectId::ZERO, + expiration_timestamp_ms: timestamp_ms, + target_address: None, + data: HashMap::new(), + }; + + let expected_time = UNIX_EPOCH + Duration::from_millis(timestamp_ms); + assert_eq!(record.expiration_time(), expected_time); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serialization() { + let mut data = HashMap::new(); + data.insert("key1".to_string(), "value1".to_string()); + data.insert("key2".to_string(), "value2".to_string()); + + let record = NameRecord { + nft_id: ObjectId::new([1; 32]), + expiration_timestamp_ms: 100, + target_address: Some(Address::new([2; 32])), + data: data.clone(), + }; + + let json = serde_json::to_string(&record).unwrap(); + let deserialized: NameRecord = serde_json::from_str(&json).unwrap(); + + assert_eq!(record, deserialized); + assert_eq!(deserialized.data, data); + } + + #[test] + #[cfg(feature = "serde")] + fn test_registry_types_serialization() { + let table = Table { + id: ObjectId::new([5; 32]), + size: 10, + }; + let table_json = serde_json::to_string(&table).unwrap(); + let table_de: Table = serde_json::from_str(&table_json).unwrap(); + assert_eq!(table, table_de); + + let registry = Registry { + registry: table.clone(), + reverse_registry: table.clone(), + }; + let reg_json = serde_json::to_string(®istry).unwrap(); + let reg_de: Registry = serde_json::from_str(®_json).unwrap(); + assert_eq!(registry.registry, reg_de.registry); + } } diff --git a/crates/iota-sdk-types/src/move_package.rs b/crates/iota-sdk-types/src/move_package.rs index 17d2db2bd2..f1686126f5 100644 --- a/crates/iota-sdk-types/src/move_package.rs +++ b/crates/iota-sdk-types/src/move_package.rs @@ -169,4 +169,56 @@ mod tests { let package = MovePackageData::new(json_package.modules, json_package.dependencies); assert_eq!(json_package.digest, package.digest); } + + // --- UpgradePolicy --- + + #[test] + fn upgrade_policy_display() { + assert_eq!(UpgradePolicy::Compatible.to_string(), "COMPATIBLE"); + assert_eq!(UpgradePolicy::Additive.to_string(), "ADDITIVE"); + assert_eq!(UpgradePolicy::DepOnly.to_string(), "DEP_ONLY"); + } + + #[test] + fn upgrade_policy_u8_constants() { + assert_eq!(UpgradePolicy::COMPATIBLE, 0); + assert_eq!(UpgradePolicy::ADDITIVE, 128); + assert_eq!(UpgradePolicy::DEP_ONLY, 192); + } + + #[test] + fn upgrade_policy_try_from_valid() { + assert_eq!(UpgradePolicy::try_from(0u8).unwrap(), UpgradePolicy::Compatible); + assert_eq!(UpgradePolicy::try_from(128u8).unwrap(), UpgradePolicy::Additive); + assert_eq!(UpgradePolicy::try_from(192u8).unwrap(), UpgradePolicy::DepOnly); + } + + #[test] + fn upgrade_policy_try_from_invalid() { + assert!(UpgradePolicy::try_from(1u8).is_err()); + assert!(UpgradePolicy::try_from(127u8).is_err()); + assert!(UpgradePolicy::try_from(255u8).is_err()); + } + + #[test] + fn upgrade_policy_is_valid_policy() { + assert!(UpgradePolicy::is_valid_policy(&0)); + assert!(UpgradePolicy::is_valid_policy(&128)); + assert!(UpgradePolicy::is_valid_policy(&192)); + assert!(!UpgradePolicy::is_valid_policy(&1)); + assert!(!UpgradePolicy::is_valid_policy(&255)); + } + + #[test] + fn upgrade_policy_to_u8_roundtrip() { + let policies = [ + UpgradePolicy::Compatible, + UpgradePolicy::Additive, + UpgradePolicy::DepOnly, + ]; + for policy in policies { + let byte = policy as u8; + assert_eq!(UpgradePolicy::try_from(byte).unwrap(), policy); + } + } } diff --git a/crates/iota-sdk-types/src/object_id.rs b/crates/iota-sdk-types/src/object_id.rs index bb83e45bbe..8d13b424f8 100644 --- a/crates/iota-sdk-types/src/object_id.rs +++ b/crates/iota-sdk-types/src/object_id.rs @@ -130,3 +130,164 @@ impl std::fmt::Display for ObjectId { self.to_canonical_string(true).fmt(f) } } + +#[cfg(test)] +mod tests { + use test_strategy::proptest; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + + // --- Construction & Accessors --- + + #[test] + fn new_returns_correct_bytes() { + let bytes = [42u8; ObjectId::LENGTH]; + let id = ObjectId::new(bytes); + assert_eq!(*id.inner(), bytes, "inner bytes must match input"); + assert_eq!(id.into_inner(), bytes, "into_inner must match input"); + } + + #[test] + fn as_bytes_returns_full_slice() { + let bytes = [7u8; ObjectId::LENGTH]; + let id = ObjectId::new(bytes); + assert_eq!(id.as_bytes(), &bytes[..]); + } + + // --- Well-known constants --- + + #[test] + fn zero_constant() { + assert_eq!(ObjectId::ZERO, ObjectId::new([0u8; 32])); + } + + #[test] + fn system_constant() { + let mut expected = [0u8; 32]; + expected[31] = 5; + assert_eq!(ObjectId::SYSTEM, ObjectId::new(expected)); + } + + #[test] + fn clock_constant() { + let mut expected = [0u8; 32]; + expected[31] = 6; + assert_eq!(ObjectId::CLOCK, ObjectId::new(expected)); + } + + // --- Hex parsing --- + + #[test] + fn from_hex_with_prefix() { + let hex = "0x0000000000000000000000000000000000000000000000000000000000000005"; + let id = ObjectId::from_hex(hex).unwrap(); + assert_eq!(id, ObjectId::SYSTEM); + } + + #[test] + fn from_hex_short_form() { + // Short hex should be left-padded with zeros to 32 bytes + let id = ObjectId::from_hex("0x6").unwrap(); + assert_eq!(id, ObjectId::CLOCK); + } + + #[test] + fn from_hex_invalid_characters() { + let result = ObjectId::from_hex("0xZZZZ"); + assert!(result.is_err(), "non-hex characters should fail"); + } + + // --- Display & FromStr roundtrip --- + + #[test] + fn display_shows_canonical_hex_with_prefix() { + let display = ObjectId::ZERO.to_string(); + assert!( + display.starts_with("0x"), + "Display should start with 0x prefix" + ); + assert_eq!(display.len(), 66, "0x + 64 hex chars = 66 chars"); + } + + #[proptest] + fn roundtrip_display_fromstr(id: ObjectId) { + let s = id.to_string(); + let parsed: ObjectId = s.parse().unwrap(); + assert_eq!(id, parsed); + } + + // --- String representations --- + + #[test] + fn to_canonical_string_with_and_without_prefix() { + let id = ObjectId::SYSTEM; + let with_prefix = id.to_canonical_string(true); + let without_prefix = id.to_canonical_string(false); + assert!(with_prefix.starts_with("0x")); + assert!(!without_prefix.starts_with("0x")); + assert_eq!(with_prefix[2..], without_prefix); + } + + #[test] + fn to_short_string_trims_leading_zeros() { + let id = ObjectId::SYSTEM; + assert_eq!(id.to_short_string(true), "0x5"); + assert_eq!(id.to_short_string(false), "5"); + } + + #[test] + fn to_short_string_zero_address_shows_zero() { + let id = ObjectId::ZERO; + assert_eq!(id.to_short_string(true), "0x0"); + assert_eq!(id.to_short_string(false), "0"); + } + + // --- Conversions --- + + #[test] + fn from_address_roundtrip() { + let addr = Address::new([0xAB; 32]); + let id = ObjectId::from(addr); + assert_eq!(*id.as_address(), addr); + } + + #[test] + fn into_byte_array() { + let bytes = [0xFF; 32]; + let id = ObjectId::new(bytes); + let out: [u8; 32] = id.into(); + assert_eq!(out, bytes); + } + + #[test] + fn into_vec_u8() { + let bytes = [0xCC; 32]; + let id = ObjectId::new(bytes); + let vec: Vec = id.into(); + assert_eq!(&vec[..], &bytes[..]); + } + + #[test] + fn as_ref_u8_slice() { + let bytes = [0x11; 32]; + let id = ObjectId::new(bytes); + let slice: &[u8] = id.as_ref(); + assert_eq!(slice, &bytes[..]); + } + + #[test] + fn as_ref_u8_array() { + let bytes = [0x22; 32]; + let id = ObjectId::new(bytes); + let arr: &[u8; 32] = id.as_ref(); + assert_eq!(arr, &bytes); + } + + #[test] + fn to_hex_matches_display() { + let id = ObjectId::CLOCK; + assert_eq!(id.to_hex(), id.to_string()); + } +} diff --git a/crates/iota-sdk-types/src/type_tag/mod.rs b/crates/iota-sdk-types/src/type_tag/mod.rs index 5a2454d3b8..4d6753ae97 100644 --- a/crates/iota-sdk-types/src/type_tag/mod.rs +++ b/crates/iota-sdk-types/src/type_tag/mod.rs @@ -678,3 +678,82 @@ impl std::str::FromStr for StructTag { parse::parse_struct_tag(s).map_err(|_| TypeParseError { source: s.into() }) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn test_primitive_types() { + assert_eq!(TypeTag::u8(), TypeTag::U8); + assert_eq!(TypeTag::u16(), TypeTag::U16); + assert_eq!(TypeTag::u32(), TypeTag::U32); + assert_eq!(TypeTag::u64(), TypeTag::U64); + assert_eq!(TypeTag::u128(), TypeTag::U128); + assert_eq!(TypeTag::u256(), TypeTag::U256); + assert_eq!(TypeTag::bool(), TypeTag::Bool); + assert_eq!(TypeTag::address(), TypeTag::Address); + assert_eq!(TypeTag::signer(), TypeTag::Signer); + + // String repr + assert_eq!(TypeTag::U8.to_string(), "u8"); + assert_eq!(TypeTag::Address.to_string(), "address"); + } + + #[test] + fn test_vector_type() { + let t = TypeTag::Vector(Box::new(TypeTag::U8)); + assert!(t.is_vector()); + assert!(!t.is_struct()); + assert_eq!(t.as_vector_type_tag(), &TypeTag::U8); + assert_eq!(t.to_string(), "vector"); + + let t2 = TypeTag::from_str("vector").unwrap(); + assert_eq!(t, t2); + + let inner = t.into_vector_type_tag(); + assert_eq!(inner, TypeTag::U8); + } + + #[test] + fn test_struct_type() { + let s = StructTag::new_iota_coin_type(); + let t = TypeTag::Struct(Box::new(s.clone())); + + assert!(t.is_struct()); + assert!(!t.is_vector()); + assert_eq!(t.as_struct_tag(), &s); + + // 0x2::iota::IOTA + let s_str = s.to_string(); + assert!(s_str.contains("::iota::IOTA")); + + let t2 = TypeTag::from_str(&s_str).unwrap(); + assert_eq!(t, t2); + } + + #[test] + fn test_identifier_validation() { + assert!(Identifier::is_valid("Coin")); + assert!(Identifier::is_valid("my_module")); + assert!(Identifier::is_valid("_private")); + assert!(Identifier::is_valid("T1")); + + assert!(!Identifier::is_valid("1StartWithDigit")); + assert!(!Identifier::is_valid("Has Spaces")); + assert!(!Identifier::is_valid("")); + } + + #[test] + fn test_struct_tag_constructors() { + let gas = StructTag::new_gas_coin(); + assert!(gas.coin_type_opt().is_some()); + + let uid = StructTag::new_uid(); + assert_eq!(uid.name.as_str(), "UID"); + + let id = StructTag::new_id(); + assert_eq!(id.name.as_str(), "ID"); + } +}