From effc9204968da305f6a35d0bb17bb9de65ff26a2 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 7 Apr 2026 15:09:23 +0300 Subject: [PATCH] test(dpp): improve coverage for epoch distribution, JSON safe serialization, and address witness Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/rs-dpp/src/address_funds/witness.rs | 219 ++++++++++++++ packages/rs-dpp/src/fee/epoch/distribution.rs | 237 +++++++++++++++ .../src/serialization/json/safe_integer.rs | 276 ++++++++++++++++++ .../serialization/json/safe_integer_map.rs | 276 ++++++++++++++++++ 4 files changed, 1008 insertions(+) diff --git a/packages/rs-dpp/src/address_funds/witness.rs b/packages/rs-dpp/src/address_funds/witness.rs index 1972cbe4f11..5c2d455a670 100644 --- a/packages/rs-dpp/src/address_funds/witness.rs +++ b/packages/rs-dpp/src/address_funds/witness.rs @@ -510,4 +510,223 @@ mod tests { MAX_P2SH_SIGNATURES + 1, ); } + + // --- Additional encode/decode round-trip tests --- + + #[test] + fn test_p2pkh_empty_signature_round_trip() { + let witness = AddressWitness::P2pkh { + signature: BinaryData::new(vec![]), + }; + + let encoded = bincode::encode_to_vec(&witness, config::standard()).unwrap(); + let decoded: AddressWitness = bincode::decode_from_slice(&encoded, config::standard()) + .unwrap() + .0; + + assert_eq!(witness, decoded); + assert!(decoded.is_p2pkh()); + } + + #[test] + fn test_p2pkh_65_byte_signature_round_trip() { + // Typical recoverable ECDSA signature is 65 bytes + let signature_data: Vec = (0..65).collect(); + let witness = AddressWitness::P2pkh { + signature: BinaryData::new(signature_data), + }; + + let encoded = bincode::encode_to_vec(&witness, config::standard()).unwrap(); + let decoded: AddressWitness = bincode::decode_from_slice(&encoded, config::standard()) + .unwrap() + .0; + + assert_eq!(witness, decoded); + } + + #[test] + fn test_p2sh_single_signature_round_trip() { + let witness = AddressWitness::P2sh { + signatures: vec![BinaryData::new(vec![0x30, 0x44, 0x02, 0x20])], + redeem_script: BinaryData::new(vec![0x51, 0xae]), + }; + + let encoded = bincode::encode_to_vec(&witness, config::standard()).unwrap(); + let decoded: AddressWitness = bincode::decode_from_slice(&encoded, config::standard()) + .unwrap() + .0; + + assert_eq!(witness, decoded); + assert!(decoded.is_p2sh()); + assert_eq!( + decoded.redeem_script(), + Some(&BinaryData::new(vec![0x51, 0xae])) + ); + } + + #[test] + fn test_p2sh_empty_signatures_vec_round_trip() { + let witness = AddressWitness::P2sh { + signatures: vec![], + redeem_script: BinaryData::new(vec![0x52, 0xae]), + }; + + let encoded = bincode::encode_to_vec(&witness, config::standard()).unwrap(); + let decoded: AddressWitness = bincode::decode_from_slice(&encoded, config::standard()) + .unwrap() + .0; + + assert_eq!(witness, decoded); + } + + #[test] + fn test_p2sh_empty_redeem_script_round_trip() { + let witness = AddressWitness::P2sh { + signatures: vec![BinaryData::new(vec![0x00])], + redeem_script: BinaryData::new(vec![]), + }; + + let encoded = bincode::encode_to_vec(&witness, config::standard()).unwrap(); + let decoded: AddressWitness = bincode::decode_from_slice(&encoded, config::standard()) + .unwrap() + .0; + + assert_eq!(witness, decoded); + } + + // --- Error path tests --- + + #[test] + fn test_invalid_discriminant_decode_fails() { + // Manually craft a payload with discriminant 2 (invalid) + let mut data = vec![]; + bincode::encode_into_std_write(&2u8, &mut data, config::standard()).unwrap(); + // Add some dummy data + data.extend_from_slice(&[0x00, 0x00, 0x00]); + + let result: Result<(AddressWitness, usize), _> = + bincode::decode_from_slice(&data, config::standard()); + assert!(result.is_err()); + let err_msg = format!("{}", result.unwrap_err()); + assert!(err_msg.contains("Invalid AddressWitness discriminant")); + } + + #[test] + fn test_invalid_discriminant_255_decode_fails() { + let mut data = vec![]; + bincode::encode_into_std_write(&255u8, &mut data, config::standard()).unwrap(); + + let result: Result<(AddressWitness, usize), _> = + bincode::decode_from_slice(&data, config::standard()); + assert!(result.is_err()); + } + + #[test] + fn test_truncated_p2pkh_payload_fails() { + // Encode only the discriminant, no signature data + let data = vec![0u8]; // discriminant for P2pkh + let result: Result<(AddressWitness, usize), _> = + bincode::decode_from_slice(&data, config::standard()); + assert!(result.is_err()); + } + + #[test] + fn test_truncated_p2sh_payload_fails() { + // Encode discriminant for P2sh but no signatures/redeem_script + let data = vec![1u8]; // discriminant for P2sh + let result: Result<(AddressWitness, usize), _> = + bincode::decode_from_slice(&data, config::standard()); + assert!(result.is_err()); + } + + #[test] + fn test_empty_payload_fails() { + let data: Vec = vec![]; + let result: Result<(AddressWitness, usize), _> = + bincode::decode_from_slice(&data, config::standard()); + assert!(result.is_err()); + } + + // --- Accessor tests --- + + #[test] + fn test_redeem_script_returns_none_for_p2pkh() { + let witness = AddressWitness::P2pkh { + signature: BinaryData::new(vec![0x30]), + }; + assert!(witness.redeem_script().is_none()); + } + + #[test] + fn test_redeem_script_returns_some_for_p2sh() { + let script = BinaryData::new(vec![0x52, 0xae]); + let witness = AddressWitness::P2sh { + signatures: vec![], + redeem_script: script.clone(), + }; + assert_eq!(witness.redeem_script(), Some(&script)); + } + + // --- BorrowDecode path tests --- + + #[test] + fn test_borrow_decode_p2pkh_round_trip() { + let witness = AddressWitness::P2pkh { + signature: BinaryData::new(vec![0xAB, 0xCD, 0xEF]), + }; + + let encoded = bincode::encode_to_vec(&witness, config::standard()).unwrap(); + // borrow_decode is exercised through decode_from_slice + let decoded: AddressWitness = bincode::decode_from_slice(&encoded, config::standard()) + .unwrap() + .0; + assert_eq!(witness, decoded); + } + + #[test] + fn test_borrow_decode_p2sh_round_trip() { + let witness = AddressWitness::P2sh { + signatures: vec![ + BinaryData::new(vec![0x00]), + BinaryData::new(vec![0x30, 0x44]), + BinaryData::new(vec![0x30, 0x45]), + ], + redeem_script: BinaryData::new(vec![0x52, 0x53, 0xae]), + }; + + let encoded = bincode::encode_to_vec(&witness, config::standard()).unwrap(); + let decoded: AddressWitness = bincode::decode_from_slice(&encoded, config::standard()) + .unwrap() + .0; + assert_eq!(witness, decoded); + } + + #[test] + fn test_borrow_decode_rejects_excessive_signatures() { + // Ensure BorrowDecode also rejects > MAX_P2SH_SIGNATURES + let signatures: Vec = (0..MAX_P2SH_SIGNATURES + 1) + .map(|_| BinaryData::new(vec![0x30])) + .collect(); + + let witness = AddressWitness::P2sh { + signatures, + redeem_script: BinaryData::new(vec![0xae]), + }; + + let encoded = bincode::encode_to_vec(&witness, config::standard()).unwrap(); + let result: Result<(AddressWitness, usize), _> = + bincode::decode_from_slice(&encoded, config::standard()); + assert!(result.is_err()); + } + + #[test] + fn test_borrow_decode_invalid_discriminant_fails() { + let mut data = vec![]; + bincode::encode_into_std_write(&3u8, &mut data, config::standard()).unwrap(); + data.extend_from_slice(&[0x00; 10]); + + let result: Result<(AddressWitness, usize), _> = + bincode::decode_from_slice(&data, config::standard()); + assert!(result.is_err()); + } } diff --git a/packages/rs-dpp/src/fee/epoch/distribution.rs b/packages/rs-dpp/src/fee/epoch/distribution.rs index 73d0b94ee99..22a0cec9111 100644 --- a/packages/rs-dpp/src/fee/epoch/distribution.rs +++ b/packages/rs-dpp/src/fee/epoch/distribution.rs @@ -1083,5 +1083,242 @@ mod tests { assert_eq!(leftovers, 400); assert_eq!(amount, storage_fee - leftovers - first_two_epochs_amount); } + + #[test] + fn should_return_zero_amount_and_zero_leftovers_for_zero_storage_fee() { + let (amount, leftovers) = + calculate_storage_fee_refund_amount_and_leftovers(0, GENESIS_EPOCH_INDEX, 10, 20) + .expect("should handle zero storage fee"); + + assert_eq!(amount, 0); + assert_eq!(leftovers, 0); + } + + #[test] + fn should_return_zero_refund_when_start_epoch_equals_current_epoch() { + // When start == current, skipped_amount covers epoch 0 only (the one epoch + // between start_epoch_index and current_epoch_index + 1 = 1). + let storage_fee = 1000000; + let epoch = 0; + + let (amount, leftovers) = + calculate_storage_fee_refund_amount_and_leftovers(storage_fee, epoch, epoch, 20) + .expect("should distribute storage fee"); + + // Only epoch 0 is skipped (cost = floor(1000000 * 0.05 / 20) = 2500). + // The refund amount is everything except the skipped epoch and leftovers. + assert_eq!(amount, storage_fee - 2500 - leftovers); + } + + #[test] + fn should_calculate_correctly_with_non_genesis_start() { + let storage_fee = 500000; + let start = 100; + let current = 110; + + let (amount, leftovers) = + calculate_storage_fee_refund_amount_and_leftovers(storage_fee, start, current, 20) + .expect("should distribute storage fee"); + + // Verify invariant: amount + skipped + leftovers = storage_fee + assert_eq!( + amount + (storage_fee - amount - leftovers) + leftovers, + storage_fee + ); + // Amount must be less than total + assert!(amount < storage_fee); + assert!(leftovers < storage_fee); + } + + #[test] + fn should_handle_large_epoch_gap() { + // current_epoch far from start + let storage_fee = 10_000_000; + let start = 0; + let current = 500; // halfway through the 1000 total epochs + + let (amount, leftovers) = + calculate_storage_fee_refund_amount_and_leftovers(storage_fee, start, current, 20) + .expect("should handle large epoch gap"); + + // Refund amount should be smaller because most epochs have been paid out + assert!(amount < storage_fee / 2); + assert!(leftovers < storage_fee); + } + } + + mod additional_original_removed_credits_multiplier_from { + use super::*; + + #[test] + fn should_create_multiplier_of_one_when_no_epochs_have_passed() { + // When start_repayment == start, paid_epochs = 0, ratio_used = full table sum = 1.0 + // So multiplier = 1/1 = 1 + let multiplier = original_removed_credits_multiplier_from(0, 0, 20); + assert_eq!(multiplier, dec!(1)); + } + + #[test] + fn should_increase_multiplier_as_more_epochs_pass() { + let m1 = original_removed_credits_multiplier_from(0, 5, 20); + let m2 = original_removed_credits_multiplier_from(0, 10, 20); + let m3 = original_removed_credits_multiplier_from(0, 19, 20); + + // More paid epochs means less ratio remaining, so multiplier increases + assert!(m1 < m2); + assert!(m2 < m3); + } + + #[test] + fn should_handle_era_boundary_crossing() { + // paid_epochs = 20 means we enter the second era exactly + let m_at_boundary = original_removed_credits_multiplier_from(0, 20, 20); + let m_before_boundary = original_removed_credits_multiplier_from(0, 19, 20); + let m_after_boundary = original_removed_credits_multiplier_from(0, 21, 20); + + // At the boundary, the entire first era (0.05) is consumed + assert!(m_at_boundary > m_before_boundary); + assert!(m_after_boundary > m_at_boundary); + } + + #[test] + fn should_handle_different_epochs_per_era() { + // With 40 epochs per era (the default), 40 paid epochs = 1 full era + let m_40 = original_removed_credits_multiplier_from(0, 40, 40); + // With 20 epochs per era, 20 paid epochs = 1 full era + let m_20 = original_removed_credits_multiplier_from(0, 20, 20); + + // Both consume exactly one full era of 0.05, so multipliers should be equal + assert_eq!(m_40, m_20); + } + + #[test] + fn should_produce_same_multiplier_regardless_of_absolute_epoch_offset() { + // The multiplier depends only on the difference, not absolute indices + let m1 = original_removed_credits_multiplier_from(0, 15, 20); + let m2 = original_removed_credits_multiplier_from(100, 115, 20); + let m3 = original_removed_credits_multiplier_from(5000, 5015, 20); + + assert_eq!(m1, m2); + assert_eq!(m2, m3); + } + } + + mod additional_restore_original_removed_credits_amount { + use super::*; + + #[test] + fn should_restore_to_original_when_no_epochs_passed() { + // If start_repayment == start, multiplier is 1.0, so restored == refund_amount + let refund = dec!(1000000); + let restored = restore_original_removed_credits_amount(refund, 0, 0, 20) + .expect("should not overflow"); + assert_eq!(restored, refund); + } + + #[test] + fn should_increase_amount_when_epochs_have_passed() { + // After some epochs, the multiplier > 1, so restored > refund + let refund = dec!(500000); + let restored = restore_original_removed_credits_amount(refund, 0, 10, 20) + .expect("should not overflow"); + assert!(restored > refund); + } + + #[test] + fn should_handle_zero_refund_amount() { + let restored = restore_original_removed_credits_amount(dec!(0), 0, 10, 20) + .expect("should handle zero"); + assert_eq!(restored, dec!(0)); + } + } + + mod additional_refund_storage_fee_to_epochs_map { + use super::*; + + #[test] + fn should_return_zero_leftovers_for_zero_storage_fee() { + let leftovers = refund_storage_fee_to_epochs_map(0, 0, 1, |_, _| Ok(()), 20) + .expect("should handle zero"); + assert_eq!(leftovers, 0); + } + + #[test] + fn should_skip_epochs_before_skip_until_index() { + let storage_fee = 1000000u64; + let start = 0u16; + let skip_until = 10u16; + + let mut min_epoch_seen = u16::MAX; + + let _leftovers = refund_storage_fee_to_epochs_map( + storage_fee, + start, + skip_until, + |epoch_index, _amount| { + if epoch_index < min_epoch_seen { + min_epoch_seen = epoch_index; + } + Ok(()) + }, + 20, + ) + .expect("should distribute refund"); + + // The first epoch called should be >= skip_until + assert!(min_epoch_seen >= skip_until); + } + + #[test] + fn should_distribute_to_single_remaining_epoch_in_era() { + // skip_until is 19 (last epoch of era 0), start is 0 + // This means only 1 epoch remains in era 0 + let storage_fee = 100000u64; + + let mut epoch_count = 0u32; + + let leftovers = refund_storage_fee_to_epochs_map( + storage_fee, + 0, + 19, + |_epoch_index, _amount| { + epoch_count += 1; + Ok(()) + }, + 20, + ) + .expect("should distribute"); + + // Total epochs = (1000 - 19) = 981 epochs should be called + assert_eq!(epoch_count, 981); + assert!(leftovers < storage_fee); + } + + #[test] + fn should_handle_skip_at_era_boundary() { + // skip_until exactly at era 1 start + let storage_fee = 500000u64; + let start = 0u16; + let skip_until = 20u16; // era 1 starts here + + let mut epochs_called = Vec::new(); + + let _leftovers = refund_storage_fee_to_epochs_map( + storage_fee, + start, + skip_until, + |epoch_index, _amount| { + epochs_called.push(epoch_index); + Ok(()) + }, + 20, + ) + .expect("should distribute"); + + // First epoch called should be exactly skip_until + assert_eq!(*epochs_called.first().unwrap(), skip_until); + // Total = 1000 - 20 = 980 + assert_eq!(epochs_called.len(), 980); + } } } diff --git a/packages/rs-dpp/src/serialization/json/safe_integer.rs b/packages/rs-dpp/src/serialization/json/safe_integer.rs index abd0683703e..d4c5b8efabb 100644 --- a/packages/rs-dpp/src/serialization/json/safe_integer.rs +++ b/packages/rs-dpp/src/serialization/json/safe_integer.rs @@ -507,4 +507,280 @@ mod tests { let restored: Versioned = serde_json::from_value(json).unwrap(); assert_eq!(v, restored); } + + // --- Additional edge-case tests for json_safe_option_i64 --- + + #[test] + fn option_i64_some_safe_value_stays_number() { + let t = TestOptionI64 { value: Some(1000) }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + assert_eq!(json["value"].as_i64().unwrap(), 1000); + + let restored: TestOptionI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn option_i64_some_unsafe_positive_becomes_string() { + // JS_MAX_SAFE_INTEGER + 1 as i64 + let unsafe_val = (JS_MAX_SAFE_INTEGER + 1) as i64; + let t = TestOptionI64 { + value: Some(unsafe_val), + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_string()); + + let restored: TestOptionI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn option_i64_some_unsafe_negative_becomes_string() { + // -(JS_MAX_SAFE_INTEGER) - 1 is below the safe boundary + let unsafe_neg = -(JS_MAX_SAFE_INTEGER as i64) - 1; + let t = TestOptionI64 { + value: Some(unsafe_neg), + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_string()); + + let restored: TestOptionI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + // --- Boundary tests --- + + #[test] + fn u64_exactly_at_max_safe_integer_round_trip() { + let t = TestU64 { + value: JS_MAX_SAFE_INTEGER, + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + + let restored: TestU64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn u64_one_above_max_safe_integer_round_trip() { + let t = TestU64 { + value: JS_MAX_SAFE_INTEGER + 1, + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_string()); + assert_eq!( + json["value"].as_str().unwrap(), + (JS_MAX_SAFE_INTEGER + 1).to_string() + ); + + let restored: TestU64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn i64_exactly_at_positive_safe_boundary_stays_number() { + let t = TestI64 { + value: JS_MAX_SAFE_INTEGER as i64, + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + + let restored: TestI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn i64_one_above_positive_safe_boundary_becomes_string() { + let t = TestI64 { + value: JS_MAX_SAFE_INTEGER as i64 + 1, + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_string()); + + let restored: TestI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn i64_exactly_at_negative_safe_boundary_stays_number() { + let t = TestI64 { + value: -(JS_MAX_SAFE_INTEGER as i64), + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + + let restored: TestI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn i64_one_below_negative_safe_boundary_becomes_string() { + let t = TestI64 { + value: -(JS_MAX_SAFE_INTEGER as i64) - 1, + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_string()); + + let restored: TestI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + // --- Zero and negative value tests --- + + #[test] + fn u64_zero_stays_number() { + let t = TestU64 { value: 0 }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + assert_eq!(json["value"].as_u64().unwrap(), 0); + + let restored: TestU64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn i64_zero_stays_number() { + let t = TestI64 { value: 0 }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + assert_eq!(json["value"].as_i64().unwrap(), 0); + + let restored: TestI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn i64_negative_one_stays_number() { + let t = TestI64 { value: -1 }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + + let restored: TestI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn option_i64_zero_round_trip() { + let t = TestOptionI64 { value: Some(0) }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + + let restored: TestOptionI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn option_u64_zero_round_trip() { + let t = TestOptionU64 { value: Some(0) }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + + let restored: TestOptionU64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn option_u64_at_max_safe_integer_stays_number() { + let t = TestOptionU64 { + value: Some(JS_MAX_SAFE_INTEGER), + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + + let restored: TestOptionU64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn option_u64_above_max_safe_integer_becomes_string() { + let t = TestOptionU64 { + value: Some(JS_MAX_SAFE_INTEGER + 1), + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_string()); + + let restored: TestOptionU64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn option_i64_at_positive_safe_boundary_stays_number() { + let t = TestOptionI64 { + value: Some(JS_MAX_SAFE_INTEGER as i64), + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + + let restored: TestOptionI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn option_i64_above_positive_safe_boundary_becomes_string() { + let t = TestOptionI64 { + value: Some(JS_MAX_SAFE_INTEGER as i64 + 1), + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_string()); + + let restored: TestOptionI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn option_i64_at_negative_safe_boundary_stays_number() { + let t = TestOptionI64 { + value: Some(-(JS_MAX_SAFE_INTEGER as i64)), + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + + let restored: TestOptionI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn option_i64_below_negative_safe_boundary_becomes_string() { + let t = TestOptionI64 { + value: Some(-(JS_MAX_SAFE_INTEGER as i64) - 1), + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_string()); + + let restored: TestOptionI64 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn platform_value_option_i64_none_round_trip() { + let t = TestOptionI64 { value: None }; + let pv = platform_value::to_value(&t).unwrap(); + let restored: TestOptionI64 = platform_value::from_value(pv).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn platform_value_option_u64_none_round_trip() { + let t = TestOptionU64 { value: None }; + let pv = platform_value::to_value(&t).unwrap(); + let restored: TestOptionU64 = platform_value::from_value(pv).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn u64_deserialize_from_string_number() { + // Deserialize a string-encoded number (even one that fits in a number) + let json = serde_json::json!({"value": "42"}); + let restored: TestU64 = serde_json::from_value(json).unwrap(); + assert_eq!(restored.value, 42); + } + + #[test] + fn i64_deserialize_from_string_number() { + let json = serde_json::json!({"value": "-12345"}); + let restored: TestI64 = serde_json::from_value(json).unwrap(); + assert_eq!(restored.value, -12345); + } } diff --git a/packages/rs-dpp/src/serialization/json/safe_integer_map.rs b/packages/rs-dpp/src/serialization/json/safe_integer_map.rs index 8bbfe9adb57..bc4e6c9ce79 100644 --- a/packages/rs-dpp/src/serialization/json/safe_integer_map.rs +++ b/packages/rs-dpp/src/serialization/json/safe_integer_map.rs @@ -576,4 +576,280 @@ mod tests { let restored: TestNestedMap = serde_json::from_value(json).unwrap(); assert_eq!(t, restored); } + + // --- Additional tests for json_safe_u64_nested_identifier_u64_map --- + + #[test] + fn nested_map_multiple_inner_keys_round_trip() { + let id1 = Identifier::random(); + let id2 = Identifier::random(); + let mut inner = BTreeMap::new(); + inner.insert(id1, 42u64); + inner.insert(id2, u64::MAX); + let mut data = BTreeMap::new(); + data.insert(1u64, inner); + let t = TestNestedMap { data }; + let json = serde_json::to_value(&t).unwrap(); + let restored: TestNestedMap = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn nested_map_multiple_outer_keys_round_trip() { + let id1 = Identifier::random(); + let id2 = Identifier::random(); + + let mut inner1 = BTreeMap::new(); + inner1.insert(id1, 100u64); + let mut inner2 = BTreeMap::new(); + inner2.insert(id2, u64::MAX); + + let mut data = BTreeMap::new(); + data.insert(0u64, inner1); + data.insert(u64::MAX, inner2); + + let t = TestNestedMap { data }; + let json = serde_json::to_value(&t).unwrap(); + + // Verify outer key u64::MAX is a string key + let map_obj = json["data"].as_object().unwrap(); + assert!(map_obj.contains_key("18446744073709551615")); + + let restored: TestNestedMap = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn nested_map_empty_outer_round_trip() { + let t = TestNestedMap { + data: BTreeMap::new(), + }; + let json = serde_json::to_value(&t).unwrap(); + let restored: TestNestedMap = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn nested_map_large_outer_key_stringified_in_json() { + let id = Identifier::random(); + let mut inner = BTreeMap::new(); + inner.insert(id, 42u64); + let mut data = BTreeMap::new(); + data.insert(u64::MAX, inner); + let t = TestNestedMap { data }; + let json = serde_json::to_value(&t).unwrap(); + // Outer key should be string since JSON map keys are always strings + let map_obj = json["data"].as_object().unwrap(); + assert!(map_obj.contains_key("18446744073709551615")); + let restored: TestNestedMap = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn nested_map_inner_small_values_stay_numbers() { + let id = Identifier::random(); + let mut inner = BTreeMap::new(); + inner.insert(id, 42u64); + let mut data = BTreeMap::new(); + data.insert(1u64, inner); + let t = TestNestedMap { data }; + let json = serde_json::to_value(&t).unwrap(); + + // Navigate to the inner map value and verify it's a number + let outer = json["data"].as_object().unwrap(); + let inner_map = outer["1"].as_object().unwrap(); + let val = inner_map.values().next().unwrap(); + assert!(val.is_number()); + } + + // --- Additional tests for json_safe_generic_u64_value_map --- + + #[test] + fn generic_map_empty_round_trip() { + let t = TestGenericMap { + data: BTreeMap::new(), + }; + let json = serde_json::to_value(&t).unwrap(); + let restored: TestGenericMap = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn generic_map_platform_value_empty_round_trip() { + let t = TestGenericMap { + data: BTreeMap::new(), + }; + let pv = platform_value::to_value(&t).unwrap(); + let restored: TestGenericMap = platform_value::from_value(pv).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn generic_map_at_safe_boundary_stays_number() { + let mut data = BTreeMap::new(); + data.insert(CustomKey("boundary".into()), JS_MAX_SAFE_INTEGER); + let t = TestGenericMap { data }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["data"]["boundary"].is_number()); + + let restored: TestGenericMap = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn generic_map_above_safe_boundary_becomes_string() { + let mut data = BTreeMap::new(); + data.insert(CustomKey("above".into()), JS_MAX_SAFE_INTEGER + 1); + let t = TestGenericMap { data }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["data"]["above"].is_string()); + + let restored: TestGenericMap = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn generic_map_zero_value_round_trip() { + let mut data = BTreeMap::new(); + data.insert(CustomKey("zero".into()), 0u64); + let t = TestGenericMap { data }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["data"]["zero"].is_number()); + assert_eq!(json["data"]["zero"].as_u64().unwrap(), 0); + + let restored: TestGenericMap = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + // --- Error path tests for generic_map --- + + #[test] + fn generic_map_invalid_value_type_fails() { + let json = serde_json::json!({"data": {"key": true}}); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } + + #[test] + fn generic_map_invalid_value_string_fails() { + let json = serde_json::json!({"data": {"key": "not_a_number"}}); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } + + #[test] + fn generic_map_null_value_fails() { + let json = serde_json::json!({"data": {"key": null}}); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } + + #[test] + fn generic_map_array_value_fails() { + let json = serde_json::json!({"data": {"key": [1, 2, 3]}}); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } + + // --- Error path tests for identifier_u64_map --- + + #[test] + fn identifier_u64_map_invalid_value_string_fails() { + let id = Identifier::random(); + let json = serde_json::json!({"data": {id.to_string(Encoding::Base58): "not_a_number"}}); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } + + #[test] + fn identifier_u64_map_null_value_fails() { + let id = Identifier::random(); + let json = serde_json::json!({"data": {id.to_string(Encoding::Base58): null}}); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } + + // --- Error path tests for nested_map --- + + #[test] + fn nested_map_invalid_inner_value_type_fails() { + let id = Identifier::random(); + let json = + serde_json::json!({"data": {"1": {id.to_string(Encoding::Base58): "not_a_number"}}}); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } + + #[test] + fn nested_map_invalid_outer_key_fails() { + let id = Identifier::random(); + let json = + serde_json::json!({"data": {"not_a_number": {id.to_string(Encoding::Base58): 42}}}); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } + + // --- u64_u64_map additional tests --- + + #[test] + fn u64_u64_map_at_safe_boundary_stays_number() { + let mut data = BTreeMap::new(); + data.insert(1u64, JS_MAX_SAFE_INTEGER); + let t = TestU64U64Map { data }; + let json = serde_json::to_value(&t).unwrap(); + let map_obj = json["data"].as_object().unwrap(); + let val = &map_obj["1"]; + assert!(val.is_number()); + + let restored: TestU64U64Map = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn u64_u64_map_above_safe_boundary_becomes_string() { + let mut data = BTreeMap::new(); + data.insert(1u64, JS_MAX_SAFE_INTEGER + 1); + let t = TestU64U64Map { data }; + let json = serde_json::to_value(&t).unwrap(); + let map_obj = json["data"].as_object().unwrap(); + let val = &map_obj["1"]; + assert!(val.is_string()); + + let restored: TestU64U64Map = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn u64_u64_map_multiple_entries_round_trip() { + let mut data = BTreeMap::new(); + data.insert(0u64, 0u64); + data.insert(42u64, JS_MAX_SAFE_INTEGER); + data.insert(u64::MAX, u64::MAX); + let t = TestU64U64Map { data }; + let json = serde_json::to_value(&t).unwrap(); + + // Value 0 should be a number + let map_obj = json["data"].as_object().unwrap(); + assert!(map_obj["0"].is_number()); + // JS_MAX_SAFE_INTEGER should be a number + assert!(map_obj["42"].is_number()); + // u64::MAX should be a string + assert!(map_obj["18446744073709551615"].is_string()); + + let restored: TestU64U64Map = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn identifier_u64_map_multiple_entries_round_trip() { + let id1 = Identifier::random(); + let id2 = Identifier::random(); + let mut data = BTreeMap::new(); + data.insert(id1, 0u64); + data.insert(id2, u64::MAX); + let t = TestIdentifierU64Map { data }; + let json = serde_json::to_value(&t).unwrap(); + let restored: TestIdentifierU64Map = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } }