diff --git a/.codecov.yml b/.codecov.yml index 41013ceb2d4..89b413c72f5 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -80,6 +80,14 @@ ignore: # Document-type property accessors — pure getter/setter trait implementations, # same category as the state-transition accessors excluded above - "packages/rs-dpp/src/data_contract/document_type/accessors/**" + # Enum type definitions — TryFrom/Display/conversion boilerplate + - "packages/rs-dpp/src/identity/identity_public_key/security_level.rs" + - "packages/rs-dpp/src/identity/identity_public_key/purpose.rs" + - "packages/rs-dpp/src/identity/identity_public_key/key_type.rs" + - "packages/rs-dpp/src/tokens/gas_fees_paid_by.rs" + # Value Display and string encoding — trivial formatting, not logic + - "packages/rs-platform-value/src/display.rs" + - "packages/rs-platform-value/src/string_encoding.rs" # Core chain type wrappers — masternode entry structs, deserialization # boilerplate, thin type aliases - "packages/rs-dpp/src/core_types/**" diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs index def46953e52..e7f0ae5652c 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs @@ -291,3 +291,113 @@ impl fmt::Display for TokenConfigurationChangeItem { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeSet; + + /// Helper: build one instance of every variant using default inner values. + fn all_variants() -> Vec { + let aat = AuthorizedActionTakers::NoOne; + vec![ + TokenConfigurationChangeItem::TokenConfigurationNoChange, + TokenConfigurationChangeItem::Conventions( + TokenConfigurationConvention::V0( + crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0::default(), + ), + ), + TokenConfigurationChangeItem::ConventionsControlGroup(aat.clone()), + TokenConfigurationChangeItem::ConventionsAdminGroup(aat.clone()), + TokenConfigurationChangeItem::MaxSupply(None), + TokenConfigurationChangeItem::MaxSupplyControlGroup(aat.clone()), + TokenConfigurationChangeItem::MaxSupplyAdminGroup(aat.clone()), + TokenConfigurationChangeItem::PerpetualDistribution(None), + TokenConfigurationChangeItem::PerpetualDistributionControlGroup(aat.clone()), + TokenConfigurationChangeItem::PerpetualDistributionAdminGroup(aat.clone()), + TokenConfigurationChangeItem::NewTokensDestinationIdentity(None), + TokenConfigurationChangeItem::NewTokensDestinationIdentityControlGroup(aat.clone()), + TokenConfigurationChangeItem::NewTokensDestinationIdentityAdminGroup(aat.clone()), + TokenConfigurationChangeItem::MintingAllowChoosingDestination(false), + TokenConfigurationChangeItem::MintingAllowChoosingDestinationControlGroup(aat.clone()), + TokenConfigurationChangeItem::MintingAllowChoosingDestinationAdminGroup(aat.clone()), + TokenConfigurationChangeItem::ManualMinting(aat.clone()), + TokenConfigurationChangeItem::ManualMintingAdminGroup(aat.clone()), + TokenConfigurationChangeItem::ManualBurning(aat.clone()), + TokenConfigurationChangeItem::ManualBurningAdminGroup(aat.clone()), + TokenConfigurationChangeItem::Freeze(aat.clone()), + TokenConfigurationChangeItem::FreezeAdminGroup(aat.clone()), + TokenConfigurationChangeItem::Unfreeze(aat.clone()), + TokenConfigurationChangeItem::UnfreezeAdminGroup(aat.clone()), + TokenConfigurationChangeItem::DestroyFrozenFunds(aat.clone()), + TokenConfigurationChangeItem::DestroyFrozenFundsAdminGroup(aat.clone()), + TokenConfigurationChangeItem::EmergencyAction(aat.clone()), + TokenConfigurationChangeItem::EmergencyActionAdminGroup(aat.clone()), + TokenConfigurationChangeItem::MarketplaceTradeMode(TokenTradeMode::default()), + TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(aat.clone()), + TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(aat.clone()), + TokenConfigurationChangeItem::MainControlGroup(None), + ] + } + + // ---- u8_item_index returns unique values 0..=31 ---- + + #[test] + fn u8_item_index_values_are_unique() { + let variants = all_variants(); + let indices: Vec = variants.iter().map(|v| v.u8_item_index()).collect(); + let unique: BTreeSet = indices.iter().cloned().collect(); + assert_eq!( + indices.len(), + unique.len(), + "Duplicate u8_item_index values found: {:?}", + indices + ); + } + + #[test] + fn u8_item_index_covers_0_through_31() { + let variants = all_variants(); + let indices: BTreeSet = variants.iter().map(|v| v.u8_item_index()).collect(); + for i in 0u8..=31 { + assert!(indices.contains(&i), "Missing u8_item_index value: {}", i); + } + } + + #[test] + fn u8_item_index_all_within_range() { + let variants = all_variants(); + for v in &variants { + let idx = v.u8_item_index(); + assert!(idx <= 31, "Index {} exceeds expected max of 31", idx); + } + } + + #[test] + fn u8_item_index_specific_known_values() { + assert_eq!( + TokenConfigurationChangeItem::TokenConfigurationNoChange.u8_item_index(), + 0 + ); + assert_eq!( + TokenConfigurationChangeItem::MaxSupply(Some(100)).u8_item_index(), + 4 + ); + assert_eq!( + TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::NoOne) + .u8_item_index(), + 16 + ); + assert_eq!( + TokenConfigurationChangeItem::MainControlGroup(Some(5)).u8_item_index(), + 31 + ); + } + + #[test] + fn u8_item_index_variant_count() { + // We expect exactly 32 variants (indices 0..=31) + let variants = all_variants(); + assert_eq!(variants.len(), 32); + } +} diff --git a/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs b/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs index a980d0549c3..cf0a4cd0c93 100644 --- a/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs +++ b/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs @@ -202,3 +202,487 @@ impl AuthorizedActionTakers { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::group::v0::GroupV0; + use std::collections::BTreeSet; + + fn make_id(byte: u8) -> Identifier { + Identifier::from([byte; 32]) + } + + fn make_group(members: Vec<(Identifier, u32)>, required_power: u32) -> Group { + Group::V0(GroupV0 { + members: members.into_iter().collect(), + required_power, + }) + } + + // --- Display tests --- + + #[test] + fn display_no_one() { + assert_eq!(format!("{}", AuthorizedActionTakers::NoOne), "NoOne"); + } + + #[test] + fn display_contract_owner() { + assert_eq!( + format!("{}", AuthorizedActionTakers::ContractOwner), + "ContractOwner" + ); + } + + #[test] + fn display_main_group() { + assert_eq!( + format!("{}", AuthorizedActionTakers::MainGroup), + "MainGroup" + ); + } + + #[test] + fn display_group_position() { + assert_eq!( + format!("{}", AuthorizedActionTakers::Group(42)), + "Group(Position: 42)" + ); + } + + #[test] + fn display_identity() { + let id = make_id(0xAB); + let display = format!("{}", AuthorizedActionTakers::Identity(id)); + assert!(display.starts_with("Identity(")); + } + + // --- to_bytes / from_bytes round-trip tests --- + + #[test] + fn round_trip_no_one() { + let original = AuthorizedActionTakers::NoOne; + let bytes = original.to_bytes(); + assert_eq!(bytes, vec![0]); + let recovered = AuthorizedActionTakers::from_bytes(&bytes).unwrap(); + assert_eq!(original, recovered); + } + + #[test] + fn round_trip_contract_owner() { + let original = AuthorizedActionTakers::ContractOwner; + let bytes = original.to_bytes(); + assert_eq!(bytes, vec![1]); + let recovered = AuthorizedActionTakers::from_bytes(&bytes).unwrap(); + assert_eq!(original, recovered); + } + + #[test] + fn round_trip_identity() { + let id = make_id(0x42); + let original = AuthorizedActionTakers::Identity(id); + let bytes = original.to_bytes(); + assert_eq!(bytes.len(), 33); // 1 tag + 32 identifier + assert_eq!(bytes[0], 2); + let recovered = AuthorizedActionTakers::from_bytes(&bytes).unwrap(); + assert_eq!(original, recovered); + } + + #[test] + fn round_trip_main_group() { + let original = AuthorizedActionTakers::MainGroup; + let bytes = original.to_bytes(); + assert_eq!(bytes, vec![3]); + let recovered = AuthorizedActionTakers::from_bytes(&bytes).unwrap(); + assert_eq!(original, recovered); + } + + #[test] + fn round_trip_group() { + let original = AuthorizedActionTakers::Group(1000); + let bytes = original.to_bytes(); + assert_eq!(bytes.len(), 3); // 1 tag + 2 for u16 + assert_eq!(bytes[0], 4); + let recovered = AuthorizedActionTakers::from_bytes(&bytes).unwrap(); + assert_eq!(original, recovered); + } + + #[test] + fn round_trip_group_max_position() { + let original = AuthorizedActionTakers::Group(u16::MAX); + let bytes = original.to_bytes(); + let recovered = AuthorizedActionTakers::from_bytes(&bytes).unwrap(); + assert_eq!(original, recovered); + } + + // --- from_bytes error path tests --- + + #[test] + fn from_bytes_empty_returns_error() { + let result = AuthorizedActionTakers::from_bytes(&[]); + assert!(result.is_err()); + } + + #[test] + fn from_bytes_unknown_tag_returns_error() { + let result = AuthorizedActionTakers::from_bytes(&[5]); + assert!(result.is_err()); + let result = AuthorizedActionTakers::from_bytes(&[255]); + assert!(result.is_err()); + } + + #[test] + fn from_bytes_identity_wrong_length_returns_error() { + // tag 2 needs exactly 33 bytes total + let short = vec![2; 10]; // only 10 bytes + let result = AuthorizedActionTakers::from_bytes(&short); + assert!(result.is_err()); + } + + #[test] + fn from_bytes_group_wrong_length_returns_error() { + // tag 4 needs exactly 3 bytes total + let short = vec![4, 0]; // only 2 bytes + let result = AuthorizedActionTakers::from_bytes(&short); + assert!(result.is_err()); + + let long = vec![4, 0, 0, 0]; // 4 bytes + let result = AuthorizedActionTakers::from_bytes(&long); + assert!(result.is_err()); + } + + // --- allowed_for_action_taker tests --- + + #[test] + fn no_one_always_returns_false() { + let aat = AuthorizedActionTakers::NoOne; + let owner = make_id(1); + let taker = ActionTaker::SingleIdentity(owner); + assert!(!aat.allowed_for_action_taker( + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn contract_owner_allows_matching_single_identity() { + let aat = AuthorizedActionTakers::ContractOwner; + let owner = make_id(1); + let taker = ActionTaker::SingleIdentity(owner); + assert!(aat.allowed_for_action_taker( + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn contract_owner_rejects_non_matching_single_identity() { + let aat = AuthorizedActionTakers::ContractOwner; + let owner = make_id(1); + let other = make_id(2); + let taker = ActionTaker::SingleIdentity(other); + assert!(!aat.allowed_for_action_taker( + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn contract_owner_rejects_action_participation() { + let aat = AuthorizedActionTakers::ContractOwner; + let owner = make_id(1); + let taker = ActionTaker::SingleIdentity(owner); + assert!(!aat.allowed_for_action_taker( + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionParticipation, + )); + } + + #[test] + fn contract_owner_allows_specified_identities_containing_owner() { + let aat = AuthorizedActionTakers::ContractOwner; + let owner = make_id(1); + let mut set = BTreeSet::new(); + set.insert(owner); + set.insert(make_id(2)); + let taker = ActionTaker::SpecifiedIdentities(set); + assert!(aat.allowed_for_action_taker( + &owner, + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn identity_allows_matching_identity() { + let authorized_id = make_id(5); + let aat = AuthorizedActionTakers::Identity(authorized_id); + let taker = ActionTaker::SingleIdentity(authorized_id); + assert!(aat.allowed_for_action_taker( + &make_id(1), + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn identity_rejects_non_matching_identity() { + let authorized_id = make_id(5); + let aat = AuthorizedActionTakers::Identity(authorized_id); + let taker = ActionTaker::SingleIdentity(make_id(6)); + assert!(!aat.allowed_for_action_taker( + &make_id(1), + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn identity_rejects_action_participation() { + let authorized_id = make_id(5); + let aat = AuthorizedActionTakers::Identity(authorized_id); + let taker = ActionTaker::SingleIdentity(authorized_id); + assert!(!aat.allowed_for_action_taker( + &make_id(1), + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionParticipation, + )); + } + + #[test] + fn group_allows_single_member_with_enough_power() { + let member = make_id(10); + let group = make_group(vec![(member, 100)], 50); + let mut groups = BTreeMap::new(); + groups.insert(0u16, group); + + let aat = AuthorizedActionTakers::Group(0); + let taker = ActionTaker::SingleIdentity(member); + assert!(aat.allowed_for_action_taker( + &make_id(1), + None, + &groups, + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn group_rejects_single_member_with_insufficient_power() { + let member = make_id(10); + let group = make_group(vec![(member, 10)], 50); + let mut groups = BTreeMap::new(); + groups.insert(0u16, group); + + let aat = AuthorizedActionTakers::Group(0); + let taker = ActionTaker::SingleIdentity(member); + assert!(!aat.allowed_for_action_taker( + &make_id(1), + None, + &groups, + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn group_allows_participation_for_member() { + let member = make_id(10); + let group = make_group(vec![(member, 10)], 50); + let mut groups = BTreeMap::new(); + groups.insert(0u16, group); + + let aat = AuthorizedActionTakers::Group(0); + let taker = ActionTaker::SingleIdentity(member); + assert!(aat.allowed_for_action_taker( + &make_id(1), + None, + &groups, + &taker, + ActionGoal::ActionParticipation, + )); + } + + #[test] + fn group_rejects_participation_for_non_member() { + let member = make_id(10); + let non_member = make_id(11); + let group = make_group(vec![(member, 10)], 50); + let mut groups = BTreeMap::new(); + groups.insert(0u16, group); + + let aat = AuthorizedActionTakers::Group(0); + let taker = ActionTaker::SingleIdentity(non_member); + assert!(!aat.allowed_for_action_taker( + &make_id(1), + None, + &groups, + &taker, + ActionGoal::ActionParticipation, + )); + } + + #[test] + fn group_rejects_when_group_not_found() { + let aat = AuthorizedActionTakers::Group(99); + let taker = ActionTaker::SingleIdentity(make_id(10)); + assert!(!aat.allowed_for_action_taker( + &make_id(1), + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn group_allows_specified_identities_with_enough_combined_power() { + let member_a = make_id(10); + let member_b = make_id(11); + let group = make_group(vec![(member_a, 30), (member_b, 30)], 50); + let mut groups = BTreeMap::new(); + groups.insert(0u16, group); + + let mut set = BTreeSet::new(); + set.insert(member_a); + set.insert(member_b); + let taker = ActionTaker::SpecifiedIdentities(set); + + let aat = AuthorizedActionTakers::Group(0); + assert!(aat.allowed_for_action_taker( + &make_id(1), + None, + &groups, + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn group_rejects_specified_identities_with_insufficient_combined_power() { + let member_a = make_id(10); + let member_b = make_id(11); + let group = make_group(vec![(member_a, 10), (member_b, 10)], 50); + let mut groups = BTreeMap::new(); + groups.insert(0u16, group); + + let mut set = BTreeSet::new(); + set.insert(member_a); + set.insert(member_b); + let taker = ActionTaker::SpecifiedIdentities(set); + + let aat = AuthorizedActionTakers::Group(0); + assert!(!aat.allowed_for_action_taker( + &make_id(1), + None, + &groups, + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn main_group_allows_when_main_group_exists_and_power_sufficient() { + let member = make_id(10); + let group = make_group(vec![(member, 100)], 50); + let mut groups = BTreeMap::new(); + groups.insert(7u16, group); + + let aat = AuthorizedActionTakers::MainGroup; + let taker = ActionTaker::SingleIdentity(member); + assert!(aat.allowed_for_action_taker( + &make_id(1), + Some(7), + &groups, + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn main_group_rejects_when_no_main_group_position() { + let aat = AuthorizedActionTakers::MainGroup; + let taker = ActionTaker::SingleIdentity(make_id(10)); + assert!(!aat.allowed_for_action_taker( + &make_id(1), + None, + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn main_group_rejects_when_group_not_in_map() { + let aat = AuthorizedActionTakers::MainGroup; + let taker = ActionTaker::SingleIdentity(make_id(10)); + assert!(!aat.allowed_for_action_taker( + &make_id(1), + Some(99), + &BTreeMap::new(), + &taker, + ActionGoal::ActionCompletion, + )); + } + + #[test] + fn main_group_participation_allows_member() { + let member = make_id(10); + let group = make_group(vec![(member, 10)], 100); + let mut groups = BTreeMap::new(); + groups.insert(0u16, group); + + let aat = AuthorizedActionTakers::MainGroup; + let taker = ActionTaker::SingleIdentity(member); + assert!(aat.allowed_for_action_taker( + &make_id(1), + Some(0), + &groups, + &taker, + ActionGoal::ActionParticipation, + )); + } + + #[test] + fn participation_rejects_specified_identities() { + let member = make_id(10); + let group = make_group(vec![(member, 10)], 50); + let mut groups = BTreeMap::new(); + groups.insert(0u16, group); + + let mut set = BTreeSet::new(); + set.insert(member); + let taker = ActionTaker::SpecifiedIdentities(set); + + let aat = AuthorizedActionTakers::Group(0); + // is_action_taker_participant returns false for SpecifiedIdentities + assert!(!aat.allowed_for_action_taker( + &make_id(1), + None, + &groups, + &taker, + ActionGoal::ActionParticipation, + )); + } +} diff --git a/packages/rs-dpp/src/fee/fee_result/mod.rs b/packages/rs-dpp/src/fee/fee_result/mod.rs index 0be1214e844..010cc04bbdb 100644 --- a/packages/rs-dpp/src/fee/fee_result/mod.rs +++ b/packages/rs-dpp/src/fee/fee_result/mod.rs @@ -280,3 +280,336 @@ impl FeeResult { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::fee::fee_error::FeeError; + use crate::fee::epoch::CreditsPerEpoch; + use crate::fee::fee_result::refunds::{CreditsPerEpochByIdentifier, FeeRefunds}; + + fn make_id(byte: u8) -> Identifier { + Identifier::from([byte; 32]) + } + + /// Build a FeeRefunds that gives `credits` to `identity_id` (all in epoch 0). + fn fee_refunds_for_identity(identity_id: Identifier, credits: Credits) -> FeeRefunds { + let mut credits_per_epoch = CreditsPerEpoch::default(); + credits_per_epoch.insert(0, credits); + let mut map = CreditsPerEpochByIdentifier::new(); + map.insert(*identity_id.as_bytes(), credits_per_epoch); + FeeRefunds(map) + } + + // --- BalanceChangeForIdentity::change() --- + + #[test] + fn balance_change_for_identity_change_returns_correct_ref() { + let id = make_id(1); + let fee_result = FeeResult::default_with_fees(100, 50); + let bci = fee_result.into_balance_change(id); + // No refunds, so it should be RemoveFromBalance + match bci.change() { + BalanceChange::RemoveFromBalance { + required_removed_balance, + desired_removed_balance, + } => { + assert_eq!(*required_removed_balance, 100); + assert_eq!(*desired_removed_balance, 150); + } + other => panic!("Expected RemoveFromBalance, got {:?}", other), + } + } + + // --- BalanceChangeForIdentity::other_refunds() --- + + #[test] + fn other_refunds_empty_when_no_refunds() { + let id = make_id(1); + let fee_result = FeeResult::default_with_fees(100, 50); + let bci = fee_result.into_balance_change(id); + let refunds = bci.other_refunds(); + assert!(refunds.is_empty()); + } + + #[test] + fn other_refunds_excludes_own_identity() { + let id = make_id(1); + let other_id = make_id(2); + // Build refunds for both identities + let mut credits_per_epoch_self = CreditsPerEpoch::default(); + credits_per_epoch_self.insert(0, 200); + let mut credits_per_epoch_other = CreditsPerEpoch::default(); + credits_per_epoch_other.insert(0, 300); + let mut map = CreditsPerEpochByIdentifier::new(); + map.insert(*id.as_bytes(), credits_per_epoch_self); + map.insert(*other_id.as_bytes(), credits_per_epoch_other); + let refunds = FeeRefunds(map); + + let fee_result = FeeResult { + storage_fee: 100, + processing_fee: 50, + fee_refunds: refunds, + removed_bytes_from_system: 0, + }; + let bci = fee_result.into_balance_change(id); + let other = bci.other_refunds(); + assert_eq!(other.len(), 1); + assert_eq!(*other.get(&other_id).unwrap(), 300); + } + + // --- BalanceChangeForIdentity::into_fee_result() --- + + #[test] + fn into_fee_result_preserves_original() { + let fee_result = FeeResult { + storage_fee: 42, + processing_fee: 58, + fee_refunds: FeeRefunds::default(), + removed_bytes_from_system: 10, + }; + let id = make_id(1); + let bci = fee_result.clone().into_balance_change(id); + let recovered = bci.into_fee_result(); + assert_eq!(recovered.storage_fee, 42); + assert_eq!(recovered.processing_fee, 58); + assert_eq!(recovered.removed_bytes_from_system, 10); + } + + // --- BalanceChangeForIdentity::fee_result_outcome() --- + + #[test] + fn fee_result_outcome_add_to_balance_returns_fee_result() { + let id = make_id(1); + // Refund more than storage + processing so we get AddToBalance + let refunds = fee_refunds_for_identity(id, 500); + let fee_result = FeeResult { + storage_fee: 100, + processing_fee: 50, + fee_refunds: refunds, + removed_bytes_from_system: 0, + }; + let bci = fee_result.into_balance_change(id); + match bci.change() { + BalanceChange::AddToBalance(amount) => assert_eq!(*amount, 350), + other => panic!("Expected AddToBalance, got {:?}", other), + } + // Cannot access change after move, re-create + let refunds2 = fee_refunds_for_identity(id, 500); + let fee_result2 = FeeResult { + storage_fee: 100, + processing_fee: 50, + fee_refunds: refunds2, + removed_bytes_from_system: 0, + }; + let bci2 = fee_result2.into_balance_change(id); + let result: Result = bci2.fee_result_outcome(0); + assert!(result.is_ok()); + } + + #[test] + fn fee_result_outcome_remove_balance_sufficient_desired() { + let id = make_id(1); + let fee_result = FeeResult::default_with_fees(100, 50); + let bci = fee_result.into_balance_change(id); + // User has enough for desired_removed_balance (150) + let result: Result = bci.fee_result_outcome(200); + let fr = result.unwrap(); + assert_eq!(fr.storage_fee, 100); + assert_eq!(fr.processing_fee, 50); + } + + #[test] + fn fee_result_outcome_remove_balance_sufficient_required_but_not_desired() { + let id = make_id(1); + let fee_result = FeeResult::default_with_fees(100, 50); + let bci = fee_result.into_balance_change(id); + // User has 120: enough for required (100) but not desired (150) + let result: Result = bci.fee_result_outcome(120); + let fr = result.unwrap(); + assert_eq!(fr.storage_fee, 100); + // processing_fee should be reduced by (desired - user_balance) = 150 - 120 = 30 + assert_eq!(fr.processing_fee, 20); + } + + #[test] + fn fee_result_outcome_remove_balance_insufficient_returns_error() { + let id = make_id(1); + let fee_result = FeeResult::default_with_fees(100, 50); + let bci = fee_result.into_balance_change(id); + // User has less than required (100) + let result: Result = bci.fee_result_outcome(50); + assert!(result.is_err()); + match result.unwrap_err() { + FeeError::BalanceIsNotEnoughError(e) => { + assert_eq!(e.balance(), 50); + assert_eq!(e.fee(), 100); + } + } + } + + #[test] + fn fee_result_outcome_no_balance_change_returns_fee_result() { + let id = make_id(1); + // Refund exactly storage + processing = 150 + let refunds = fee_refunds_for_identity(id, 150); + let fee_result = FeeResult { + storage_fee: 100, + processing_fee: 50, + fee_refunds: refunds, + removed_bytes_from_system: 0, + }; + let bci = fee_result.into_balance_change(id); + match bci.change() { + BalanceChange::NoBalanceChange => {} + other => panic!("Expected NoBalanceChange, got {:?}", other), + } + // Re-create for outcome check + let refunds2 = fee_refunds_for_identity(id, 150); + let fee_result2 = FeeResult { + storage_fee: 100, + processing_fee: 50, + fee_refunds: refunds2, + removed_bytes_from_system: 0, + }; + let bci2 = fee_result2.into_balance_change(id); + let result: Result = bci2.fee_result_outcome(0); + assert!(result.is_ok()); + } + + // --- FeeResult::into_balance_change() with 3 ordering branches --- + + #[test] + fn into_balance_change_less_refund_than_fees() { + let id = make_id(1); + // Refund 50, but storage=100 processing=50 total=150 + let refunds = fee_refunds_for_identity(id, 50); + let fee_result = FeeResult { + storage_fee: 100, + processing_fee: 50, + fee_refunds: refunds, + removed_bytes_from_system: 0, + }; + let bci = fee_result.into_balance_change(id); + match bci.change() { + BalanceChange::RemoveFromBalance { + required_removed_balance, + desired_removed_balance, + } => { + // required = max(0, 100 - 50) = 50 + assert_eq!(*required_removed_balance, 50); + // desired = 150 - 50 = 100 + assert_eq!(*desired_removed_balance, 100); + } + other => panic!("Expected RemoveFromBalance, got {:?}", other), + } + } + + #[test] + fn into_balance_change_refund_equals_fees() { + let id = make_id(1); + let refunds = fee_refunds_for_identity(id, 150); + let fee_result = FeeResult { + storage_fee: 100, + processing_fee: 50, + fee_refunds: refunds, + removed_bytes_from_system: 0, + }; + let bci = fee_result.into_balance_change(id); + assert_eq!(bci.change(), &BalanceChange::NoBalanceChange); + } + + #[test] + fn into_balance_change_refund_greater_than_fees() { + let id = make_id(1); + let refunds = fee_refunds_for_identity(id, 300); + let fee_result = FeeResult { + storage_fee: 100, + processing_fee: 50, + fee_refunds: refunds, + removed_bytes_from_system: 0, + }; + let bci = fee_result.into_balance_change(id); + match bci.change() { + BalanceChange::AddToBalance(amount) => { + assert_eq!(*amount, 150); // 300 - 150 + } + other => panic!("Expected AddToBalance, got {:?}", other), + } + } + + #[test] + fn into_balance_change_no_refunds_no_fees() { + let id = make_id(1); + let fee_result = FeeResult::default(); + let bci = fee_result.into_balance_change(id); + // 0 == 0, so NoBalanceChange? Actually 0.cmp(&0) is Equal + assert_eq!(bci.change(), &BalanceChange::NoBalanceChange); + } + + #[test] + fn into_balance_change_no_refunds_with_fees() { + let id = make_id(1); + let fee_result = FeeResult::default_with_fees(200, 100); + let bci = fee_result.into_balance_change(id); + match bci.change() { + BalanceChange::RemoveFromBalance { + required_removed_balance, + desired_removed_balance, + } => { + assert_eq!(*required_removed_balance, 200); + assert_eq!(*desired_removed_balance, 300); + } + other => panic!("Expected RemoveFromBalance, got {:?}", other), + } + } + + // --- apply_user_fee_increase --- + + #[test] + fn apply_user_fee_increase_zero_percent() { + let mut fr = FeeResult::default_with_fees(100, 1000); + fr.apply_user_fee_increase(0); + assert_eq!(fr.processing_fee, 1000); + } + + #[test] + fn apply_user_fee_increase_100_percent() { + let mut fr = FeeResult::default_with_fees(100, 1000); + fr.apply_user_fee_increase(100); + // 100% additional = doubles the processing fee + assert_eq!(fr.processing_fee, 2000); + } + + #[test] + fn apply_user_fee_increase_50_percent() { + let mut fr = FeeResult::default_with_fees(100, 1000); + fr.apply_user_fee_increase(50); + // 50% additional = 1000 + 500 + assert_eq!(fr.processing_fee, 1500); + } + + #[test] + fn apply_user_fee_increase_does_not_affect_storage_fee() { + let mut fr = FeeResult::default_with_fees(500, 1000); + fr.apply_user_fee_increase(100); + assert_eq!(fr.storage_fee, 500); + assert_eq!(fr.processing_fee, 2000); + } + + #[test] + fn apply_user_fee_increase_saturates_on_overflow() { + let mut fr = FeeResult::default_with_fees(0, u64::MAX); + fr.apply_user_fee_increase(100); + // Should saturate to u64::MAX rather than panicking + assert_eq!(fr.processing_fee, u64::MAX); + } + + #[test] + fn apply_user_fee_increase_1_percent() { + let mut fr = FeeResult::default_with_fees(0, 10000); + fr.apply_user_fee_increase(1); + // 1% of 10000 = 100 + assert_eq!(fr.processing_fee, 10100); + } +} diff --git a/packages/rs-dpp/src/identity/identity_public_key/key_type.rs b/packages/rs-dpp/src/identity/identity_public_key/key_type.rs index 01e1a31c8ac..3f5ddae640a 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/key_type.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/key_type.rs @@ -360,3 +360,214 @@ impl Into for KeyType { CborValue::from(self as u128) } } + +#[cfg(test)] +mod tests { + use super::*; + + // -- default_size() -- + + #[test] + fn test_default_size_ecdsa_secp256k1() { + assert_eq!(KeyType::ECDSA_SECP256K1.default_size(), 33); + } + + #[test] + fn test_default_size_bls12_381() { + assert_eq!(KeyType::BLS12_381.default_size(), 48); + } + + #[test] + fn test_default_size_ecdsa_hash160() { + assert_eq!(KeyType::ECDSA_HASH160.default_size(), 20); + } + + #[test] + fn test_default_size_bip13_script_hash() { + assert_eq!(KeyType::BIP13_SCRIPT_HASH.default_size(), 20); + } + + #[test] + fn test_default_size_eddsa_25519_hash160() { + assert_eq!(KeyType::EDDSA_25519_HASH160.default_size(), 20); + } + + // -- all_key_types() -- + + #[test] + fn test_all_key_types_has_five_elements() { + let types = KeyType::all_key_types(); + assert_eq!(types.len(), 5); + } + + #[test] + fn test_all_key_types_contains_all_variants() { + let types = KeyType::all_key_types(); + assert_eq!( + types, + [ + KeyType::ECDSA_SECP256K1, + KeyType::BLS12_381, + KeyType::ECDSA_HASH160, + KeyType::BIP13_SCRIPT_HASH, + KeyType::EDDSA_25519_HASH160, + ] + ); + } + + // -- is_unique_key_type() -- + + #[test] + fn test_ecdsa_secp256k1_is_unique() { + assert!(KeyType::ECDSA_SECP256K1.is_unique_key_type()); + } + + #[test] + fn test_bls12_381_is_unique() { + assert!(KeyType::BLS12_381.is_unique_key_type()); + } + + #[test] + fn test_ecdsa_hash160_is_not_unique() { + assert!(!KeyType::ECDSA_HASH160.is_unique_key_type()); + } + + #[test] + fn test_bip13_script_hash_is_not_unique() { + assert!(!KeyType::BIP13_SCRIPT_HASH.is_unique_key_type()); + } + + #[test] + fn test_eddsa_25519_hash160_is_not_unique() { + assert!(!KeyType::EDDSA_25519_HASH160.is_unique_key_type()); + } + + // -- is_core_address_key_type() -- + + #[test] + fn test_ecdsa_secp256k1_not_core_address() { + assert!(!KeyType::ECDSA_SECP256K1.is_core_address_key_type()); + } + + #[test] + fn test_bls12_381_not_core_address() { + assert!(!KeyType::BLS12_381.is_core_address_key_type()); + } + + #[test] + fn test_ecdsa_hash160_is_core_address() { + assert!(KeyType::ECDSA_HASH160.is_core_address_key_type()); + } + + #[test] + fn test_bip13_script_hash_is_core_address() { + assert!(KeyType::BIP13_SCRIPT_HASH.is_core_address_key_type()); + } + + #[test] + fn test_eddsa_25519_hash160_not_core_address() { + assert!(!KeyType::EDDSA_25519_HASH160.is_core_address_key_type()); + } + + // -- TryFrom valid -- + + #[test] + fn test_try_from_u8_ecdsa_secp256k1() { + assert_eq!(KeyType::try_from(0u8).unwrap(), KeyType::ECDSA_SECP256K1); + } + + #[test] + fn test_try_from_u8_bls12_381() { + assert_eq!(KeyType::try_from(1u8).unwrap(), KeyType::BLS12_381); + } + + #[test] + fn test_try_from_u8_ecdsa_hash160() { + assert_eq!(KeyType::try_from(2u8).unwrap(), KeyType::ECDSA_HASH160); + } + + #[test] + fn test_try_from_u8_bip13_script_hash() { + assert_eq!(KeyType::try_from(3u8).unwrap(), KeyType::BIP13_SCRIPT_HASH); + } + + #[test] + fn test_try_from_u8_eddsa_25519_hash160() { + assert_eq!( + KeyType::try_from(4u8).unwrap(), + KeyType::EDDSA_25519_HASH160 + ); + } + + // -- TryFrom invalid -- + + #[test] + fn test_try_from_u8_invalid_5() { + assert!(KeyType::try_from(5u8).is_err()); + } + + #[test] + fn test_try_from_u8_invalid_255() { + assert!(KeyType::try_from(255u8).is_err()); + } + + // -- Display -- + + #[test] + fn test_display_ecdsa_secp256k1() { + assert_eq!(format!("{}", KeyType::ECDSA_SECP256K1), "ECDSA_SECP256K1"); + } + + #[test] + fn test_display_bls12_381() { + assert_eq!(format!("{}", KeyType::BLS12_381), "BLS12_381"); + } + + #[test] + fn test_display_ecdsa_hash160() { + assert_eq!(format!("{}", KeyType::ECDSA_HASH160), "ECDSA_HASH160"); + } + + #[test] + fn test_display_bip13_script_hash() { + assert_eq!( + format!("{}", KeyType::BIP13_SCRIPT_HASH), + "BIP13_SCRIPT_HASH" + ); + } + + #[test] + fn test_display_eddsa_25519_hash160() { + assert_eq!( + format!("{}", KeyType::EDDSA_25519_HASH160), + "EDDSA_25519_HASH160" + ); + } + + // -- Default -- + + #[test] + fn test_default_is_ecdsa_secp256k1() { + assert_eq!(KeyType::default(), KeyType::ECDSA_SECP256K1); + } + + // -- round-trip: u8 -> KeyType -> u8 -- + + #[test] + fn test_round_trip_all_valid() { + for val in 0u8..=4 { + let key_type = KeyType::try_from(val).unwrap(); + assert_eq!(key_type as u8, val); + } + } + + // -- unique vs core address are complementary for full-size key types -- + + #[test] + fn test_unique_and_core_address_are_mutually_exclusive() { + for kt in KeyType::all_key_types() { + // A key type should not be both unique and a core address key type + assert!(!(kt.is_unique_key_type() && kt.is_core_address_key_type())); + } + } +} diff --git a/packages/rs-dpp/src/tokens/token_event.rs b/packages/rs-dpp/src/tokens/token_event.rs index 4f508bfb7fe..49a63b9b651 100644 --- a/packages/rs-dpp/src/tokens/token_event.rs +++ b/packages/rs-dpp/src/tokens/token_event.rs @@ -479,3 +479,135 @@ impl TokenEvent { Ok(document) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn test_id() -> Identifier { + Identifier::from([1u8; 32]) + } + + fn test_id_2() -> Identifier { + Identifier::from([2u8; 32]) + } + + // ---- associated_document_type_name tests ---- + + #[test] + fn associated_name_mint() { + let event = TokenEvent::Mint(0, test_id(), None); + assert_eq!(event.associated_document_type_name(), "mint"); + } + + #[test] + fn associated_name_burn() { + let event = TokenEvent::Burn(0, test_id(), None); + assert_eq!(event.associated_document_type_name(), "burn"); + } + + #[test] + fn associated_name_freeze() { + let event = TokenEvent::Freeze(test_id(), None); + assert_eq!(event.associated_document_type_name(), "freeze"); + } + + #[test] + fn associated_name_unfreeze() { + let event = TokenEvent::Unfreeze(test_id(), None); + assert_eq!(event.associated_document_type_name(), "unfreeze"); + } + + #[test] + fn associated_name_destroy_frozen_funds() { + let event = TokenEvent::DestroyFrozenFunds(test_id(), 0, None); + assert_eq!(event.associated_document_type_name(), "destroyFrozenFunds"); + } + + #[test] + fn associated_name_transfer() { + let event = TokenEvent::Transfer(test_id(), None, None, None, 0); + assert_eq!(event.associated_document_type_name(), "transfer"); + } + + #[test] + fn associated_name_claim() { + let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id()); + let event = TokenEvent::Claim(recipient, 0, None); + assert_eq!(event.associated_document_type_name(), "claim"); + } + + #[test] + fn associated_name_emergency_action() { + let event = TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None); + assert_eq!(event.associated_document_type_name(), "emergencyAction"); + } + + #[test] + fn associated_name_config_update() { + let event = TokenEvent::ConfigUpdate( + TokenConfigurationChangeItem::TokenConfigurationNoChange, + None, + ); + assert_eq!(event.associated_document_type_name(), "configUpdate"); + } + + #[test] + fn associated_name_direct_purchase() { + let event = TokenEvent::DirectPurchase(0, 0); + assert_eq!(event.associated_document_type_name(), "directPurchase"); + } + + #[test] + fn associated_name_change_price() { + let event = TokenEvent::ChangePriceForDirectPurchase(None, None); + assert_eq!(event.associated_document_type_name(), "directPricing"); + } + + // ---- all associated_document_type_name values are distinct ---- + + #[test] + fn all_document_type_names_are_unique() { + let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id()); + let events: Vec = vec![ + TokenEvent::Mint(0, test_id(), None), + TokenEvent::Burn(0, test_id(), None), + TokenEvent::Freeze(test_id(), None), + TokenEvent::Unfreeze(test_id(), None), + TokenEvent::DestroyFrozenFunds(test_id(), 0, None), + TokenEvent::Transfer(test_id(), None, None, None, 0), + TokenEvent::Claim(recipient, 0, None), + TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None), + TokenEvent::ConfigUpdate( + TokenConfigurationChangeItem::TokenConfigurationNoChange, + None, + ), + TokenEvent::DirectPurchase(0, 0), + TokenEvent::ChangePriceForDirectPurchase(None, None), + ]; + let names: Vec<&str> = events + .iter() + .map(|e| e.associated_document_type_name()) + .collect(); + let mut unique = names.clone(); + unique.sort(); + unique.dedup(); + assert_eq!( + names.len(), + unique.len(), + "Duplicate document type names found" + ); + } + + // ---- format_note helper ---- + + #[test] + fn format_note_none_returns_empty() { + assert_eq!(format_note(&None), ""); + } + + #[test] + fn format_note_some_returns_formatted() { + assert_eq!(format_note(&Some("hello".to_string())), " (note: hello)"); + } +} diff --git a/packages/rs-dpp/src/tokens/token_pricing_schedule.rs b/packages/rs-dpp/src/tokens/token_pricing_schedule.rs index 97c553b49f3..5af1f3ebb9a 100644 --- a/packages/rs-dpp/src/tokens/token_pricing_schedule.rs +++ b/packages/rs-dpp/src/tokens/token_pricing_schedule.rs @@ -75,3 +75,87 @@ impl Display for TokenPricingSchedule { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn single_price_minimum_purchase_amount_and_price() { + let schedule = TokenPricingSchedule::SinglePrice(500); + let (amount, price) = schedule.minimum_purchase_amount_and_price(); + assert_eq!(amount, 1); + assert_eq!(price, 500); + } + + #[test] + fn single_price_zero_credits() { + let schedule = TokenPricingSchedule::SinglePrice(0); + let (amount, price) = schedule.minimum_purchase_amount_and_price(); + assert_eq!(amount, 1); + assert_eq!(price, 0); + } + + #[test] + fn set_prices_minimum_purchase_amount_and_price_single_entry() { + let mut prices = BTreeMap::new(); + prices.insert(10u64, 100u64); + let schedule = TokenPricingSchedule::SetPrices(prices); + let (amount, price) = schedule.minimum_purchase_amount_and_price(); + assert_eq!(amount, 10); + assert_eq!(price, 100); + } + + #[test] + fn set_prices_minimum_purchase_amount_and_price_multiple_entries() { + let mut prices = BTreeMap::new(); + prices.insert(5u64, 50u64); + prices.insert(10u64, 80u64); + prices.insert(100u64, 500u64); + let schedule = TokenPricingSchedule::SetPrices(prices); + // BTreeMap orders by key, so the first entry is the minimum amount + let (amount, price) = schedule.minimum_purchase_amount_and_price(); + assert_eq!(amount, 5); + assert_eq!(price, 50); + } + + #[test] + fn set_prices_empty_map_returns_default() { + let prices = BTreeMap::new(); + let schedule = TokenPricingSchedule::SetPrices(prices); + let (amount, price) = schedule.minimum_purchase_amount_and_price(); + // unwrap_or_default returns (0, 0) for empty map + assert_eq!(amount, 0); + assert_eq!(price, 0); + } + + #[test] + fn display_single_price() { + let schedule = TokenPricingSchedule::SinglePrice(1234); + assert_eq!(format!("{}", schedule), "SinglePrice: 1234"); + } + + #[test] + fn display_set_prices_empty() { + let schedule = TokenPricingSchedule::SetPrices(BTreeMap::new()); + assert_eq!(format!("{}", schedule), "SetPrices: []"); + } + + #[test] + fn display_set_prices_single_entry() { + let mut prices = BTreeMap::new(); + prices.insert(10u64, 100u64); + let schedule = TokenPricingSchedule::SetPrices(prices); + assert_eq!(format!("{}", schedule), "SetPrices: [10 => 100]"); + } + + #[test] + fn display_set_prices_multiple_entries() { + let mut prices = BTreeMap::new(); + prices.insert(5u64, 50u64); + prices.insert(10u64, 80u64); + let schedule = TokenPricingSchedule::SetPrices(prices); + // BTreeMap iterates in sorted key order + assert_eq!(format!("{}", schedule), "SetPrices: [5 => 50, 10 => 80]"); + } +} diff --git a/packages/rs-dpp/src/util/vec.rs b/packages/rs-dpp/src/util/vec.rs index edbb63b7d76..c1b78bd11c8 100644 --- a/packages/rs-dpp/src/util/vec.rs +++ b/packages/rs-dpp/src/util/vec.rs @@ -65,3 +65,216 @@ pub fn vec_to_array(vec: &[u8]) -> Result<[u8; N], InvalidVector } Ok(v) } + +#[cfg(test)] +mod tests { + use super::*; + + // -- encode_hex -- + + #[test] + fn test_encode_hex_empty() { + let bytes: Vec = vec![]; + assert_eq!(encode_hex(&bytes), ""); + } + + #[test] + fn test_encode_hex_single_byte() { + let bytes: Vec = vec![0xff]; + assert_eq!(encode_hex(&bytes), "ff"); + } + + #[test] + fn test_encode_hex_multiple_bytes() { + let bytes: Vec = vec![0xde, 0xad, 0xbe, 0xef]; + assert_eq!(encode_hex(&bytes), "deadbeef"); + } + + #[test] + fn test_encode_hex_leading_zeros() { + let bytes: Vec = vec![0x00, 0x01, 0x0a]; + assert_eq!(encode_hex(&bytes), "00010a"); + } + + #[test] + fn test_encode_hex_all_zeros() { + let bytes: Vec = vec![0x00, 0x00, 0x00]; + assert_eq!(encode_hex(&bytes), "000000"); + } + + // -- decode_hex -- + + #[test] + fn test_decode_hex_empty() { + let result = decode_hex("").unwrap(); + assert!(result.is_empty()); + } + + #[test] + fn test_decode_hex_valid() { + let result = decode_hex("deadbeef").unwrap(); + assert_eq!(result, vec![0xde, 0xad, 0xbe, 0xef]); + } + + #[test] + fn test_decode_hex_uppercase() { + let result = decode_hex("DEADBEEF").unwrap(); + assert_eq!(result, vec![0xde, 0xad, 0xbe, 0xef]); + } + + #[test] + fn test_decode_hex_mixed_case() { + let result = decode_hex("DeAdBeEf").unwrap(); + assert_eq!(result, vec![0xde, 0xad, 0xbe, 0xef]); + } + + #[test] + fn test_decode_hex_leading_zeros() { + let result = decode_hex("00010a").unwrap(); + assert_eq!(result, vec![0x00, 0x01, 0x0a]); + } + + #[test] + fn test_decode_hex_invalid_chars() { + let result = decode_hex("zzzz"); + assert!(result.is_err()); + } + + #[test] + #[should_panic] + fn test_decode_hex_odd_length_panics() { + // Known issue: odd-length hex strings panic instead of returning Err + // because s[i..i+2] goes out of bounds on the last byte. + let _ = decode_hex("abc"); + } + + // -- round-trip encode/decode -- + + #[test] + fn test_hex_round_trip() { + let original: Vec = vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]; + let hex = encode_hex(&original); + let decoded = decode_hex(&hex).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn test_hex_round_trip_empty() { + let original: Vec = vec![]; + let hex = encode_hex(&original); + let decoded = decode_hex(&hex).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn test_hex_round_trip_all_byte_values() { + let original: Vec = (0..=255).collect(); + let hex = encode_hex(&original); + let decoded = decode_hex(&hex).unwrap(); + assert_eq!(original, decoded); + } + + // -- hex_to_array -- + + #[test] + fn test_hex_to_array_valid_4_bytes() { + let result = hex_to_array::<4>("deadbeef").unwrap(); + assert_eq!(result, [0xde, 0xad, 0xbe, 0xef]); + } + + #[test] + fn test_hex_to_array_valid_32_bytes() { + let hex = "a".repeat(64); // 32 bytes encoded as 64 hex chars + let result = hex_to_array::<32>(&hex).unwrap(); + assert_eq!(result.len(), 32); + assert!(result.iter().all(|&b| b == 0xaa)); + } + + #[test] + fn test_hex_to_array_wrong_size() { + // Provide 4 bytes of hex (8 chars) but expect a 2-byte array + let result = hex_to_array::<2>("deadbeef"); + assert!(result.is_err()); + } + + #[test] + fn test_hex_to_array_invalid_hex() { + let result = hex_to_array::<2>("zzzz"); + assert!(result.is_err()); + } + + // -- vec_to_array -- + + #[test] + fn test_vec_to_array_valid() { + let vec = vec![1u8, 2, 3, 4]; + let result = vec_to_array::<4>(&vec).unwrap(); + assert_eq!(result, [1, 2, 3, 4]); + } + + #[test] + fn test_vec_to_array_too_short() { + let vec = vec![1u8, 2]; + let result = vec_to_array::<4>(&vec); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.expected_size(), 4); + assert_eq!(err.actual_size(), 2); + } + + #[test] + fn test_vec_to_array_too_long() { + let vec = vec![1u8, 2, 3, 4, 5]; + let result = vec_to_array::<4>(&vec); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.expected_size(), 4); + assert_eq!(err.actual_size(), 5); + } + + #[test] + fn test_vec_to_array_empty_to_zero() { + let vec: Vec = vec![]; + let result = vec_to_array::<0>(&vec).unwrap(); + assert_eq!(result, [0u8; 0]); + } + + #[test] + fn test_vec_to_array_single_element() { + let vec = vec![0xffu8]; + let result = vec_to_array::<1>(&vec).unwrap(); + assert_eq!(result, [0xff]); + } + + // -- decode_hex_sha256 / decode_hex_bls_sig -- + + #[test] + fn test_decode_hex_sha256_valid() { + let hex = "ab".repeat(32); // 32 bytes + let result = decode_hex_sha256(&hex).unwrap(); + assert_eq!(result.len(), 32); + assert!(result.iter().all(|&b| b == 0xab)); + } + + #[test] + fn test_decode_hex_sha256_wrong_length() { + let hex = "ab".repeat(16); // 16 bytes, not 32 + let result = decode_hex_sha256(&hex); + assert!(result.is_err()); + } + + #[test] + fn test_decode_hex_bls_sig_valid() { + let hex = "cd".repeat(96); // 96 bytes + let result = decode_hex_bls_sig(&hex).unwrap(); + assert_eq!(result.len(), 96); + assert!(result.iter().all(|&b| b == 0xcd)); + } + + #[test] + fn test_decode_hex_bls_sig_wrong_length() { + let hex = "cd".repeat(48); // 48 bytes, not 96 + let result = decode_hex_bls_sig(&hex); + assert!(result.is_err()); + } +} diff --git a/packages/rs-dpp/src/validation/validation_result.rs b/packages/rs-dpp/src/validation/validation_result.rs index bc9e7bce34f..505e65edef4 100644 --- a/packages/rs-dpp/src/validation/validation_result.rs +++ b/packages/rs-dpp/src/validation/validation_result.rs @@ -289,3 +289,417 @@ impl> From> for ValidationRe } } } + +#[cfg(test)] +mod tests { + use super::*; + + // -- new() -- + + #[test] + fn test_new_has_no_errors() { + let result: ValidationResult = ValidationResult::new(); + assert!(result.errors.is_empty()); + } + + #[test] + fn test_new_has_no_data() { + let result: ValidationResult = ValidationResult::new(); + assert!(result.data.is_none()); + } + + // -- new_with_data() -- + + #[test] + fn test_new_with_data_stores_data() { + let result: ValidationResult = ValidationResult::new_with_data(42); + assert_eq!(result.data, Some(42)); + assert!(result.errors.is_empty()); + } + + // -- new_with_error() -- + + #[test] + fn test_new_with_error_stores_single_error() { + let result: ValidationResult = + ValidationResult::new_with_error("bad".to_string()); + assert_eq!(result.errors.len(), 1); + assert_eq!(result.errors[0], "bad"); + assert!(result.data.is_none()); + } + + // -- new_with_errors() -- + + #[test] + fn test_new_with_errors_stores_multiple_errors() { + let result: ValidationResult = + ValidationResult::new_with_errors(vec!["a".to_string(), "b".to_string()]); + assert_eq!(result.errors.len(), 2); + assert_eq!(result.errors[0], "a"); + assert_eq!(result.errors[1], "b"); + assert!(result.data.is_none()); + } + + #[test] + fn test_new_with_errors_empty_vec() { + let result: ValidationResult = ValidationResult::new_with_errors(vec![]); + assert!(result.errors.is_empty()); + assert!(result.data.is_none()); + } + + // -- map() -- + + #[test] + fn test_map_transforms_data() { + let result: ValidationResult = ValidationResult::new_with_data(10); + let mapped = result.map(|x| x * 2); + assert_eq!(mapped.data, Some(20)); + assert!(mapped.errors.is_empty()); + } + + #[test] + fn test_map_preserves_errors() { + let result: ValidationResult = + ValidationResult::new_with_data_and_errors(5, vec!["err".to_string()]); + let mapped = result.map(|x| x + 1); + assert_eq!(mapped.data, Some(6)); + assert_eq!(mapped.errors, vec!["err".to_string()]); + } + + #[test] + fn test_map_with_no_data() { + let result: ValidationResult = + ValidationResult::new_with_error("err".to_string()); + let mapped = result.map(|x| x + 1); + assert!(mapped.data.is_none()); + assert_eq!(mapped.errors.len(), 1); + } + + // -- map_result() -- + + #[test] + fn test_map_result_with_ok_closure() { + let result: ValidationResult = ValidationResult::new_with_data(10); + let mapped: Result, String> = + result.map_result(|x| Ok(format!("val={}", x))); + let mapped = mapped.unwrap(); + assert_eq!(mapped.data, Some("val=10".to_string())); + } + + #[test] + fn test_map_result_with_err_closure() { + let result: ValidationResult = ValidationResult::new_with_data(10); + let mapped: Result, String> = + result.map_result(|_| Err("fail".to_string())); + assert!(mapped.is_err()); + assert_eq!(mapped.unwrap_err(), "fail"); + } + + #[test] + fn test_map_result_with_no_data() { + let result: ValidationResult = + ValidationResult::new_with_error("err".to_string()); + let mapped: Result, String> = + result.map_result(|x| Ok(x + 1)); + let mapped = mapped.unwrap(); + assert!(mapped.data.is_none()); + assert_eq!(mapped.errors, vec!["err".to_string()]); + } + + // -- is_valid() / is_err() -- + + #[test] + fn test_is_valid_true_when_no_errors() { + let result: ValidationResult = ValidationResult::new(); + assert!(result.is_valid()); + assert!(!result.is_err()); + } + + #[test] + fn test_is_valid_false_when_errors_present() { + let result: ValidationResult = + ValidationResult::new_with_error("e".to_string()); + assert!(!result.is_valid()); + assert!(result.is_err()); + } + + #[test] + fn test_is_valid_with_data_and_no_errors() { + let result: ValidationResult = ValidationResult::new_with_data(1); + assert!(result.is_valid()); + } + + #[test] + fn test_is_err_with_data_and_errors() { + let result: ValidationResult = + ValidationResult::new_with_data_and_errors(1, vec!["e".to_string()]); + assert!(result.is_err()); + } + + // -- first_error() -- + + #[test] + fn test_first_error_returns_first() { + let result: ValidationResult = + ValidationResult::new_with_errors(vec!["first".to_string(), "second".to_string()]); + assert_eq!(result.first_error(), Some(&"first".to_string())); + } + + #[test] + fn test_first_error_returns_none_when_no_errors() { + let result: ValidationResult = ValidationResult::new(); + assert_eq!(result.first_error(), None); + } + + // -- into_data() -- + + #[test] + fn test_into_data_returns_data_when_present() { + let result: ValidationResult = ValidationResult::new_with_data(42); + assert_eq!(result.into_data().unwrap(), 42); + } + + #[test] + fn test_into_data_returns_error_when_no_data() { + let result: ValidationResult = ValidationResult::new(); + assert!(result.into_data().is_err()); + } + + // -- into_data_with_error() -- + + #[test] + fn test_into_data_with_error_returns_data_when_valid() { + let result: ValidationResult = ValidationResult::new_with_data(42); + let inner = result.into_data_with_error().unwrap(); + assert_eq!(inner.unwrap(), 42); + } + + #[test] + fn test_into_data_with_error_returns_last_error_when_errors_present() { + let result: ValidationResult = + ValidationResult::new_with_errors(vec!["first".to_string(), "last".to_string()]); + let inner = result.into_data_with_error().unwrap(); + assert_eq!(inner.unwrap_err(), "last"); + } + + #[test] + fn test_into_data_with_error_returns_protocol_error_when_no_data_and_no_errors() { + let result: ValidationResult = ValidationResult::new(); + assert!(result.into_data_with_error().is_err()); + } + + // -- into_data_and_errors() -- + + #[test] + fn test_into_data_and_errors_returns_both() { + let result: ValidationResult = + ValidationResult::new_with_data_and_errors(10, vec!["e".to_string()]); + let (data, errors) = result.into_data_and_errors().unwrap(); + assert_eq!(data, 10); + assert_eq!(errors, vec!["e".to_string()]); + } + + #[test] + fn test_into_data_and_errors_returns_empty_errors_when_valid() { + let result: ValidationResult = ValidationResult::new_with_data(10); + let (data, errors) = result.into_data_and_errors().unwrap(); + assert_eq!(data, 10); + assert!(errors.is_empty()); + } + + #[test] + fn test_into_data_and_errors_fails_without_data() { + let result: ValidationResult = + ValidationResult::new_with_error("e".to_string()); + assert!(result.into_data_and_errors().is_err()); + } + + // -- From impls -- + + #[test] + fn test_from_data_creates_valid_result() { + let result: ValidationResult = 42.into(); + assert_eq!(result.data, Some(42)); + assert!(result.errors.is_empty()); + } + + #[test] + fn test_from_ok_result_creates_valid_result() { + let ok_result: Result = Ok(42); + let result: ValidationResult = ok_result.into(); + assert_eq!(result.data, Some(42)); + assert!(result.errors.is_empty()); + } + + #[test] + fn test_from_err_result_creates_error_result() { + let err_result: Result = Err("bad".to_string()); + let result: ValidationResult = err_result.into(); + assert!(result.data.is_none()); + assert_eq!(result.errors, vec!["bad".to_string()]); + } + + // -- flatten() -- + + #[test] + fn test_flatten_merges_data_and_errors() { + let r1: ValidationResult, String> = ValidationResult::new_with_data(vec![1, 2]); + let r2: ValidationResult, String> = + ValidationResult::new_with_data_and_errors(vec![3], vec!["e".to_string()]); + let r3: ValidationResult, String> = + ValidationResult::new_with_error("e2".to_string()); + + let flat = ValidationResult::flatten(vec![r1, r2, r3]); + assert_eq!(flat.data, Some(vec![1, 2, 3])); + assert_eq!(flat.errors, vec!["e".to_string(), "e2".to_string()]); + } + + #[test] + fn test_flatten_empty_input() { + let flat: ValidationResult, String> = + ValidationResult::flatten(std::iter::empty()); + assert_eq!(flat.data, Some(vec![])); + assert!(flat.errors.is_empty()); + } + + // -- merge_many() -- + + #[test] + fn test_merge_many_collects_data_into_vec() { + let r1: ValidationResult = ValidationResult::new_with_data(1); + let r2: ValidationResult = ValidationResult::new_with_data(2); + let r3: ValidationResult = ValidationResult::new_with_error("e".to_string()); + + let merged = ValidationResult::merge_many(vec![r1, r2, r3]); + assert_eq!(merged.data, Some(vec![1, 2])); + assert_eq!(merged.errors, vec!["e".to_string()]); + } + + #[test] + fn test_merge_many_empty_input() { + let merged: ValidationResult, String> = + ValidationResult::merge_many(std::iter::empty::>()); + assert_eq!(merged.data, Some(vec![])); + assert!(merged.errors.is_empty()); + } + + // -- merge_many_errors() -- + + #[test] + fn test_merge_many_errors_collects_all_errors() { + let r1: SimpleValidationResult = + SimpleValidationResult::new_with_errors(vec!["a".to_string()]); + let r2: SimpleValidationResult = + SimpleValidationResult::new_with_errors(vec!["b".to_string(), "c".to_string()]); + let r3: SimpleValidationResult = SimpleValidationResult::new(); + + let merged = SimpleValidationResult::merge_many_errors(vec![r1, r2, r3]); + assert_eq!( + merged.errors, + vec!["a".to_string(), "b".to_string(), "c".to_string()] + ); + } + + #[test] + fn test_merge_many_errors_empty_input() { + let merged: SimpleValidationResult = + SimpleValidationResult::merge_many_errors(std::iter::empty()); + assert!(merged.errors.is_empty()); + } + + // -- Default -- + + #[test] + fn test_default_is_empty() { + let result: ValidationResult = ValidationResult::default(); + assert!(result.errors.is_empty()); + assert!(result.data.is_none()); + } + + // -- add_error / add_errors / merge -- + + #[test] + fn test_add_error() { + let mut result: ValidationResult = ValidationResult::new(); + result.add_error("e1".to_string()); + result.add_error("e2".to_string()); + assert_eq!(result.errors, vec!["e1".to_string(), "e2".to_string()]); + } + + #[test] + fn test_add_errors() { + let mut result: ValidationResult = + ValidationResult::new_with_error("e1".to_string()); + result.add_errors(vec!["e2".to_string(), "e3".to_string()]); + assert_eq!(result.errors.len(), 3); + } + + #[test] + fn test_merge_appends_errors_from_other() { + let mut r1: ValidationResult = + ValidationResult::new_with_error("a".to_string()); + let r2: ValidationResult = + ValidationResult::new_with_error("b".to_string()); + r1.merge(r2); + assert_eq!(r1.errors, vec!["a".to_string(), "b".to_string()]); + } + + // -- get_error / has_data / is_valid_with_data / set_data -- + + #[test] + fn test_get_error() { + let result: ValidationResult = + ValidationResult::new_with_errors(vec!["a".to_string(), "b".to_string()]); + assert_eq!(result.get_error(0), Some(&"a".to_string())); + assert_eq!(result.get_error(1), Some(&"b".to_string())); + assert_eq!(result.get_error(2), None); + } + + #[test] + fn test_has_data() { + let with: ValidationResult = ValidationResult::new_with_data(1); + let without: ValidationResult = ValidationResult::new(); + assert!(with.has_data()); + assert!(!without.has_data()); + } + + #[test] + fn test_is_valid_with_data() { + let valid_with_data: ValidationResult = ValidationResult::new_with_data(1); + let valid_no_data: ValidationResult = ValidationResult::new(); + let invalid_with_data: ValidationResult = + ValidationResult::new_with_data_and_errors(1, vec!["e".to_string()]); + assert!(valid_with_data.is_valid_with_data()); + assert!(!valid_no_data.is_valid_with_data()); + assert!(!invalid_with_data.is_valid_with_data()); + } + + #[test] + fn test_set_data() { + let mut result: ValidationResult = ValidationResult::new(); + assert!(result.data.is_none()); + result.set_data(99); + assert_eq!(result.data, Some(99)); + } + + #[test] + fn test_into_result_without_data() { + let result: ValidationResult = + ValidationResult::new_with_data_and_errors(42, vec!["e".to_string()]); + let without_data = result.into_result_without_data(); + assert!(without_data.data.is_none()); + assert_eq!(without_data.errors, vec!["e".to_string()]); + } + + #[test] + fn test_data_as_borrowed() { + let result: ValidationResult = ValidationResult::new_with_data(42); + assert_eq!(result.data_as_borrowed().unwrap(), &42); + } + + #[test] + fn test_data_as_borrowed_no_data() { + let result: ValidationResult = ValidationResult::new(); + assert!(result.data_as_borrowed().is_err()); + } +} diff --git a/packages/rs-drive/src/util/common/encode.rs b/packages/rs-drive/src/util/common/encode.rs index 23e630743f1..cb563a9d1e3 100644 --- a/packages/rs-drive/src/util/common/encode.rs +++ b/packages/rs-drive/src/util/common/encode.rs @@ -219,3 +219,224 @@ pub fn encode_u32(val: u32) -> Vec { wtr } + +#[cfg(test)] +mod tests { + use super::*; + + // --- encode_u64 / decode_u64 round-trip tests --- + + #[test] + fn encode_decode_u64_zero() { + let encoded = encode_u64(0); + assert_eq!(encoded.len(), 8); + let decoded = decode_u64(&encoded).unwrap(); + assert_eq!(decoded, 0); + } + + #[test] + fn encode_decode_u64_one() { + let encoded = encode_u64(1); + let decoded = decode_u64(&encoded).unwrap(); + assert_eq!(decoded, 1); + } + + #[test] + fn encode_decode_u64_max() { + let encoded = encode_u64(u64::MAX); + let decoded = decode_u64(&encoded).unwrap(); + assert_eq!(decoded, u64::MAX); + } + + #[test] + fn encode_decode_u64_owned_round_trip() { + for val in [0u64, 1, 42, 1000, u64::MAX / 2, u64::MAX] { + let encoded = encode_u64(val); + let decoded = decode_u64_owned(encoded).unwrap(); + assert_eq!(decoded, val); + } + } + + #[test] + fn encode_u64_preserves_sort_order_in_positive_range() { + // The sign-bit flip means lexicographic ordering matches signed interpretation. + // Values in 0..=i64::MAX sort correctly among themselves. + let values = [0u64, 1, 2, 100, 1000, i64::MAX as u64]; + let encoded: Vec> = values.iter().map(|&v| encode_u64(v)).collect(); + for i in 0..encoded.len() - 1 { + assert!( + encoded[i] < encoded[i + 1], + "Sort order violated: encode_u64({}) >= encode_u64({})", + values[i], + values[i + 1] + ); + } + } + + #[test] + fn encode_u64_sign_bit_flip_makes_high_values_sort_lower() { + // Values above i64::MAX have the sign bit set in big-endian, so the flip + // clears it, making them sort below values in the 0..=i64::MAX range. + // This is the intended behavior: the encoding treats u64 as if it were i64. + let below_midpoint = encode_u64(100); + let above_midpoint = encode_u64(u64::MAX); + assert!(above_midpoint < below_midpoint); + } + + #[test] + fn decode_u64_wrong_length_returns_error() { + assert!(decode_u64(&[]).is_err()); + assert!(decode_u64(&[0; 7]).is_err()); + assert!(decode_u64(&[0; 9]).is_err()); + assert!(decode_u64(&[0; 1]).is_err()); + } + + #[test] + fn decode_u64_owned_wrong_length_returns_error() { + assert!(decode_u64_owned(vec![]).is_err()); + assert!(decode_u64_owned(vec![0; 7]).is_err()); + assert!(decode_u64_owned(vec![0; 9]).is_err()); + } + + // --- encode_i64 tests --- + + #[test] + fn encode_i64_positive() { + let encoded = encode_i64(42); + assert_eq!(encoded.len(), 8); + } + + #[test] + fn encode_i64_negative() { + let encoded = encode_i64(-42); + assert_eq!(encoded.len(), 8); + } + + #[test] + fn encode_i64_zero() { + let encoded = encode_i64(0); + assert_eq!(encoded.len(), 8); + } + + #[test] + fn encode_i64_preserves_sort_order() { + let values = [i64::MIN, -1000, -1, 0, 1, 1000, i64::MAX]; + let encoded: Vec> = values.iter().map(|&v| encode_i64(v)).collect(); + for i in 0..encoded.len() - 1 { + assert!( + encoded[i] < encoded[i + 1], + "Sort order violated: encode_i64({}) >= encode_i64({})", + values[i], + values[i + 1] + ); + } + } + + #[test] + fn encode_i64_negative_less_than_positive() { + let neg = encode_i64(-1); + let pos = encode_i64(1); + assert!(neg < pos); + } + + // --- encode_float tests --- + + #[test] + fn encode_float_positive() { + let encoded = encode_float(3.14); + assert_eq!(encoded.len(), 8); + } + + #[test] + fn encode_float_negative() { + let encoded = encode_float(-3.14); + assert_eq!(encoded.len(), 8); + } + + #[test] + fn encode_float_zero() { + let encoded = encode_float(0.0); + assert_eq!(encoded.len(), 8); + } + + #[test] + fn encode_float_preserves_sort_order() { + let values = [-1000.0f64, -1.0, -0.001, 0.0, 0.001, 1.0, 1000.0]; + let encoded: Vec> = values.iter().map(|&v| encode_float(v)).collect(); + for i in 0..encoded.len() - 1 { + assert!( + encoded[i] < encoded[i + 1], + "Sort order violated: encode_float({}) >= encode_float({})", + values[i], + values[i + 1] + ); + } + } + + #[test] + fn encode_float_negative_less_than_positive() { + let neg = encode_float(-0.5); + let pos = encode_float(0.5); + assert!(neg < pos); + } + + // --- encode_u16 tests --- + + #[test] + fn encode_u16_basic() { + assert_eq!(encode_u16(0).len(), 2); + assert_eq!(encode_u16(u16::MAX).len(), 2); + } + + #[test] + fn encode_u16_preserves_sort_order_in_positive_range() { + // Values in 0..=i16::MAX sort correctly after sign-bit flip. + let values = [0u16, 1, 100, 1000, i16::MAX as u16]; + let encoded: Vec> = values.iter().map(|&v| encode_u16(v)).collect(); + for i in 0..encoded.len() - 1 { + assert!( + encoded[i] < encoded[i + 1], + "Sort order violated: encode_u16({}) >= encode_u16({})", + values[i], + values[i + 1] + ); + } + } + + #[test] + fn encode_u16_sign_bit_flip_makes_high_values_sort_lower() { + let below = encode_u16(100); + let above = encode_u16(u16::MAX); + assert!(above < below); + } + + // --- encode_u32 tests --- + + #[test] + fn encode_u32_basic() { + assert_eq!(encode_u32(0).len(), 4); + assert_eq!(encode_u32(u32::MAX).len(), 4); + } + + #[test] + fn encode_u32_preserves_sort_order_in_positive_range() { + // Values in 0..=i32::MAX sort correctly after sign-bit flip. + let values = [0u32, 1, 100, 10000, i32::MAX as u32]; + let encoded: Vec> = values.iter().map(|&v| encode_u32(v)).collect(); + for i in 0..encoded.len() - 1 { + assert!( + encoded[i] < encoded[i + 1], + "Sort order violated: encode_u32({}) >= encode_u32({})", + values[i], + values[i + 1] + ); + } + } + + #[test] + fn encode_u32_sign_bit_flip_makes_high_values_sort_lower() { + let below = encode_u32(100); + let above = encode_u32(u32::MAX); + assert!(above < below); + } +} diff --git a/packages/rs-platform-value/src/eq.rs b/packages/rs-platform-value/src/eq.rs index 53639f0032d..0c7d506270a 100644 --- a/packages/rs-platform-value/src/eq.rs +++ b/packages/rs-platform-value/src/eq.rs @@ -169,3 +169,372 @@ impl Value { self == other } } + +#[cfg(test)] +mod tests { + use crate::Value; + + // ---- PartialEq ---- + + #[test] + fn u8_eq() { + assert_eq!(Value::U8(42), 42u8); + assert_ne!(Value::U8(42), 43u8); + } + + #[test] + fn i8_eq() { + assert_eq!(Value::I8(-1), -1i8); + assert_ne!(Value::I8(-1), 0i8); + } + + #[test] + fn u16_eq() { + assert_eq!(Value::U16(1000), 1000u16); + assert_ne!(Value::U16(1000), 999u16); + } + + #[test] + fn i16_eq() { + assert_eq!(Value::I16(-500), -500i16); + assert_ne!(Value::I16(-500), 500i16); + } + + #[test] + fn u32_eq() { + assert_eq!(Value::U32(100_000), 100_000u32); + assert_ne!(Value::U32(100_000), 0u32); + } + + #[test] + fn i32_eq() { + assert_eq!(Value::I32(-100), -100i32); + assert_ne!(Value::I32(-100), 100i32); + } + + #[test] + fn u64_eq() { + assert_eq!(Value::U64(u64::MAX), u64::MAX); + assert_ne!(Value::U64(0), 1u64); + } + + #[test] + fn i64_eq() { + assert_eq!(Value::I64(i64::MIN), i64::MIN); + assert_ne!(Value::I64(0), 1i64); + } + + #[test] + fn u128_eq() { + assert_eq!(Value::U128(u128::MAX), u128::MAX); + assert_ne!(Value::U128(0), 1u128); + } + + #[test] + fn i128_eq() { + assert_eq!(Value::I128(i128::MIN), i128::MIN); + assert_ne!(Value::I128(0), 1i128); + } + + // ---- cross-type integer comparison via as_integer ---- + + #[test] + fn u8_value_eq_u64_type() { + // Value::U8(10) should equal 10u64 through as_integer + assert_eq!(Value::U8(10), 10u64); + } + + #[test] + fn u64_value_eq_u8_type_when_fits() { + assert_eq!(Value::U64(200), 200u8); + } + + #[test] + fn u64_value_ne_u8_type_when_overflow() { + // 256 doesn't fit in u8 + assert_ne!(Value::U64(256), 0u8); // as_integer:: returns None + } + + #[test] + fn i8_value_eq_i64_type() { + assert_eq!(Value::I8(-10), -10i64); + } + + #[test] + fn non_integer_ne_integer() { + assert_ne!(Value::Text("hello".to_string()), 0u64); + assert_ne!(Value::Null, 0i32); + assert_ne!(Value::Bool(true), 1u8); + } + + // ---- PartialEq ---- + + #[test] + fn string_eq() { + let val = Value::Text("hello".to_string()); + assert_eq!(val, "hello".to_string()); + assert_ne!(val, "world".to_string()); + } + + #[test] + fn non_text_ne_string() { + assert_ne!(Value::U8(0), "0".to_string()); + assert_ne!(Value::Null, "".to_string()); + } + + // ---- PartialEq<&str> ---- + + #[test] + fn str_ref_eq() { + let val = Value::Text("test".to_string()); + assert_eq!(val, "test"); + assert_ne!(val, "other"); + } + + #[test] + fn non_text_ne_str_ref() { + assert_ne!(Value::Bool(false), "false"); + } + + // ---- PartialEq ---- + + #[test] + fn float_eq() { + assert_eq!(Value::Float(3.14), 3.14f64); + assert_ne!(Value::Float(3.14), 3.15f64); + } + + #[test] + fn integer_eq_float_through_as_float() { + // as_float converts integers to f64, so Value::U64(10) == 10.0f64 + assert_eq!(Value::U64(10), 10.0f64); + } + + #[test] + fn non_numeric_ne_float() { + assert_ne!(Value::Text("3.14".to_string()), 3.14f64); + } + + // ---- PartialEq> ---- + + #[test] + fn bytes_eq_vec_u8() { + let data = vec![1, 2, 3]; + assert_eq!(Value::Bytes(data.clone()), data); + } + + #[test] + fn bytes_ne_vec_u8() { + assert_ne!(Value::Bytes(vec![1, 2, 3]), vec![1, 2, 4]); + } + + #[test] + fn identifier_eq_vec_u8() { + let id = [42u8; 32]; + assert_eq!(Value::Identifier(id), id.to_vec()); + } + + #[test] + fn bytes20_eq_vec_u8() { + let b = [5u8; 20]; + assert_eq!(Value::Bytes20(b), b.to_vec()); + } + + #[test] + fn non_bytes_ne_vec_u8() { + assert_ne!(Value::U8(1), vec![1u8]); + } + + // ---- PartialEq<[u8; 32]> ---- + + #[test] + fn bytes32_eq_array() { + let b = [0xffu8; 32]; + assert_eq!(Value::Bytes32(b), b); + } + + #[test] + fn identifier_eq_array_32() { + let id = [7u8; 32]; + assert_eq!(Value::Identifier(id), id); + } + + #[test] + fn bytes_eq_array_32() { + let data = [3u8; 32]; + assert_eq!(Value::Bytes(data.to_vec()), data); + } + + #[test] + fn non_bytes_ne_array_32() { + assert_ne!(Value::Null, [0u8; 32]); + } + + // ---- PartialEq<[u8; 20]> ---- + + #[test] + fn bytes20_eq_array_20() { + let b = [1u8; 20]; + assert_eq!(Value::Bytes20(b), b); + } + + // ---- PartialEq<[u8; 36]> ---- + + #[test] + fn bytes36_eq_array_36() { + let b = [2u8; 36]; + assert_eq!(Value::Bytes36(b), b); + } + + // ---- PartialEq for &Value ---- + + #[test] + fn ref_value_eq_integer() { + let val = Value::U64(42); + assert_eq!(&val, 42u64); + } + + #[test] + fn ref_value_eq_string() { + let val = Value::Text("hi".to_string()); + assert_eq!(&val, "hi".to_string()); + } + + #[test] + fn ref_value_eq_str_ref() { + let val = Value::Text("hi".to_string()); + assert_eq!(&val, "hi"); + } + + #[test] + fn ref_value_eq_float() { + let val = Value::Float(1.0); + assert_eq!(&val, 1.0f64); + } + + #[test] + fn ref_value_eq_vec_u8() { + let val = Value::Bytes(vec![10, 20]); + assert_eq!(&val, vec![10u8, 20]); + } + + #[test] + fn ref_value_eq_array_32() { + let b = [0u8; 32]; + let val = Value::Bytes32(b); + assert_eq!(&val, b); + } + + // ---- equal_underlying_data tests ---- + + #[test] + fn equal_underlying_data_bytes_vs_identifier_same_data() { + let data = [42u8; 32]; + let bytes = Value::Bytes(data.to_vec()); + let ident = Value::Identifier(data); + assert!(bytes.equal_underlying_data(&ident)); + assert!(ident.equal_underlying_data(&bytes)); + } + + #[test] + fn equal_underlying_data_bytes_vs_identifier_different_data() { + let bytes = Value::Bytes(vec![0u8; 32]); + let ident = Value::Identifier([1u8; 32]); + assert!(!bytes.equal_underlying_data(&ident)); + } + + #[test] + fn equal_underlying_data_bytes32_vs_identifier() { + let data = [99u8; 32]; + let b32 = Value::Bytes32(data); + let ident = Value::Identifier(data); + assert!(b32.equal_underlying_data(&ident)); + } + + #[test] + fn equal_underlying_data_bytes20_vs_bytes() { + let data = [5u8; 20]; + let b20 = Value::Bytes20(data); + let bytes = Value::Bytes(data.to_vec()); + assert!(b20.equal_underlying_data(&bytes)); + } + + #[test] + fn equal_underlying_data_u8_vs_u64_same_value() { + let a = Value::U8(10); + let b = Value::U64(10); + assert!(a.equal_underlying_data(&b)); + } + + #[test] + fn equal_underlying_data_i8_vs_i128_same_value() { + let a = Value::I8(-5); + let b = Value::I128(-5); + assert!(a.equal_underlying_data(&b)); + } + + #[test] + fn equal_underlying_data_u8_vs_u64_different_value() { + let a = Value::U8(10); + let b = Value::U64(20); + assert!(!a.equal_underlying_data(&b)); + } + + #[test] + fn equal_underlying_data_u16_vs_i32_same_value() { + let a = Value::U16(100); + let b = Value::I32(100); + assert!(a.equal_underlying_data(&b)); + } + + #[test] + fn equal_underlying_data_negative_i8_vs_u64() { + // negative can't match unsigned + let a = Value::I8(-1); + let b = Value::U64(255); + assert!(!a.equal_underlying_data(&b)); + } + + #[test] + fn equal_underlying_data_same_variant_same_value() { + let a = Value::U64(42); + let b = Value::U64(42); + assert!(a.equal_underlying_data(&b)); + } + + #[test] + fn equal_underlying_data_fallback_to_partial_eq() { + // Text vs Text uses default PartialEq + let a = Value::Text("hello".to_string()); + let b = Value::Text("hello".to_string()); + assert!(a.equal_underlying_data(&b)); + + let c = Value::Text("world".to_string()); + assert!(!a.equal_underlying_data(&c)); + } + + #[test] + fn equal_underlying_data_null_vs_null() { + assert!(Value::Null.equal_underlying_data(&Value::Null)); + } + + #[test] + fn equal_underlying_data_different_types_not_equal() { + // A string vs a number should not be equal + let a = Value::Text("42".to_string()); + let b = Value::U64(42); + assert!(!a.equal_underlying_data(&b)); + } + + #[test] + fn equal_underlying_data_bool_vs_bool() { + assert!(Value::Bool(true).equal_underlying_data(&Value::Bool(true))); + assert!(!Value::Bool(true).equal_underlying_data(&Value::Bool(false))); + } + + #[test] + fn equal_underlying_data_float_vs_float() { + assert!(Value::Float(1.5).equal_underlying_data(&Value::Float(1.5))); + assert!(!Value::Float(1.5).equal_underlying_data(&Value::Float(2.5))); + } +}