From ec0d0157d72bbf11102816d353e9663d33b38012 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 7 Apr 2026 13:46:44 +0300 Subject: [PATCH] test(dpp): improve coverage for data contract serialization and index validation Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/rs-dpp/src/balances/credits.rs | 262 +++++++ .../data_contract/document_type/index/mod.rs | 209 ++++++ .../data_contract/serialized_version/mod.rs | 687 ++++++++++++++++++ 3 files changed, 1158 insertions(+) diff --git a/packages/rs-dpp/src/balances/credits.rs b/packages/rs-dpp/src/balances/credits.rs index c3f7330aaee..9f9b4720551 100644 --- a/packages/rs-dpp/src/balances/credits.rs +++ b/packages/rs-dpp/src/balances/credits.rs @@ -358,4 +358,266 @@ mod tests { // This is by design - if the balance was SET, client must use the full compacted value } } + + // ----------------------------------------------------------------------- + // Creditable::to_signed() on Credits (u64) + // ----------------------------------------------------------------------- + + #[test] + fn credits_to_signed_within_range() { + let credits: Credits = 1000; + let result = credits.to_signed(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1000i64); + } + + #[test] + fn credits_to_signed_zero() { + let credits: Credits = 0; + let result = credits.to_signed(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 0i64); + } + + #[test] + fn credits_to_signed_max_i64() { + let credits: Credits = i64::MAX as u64; + let result = credits.to_signed(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), i64::MAX); + } + + #[test] + fn credits_to_signed_overflow() { + // u64::MAX cannot be represented as i64 + let credits: Credits = u64::MAX; + let result = credits.to_signed(); + assert!(result.is_err()); + match result.unwrap_err() { + ProtocolError::Overflow(msg) => { + assert!(msg.contains("too big")); + } + other => panic!("Expected Overflow error, got: {:?}", other), + } + } + + #[test] + fn credits_to_signed_just_over_i64_max() { + // i64::MAX + 1 should overflow + let credits: Credits = (i64::MAX as u64) + 1; + let result = credits.to_signed(); + assert!(result.is_err()); + } + + // ----------------------------------------------------------------------- + // Creditable::to_unsigned() on Credits (u64) + // ----------------------------------------------------------------------- + + #[test] + fn credits_to_unsigned_returns_self() { + let credits: Credits = 42; + assert_eq!(credits.to_unsigned(), 42); + } + + #[test] + fn credits_to_unsigned_zero() { + let credits: Credits = 0; + assert_eq!(credits.to_unsigned(), 0); + } + + #[test] + fn credits_to_unsigned_max() { + let credits: Credits = u64::MAX; + assert_eq!(credits.to_unsigned(), u64::MAX); + } + + // ----------------------------------------------------------------------- + // Creditable on SignedCredits (i64) + // ----------------------------------------------------------------------- + + #[test] + fn signed_credits_to_signed_returns_self() { + let sc: SignedCredits = -500; + assert_eq!(sc.to_signed().unwrap(), -500); + } + + #[test] + fn signed_credits_to_unsigned_returns_abs() { + let sc: SignedCredits = -500; + assert_eq!(sc.to_unsigned(), 500); + + let sc_pos: SignedCredits = 500; + assert_eq!(sc_pos.to_unsigned(), 500); + } + + #[test] + fn signed_credits_to_unsigned_zero() { + let sc: SignedCredits = 0; + assert_eq!(sc.to_unsigned(), 0); + } + + // ----------------------------------------------------------------------- + // from_vec_bytes / to_vec_bytes round-trip for Credits (u64) + // ----------------------------------------------------------------------- + + #[test] + fn credits_roundtrip_zero() { + let original: Credits = 0; + let bytes = original.to_vec_bytes(); + let decoded = Credits::from_vec_bytes(bytes).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn credits_roundtrip_one() { + let original: Credits = 1; + let bytes = original.to_vec_bytes(); + let decoded = Credits::from_vec_bytes(bytes).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn credits_roundtrip_max() { + let original: Credits = u64::MAX; + let bytes = original.to_vec_bytes(); + let decoded = Credits::from_vec_bytes(bytes).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn credits_roundtrip_large_value() { + let original: Credits = 1_000_000_000_000; + let bytes = original.to_vec_bytes(); + let decoded = Credits::from_vec_bytes(bytes).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn credits_roundtrip_max_credits_constant() { + let original: Credits = MAX_CREDITS; + let bytes = original.to_vec_bytes(); + let decoded = Credits::from_vec_bytes(bytes).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn credits_from_vec_bytes_empty_vec_error() { + let result = Credits::from_vec_bytes(vec![]); + assert!(result.is_err()); + } + + // ----------------------------------------------------------------------- + // from_vec_bytes / to_vec_bytes round-trip for SignedCredits (i64) + // ----------------------------------------------------------------------- + + #[test] + fn signed_credits_roundtrip_zero() { + let original: SignedCredits = 0; + let bytes = original.to_vec_bytes(); + let decoded = SignedCredits::from_vec_bytes(bytes).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn signed_credits_roundtrip_positive() { + let original: SignedCredits = 123456789; + let bytes = original.to_vec_bytes(); + let decoded = SignedCredits::from_vec_bytes(bytes).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn signed_credits_roundtrip_negative() { + let original: SignedCredits = -987654321; + let bytes = original.to_vec_bytes(); + let decoded = SignedCredits::from_vec_bytes(bytes).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn signed_credits_roundtrip_max() { + let original: SignedCredits = i64::MAX; + let bytes = original.to_vec_bytes(); + let decoded = SignedCredits::from_vec_bytes(bytes).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn signed_credits_roundtrip_min() { + let original: SignedCredits = i64::MIN; + let bytes = original.to_vec_bytes(); + let decoded = SignedCredits::from_vec_bytes(bytes).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn signed_credits_from_vec_bytes_empty_vec_error() { + let result = SignedCredits::from_vec_bytes(vec![]); + assert!(result.is_err()); + } + + // ----------------------------------------------------------------------- + // MAX_CREDITS constant + // ----------------------------------------------------------------------- + + #[test] + fn max_credits_equals_i64_max() { + assert_eq!(MAX_CREDITS, i64::MAX as u64); + } + + // ----------------------------------------------------------------------- + // CreditOperation::merge + // ----------------------------------------------------------------------- + + #[test] + fn credit_operation_merge_set_set() { + let a = CreditOperation::SetCredits(100); + let b = CreditOperation::SetCredits(200); + assert_eq!(a.merge(&b), CreditOperation::SetCredits(200)); + } + + #[test] + fn credit_operation_merge_set_add() { + let a = CreditOperation::SetCredits(100); + let b = CreditOperation::AddToCredits(50); + assert_eq!(a.merge(&b), CreditOperation::SetCredits(150)); + } + + #[test] + fn credit_operation_merge_add_set() { + let a = CreditOperation::AddToCredits(100); + let b = CreditOperation::SetCredits(200); + assert_eq!(a.merge(&b), CreditOperation::SetCredits(200)); + } + + #[test] + fn credit_operation_merge_add_add() { + let a = CreditOperation::AddToCredits(100); + let b = CreditOperation::AddToCredits(50); + assert_eq!(a.merge(&b), CreditOperation::AddToCredits(150)); + } + + #[test] + fn credit_operation_merge_set_add_saturating() { + let a = CreditOperation::SetCredits(u64::MAX); + let b = CreditOperation::AddToCredits(1); + // Should saturate, not overflow + assert_eq!(a.merge(&b), CreditOperation::SetCredits(u64::MAX)); + } + + #[test] + fn credit_operation_merge_add_add_saturating() { + let a = CreditOperation::AddToCredits(u64::MAX); + let b = CreditOperation::AddToCredits(1); + assert_eq!(a.merge(&b), CreditOperation::AddToCredits(u64::MAX)); + } + + // ----------------------------------------------------------------------- + // CREDITS_PER_DUFF constant + // ----------------------------------------------------------------------- + + #[test] + fn credits_per_duff_is_1000() { + assert_eq!(CREDITS_PER_DUFF, 1000); + } } diff --git a/packages/rs-dpp/src/data_contract/document_type/index/mod.rs b/packages/rs-dpp/src/data_contract/document_type/index/mod.rs index 4674375c052..5c2fe9539c3 100644 --- a/packages/rs-dpp/src/data_contract/document_type/index/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/index/mod.rs @@ -1254,4 +1254,213 @@ mod tests { fn test_order_by_partial_ord() { assert!(OrderBy::Asc < OrderBy::Desc); } + + // ----------------------------------------------------------------------- + // Additional objects_are_conflicting tests + // ----------------------------------------------------------------------- + + #[test] + fn test_objects_are_conflicting_both_null_values_not_conflicting() { + // If either property is null (missing) for either object, they should not conflict + let index = make_index("idx", vec![("name", true), ("age", true)], true); + // obj1 has name but not age, obj2 has name but not age + let obj1: ValueMap = vec![( + Value::Text("name".to_string()), + Value::Text("Sam".to_string()), + )]; + let obj2: ValueMap = vec![( + Value::Text("name".to_string()), + Value::Text("Sam".to_string()), + )]; + // Even though "name" matches, "age" is missing in both, so no conflict + assert!(!index.objects_are_conflicting(&obj1, &obj2)); + } + + #[test] + fn test_objects_are_conflicting_unique_three_properties_all_match() { + let index = make_index("idx", vec![("a", true), ("b", true), ("c", true)], true); + let obj1: ValueMap = vec![ + (Value::Text("a".to_string()), Value::U64(1)), + (Value::Text("b".to_string()), Value::U64(2)), + (Value::Text("c".to_string()), Value::U64(3)), + ]; + let obj2: ValueMap = vec![ + (Value::Text("a".to_string()), Value::U64(1)), + (Value::Text("b".to_string()), Value::U64(2)), + (Value::Text("c".to_string()), Value::U64(3)), + ]; + assert!(index.objects_are_conflicting(&obj1, &obj2)); + } + + #[test] + fn test_objects_are_conflicting_unique_three_properties_one_different() { + let index = make_index("idx", vec![("a", true), ("b", true), ("c", true)], true); + let obj1: ValueMap = vec![ + (Value::Text("a".to_string()), Value::U64(1)), + (Value::Text("b".to_string()), Value::U64(2)), + (Value::Text("c".to_string()), Value::U64(3)), + ]; + let obj2: ValueMap = vec![ + (Value::Text("a".to_string()), Value::U64(1)), + (Value::Text("b".to_string()), Value::U64(999)), // different + (Value::Text("c".to_string()), Value::U64(3)), + ]; + assert!(!index.objects_are_conflicting(&obj1, &obj2)); + } + + #[test] + fn test_objects_are_conflicting_non_unique_same_values_still_false() { + // Even with identical values, non-unique index should never conflict + let index = make_index("idx", vec![("x", true), ("y", true)], false); + let obj1: ValueMap = vec![ + (Value::Text("x".to_string()), Value::U64(1)), + (Value::Text("y".to_string()), Value::U64(2)), + ]; + let obj2: ValueMap = vec![ + (Value::Text("x".to_string()), Value::U64(1)), + (Value::Text("y".to_string()), Value::U64(2)), + ]; + assert!(!index.objects_are_conflicting(&obj1, &obj2)); + } + + #[test] + fn test_objects_are_conflicting_first_obj_missing_property() { + let index = make_index("idx", vec![("name", true)], true); + let obj1: ValueMap = vec![]; + let obj2: ValueMap = vec![( + Value::Text("name".to_string()), + Value::Text("Sam".to_string()), + )]; + assert!(!index.objects_are_conflicting(&obj1, &obj2)); + } + + // ----------------------------------------------------------------------- + // Additional ContestedIndexFieldMatch::matches() tests + // ----------------------------------------------------------------------- + + #[test] + fn test_contested_index_field_match_regex_full_match() { + let m = ContestedIndexFieldMatch::Regex(LazyRegex::new("^[0-9]{3}$".to_string())); + assert!(m.matches(&Value::Text("123".to_string()))); + assert!(!m.matches(&Value::Text("1234".to_string()))); + assert!(!m.matches(&Value::Text("ab3".to_string()))); + } + + #[test] + fn test_contested_index_field_match_regex_empty_string() { + let m = ContestedIndexFieldMatch::Regex(LazyRegex::new("^$".to_string())); + assert!(m.matches(&Value::Text("".to_string()))); + assert!(!m.matches(&Value::Text("x".to_string()))); + } + + #[test] + fn test_contested_index_field_match_regex_null_value() { + let m = ContestedIndexFieldMatch::Regex(LazyRegex::new(".*".to_string())); + assert!(!m.matches(&Value::Null)); + } + + #[test] + fn test_contested_index_field_match_regex_bool_value() { + let m = ContestedIndexFieldMatch::Regex(LazyRegex::new("true".to_string())); + assert!(!m.matches(&Value::Bool(true))); + } + + #[test] + fn test_contested_index_field_match_positive_integer_zero() { + let m = ContestedIndexFieldMatch::PositiveIntegerMatch(0); + assert!(m.matches(&Value::U64(0))); + assert!(!m.matches(&Value::U64(1))); + } + + #[test] + fn test_contested_index_field_match_positive_integer_null_value() { + let m = ContestedIndexFieldMatch::PositiveIntegerMatch(42); + assert!(!m.matches(&Value::Null)); + } + + #[test] + fn test_contested_index_field_match_positive_integer_bool_value() { + let m = ContestedIndexFieldMatch::PositiveIntegerMatch(1); + assert!(!m.matches(&Value::Bool(true))); + } + + // ----------------------------------------------------------------------- + // Additional ContestedIndexFieldMatch Ord tests + // ----------------------------------------------------------------------- + + #[test] + fn test_contested_index_field_match_ord_regex_same_length() { + let a = ContestedIndexFieldMatch::Regex(LazyRegex::new("ab".to_string())); + let b = ContestedIndexFieldMatch::Regex(LazyRegex::new("cd".to_string())); + // Same length means Equal + assert_eq!(a.cmp(&b), Ordering::Equal); + } + + #[test] + fn test_contested_index_field_match_ord_integer_equal() { + let a = ContestedIndexFieldMatch::PositiveIntegerMatch(100); + let b = ContestedIndexFieldMatch::PositiveIntegerMatch(100); + assert_eq!(a.cmp(&b), Ordering::Equal); + } + + #[test] + fn test_contested_index_field_match_partial_ord_regex_vs_integer() { + let regex = ContestedIndexFieldMatch::Regex(LazyRegex::new("abc".to_string())); + let integer = ContestedIndexFieldMatch::PositiveIntegerMatch(10); + assert_eq!(regex.partial_cmp(&integer), Some(Ordering::Less)); + assert_eq!(integer.partial_cmp(®ex), Some(Ordering::Greater)); + } + + #[test] + fn test_contested_index_field_match_partial_ord_integers() { + let a = ContestedIndexFieldMatch::PositiveIntegerMatch(5); + let b = ContestedIndexFieldMatch::PositiveIntegerMatch(10); + assert_eq!(a.partial_cmp(&b), Some(Ordering::Less)); + assert_eq!(b.partial_cmp(&a), Some(Ordering::Greater)); + let c = ContestedIndexFieldMatch::PositiveIntegerMatch(5); + assert_eq!(a.partial_cmp(&c), Some(Ordering::Equal)); + } + + #[test] + fn test_contested_index_field_match_partial_ord_regex_by_length() { + let short = ContestedIndexFieldMatch::Regex(LazyRegex::new("x".to_string())); + let long = ContestedIndexFieldMatch::Regex(LazyRegex::new("xxxxxxxxxxxx".to_string())); + assert_eq!(short.partial_cmp(&long), Some(Ordering::Less)); + assert_eq!(long.partial_cmp(&short), Some(Ordering::Greater)); + } + + // ----------------------------------------------------------------------- + // Additional IndexProperty::TryFrom tests + // ----------------------------------------------------------------------- + + #[test] + fn test_index_property_try_from_unknown_direction() { + let mut map = BTreeMap::new(); + map.insert("field".to_string(), "up".to_string()); + let result = IndexProperty::try_from(map); + assert!(result.is_err()); + let err_msg = format!("{}", result.unwrap_err()); + assert!(err_msg.contains("up")); + } + + #[test] + fn test_index_property_try_from_empty_map() { + let map: BTreeMap = BTreeMap::new(); + let result = IndexProperty::try_from(map); + assert!(result.is_err()); + let err_msg = format!("{}", result.unwrap_err()); + assert!(err_msg.contains("empty")); + } + + #[test] + fn test_index_property_try_from_three_entries_error() { + let mut map = BTreeMap::new(); + map.insert("a".to_string(), "asc".to_string()); + map.insert("b".to_string(), "desc".to_string()); + map.insert("c".to_string(), "asc".to_string()); + let result = IndexProperty::try_from(map); + assert!(result.is_err()); + let err_msg = format!("{}", result.unwrap_err()); + assert!(err_msg.contains("more than one")); + } } diff --git a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs index 3bf70b8971e..d1188a2899a 100644 --- a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs +++ b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs @@ -472,3 +472,690 @@ impl DataContract { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::config::v0::DataContractConfigV0; + use crate::data_contract::config::v1::DataContractConfigV1; + use crate::data_contract::group::v0::GroupV0; + use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0; + use crate::data_contract::serialized_version::v1::DataContractInSerializationFormatV1; + use platform_value::Identifier; + use std::collections::BTreeMap; + + /// Helper to create a default V0 serialization format. + fn make_v0() -> DataContractInSerializationFormatV0 { + DataContractInSerializationFormatV0 { + id: Identifier::default(), + config: DataContractConfig::V0(DataContractConfigV0::default()), + version: 1, + owner_id: Identifier::default(), + schema_defs: None, + document_schemas: BTreeMap::new(), + } + } + + /// Helper to create a default V1 serialization format. + fn make_v1() -> DataContractInSerializationFormatV1 { + DataContractInSerializationFormatV1 { + id: Identifier::default(), + config: DataContractConfig::V1(DataContractConfigV1::default()), + version: 1, + owner_id: Identifier::default(), + schema_defs: None, + document_schemas: BTreeMap::new(), + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: BTreeMap::new(), + tokens: BTreeMap::new(), + keywords: vec![], + description: None, + } + } + + // ----------------------------------------------------------------------- + // first_mismatch: V0-V0 + // ----------------------------------------------------------------------- + + #[test] + fn first_mismatch_v0_v0_identical_returns_none() { + let a = DataContractInSerializationFormat::V0(make_v0()); + let b = DataContractInSerializationFormat::V0(make_v0()); + assert_eq!(a.first_mismatch(&b), None); + } + + #[test] + fn first_mismatch_v0_v0_different_id() { + let mut v0_b = make_v0(); + v0_b.id = Identifier::from([1u8; 32]); + let a = DataContractInSerializationFormat::V0(make_v0()); + let b = DataContractInSerializationFormat::V0(v0_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::V0Mismatch)); + } + + #[test] + fn first_mismatch_v0_v0_different_config() { + let mut v0_b = make_v0(); + let mut cfg = DataContractConfigV0::default(); + cfg.readonly = !cfg.readonly; + v0_b.config = DataContractConfig::V0(cfg); + let a = DataContractInSerializationFormat::V0(make_v0()); + let b = DataContractInSerializationFormat::V0(v0_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::V0Mismatch)); + } + + #[test] + fn first_mismatch_v0_v0_different_version() { + let mut v0_b = make_v0(); + v0_b.version = 99; + let a = DataContractInSerializationFormat::V0(make_v0()); + let b = DataContractInSerializationFormat::V0(v0_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::V0Mismatch)); + } + + #[test] + fn first_mismatch_v0_v0_different_owner_id() { + let mut v0_b = make_v0(); + v0_b.owner_id = Identifier::from([2u8; 32]); + let a = DataContractInSerializationFormat::V0(make_v0()); + let b = DataContractInSerializationFormat::V0(v0_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::V0Mismatch)); + } + + #[test] + fn first_mismatch_v0_v0_different_document_schemas() { + let mut v0_b = make_v0(); + v0_b.document_schemas + .insert("doc".to_string(), Value::Bool(true)); + let a = DataContractInSerializationFormat::V0(make_v0()); + let b = DataContractInSerializationFormat::V0(v0_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::V0Mismatch)); + } + + // ----------------------------------------------------------------------- + // first_mismatch: format mismatch (V0 vs V1) + // ----------------------------------------------------------------------- + + #[test] + fn first_mismatch_v0_v1_returns_format_version_mismatch() { + let a = DataContractInSerializationFormat::V0(make_v0()); + let b = DataContractInSerializationFormat::V1(make_v1()); + assert_eq!( + a.first_mismatch(&b), + Some(DataContractMismatch::FormatVersionMismatch) + ); + } + + #[test] + fn first_mismatch_v1_v0_returns_format_version_mismatch() { + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V0(make_v0()); + assert_eq!( + a.first_mismatch(&b), + Some(DataContractMismatch::FormatVersionMismatch) + ); + } + + // ----------------------------------------------------------------------- + // first_mismatch: V1-V1 identical + // ----------------------------------------------------------------------- + + #[test] + fn first_mismatch_v1_v1_identical_returns_none() { + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(make_v1()); + assert_eq!(a.first_mismatch(&b), None); + } + + // ----------------------------------------------------------------------- + // first_mismatch: V1-V1 field-by-field mismatches + // ----------------------------------------------------------------------- + + #[test] + fn first_mismatch_v1_v1_different_id() { + let mut v1_b = make_v1(); + v1_b.id = Identifier::from([1u8; 32]); + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(v1_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Id)); + } + + #[test] + fn first_mismatch_v1_v1_different_config() { + let mut v1_b = make_v1(); + let mut cfg = DataContractConfigV1::default(); + cfg.readonly = !cfg.readonly; + v1_b.config = DataContractConfig::V1(cfg); + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(v1_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Config)); + } + + #[test] + fn first_mismatch_v1_v1_different_version() { + let mut v1_b = make_v1(); + v1_b.version = 42; + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(v1_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Version)); + } + + #[test] + fn first_mismatch_v1_v1_different_owner_id() { + let mut v1_b = make_v1(); + v1_b.owner_id = Identifier::from([3u8; 32]); + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(v1_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::OwnerId)); + } + + #[test] + fn first_mismatch_v1_v1_different_schema_defs() { + let mut v1_b = make_v1(); + let mut defs = BTreeMap::new(); + defs.insert("someDef".to_string(), Value::Bool(true)); + v1_b.schema_defs = Some(defs); + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(v1_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::SchemaDefs)); + } + + #[test] + fn first_mismatch_v1_v1_different_document_schemas() { + let mut v1_b = make_v1(); + v1_b.document_schemas + .insert("doc".to_string(), Value::U64(1)); + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(v1_b); + assert_eq!( + a.first_mismatch(&b), + Some(DataContractMismatch::DocumentSchemas) + ); + } + + #[test] + fn first_mismatch_v1_v1_different_groups() { + let mut v1_b = make_v1(); + v1_b.groups.insert( + 0, + Group::V0(GroupV0 { + members: Default::default(), + required_power: 1, + }), + ); + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(v1_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Groups)); + } + + #[test] + fn first_mismatch_v1_v1_different_tokens() { + let mut v1_b = make_v1(); + v1_b.tokens.insert( + 0, + TokenConfiguration::V0( + crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0::default_most_restrictive(), + ), + ); + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(v1_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Tokens)); + } + + #[test] + fn first_mismatch_v1_v1_different_keywords() { + let mut v1_b = make_v1(); + v1_b.keywords = vec!["test".to_string()]; + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(v1_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Keywords)); + } + + #[test] + fn first_mismatch_v1_v1_keywords_case_insensitive_match() { + let mut v1_a = make_v1(); + v1_a.keywords = vec!["Test".to_string()]; + let mut v1_b = make_v1(); + v1_b.keywords = vec!["test".to_string()]; + let a = DataContractInSerializationFormat::V1(v1_a); + let b = DataContractInSerializationFormat::V1(v1_b); + // The comparison uses to_lowercase, so "Test" and "test" should match + assert_eq!(a.first_mismatch(&b), None); + } + + #[test] + fn first_mismatch_v1_v1_keywords_different_length() { + let mut v1_a = make_v1(); + v1_a.keywords = vec!["a".to_string()]; + let mut v1_b = make_v1(); + v1_b.keywords = vec!["a".to_string(), "b".to_string()]; + let a = DataContractInSerializationFormat::V1(v1_a); + let b = DataContractInSerializationFormat::V1(v1_b); + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Keywords)); + } + + #[test] + fn first_mismatch_v1_v1_different_description() { + let mut v1_b = make_v1(); + v1_b.description = Some("a description".to_string()); + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(v1_b); + assert_eq!( + a.first_mismatch(&b), + Some(DataContractMismatch::Description) + ); + } + + // ----------------------------------------------------------------------- + // first_mismatch: priority ordering in V1 (id detected before config, etc.) + // ----------------------------------------------------------------------- + + #[test] + fn first_mismatch_v1_v1_id_takes_priority_over_config() { + let mut v1_b = make_v1(); + v1_b.id = Identifier::from([5u8; 32]); + let mut cfg = DataContractConfigV1::default(); + cfg.readonly = !cfg.readonly; + v1_b.config = DataContractConfig::V1(cfg); + let a = DataContractInSerializationFormat::V1(make_v1()); + let b = DataContractInSerializationFormat::V1(v1_b); + // Id is checked before config + assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Id)); + } + + // ----------------------------------------------------------------------- + // DataContractMismatch Display + // ----------------------------------------------------------------------- + + #[test] + fn data_contract_mismatch_display() { + assert_eq!(format!("{}", DataContractMismatch::Id), "ID fields differ"); + assert_eq!( + format!("{}", DataContractMismatch::FormatVersionMismatch), + "Serialization format versions differ (e.g., V0 vs V1)" + ); + assert_eq!( + format!("{}", DataContractMismatch::V0Mismatch), + "V0 versions differ" + ); + assert_eq!(format!("{}", DataContractMismatch::Tokens), "Tokens differ"); + assert_eq!( + format!("{}", DataContractMismatch::Keywords), + "Keywords differ" + ); + assert_eq!( + format!("{}", DataContractMismatch::Description), + "Description fields differ" + ); + } + + // ----------------------------------------------------------------------- + // Accessor methods + // ----------------------------------------------------------------------- + + #[test] + fn accessor_id_v0() { + let v0 = make_v0(); + let expected_id = v0.id; + let format = DataContractInSerializationFormat::V0(v0); + assert_eq!(format.id(), expected_id); + } + + #[test] + fn accessor_id_v1() { + let v1 = make_v1(); + let expected_id = v1.id; + let format = DataContractInSerializationFormat::V1(v1); + assert_eq!(format.id(), expected_id); + } + + #[test] + fn accessor_owner_id_v0() { + let mut v0 = make_v0(); + v0.owner_id = Identifier::from([7u8; 32]); + let expected = v0.owner_id; + let format = DataContractInSerializationFormat::V0(v0); + assert_eq!(format.owner_id(), expected); + } + + #[test] + fn accessor_version_v0() { + let mut v0 = make_v0(); + v0.version = 10; + let format = DataContractInSerializationFormat::V0(v0); + assert_eq!(format.version(), 10); + } + + #[test] + fn accessor_version_v1() { + let mut v1 = make_v1(); + v1.version = 20; + let format = DataContractInSerializationFormat::V1(v1); + assert_eq!(format.version(), 20); + } + + #[test] + fn accessor_groups_v0_returns_empty() { + let format = DataContractInSerializationFormat::V0(make_v0()); + assert!(format.groups().is_empty()); + } + + #[test] + fn accessor_tokens_v0_returns_empty() { + let format = DataContractInSerializationFormat::V0(make_v0()); + assert!(format.tokens().is_empty()); + } + + #[test] + fn accessor_keywords_v0_returns_empty() { + let format = DataContractInSerializationFormat::V0(make_v0()); + assert!(format.keywords().is_empty()); + } + + #[test] + fn accessor_description_v0_returns_none() { + let format = DataContractInSerializationFormat::V0(make_v0()); + assert_eq!(format.description(), &None); + } + + #[test] + fn accessor_keywords_v1() { + let mut v1 = make_v1(); + v1.keywords = vec!["hello".to_string()]; + let format = DataContractInSerializationFormat::V1(v1); + assert_eq!(format.keywords(), &vec!["hello".to_string()]); + } + + #[test] + fn accessor_description_v1_some() { + let mut v1 = make_v1(); + v1.description = Some("desc".to_string()); + let format = DataContractInSerializationFormat::V1(v1); + assert_eq!(format.description(), &Some("desc".to_string())); + } + + #[test] + fn accessor_document_schemas_v0() { + let mut v0 = make_v0(); + v0.document_schemas + .insert("note".to_string(), Value::Bool(true)); + let format = DataContractInSerializationFormat::V0(v0); + assert_eq!(format.document_schemas().len(), 1); + assert!(format.document_schemas().contains_key("note")); + } + + #[test] + fn accessor_schema_defs_v0_none() { + let format = DataContractInSerializationFormat::V0(make_v0()); + assert!(format.schema_defs().is_none()); + } + + #[test] + fn accessor_schema_defs_v1_some() { + let mut v1 = make_v1(); + let mut defs = BTreeMap::new(); + defs.insert("def1".to_string(), Value::Null); + v1.schema_defs = Some(defs); + let format = DataContractInSerializationFormat::V1(v1); + assert!(format.schema_defs().is_some()); + assert!(format.schema_defs().unwrap().contains_key("def1")); + } + + // ----------------------------------------------------------------------- + // TryFromPlatformVersioned: DataContractV0 -> DataContractInSerializationFormat + // ----------------------------------------------------------------------- + + #[test] + fn try_from_platform_versioned_data_contract_v0_version_0() { + let platform_version = PlatformVersion::first(); + // V1 contract versions use default_current_version: 0 + let v0 = DataContractV0 { + id: Identifier::from([10u8; 32]), + config: DataContractConfig::V0(DataContractConfigV0::default()), + version: 1, + owner_id: Identifier::from([20u8; 32]), + schema_defs: None, + document_types: BTreeMap::new(), + metadata: None, + }; + let result = DataContractInSerializationFormat::try_from_platform_versioned( + v0.clone(), + platform_version, + ); + assert!(result.is_ok()); + let format = result.unwrap(); + assert!(matches!(format, DataContractInSerializationFormat::V0(_))); + assert_eq!(format.id(), Identifier::from([10u8; 32])); + assert_eq!(format.owner_id(), Identifier::from([20u8; 32])); + } + + #[test] + fn try_from_platform_versioned_data_contract_v0_ref_version_0() { + let platform_version = PlatformVersion::first(); + let v0 = DataContractV0 { + id: Identifier::from([11u8; 32]), + config: DataContractConfig::V0(DataContractConfigV0::default()), + version: 2, + owner_id: Identifier::from([22u8; 32]), + schema_defs: None, + document_types: BTreeMap::new(), + metadata: None, + }; + let result = + DataContractInSerializationFormat::try_from_platform_versioned(&v0, platform_version); + assert!(result.is_ok()); + let format = result.unwrap(); + assert!(matches!(format, DataContractInSerializationFormat::V0(_))); + assert_eq!(format.version(), 2); + } + + #[test] + fn try_from_platform_versioned_data_contract_v0_version_1() { + let platform_version = PlatformVersion::latest(); + // Latest uses default_current_version: 1 + let v0 = DataContractV0 { + id: Identifier::from([10u8; 32]), + config: DataContractConfig::V0(DataContractConfigV0::default()), + version: 1, + owner_id: Identifier::from([20u8; 32]), + schema_defs: None, + document_types: BTreeMap::new(), + metadata: None, + }; + let result = DataContractInSerializationFormat::try_from_platform_versioned( + v0.clone(), + platform_version, + ); + assert!(result.is_ok()); + let format = result.unwrap(); + assert!(matches!(format, DataContractInSerializationFormat::V1(_))); + } + + // ----------------------------------------------------------------------- + // TryFromPlatformVersioned: DataContractV1 -> DataContractInSerializationFormat + // ----------------------------------------------------------------------- + + #[test] + fn try_from_platform_versioned_data_contract_v1_version_0() { + let platform_version = PlatformVersion::first(); + let v1 = DataContractV1 { + id: Identifier::from([10u8; 32]), + config: DataContractConfig::V0(DataContractConfigV0::default()), + version: 1, + owner_id: Identifier::from([20u8; 32]), + schema_defs: None, + document_types: BTreeMap::new(), + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: BTreeMap::new(), + tokens: BTreeMap::new(), + keywords: vec![], + description: None, + }; + let result = DataContractInSerializationFormat::try_from_platform_versioned( + v1.clone(), + platform_version, + ); + assert!(result.is_ok()); + let format = result.unwrap(); + assert!(matches!(format, DataContractInSerializationFormat::V0(_))); + } + + #[test] + fn try_from_platform_versioned_data_contract_v1_version_1() { + let platform_version = PlatformVersion::latest(); + let v1 = DataContractV1 { + id: Identifier::from([10u8; 32]), + config: DataContractConfig::V1(DataContractConfigV1::default()), + version: 1, + owner_id: Identifier::from([20u8; 32]), + schema_defs: None, + document_types: BTreeMap::new(), + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: BTreeMap::new(), + tokens: BTreeMap::new(), + keywords: vec![], + description: None, + }; + let result = DataContractInSerializationFormat::try_from_platform_versioned( + v1.clone(), + platform_version, + ); + assert!(result.is_ok()); + let format = result.unwrap(); + assert!(matches!(format, DataContractInSerializationFormat::V1(_))); + } + + #[test] + fn try_from_platform_versioned_data_contract_v1_ref_version_1() { + let platform_version = PlatformVersion::latest(); + let v1 = DataContractV1 { + id: Identifier::from([10u8; 32]), + config: DataContractConfig::V1(DataContractConfigV1::default()), + version: 3, + owner_id: Identifier::from([20u8; 32]), + schema_defs: None, + document_types: BTreeMap::new(), + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: BTreeMap::new(), + tokens: BTreeMap::new(), + keywords: vec![], + description: None, + }; + let result = + DataContractInSerializationFormat::try_from_platform_versioned(&v1, platform_version); + assert!(result.is_ok()); + let format = result.unwrap(); + assert!(matches!(format, DataContractInSerializationFormat::V1(_))); + assert_eq!(format.version(), 3); + } + + // ----------------------------------------------------------------------- + // TryFromPlatformVersioned: DataContract -> DataContractInSerializationFormat + // ----------------------------------------------------------------------- + + #[test] + fn try_from_platform_versioned_data_contract_ref_version_0() { + let platform_version = PlatformVersion::first(); + let contract = DataContract::V0(DataContractV0 { + id: Identifier::from([10u8; 32]), + config: DataContractConfig::V0(DataContractConfigV0::default()), + version: 1, + owner_id: Identifier::from([20u8; 32]), + schema_defs: None, + document_types: BTreeMap::new(), + metadata: None, + }); + let result = DataContractInSerializationFormat::try_from_platform_versioned( + &contract, + platform_version, + ); + assert!(result.is_ok()); + assert!(matches!( + result.unwrap(), + DataContractInSerializationFormat::V0(_) + )); + } + + #[test] + fn try_from_platform_versioned_data_contract_owned_version_1() { + let platform_version = PlatformVersion::latest(); + let contract = DataContract::V0(DataContractV0 { + id: Identifier::from([10u8; 32]), + config: DataContractConfig::V0(DataContractConfigV0::default()), + version: 1, + owner_id: Identifier::from([20u8; 32]), + schema_defs: None, + document_types: BTreeMap::new(), + metadata: None, + }); + let result = DataContractInSerializationFormat::try_from_platform_versioned( + contract, + platform_version, + ); + assert!(result.is_ok()); + assert!(matches!( + result.unwrap(), + DataContractInSerializationFormat::V1(_) + )); + } + + // ----------------------------------------------------------------------- + // Verify serialization version routing + // ----------------------------------------------------------------------- + + #[test] + fn first_platform_version_uses_serialization_version_0() { + let pv = PlatformVersion::first(); + assert_eq!( + pv.dpp + .contract_versions + .contract_serialization_version + .default_current_version, + 0 + ); + } + + #[test] + fn latest_platform_version_uses_serialization_version_1() { + let pv = PlatformVersion::latest(); + assert_eq!( + pv.dpp + .contract_versions + .contract_serialization_version + .default_current_version, + 1 + ); + } + + #[test] + fn first_platform_version_uses_contract_structure_0() { + let pv = PlatformVersion::first(); + assert_eq!(pv.dpp.contract_versions.contract_structure_version, 0); + } + + #[test] + fn latest_platform_version_uses_contract_structure_1() { + let pv = PlatformVersion::latest(); + assert_eq!(pv.dpp.contract_versions.contract_structure_version, 1); + } +}