From bfd3e3d5eb3727d0f524ca17169b279c0d041041 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 3 Apr 2026 18:59:26 +0300 Subject: [PATCH 1/5] test(platform): add 466 unit tests across 15 files for coverage gains Add comprehensive unit tests to previously untested utility modules, enum conversions, serialization helpers, and validation logic across rs-dpp, rs-drive, and rs-platform-value. Tested modules: - validation_result: all builder/query/transform methods - identity key enums: security_level, purpose, key_type conversions - token types: gas_fees_paid_by, token_pricing_schedule, token_event - token_configuration_item: 32-variant index uniqueness - authorized_action_takers: bytes round-trips and permission logic - fee_result: BalanceChangeForIdentity and FeeResult balance methods - encode.rs: sort-preserving u64/i64/float/u16/u32 encoding - platform-value: Display, PartialEq cross-type, string_encoding - util/vec.rs: hex encode/decode round-trips Co-Authored-By: Claude Opus 4.6 (1M context) --- .../token_configuration_item.rs | 275 ++++ .../authorized_action_takers.rs | 487 +++++++ packages/rs-dpp/src/fee/fee_result/mod.rs | 333 +++++ .../identity/identity_public_key/key_type.rs | 220 ++++ .../identity/identity_public_key/purpose.rs | 215 +++ .../identity_public_key/security_level.rs | 210 +++ .../rs-dpp/src/tokens/gas_fees_paid_by.rs | 102 ++ packages/rs-dpp/src/tokens/token_event.rs | 457 +++++++ .../src/tokens/token_pricing_schedule.rs | 84 ++ packages/rs-dpp/src/util/vec.rs | 205 +++ .../src/validation/validation_result.rs | 420 ++++++ packages/rs-drive/src/util/common/encode.rs | 221 ++++ packages/rs-platform-value/src/display.rs | 345 +++++ packages/rs-platform-value/src/eq.rs | 369 ++++++ .../rs-platform-value/src/string_encoding.rs | 186 +++ presentation.html | 1157 +++++++++++++++++ 16 files changed, 5286 insertions(+) create mode 100644 presentation.html 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..ff8565db732 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,278 @@ 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); + } + + // ---- Display tests for representative variants ---- + + #[test] + fn display_no_change() { + let s = format!( + "{}", + TokenConfigurationChangeItem::TokenConfigurationNoChange + ); + assert_eq!(s, "No Change in Token Configuration"); + } + + #[test] + fn display_max_supply_some() { + let s = format!("{}", TokenConfigurationChangeItem::MaxSupply(Some(1000))); + assert_eq!(s, "Max Supply: 1000"); + } + + #[test] + fn display_max_supply_none() { + let s = format!("{}", TokenConfigurationChangeItem::MaxSupply(None)); + assert_eq!(s, "Max Supply: No Limit"); + } + + #[test] + fn display_minting_allow_choosing_destination_true() { + let s = format!( + "{}", + TokenConfigurationChangeItem::MintingAllowChoosingDestination(true) + ); + assert_eq!(s, "Minting Allow Choosing Destination: true"); + } + + #[test] + fn display_minting_allow_choosing_destination_false() { + let s = format!( + "{}", + TokenConfigurationChangeItem::MintingAllowChoosingDestination(false) + ); + assert_eq!(s, "Minting Allow Choosing Destination: false"); + } + + #[test] + fn display_freeze() { + let s = format!( + "{}", + TokenConfigurationChangeItem::Freeze(AuthorizedActionTakers::ContractOwner) + ); + assert_eq!(s, "Freeze: ContractOwner"); + } + + #[test] + fn display_manual_minting() { + let s = format!( + "{}", + TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::NoOne) + ); + assert_eq!(s, "Manual Minting: NoOne"); + } + + #[test] + fn display_manual_burning() { + let s = format!( + "{}", + TokenConfigurationChangeItem::ManualBurning(AuthorizedActionTakers::ContractOwner) + ); + assert_eq!(s, "Manual Burning: ContractOwner"); + } + + #[test] + fn display_emergency_action() { + let s = format!( + "{}", + TokenConfigurationChangeItem::EmergencyAction(AuthorizedActionTakers::NoOne) + ); + assert_eq!(s, "Emergency Action: NoOne"); + } + + #[test] + fn display_main_control_group_some() { + let s = format!( + "{}", + TokenConfigurationChangeItem::MainControlGroup(Some(42)) + ); + assert_eq!(s, "Main Control Group: 42"); + } + + #[test] + fn display_main_control_group_none() { + let s = format!( + "{}", + TokenConfigurationChangeItem::MainControlGroup(None) + ); + assert_eq!(s, "Main Control Group: None"); + } + + #[test] + fn display_perpetual_distribution_none() { + let s = format!( + "{}", + TokenConfigurationChangeItem::PerpetualDistribution(None) + ); + assert_eq!(s, "Perpetual Distribution: None"); + } + + #[test] + fn display_new_tokens_destination_identity_none() { + let s = format!( + "{}", + TokenConfigurationChangeItem::NewTokensDestinationIdentity(None) + ); + assert_eq!(s, "New Tokens Destination Identity: None"); + } + + #[test] + fn display_new_tokens_destination_identity_some() { + let id = Identifier::from([3u8; 32]); + let s = format!( + "{}", + TokenConfigurationChangeItem::NewTokensDestinationIdentity(Some(id)) + ); + assert!(s.starts_with("New Tokens Destination Identity: ")); + assert!(!s.contains("None")); + } + + #[test] + fn display_unfreeze() { + let s = format!( + "{}", + TokenConfigurationChangeItem::Unfreeze(AuthorizedActionTakers::NoOne) + ); + assert_eq!(s, "Unfreeze: NoOne"); + } + + #[test] + fn display_destroy_frozen_funds() { + let s = format!( + "{}", + TokenConfigurationChangeItem::DestroyFrozenFunds(AuthorizedActionTakers::ContractOwner) + ); + assert_eq!(s, "Destroy Frozen Funds: ContractOwner"); + } + + #[test] + fn display_marketplace_trade_mode() { + let s = format!( + "{}", + TokenConfigurationChangeItem::MarketplaceTradeMode(TokenTradeMode::NotTradeable) + ); + assert_eq!(s, "Marketplace Trade Mode: NotTradeable"); + } + + // ---- display all variants don't panic ---- + + #[test] + fn display_all_variants_no_panic() { + for v in all_variants() { + let s = format!("{}", v); + assert!(!s.is_empty()); + } + } +} 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..a433f35499c 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,490 @@ 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..5e98988a80b 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,223 @@ 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/identity/identity_public_key/purpose.rs b/packages/rs-dpp/src/identity/identity_public_key/purpose.rs index fbd5fcb1cb5..84a3680c539 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/purpose.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/purpose.rs @@ -129,3 +129,218 @@ impl Purpose { [ENCRYPTION, DECRYPTION] } } + +#[cfg(test)] +mod tests { + use super::*; + + // -- TryFrom valid values -- + + #[test] + fn test_try_from_u8_authentication() { + assert_eq!(Purpose::try_from(0u8).unwrap(), AUTHENTICATION); + } + + #[test] + fn test_try_from_u8_encryption() { + assert_eq!(Purpose::try_from(1u8).unwrap(), ENCRYPTION); + } + + #[test] + fn test_try_from_u8_decryption() { + assert_eq!(Purpose::try_from(2u8).unwrap(), DECRYPTION); + } + + #[test] + fn test_try_from_u8_transfer() { + assert_eq!(Purpose::try_from(3u8).unwrap(), TRANSFER); + } + + #[test] + fn test_try_from_u8_system() { + assert_eq!(Purpose::try_from(4u8).unwrap(), SYSTEM); + } + + #[test] + fn test_try_from_u8_voting() { + assert_eq!(Purpose::try_from(5u8).unwrap(), VOTING); + } + + #[test] + fn test_try_from_u8_owner() { + assert_eq!(Purpose::try_from(6u8).unwrap(), OWNER); + } + + // -- TryFrom invalid values -- + + #[test] + fn test_try_from_u8_invalid_7() { + assert!(Purpose::try_from(7u8).is_err()); + } + + #[test] + fn test_try_from_u8_invalid_255() { + assert!(Purpose::try_from(255u8).is_err()); + } + + // -- TryFrom valid values -- + + #[test] + fn test_try_from_i32_all_valid() { + assert_eq!(Purpose::try_from(0i32).unwrap(), AUTHENTICATION); + assert_eq!(Purpose::try_from(1i32).unwrap(), ENCRYPTION); + assert_eq!(Purpose::try_from(2i32).unwrap(), DECRYPTION); + assert_eq!(Purpose::try_from(3i32).unwrap(), TRANSFER); + assert_eq!(Purpose::try_from(4i32).unwrap(), SYSTEM); + assert_eq!(Purpose::try_from(5i32).unwrap(), VOTING); + assert_eq!(Purpose::try_from(6i32).unwrap(), OWNER); + } + + // -- TryFrom invalid values -- + + #[test] + fn test_try_from_i32_invalid_positive() { + assert!(Purpose::try_from(7i32).is_err()); + } + + #[test] + fn test_try_from_i32_invalid_negative() { + assert!(Purpose::try_from(-1i32).is_err()); + } + + // -- From for [u8; 1] -- + + #[test] + fn test_into_u8_array_all() { + let arr: [u8; 1] = AUTHENTICATION.into(); + assert_eq!(arr, [0]); + let arr: [u8; 1] = ENCRYPTION.into(); + assert_eq!(arr, [1]); + let arr: [u8; 1] = DECRYPTION.into(); + assert_eq!(arr, [2]); + let arr: [u8; 1] = TRANSFER.into(); + assert_eq!(arr, [3]); + let arr: [u8; 1] = SYSTEM.into(); + assert_eq!(arr, [4]); + let arr: [u8; 1] = VOTING.into(); + assert_eq!(arr, [5]); + let arr: [u8; 1] = OWNER.into(); + assert_eq!(arr, [6]); + } + + // -- From for &'static [u8; 1] -- + + #[test] + fn test_into_static_u8_array_ref_all() { + let r: &'static [u8; 1] = AUTHENTICATION.into(); + assert_eq!(r, &[0]); + let r: &'static [u8; 1] = ENCRYPTION.into(); + assert_eq!(r, &[1]); + let r: &'static [u8; 1] = DECRYPTION.into(); + assert_eq!(r, &[2]); + let r: &'static [u8; 1] = TRANSFER.into(); + assert_eq!(r, &[3]); + let r: &'static [u8; 1] = SYSTEM.into(); + assert_eq!(r, &[4]); + let r: &'static [u8; 1] = VOTING.into(); + assert_eq!(r, &[5]); + let r: &'static [u8; 1] = OWNER.into(); + assert_eq!(r, &[6]); + } + + // -- full_range() -- + + #[test] + fn test_full_range_has_six_elements() { + let range = Purpose::full_range(); + assert_eq!(range.len(), 6); + } + + #[test] + fn test_full_range_excludes_system() { + let range = Purpose::full_range(); + assert!(!range.contains(&SYSTEM)); + } + + #[test] + fn test_full_range_contains_expected() { + let range = Purpose::full_range(); + assert_eq!( + range, + [AUTHENTICATION, ENCRYPTION, DECRYPTION, TRANSFER, VOTING, OWNER] + ); + } + + // -- searchable_purposes() -- + + #[test] + fn test_searchable_purposes() { + let purposes = Purpose::searchable_purposes(); + assert_eq!(purposes.len(), 3); + assert_eq!(purposes, [AUTHENTICATION, TRANSFER, VOTING]); + } + + // -- encryption_decryption() -- + + #[test] + fn test_encryption_decryption() { + let purposes = Purpose::encryption_decryption(); + assert_eq!(purposes.len(), 2); + assert_eq!(purposes, [ENCRYPTION, DECRYPTION]); + } + + // -- Display -- + + #[test] + fn test_display_authentication() { + assert_eq!(format!("{}", AUTHENTICATION), "AUTHENTICATION"); + } + + #[test] + fn test_display_encryption() { + assert_eq!(format!("{}", ENCRYPTION), "ENCRYPTION"); + } + + #[test] + fn test_display_decryption() { + assert_eq!(format!("{}", DECRYPTION), "DECRYPTION"); + } + + #[test] + fn test_display_transfer() { + assert_eq!(format!("{}", TRANSFER), "TRANSFER"); + } + + #[test] + fn test_display_system() { + assert_eq!(format!("{}", SYSTEM), "SYSTEM"); + } + + #[test] + fn test_display_voting() { + assert_eq!(format!("{}", VOTING), "VOTING"); + } + + #[test] + fn test_display_owner() { + assert_eq!(format!("{}", OWNER), "OWNER"); + } + + // -- Default -- + + #[test] + fn test_default_is_authentication() { + assert_eq!(Purpose::default(), AUTHENTICATION); + } + + // -- round-trip: u8 -> Purpose -> [u8; 1] -- + + #[test] + fn test_round_trip_all_valid_values() { + for val in 0u8..=6 { + let purpose = Purpose::try_from(val).unwrap(); + let arr: [u8; 1] = purpose.into(); + assert_eq!(arr[0], val); + } + } +} diff --git a/packages/rs-dpp/src/identity/identity_public_key/security_level.rs b/packages/rs-dpp/src/identity/identity_public_key/security_level.rs index 4cd9f0d2ed4..46e30e722e6 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/security_level.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/security_level.rs @@ -111,3 +111,213 @@ impl std::fmt::Display for SecurityLevel { write!(f, "{self:?}") } } + +#[cfg(test)] +mod tests { + use super::*; + + // -- TryFrom valid values -- + + #[test] + fn test_try_from_u8_master() { + assert_eq!(SecurityLevel::try_from(0u8).unwrap(), SecurityLevel::MASTER); + } + + #[test] + fn test_try_from_u8_critical() { + assert_eq!( + SecurityLevel::try_from(1u8).unwrap(), + SecurityLevel::CRITICAL + ); + } + + #[test] + fn test_try_from_u8_high() { + assert_eq!(SecurityLevel::try_from(2u8).unwrap(), SecurityLevel::HIGH); + } + + #[test] + fn test_try_from_u8_medium() { + assert_eq!(SecurityLevel::try_from(3u8).unwrap(), SecurityLevel::MEDIUM); + } + + // -- TryFrom invalid values -- + + #[test] + fn test_try_from_u8_invalid_4() { + assert!(SecurityLevel::try_from(4u8).is_err()); + } + + #[test] + fn test_try_from_u8_invalid_255() { + assert!(SecurityLevel::try_from(255u8).is_err()); + } + + // -- From for [u8; 1] -- + + #[test] + fn test_into_u8_array_master() { + let arr: [u8; 1] = SecurityLevel::MASTER.into(); + assert_eq!(arr, [0]); + } + + #[test] + fn test_into_u8_array_critical() { + let arr: [u8; 1] = SecurityLevel::CRITICAL.into(); + assert_eq!(arr, [1]); + } + + #[test] + fn test_into_u8_array_high() { + let arr: [u8; 1] = SecurityLevel::HIGH.into(); + assert_eq!(arr, [2]); + } + + #[test] + fn test_into_u8_array_medium() { + let arr: [u8; 1] = SecurityLevel::MEDIUM.into(); + assert_eq!(arr, [3]); + } + + // -- From for &'static [u8; 1] -- + + #[test] + fn test_into_static_u8_array_ref() { + let r: &'static [u8; 1] = SecurityLevel::MASTER.into(); + assert_eq!(r, &[0]); + let r: &'static [u8; 1] = SecurityLevel::CRITICAL.into(); + assert_eq!(r, &[1]); + let r: &'static [u8; 1] = SecurityLevel::HIGH.into(); + assert_eq!(r, &[2]); + let r: &'static [u8; 1] = SecurityLevel::MEDIUM.into(); + assert_eq!(r, &[3]); + } + + // -- full_range() -- + + #[test] + fn test_full_range_contains_all_variants() { + let range = SecurityLevel::full_range(); + assert_eq!(range.len(), 4); + assert_eq!(range[0], SecurityLevel::MASTER); + assert_eq!(range[1], SecurityLevel::CRITICAL); + assert_eq!(range[2], SecurityLevel::HIGH); + assert_eq!(range[3], SecurityLevel::MEDIUM); + } + + // -- stronger_security_than -- + + #[test] + fn test_master_stronger_than_critical() { + assert!(SecurityLevel::MASTER.stronger_security_than(SecurityLevel::CRITICAL)); + } + + #[test] + fn test_master_stronger_than_high() { + assert!(SecurityLevel::MASTER.stronger_security_than(SecurityLevel::HIGH)); + } + + #[test] + fn test_master_stronger_than_medium() { + assert!(SecurityLevel::MASTER.stronger_security_than(SecurityLevel::MEDIUM)); + } + + #[test] + fn test_critical_not_stronger_than_master() { + assert!(!SecurityLevel::CRITICAL.stronger_security_than(SecurityLevel::MASTER)); + } + + #[test] + fn test_same_level_not_stronger() { + assert!(!SecurityLevel::HIGH.stronger_security_than(SecurityLevel::HIGH)); + } + + #[test] + fn test_medium_not_stronger_than_high() { + assert!(!SecurityLevel::MEDIUM.stronger_security_than(SecurityLevel::HIGH)); + } + + // -- stronger_or_equal_security_than -- + + #[test] + fn test_master_stronger_or_equal_to_master() { + assert!(SecurityLevel::MASTER.stronger_or_equal_security_than(SecurityLevel::MASTER)); + } + + #[test] + fn test_master_stronger_or_equal_to_medium() { + assert!(SecurityLevel::MASTER.stronger_or_equal_security_than(SecurityLevel::MEDIUM)); + } + + #[test] + fn test_medium_not_stronger_or_equal_to_master() { + assert!(!SecurityLevel::MEDIUM.stronger_or_equal_security_than(SecurityLevel::MASTER)); + } + + #[test] + fn test_high_stronger_or_equal_to_high() { + assert!(SecurityLevel::HIGH.stronger_or_equal_security_than(SecurityLevel::HIGH)); + } + + #[test] + fn test_critical_stronger_or_equal_to_high() { + assert!(SecurityLevel::CRITICAL.stronger_or_equal_security_than(SecurityLevel::HIGH)); + } + + // -- Display -- + + #[test] + fn test_display_master() { + assert_eq!(format!("{}", SecurityLevel::MASTER), "MASTER"); + } + + #[test] + fn test_display_critical() { + assert_eq!(format!("{}", SecurityLevel::CRITICAL), "CRITICAL"); + } + + #[test] + fn test_display_high() { + assert_eq!(format!("{}", SecurityLevel::HIGH), "HIGH"); + } + + #[test] + fn test_display_medium() { + assert_eq!(format!("{}", SecurityLevel::MEDIUM), "MEDIUM"); + } + + // -- last / lowest_level / highest_level -- + + #[test] + fn test_last_is_medium() { + assert_eq!(SecurityLevel::last(), SecurityLevel::MEDIUM); + } + + #[test] + fn test_lowest_level_is_medium() { + assert_eq!(SecurityLevel::lowest_level(), SecurityLevel::MEDIUM); + } + + #[test] + fn test_highest_level_is_master() { + assert_eq!(SecurityLevel::highest_level(), SecurityLevel::MASTER); + } + + // -- Default -- + + #[test] + fn test_default_is_high() { + assert_eq!(SecurityLevel::default(), SecurityLevel::HIGH); + } + + // -- round-trip: u8 -> SecurityLevel -> [u8; 1] -- + + #[test] + fn test_round_trip_all_valid_values() { + for val in 0u8..=3 { + let level = SecurityLevel::try_from(val).unwrap(); + let arr: [u8; 1] = level.into(); + assert_eq!(arr[0], val); + } + } +} diff --git a/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs b/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs index cec7564da4f..c83fb53e074 100644 --- a/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs +++ b/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs @@ -73,3 +73,105 @@ impl TryFrom for GasFeesPaidBy { .try_into() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_gas_fees_paid_by_to_u8_document_owner() { + assert_eq!(u8::from(GasFeesPaidBy::DocumentOwner), 0); + } + + #[test] + fn from_gas_fees_paid_by_to_u8_contract_owner() { + assert_eq!(u8::from(GasFeesPaidBy::ContractOwner), 1); + } + + #[test] + fn from_gas_fees_paid_by_to_u8_prefer_contract_owner() { + assert_eq!(u8::from(GasFeesPaidBy::PreferContractOwner), 2); + } + + #[test] + fn try_from_u8_valid_values() { + assert_eq!( + GasFeesPaidBy::try_from(0u8).unwrap(), + GasFeesPaidBy::DocumentOwner + ); + assert_eq!( + GasFeesPaidBy::try_from(1u8).unwrap(), + GasFeesPaidBy::ContractOwner + ); + assert_eq!( + GasFeesPaidBy::try_from(2u8).unwrap(), + GasFeesPaidBy::PreferContractOwner + ); + } + + #[test] + fn try_from_u8_invalid_value_returns_error() { + let result = GasFeesPaidBy::try_from(3u8); + assert!(result.is_err()); + let result = GasFeesPaidBy::try_from(255u8); + assert!(result.is_err()); + } + + #[test] + fn try_from_u64_valid_values() { + assert_eq!( + GasFeesPaidBy::try_from(0u64).unwrap(), + GasFeesPaidBy::DocumentOwner + ); + assert_eq!( + GasFeesPaidBy::try_from(1u64).unwrap(), + GasFeesPaidBy::ContractOwner + ); + assert_eq!( + GasFeesPaidBy::try_from(2u64).unwrap(), + GasFeesPaidBy::PreferContractOwner + ); + } + + #[test] + fn try_from_u64_invalid_small_value_returns_error() { + let result = GasFeesPaidBy::try_from(3u64); + assert!(result.is_err()); + } + + #[test] + fn try_from_u64_large_value_returns_error() { + let result = GasFeesPaidBy::try_from(256u64); + assert!(result.is_err()); + let result = GasFeesPaidBy::try_from(u64::MAX); + assert!(result.is_err()); + } + + #[test] + fn display_variants() { + assert_eq!(format!("{}", GasFeesPaidBy::DocumentOwner), "DocumentOwner"); + assert_eq!(format!("{}", GasFeesPaidBy::ContractOwner), "ContractOwner"); + assert_eq!( + format!("{}", GasFeesPaidBy::PreferContractOwner), + "PreferContractOwner" + ); + } + + #[test] + fn default_is_document_owner() { + assert_eq!(GasFeesPaidBy::default(), GasFeesPaidBy::DocumentOwner); + } + + #[test] + fn u8_round_trip_all_variants() { + for variant in [ + GasFeesPaidBy::DocumentOwner, + GasFeesPaidBy::ContractOwner, + GasFeesPaidBy::PreferContractOwner, + ] { + let byte_val: u8 = variant.into(); + let recovered = GasFeesPaidBy::try_from(byte_val).unwrap(); + assert_eq!(variant, recovered); + } + } +} diff --git a/packages/rs-dpp/src/tokens/token_event.rs b/packages/rs-dpp/src/tokens/token_event.rs index 4f508bfb7fe..973e593d3a9 100644 --- a/packages/rs-dpp/src/tokens/token_event.rs +++ b/packages/rs-dpp/src/tokens/token_event.rs @@ -479,3 +479,460 @@ 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]) + } + + // ---- Display tests ---- + + #[test] + fn display_mint_without_note() { + let event = TokenEvent::Mint(1000, test_id(), None); + let s = format!("{}", event); + assert!(s.starts_with("Mint 1000 to ")); + assert!(!s.contains("(note:")); + } + + #[test] + fn display_mint_with_note() { + let event = TokenEvent::Mint(500, test_id(), Some("test note".to_string())); + let s = format!("{}", event); + assert!(s.starts_with("Mint 500 to ")); + assert!(s.contains("(note: test note)")); + } + + #[test] + fn display_burn_without_note() { + let event = TokenEvent::Burn(200, test_id(), None); + let s = format!("{}", event); + assert!(s.starts_with("Burn 200 from ")); + assert!(!s.contains("(note:")); + } + + #[test] + fn display_burn_with_note() { + let event = TokenEvent::Burn(200, test_id(), Some("burn note".to_string())); + let s = format!("{}", event); + assert!(s.contains("Burn 200 from ")); + assert!(s.contains("(note: burn note)")); + } + + #[test] + fn display_freeze_without_note() { + let event = TokenEvent::Freeze(test_id(), None); + let s = format!("{}", event); + assert!(s.starts_with("Freeze ")); + assert!(!s.contains("(note:")); + } + + #[test] + fn display_freeze_with_note() { + let event = TokenEvent::Freeze(test_id(), Some("frozen".to_string())); + let s = format!("{}", event); + assert!(s.starts_with("Freeze ")); + assert!(s.contains("(note: frozen)")); + } + + #[test] + fn display_unfreeze_without_note() { + let event = TokenEvent::Unfreeze(test_id(), None); + let s = format!("{}", event); + assert!(s.starts_with("Unfreeze ")); + assert!(!s.contains("(note:")); + } + + #[test] + fn display_unfreeze_with_note() { + let event = TokenEvent::Unfreeze(test_id(), Some("thawed".to_string())); + let s = format!("{}", event); + assert!(s.contains("(note: thawed)")); + } + + #[test] + fn display_destroy_frozen_funds_without_note() { + let event = TokenEvent::DestroyFrozenFunds(test_id(), 999, None); + let s = format!("{}", event); + assert!(s.contains("Destroy 999 frozen from ")); + assert!(!s.contains("(note:")); + } + + #[test] + fn display_destroy_frozen_funds_with_note() { + let event = + TokenEvent::DestroyFrozenFunds(test_id(), 999, Some("destroyed".to_string())); + let s = format!("{}", event); + assert!(s.contains("Destroy 999 frozen from ")); + assert!(s.contains("(note: destroyed)")); + } + + #[test] + fn display_transfer_without_note() { + let event = TokenEvent::Transfer(test_id(), None, None, None, 100); + let s = format!("{}", event); + assert!(s.contains("Transfer 100 to ")); + assert!(!s.contains("(note:")); + } + + #[test] + fn display_transfer_with_note() { + let event = TokenEvent::Transfer( + test_id(), + Some("payment".to_string()), + None, + None, + 100, + ); + let s = format!("{}", event); + assert!(s.contains("Transfer 100 to ")); + assert!(s.contains("(note: payment)")); + } + + #[test] + fn display_claim() { + let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id()); + let event = TokenEvent::Claim(recipient, 50, None); + let s = format!("{}", event); + assert!(s.starts_with("Claim 50 by ")); + } + + #[test] + fn display_emergency_action() { + let event = TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None); + let s = format!("{}", event); + assert!(s.starts_with("Emergency action ")); + } + + #[test] + fn display_emergency_action_with_note() { + let event = TokenEvent::EmergencyAction( + TokenEmergencyAction::Resume, + Some("resuming".to_string()), + ); + let s = format!("{}", event); + assert!(s.contains("Emergency action ")); + assert!(s.contains("(note: resuming)")); + } + + #[test] + fn display_config_update() { + let event = TokenEvent::ConfigUpdate( + TokenConfigurationChangeItem::TokenConfigurationNoChange, + None, + ); + let s = format!("{}", event); + assert!(s.starts_with("Configuration update ")); + } + + #[test] + fn display_change_price_with_schedule() { + let schedule = TokenPricingSchedule::SinglePrice(1000); + let event = TokenEvent::ChangePriceForDirectPurchase(Some(schedule), None); + let s = format!("{}", event); + assert!(s.starts_with("Change price schedule to ")); + } + + #[test] + fn display_change_price_disable() { + let event = TokenEvent::ChangePriceForDirectPurchase(None, None); + let s = format!("{}", event); + assert!(s.starts_with("Disable direct purchase")); + } + + #[test] + fn display_change_price_disable_with_note() { + let event = TokenEvent::ChangePriceForDirectPurchase( + None, + Some("no more sales".to_string()), + ); + let s = format!("{}", event); + assert_eq!(s, "Disable direct purchase (note: no more sales)"); + } + + #[test] + fn display_direct_purchase() { + let event = TokenEvent::DirectPurchase(50, 5000); + let s = format!("{}", event); + assert_eq!(s, "Direct purchase of 50 for 5000 credits"); + } + + // ---- 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"); + } + + // ---- public_note tests ---- + + #[test] + fn public_note_mint_some() { + let event = TokenEvent::Mint(100, test_id(), Some("note".to_string())); + assert_eq!(event.public_note(), Some("note")); + } + + #[test] + fn public_note_mint_none() { + let event = TokenEvent::Mint(100, test_id(), None); + assert_eq!(event.public_note(), None); + } + + #[test] + fn public_note_burn_some() { + let event = TokenEvent::Burn(100, test_id(), Some("burn note".to_string())); + assert_eq!(event.public_note(), Some("burn note")); + } + + #[test] + fn public_note_burn_none() { + let event = TokenEvent::Burn(100, test_id(), None); + assert_eq!(event.public_note(), None); + } + + #[test] + fn public_note_freeze_some() { + let event = TokenEvent::Freeze(test_id(), Some("freeze note".to_string())); + assert_eq!(event.public_note(), Some("freeze note")); + } + + #[test] + fn public_note_freeze_none() { + let event = TokenEvent::Freeze(test_id(), None); + assert_eq!(event.public_note(), None); + } + + #[test] + fn public_note_unfreeze_some() { + let event = TokenEvent::Unfreeze(test_id(), Some("uf note".to_string())); + assert_eq!(event.public_note(), Some("uf note")); + } + + #[test] + fn public_note_unfreeze_none() { + let event = TokenEvent::Unfreeze(test_id(), None); + assert_eq!(event.public_note(), None); + } + + #[test] + fn public_note_destroy_frozen_some() { + let event = + TokenEvent::DestroyFrozenFunds(test_id(), 10, Some("destroy note".to_string())); + assert_eq!(event.public_note(), Some("destroy note")); + } + + #[test] + fn public_note_destroy_frozen_none() { + let event = TokenEvent::DestroyFrozenFunds(test_id(), 10, None); + assert_eq!(event.public_note(), None); + } + + #[test] + fn public_note_transfer_some() { + let event = TokenEvent::Transfer( + test_id(), + Some("tx note".to_string()), + None, + None, + 100, + ); + assert_eq!(event.public_note(), Some("tx note")); + } + + #[test] + fn public_note_transfer_none() { + let event = TokenEvent::Transfer(test_id(), None, None, None, 100); + assert_eq!(event.public_note(), None); + } + + #[test] + fn public_note_claim_some() { + let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id()); + let event = TokenEvent::Claim(recipient, 10, Some("claim note".to_string())); + assert_eq!(event.public_note(), Some("claim note")); + } + + #[test] + fn public_note_claim_none() { + let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id()); + let event = TokenEvent::Claim(recipient, 10, None); + assert_eq!(event.public_note(), None); + } + + #[test] + fn public_note_emergency_action_some() { + let event = TokenEvent::EmergencyAction( + TokenEmergencyAction::Pause, + Some("emergency".to_string()), + ); + assert_eq!(event.public_note(), Some("emergency")); + } + + #[test] + fn public_note_emergency_action_none() { + let event = TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None); + assert_eq!(event.public_note(), None); + } + + #[test] + fn public_note_config_update_some() { + let event = TokenEvent::ConfigUpdate( + TokenConfigurationChangeItem::TokenConfigurationNoChange, + Some("config note".to_string()), + ); + assert_eq!(event.public_note(), Some("config note")); + } + + #[test] + fn public_note_config_update_none() { + let event = TokenEvent::ConfigUpdate( + TokenConfigurationChangeItem::TokenConfigurationNoChange, + None, + ); + assert_eq!(event.public_note(), None); + } + + #[test] + fn public_note_change_price_some() { + let event = TokenEvent::ChangePriceForDirectPurchase( + None, + Some("price note".to_string()), + ); + assert_eq!(event.public_note(), Some("price note")); + } + + #[test] + fn public_note_change_price_none() { + let event = TokenEvent::ChangePriceForDirectPurchase(None, None); + assert_eq!(event.public_note(), None); + } + + #[test] + fn public_note_direct_purchase_returns_none() { + // DirectPurchase has no note field at all + let event = TokenEvent::DirectPurchase(100, 500); + assert_eq!(event.public_note(), None); + } + + // ---- 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..54bd43d0088 100644 --- a/packages/rs-dpp/src/util/vec.rs +++ b/packages/rs-dpp/src/util/vec.rs @@ -65,3 +65,208 @@ 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()); + } + + // -- 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..bc5454d650c 100644 --- a/packages/rs-dpp/src/validation/validation_result.rs +++ b/packages/rs-dpp/src/validation/validation_result.rs @@ -289,3 +289,423 @@ 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/display.rs b/packages/rs-platform-value/src/display.rs index e7807fbdf63..061cc518e4d 100644 --- a/packages/rs-platform-value/src/display.rs +++ b/packages/rs-platform-value/src/display.rs @@ -128,3 +128,348 @@ impl Value { } } } + +#[cfg(test)] +mod tests { + use crate::Value; + use base64::Engine; + + // ---- Display (string_representation) tests ---- + + #[test] + fn display_null() { + assert_eq!(format!("{}", Value::Null), "Null"); + } + + #[test] + fn display_bool_true() { + assert_eq!(format!("{}", Value::Bool(true)), "bool true"); + } + + #[test] + fn display_bool_false() { + assert_eq!(format!("{}", Value::Bool(false)), "bool false"); + } + + #[test] + fn display_u128() { + assert_eq!( + format!("{}", Value::U128(340282366920938463463374607431768211455)), + "(u128)340282366920938463463374607431768211455" + ); + } + + #[test] + fn display_i128() { + assert_eq!(format!("{}", Value::I128(-42)), "(i128)-42"); + } + + #[test] + fn display_u64() { + assert_eq!(format!("{}", Value::U64(1000)), "(u64)1000"); + } + + #[test] + fn display_i64() { + assert_eq!(format!("{}", Value::I64(-999)), "(i64)-999"); + } + + #[test] + fn display_u32() { + assert_eq!(format!("{}", Value::U32(42)), "(u32)42"); + } + + #[test] + fn display_i32() { + assert_eq!(format!("{}", Value::I32(-1)), "(i32)-1"); + } + + #[test] + fn display_u16() { + assert_eq!(format!("{}", Value::U16(65535)), "(u16)65535"); + } + + #[test] + fn display_i16() { + assert_eq!(format!("{}", Value::I16(-32768)), "(i16)-32768"); + } + + #[test] + fn display_u8() { + assert_eq!(format!("{}", Value::U8(255)), "(u8)255"); + } + + #[test] + fn display_i8() { + assert_eq!(format!("{}", Value::I8(-128)), "(i8)-128"); + } + + #[test] + fn display_float() { + let s = format!("{}", Value::Float(3.14)); + assert!(s.starts_with("float 3.14")); + } + + #[test] + fn display_text_short() { + let text = "hello"; + assert_eq!( + format!("{}", Value::Text(text.to_string())), + "string hello" + ); + } + + #[test] + fn display_text_exactly_20_chars() { + let text = "12345678901234567890"; // exactly 20 + assert_eq!( + format!("{}", Value::Text(text.to_string())), + format!("string {}", text) + ); + } + + #[test] + fn display_text_long_truncated() { + let text = "123456789012345678901"; // 21 chars + let result = format!("{}", Value::Text(text.to_string())); + assert_eq!(result, "string 12345678901234567890[...(21)]"); + } + + #[test] + fn display_text_long_50_chars() { + let text = "a".repeat(50); + let result = format!("{}", Value::Text(text)); + assert_eq!(result, "string aaaaaaaaaaaaaaaaaaaa[...(50)]"); + } + + #[test] + fn display_bytes_empty() { + assert_eq!(format!("{}", Value::Bytes(vec![])), "bytes "); + } + + #[test] + fn display_bytes_non_empty() { + let bytes = vec![0xde, 0xad, 0xbe, 0xef]; + assert_eq!(format!("{}", Value::Bytes(bytes)), "bytes deadbeef"); + } + + #[test] + fn display_bytes20() { + let bytes = [0u8; 20]; + let result = format!("{}", Value::Bytes20(bytes)); + assert!(result.starts_with("bytes20 ")); + // base64 of 20 zero bytes + let expected_b64 = base64::prelude::BASE64_STANDARD.encode(&bytes); + assert_eq!(result, format!("bytes20 {}", expected_b64)); + } + + #[test] + fn display_bytes32() { + let bytes = [1u8; 32]; + let result = format!("{}", Value::Bytes32(bytes)); + let expected_b64 = base64::prelude::BASE64_STANDARD.encode(&bytes); + assert_eq!(result, format!("bytes32 {}", expected_b64)); + } + + #[test] + fn display_bytes36() { + let bytes = [0xffu8; 36]; + let result = format!("{}", Value::Bytes36(bytes)); + let expected_b64 = base64::prelude::BASE64_STANDARD.encode(&bytes); + assert_eq!(result, format!("bytes36 {}", expected_b64)); + } + + #[test] + fn display_identifier() { + let id = [42u8; 32]; + let result = format!("{}", Value::Identifier(id)); + let expected_b58 = bs58::encode(&id).into_string(); + assert_eq!(result, format!("identifier {}", expected_b58)); + } + + #[test] + fn display_array_empty() { + assert_eq!(format!("{}", Value::Array(vec![])), "array of []"); + } + + #[test] + fn display_array_with_elements() { + let arr = vec![Value::U8(1), Value::Bool(true)]; + let result = format!("{}", Value::Array(arr)); + assert_eq!(result, "array of [(u8)1, bool true]"); + } + + #[test] + fn display_map_empty() { + assert_eq!(format!("{}", Value::Map(vec![])), "Map { }"); + } + + #[test] + fn display_map_with_entries() { + let map = vec![(Value::Text("key".to_string()), Value::U32(99))]; + let result = format!("{}", Value::Map(map)); + assert_eq!(result, "Map { string key: (u32)99 }"); + } + + #[test] + fn display_enum_u8() { + assert_eq!(format!("{}", Value::EnumU8(vec![1, 2])), "enum u8"); + } + + #[test] + fn display_enum_string() { + assert_eq!( + format!( + "{}", + Value::EnumString(vec!["a".to_string(), "b".to_string()]) + ), + "enum string" + ); + } + + // ---- non_qualified_string_representation tests ---- + + #[test] + fn non_qualified_null() { + assert_eq!(Value::Null.non_qualified_string_representation(), "Null"); + } + + #[test] + fn non_qualified_bool_true() { + assert_eq!( + Value::Bool(true).non_qualified_string_representation(), + "true" + ); + } + + #[test] + fn non_qualified_bool_false() { + assert_eq!( + Value::Bool(false).non_qualified_string_representation(), + "false" + ); + } + + #[test] + fn non_qualified_u64() { + assert_eq!( + Value::U64(12345).non_qualified_string_representation(), + "12345" + ); + } + + #[test] + fn non_qualified_i64() { + assert_eq!( + Value::I64(-42).non_qualified_string_representation(), + "-42" + ); + } + + #[test] + fn non_qualified_float() { + let s = Value::Float(2.5).non_qualified_string_representation(); + assert_eq!(s, "2.5"); + } + + #[test] + fn non_qualified_text_returns_raw_string() { + // non_qualified does NOT prepend "string" or truncate + let text = "a long text that is more than twenty characters"; + assert_eq!( + Value::Text(text.to_string()).non_qualified_string_representation(), + text + ); + } + + #[test] + fn non_qualified_bytes() { + let bytes = vec![0xca, 0xfe]; + assert_eq!( + Value::Bytes(bytes).non_qualified_string_representation(), + "bytes cafe" + ); + } + + #[test] + fn non_qualified_identifier() { + let id = [7u8; 32]; + let expected_b58 = bs58::encode(&id).into_string(); + assert_eq!( + Value::Identifier(id).non_qualified_string_representation(), + format!("identifier {}", expected_b58) + ); + } + + #[test] + fn non_qualified_array() { + let arr = vec![Value::U8(1)]; + let result = Value::Array(arr).non_qualified_string_representation(); + assert_eq!(result, "array of [(u8)1]"); + } + + #[test] + fn non_qualified_map() { + let map = vec![(Value::Text("k".to_string()), Value::Null)]; + let result = Value::Map(map).non_qualified_string_representation(); + assert_eq!(result, "Map { string k: Null }"); + } + + #[test] + fn non_qualified_u128() { + assert_eq!( + Value::U128(999).non_qualified_string_representation(), + "999" + ); + } + + #[test] + fn non_qualified_i128() { + assert_eq!( + Value::I128(-1).non_qualified_string_representation(), + "-1" + ); + } + + #[test] + fn non_qualified_u32() { + assert_eq!( + Value::U32(100).non_qualified_string_representation(), + "100" + ); + } + + #[test] + fn non_qualified_i32() { + assert_eq!( + Value::I32(-100).non_qualified_string_representation(), + "-100" + ); + } + + #[test] + fn non_qualified_u16() { + assert_eq!( + Value::U16(500).non_qualified_string_representation(), + "500" + ); + } + + #[test] + fn non_qualified_i16() { + assert_eq!( + Value::I16(-500).non_qualified_string_representation(), + "-500" + ); + } + + #[test] + fn non_qualified_u8() { + assert_eq!(Value::U8(7).non_qualified_string_representation(), "7"); + } + + #[test] + fn non_qualified_i8() { + assert_eq!(Value::I8(-7).non_qualified_string_representation(), "-7"); + } +} 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))); + } +} diff --git a/packages/rs-platform-value/src/string_encoding.rs b/packages/rs-platform-value/src/string_encoding.rs index bce2c69fbbf..168cb35aeb3 100644 --- a/packages/rs-platform-value/src/string_encoding.rs +++ b/packages/rs-platform-value/src/string_encoding.rs @@ -45,3 +45,189 @@ pub fn encode(value: &[u8], encoding: Encoding) -> String { Encoding::Hex => hex::encode(value), } } + +#[cfg(test)] +mod tests { + use super::*; + + // ---- encode/decode round-trips ---- + + #[test] + fn round_trip_base58_empty() { + let data: &[u8] = &[]; + let encoded = encode(data, Encoding::Base58); + let decoded = decode(&encoded, Encoding::Base58).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn round_trip_base58_non_empty() { + let data = b"hello world"; + let encoded = encode(data, Encoding::Base58); + let decoded = decode(&encoded, Encoding::Base58).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn round_trip_base58_binary() { + let data: Vec = (0u8..=255).collect(); + let encoded = encode(&data, Encoding::Base58); + let decoded = decode(&encoded, Encoding::Base58).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn round_trip_base64_empty() { + let data: &[u8] = &[]; + let encoded = encode(data, Encoding::Base64); + let decoded = decode(&encoded, Encoding::Base64).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn round_trip_base64_non_empty() { + let data = b"hello world"; + let encoded = encode(data, Encoding::Base64); + let decoded = decode(&encoded, Encoding::Base64).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn round_trip_base64_binary() { + let data: Vec = (0u8..=255).collect(); + let encoded = encode(&data, Encoding::Base64); + let decoded = decode(&encoded, Encoding::Base64).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn round_trip_hex_empty() { + let data: &[u8] = &[]; + let encoded = encode(data, Encoding::Hex); + let decoded = decode(&encoded, Encoding::Hex).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn round_trip_hex_non_empty() { + let data = b"hello world"; + let encoded = encode(data, Encoding::Hex); + let decoded = decode(&encoded, Encoding::Hex).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn round_trip_hex_binary() { + let data: Vec = (0u8..=255).collect(); + let encoded = encode(&data, Encoding::Hex); + let decoded = decode(&encoded, Encoding::Hex).unwrap(); + assert_eq!(decoded, data); + } + + // ---- encode produces expected output ---- + + #[test] + fn encode_hex_known_value() { + assert_eq!(encode(&[0xde, 0xad, 0xbe, 0xef], Encoding::Hex), "deadbeef"); + } + + #[test] + fn encode_base64_known_value() { + // "hello" -> "aGVsbG8=" + assert_eq!(encode(b"hello", Encoding::Base64), "aGVsbG8="); + } + + #[test] + fn encode_base58_known_value() { + // bs58 of "Hello" is "9Ajdvzr" + let encoded = encode(b"Hello", Encoding::Base58); + assert_eq!(encoded, bs58::encode(b"Hello").into_string()); + } + + // ---- Display for Encoding ---- + + #[test] + fn display_encoding_base58() { + assert_eq!(format!("{}", Encoding::Base58), "Base58"); + } + + #[test] + fn display_encoding_base64() { + assert_eq!(format!("{}", Encoding::Base64), "Base64"); + } + + #[test] + fn display_encoding_hex() { + assert_eq!(format!("{}", Encoding::Hex), "Hex"); + } + + // ---- ALL_ENCODINGS covers all variants ---- + + #[test] + fn all_encodings_has_three_entries() { + assert_eq!(ALL_ENCODINGS.len(), 3); + } + + // ---- error cases for invalid encoded strings ---- + + #[test] + fn decode_invalid_hex_returns_error() { + // 'zz' is not valid hex + let result = decode("zzzz", Encoding::Hex); + assert!(result.is_err()); + match result.unwrap_err() { + Error::StringDecodingError(msg) => { + assert!(!msg.is_empty()); + } + other => panic!("Expected StringDecodingError, got {:?}", other), + } + } + + #[test] + fn decode_invalid_hex_odd_length_returns_error() { + let result = decode("abc", Encoding::Hex); + assert!(result.is_err()); + } + + #[test] + fn decode_invalid_base64_returns_error() { + // '!!!' is not valid base64 + let result = decode("!!!", Encoding::Base64); + assert!(result.is_err()); + match result.unwrap_err() { + Error::StringDecodingError(msg) => { + assert!(!msg.is_empty()); + } + other => panic!("Expected StringDecodingError, got {:?}", other), + } + } + + #[test] + fn decode_invalid_base58_returns_error() { + // '0OIl' contains characters invalid in base58 + let result = decode("0OIl", Encoding::Base58); + assert!(result.is_err()); + match result.unwrap_err() { + Error::StringDecodingError(msg) => { + assert!(!msg.is_empty()); + } + other => panic!("Expected StringDecodingError, got {:?}", other), + } + } + + // ---- round-trip through all encodings systematically ---- + + #[test] + fn round_trip_all_encodings_same_data() { + let data = b"The quick brown fox jumps over the lazy dog"; + for encoding in &ALL_ENCODINGS { + let encoded = encode(data, *encoding); + let decoded = decode(&encoded, *encoding).unwrap(); + assert_eq!( + decoded, data, + "Round-trip failed for encoding {}", + encoding + ); + } + } +} diff --git a/presentation.html b/presentation.html new file mode 100644 index 00000000000..3a4b716c3d5 --- /dev/null +++ b/presentation.html @@ -0,0 +1,1157 @@ + + + + + +Dash Platform Deep Dive + + + + + +
+ +

Developer Deep Dive

+

Finding docs, navigating code, asking questions, and building your first app

+

Quantum Explorer

+
+ + +
+
+

What we'll cover

+
+
+

1. Documentation

+

Where to find it, how to navigate it, where to start reading

+
+
+

2. GitHub

+

Where the code lives, how to explore the monorepo

+
+
+

3. How it works

+

GroveDB, Tenderdash, and why every query is provable

+
+
+

4. Developer Flow

+

From zero to running a local network

+
+
+

5. Example: Yappr

+

What you can build today on Dash Platform

+
+
+

6. Discord & Help

+

Where to ask questions, which channels are active

+
+
+
+
+ + +
+
+

The problem we're solving

+

+ Decentralized apps need to store and query data. Today's blockchains make this surprisingly hard. +

+ +
+
+

Ethereum

+
    +
  • Data lives inside smart contracts — no structured queries
  • +
  • Want to list "all posts by user X"? You need an off-chain indexer (The Graph, Alchemy)
  • +
  • Merkle proofs exist but only for single key lookups, not ranges or filters
  • +
  • Light clients can't verify query results — they trust the indexer
  • +
+
+
+

Solana

+
    +
  • Fast writes, but no state proofs at all
  • +
  • Querying account data? You call an RPC node and trust the response
  • +
  • No way for a light client to verify what a node returns
  • +
  • Data schemas are ad-hoc — each program defines its own serialization
  • +
+
+
+ +
+

The gap: Every major blockchain forces apps to choose between decentralization and usability. You either trust a centralized indexer for queries, or you run a full node. There's no middle ground — until now.

+
+
+
+ + +
+
+

Dash Platform's answer

+ +
+
+

What if the chain itself was a database?

+
    +
  • Define your data schema as a data contract (JSON Schema)
  • +
  • The network stores, indexes, and validates your data natively
  • +
  • Query with filters, ranges, sorting, pagination — directly from masternodes
  • +
  • Every response comes with a cryptographic proof
  • +
  • A phone can verify responses with full-node security
  • +
+
+
+

The key insight

+
+

By storing data in a Merkle tree (GroveDB) and having consensus sign the root hash (Tenderdash), we can prove any query result — not just "does this key exist" but "give me all documents where status = active, sorted by date, page 2 of 5."

+
+ +

No indexer needed

+

+ No The Graph. No Alchemy. No centralized API sitting between your app and the chain. Just a light client, a masternode, and a proof. +

+
+
+
+
+ + +
+
01
+

Documentation

+

Where to find it, how to navigate it

+
+ + +
+
+

Three documentation sources

+ + + + + + +
+
+ + +
+
+

Where to start reading

+ +
+
1
+
+

New to Dash Platform?
+ Start with dashplatform.readme.io → "Introduction" → "What is Dash Platform"

+
+
+ +
+
2
+
+

Want to understand the architecture?
+ The Platform Book → "Architecture" → "Component Pipeline"

+
+
+ +
+
3
+
+

Ready to write code?
+ The Platform Book → "Getting Started" (prereqs, setup, first test)

+
+
+ +
+
4
+
+

Building a web/JS app?
+ Platform Book → "Evo SDK" section (6 chapters + tutorials, just added)

+
+
+
+
+ + +
+
+

Platform Book — key chapters

+ +
+
+

Architecture & Internals

+ + + + + + +
ChapterCovers
Data ContractsSchemas for your app's data
DocumentsUser data storage & querying
State TransitionsClient → chain write path
Drive / Grove OpsGroveDB Merkle tree storage
+
+
+

Evo SDK (JS/TS)

+ + + + + + +
ChapterCovers
Getting StartedInstall, connect, first query
State TransitionsWrite identities, docs, tokens
Trusted ModeProofs vs. trusted queries
Wallet UtilitiesKeys, mnemonics, signing
+

+ Tutorials: Token, Car Sales, Card Game

+
+
+
+
+ + +
+
02
+

GitHub

+

Where the code lives, how to explore it

+
+ + +
+
+

One monorepo, everything inside

+ + + +
+
+

Core (Rust)

+
    +
  • rs-dpp — Protocol rules & validation
  • +
  • rs-drive — Storage engine (GroveDB)
  • +
  • rs-drive-abci — Consensus (Tenderdash)
  • +
  • rs-sdk — Rust client SDK
  • +
  • rs-dapi — API server
  • +
+
+
+

Client SDKs

+
    +
  • wasm-sdk — JS/TS (browser + Node)
  • +
  • js-dash-sdk — Legacy JS SDK
  • +
  • swift-sdk — iOS (new in v3.1)
  • +
  • dashmate — Node management CLI
  • +
  • platform-test-suite — E2E tests
  • +
+
+
+
+
+ + +
+
+

Files to know about

+ +
+
1
+
+

README.md — Architecture overview, comparison table, SDK support matrix

+
+
+ +
+
2
+
+

Cargo.toml — Workspace root. Shows all Rust crates and shared dependencies

+
+
+ +
+
3
+
+

packages/*/Cargo.toml or package.json — Each package's deps and features

+
+
+ +
+
4
+
+

NIGHTLY_STATUS.md — What's passing/failing in CI right now

+
+
+ +
+
5
+
+

Data contracts: packages/*-contract/ — DPNS, DashPay, Withdrawals, etc.

+
+
+
+
+ + + +
+
+

How GroveDB works

+

+ GroveDB is a hierarchical authenticated data structure — a tree of Merkle trees. +

+ +
+
+

Structure

+
    +
  • Top level: a tree of subtrees organized by path
  • +
  • Each subtree is a Merk tree (Merkle AVL tree)
  • +
  • Every node stores its hash and its children's hashes
  • +
  • The root hash commits to the entire state
  • +
+ +

Why it matters

+
    +
  • Any key-value lookup produces a Merkle proof
  • +
  • Proofs are compact (log N hashes)
  • +
  • Supports range queries with proofs (not just key lookups)
  • +
+
+
+
        Root Hash
+       /         \
+    [contracts]   [identities]
+     /     \          |
+ [dpns] [dashpay]  [id:abc...]
+   |                  |
+ [domains]         [keys]
+  /    \
+alice  bob
+
+Each level is a Merk tree
+Root hash = hash of everything
+
+
+
+
+ + +
+
+

How Tenderdash signs the root

+

+ Tenderdash is Dash's BFT consensus engine. It produces blocks and signs the GroveDB root hash. +

+ +
+
1
+
+

Propose — A masternode proposes a block containing state transitions

+
+
+
+
2
+
+

Execute — Drive applies the transitions to GroveDB, producing a new root hash

+
+
+
+
3
+
+

Vote — Masternodes verify they get the same root hash, then sign with BLS threshold signatures

+
+
+
+
4
+
+

Commit — Block is finalized instantly (no confirmations needed). The signed root hash is the chain's commitment to the entire platform state.

+
+
+ +
+

Result: every block has a quorum-signed root hash that commits to every document, identity, and contract in the system.

+
+
+
+ + +
+
+

Why every query is provable

+ +

+ GroveDB's Merkle structure + Tenderdash's signed root = cryptographic proofs for any query. +

+ +
+
+

How a proof works

+
+
1
+

Client sends a query (e.g. "get alice's profile")

+
+
+
2
+

Masternode returns the data + a Merkle proof path

+
+
+
3
+

Client hashes up the proof path to reconstruct the root hash

+
+
+
4
+

Client checks: does my root hash match the quorum-signed root?

+
+
+
+

What this means

+
    +
  • A single masternode can serve your query
  • +
  • You don't need to trust it — the proof is the trust
  • +
  • Light clients get full-node-level security
  • +
  • Works for key lookups, ranges, filters, ordering
  • +
  • No other L1 blockchain does this for structured data queries
  • +
+ +
+

Ethereum: Merkle proofs for key-value only
+ Solana: No proofs at all
+ Dash Platform: Proofs for any query, including ranges and sorts

+
+
+
+
+
+ + +
+
04
+

Developer Flow

+

From zero to building

+
+ + +
+
+

Prerequisites

+ +
+
+

Required

+
    +
  • Node.js v20+ (v24 recommended)
  • +
  • Docker Desktop
  • +
  • Rust (latest stable)
  • +
  • Yarn v4 (via corepack)
  • +
+
+
+

For full builds

+
    +
  • protoc (Protocol Buffers compiler)
  • +
  • wasm-pack
  • +
  • wasm-bindgen-cli (match lockfile version)
  • +
  • wasm32-unknown-unknown target
  • +
+
+
+ +
# Install Rust targets and tools
+rustup target add wasm32-unknown-unknown
+cargo install wasm-pack
+cargo install wasm-bindgen-cli@0.2.108  # match Cargo.lock version
+ +
+

macOS note: You need LLVM installed via Homebrew (brew install llvm) and configured in your PATH. The Xcode-bundled clang lacks some flags required by RocksDB and other native dependencies. Set LLVM_CONFIG_PATH and update PATH / CC / AR to point to the Homebrew LLVM.

+
+
+
+ + +
+
+

Getting started — 3 commands

+ +
+
1
+
+

Clone and setup

+
git clone https://github.com/dashpay/platform
+cd platform
+yarn setup   # installs deps, builds JS + WASM packages
+
+
+ +
+
2
+
+

Start local network

+
yarn start   # launches dashmate local network (3 masternodes + seed)
+
+
+ +
+
3
+
+

Run tests

+
yarn test    # runs the platform test suite against your local network
+
+
+
+
+ + +
+
+

Dashmate — your local network tool

+ +
yarn dashmate setup local -c 3    # create 3-node local network
+yarn dashmate start                  # start all nodes
+yarn dashmate group status           # see what's running
+yarn dashmate stop                   # stop everything
+yarn dashmate reset --hard           # wipe and start fresh
+ +
+

Dashmate manages Docker containers for Core nodes, Tenderdash, Drive, and DAPI. You get a full Platform network locally in minutes.

+
+ +
+
+

What it runs

+
    +
  • 3 Dash Core masternodes
  • +
  • 1 seed/miner node
  • +
  • Tenderdash consensus
  • +
  • Drive (GroveDB storage)
  • +
  • DAPI (gRPC API server)
  • +
+
+
+

Useful commands

+
    +
  • dashmate config get ... — inspect config
  • +
  • dashmate group status — node status
  • +
  • dashmate status — single node
  • +
+
+
+
+
+ + +
+
+

Choose your SDK

+ + + + + + + + + + + + + + + + + + + + + +
SDKLanguageUse caseStatus
JS Evo SDKJS/TSWeb apps, Node.js backendsStable
Rust SDKRustBackend services, tools, CLIStable
Swift SDKSwiftiOS/macOS appsNew (v3.1)
+ +
+

Recommendation: Use the JS Evo SDK for new JS/TS projects. It's built from Rust via WebAssembly and gives you the same verification guarantees as the Rust SDK.

+
+
+
+ + +
+
+

WASM SDK — quick example

+ +
npm install @dashevo/wasm-sdk
+ +
// Connect to testnet and query an identity
+import { WasmSdkBuilder } from '@dashevo/wasm-sdk';
+
+const sdk = await new WasmSdkBuilder()
+  .withNetworkTestnet()
+  .build();
+
+// Fetch an identity by name (DPNS)
+const identity = await sdk.getIdentityByName('alice');
+console.log(identity.getId());
+
+// Query documents from a data contract
+const docs = await sdk.getDocuments(
+  contractId,
+  'profile',
+  { where: [['$ownerId', '==', identityId]], limit: 10 }
+);
+
+
+
+ + +
+
+

Data contracts — the core concept

+ +

+ A data contract is a JSON Schema that defines your app's data structure. The network stores, indexes, and enforces it. +

+ +
+
+
// Define your contract
+{
+  "note": {
+    "type": "object",
+    "properties": {
+      "message": {
+        "type": "string",
+        "maxLength": 500
+      }
+    },
+    "indices": [{
+      "name": "byOwner",
+      "properties": [
+        {"$ownerId": "asc"}
+      ]
+    }]
+  }
+}
+
+
+

What you get

+
    +
  • Schema validation on every write
  • +
  • Indexed queries with cryptographic proofs
  • +
  • Versioned updates (no data loss)
  • +
  • Per-document ownership and access control
  • +
+ +

System contracts

+
    +
  • DPNS — username registration
  • +
  • DashPay — social payments / profiles
  • +
  • Withdrawals — credit → Dash
  • +
+
+
+
+
+ + +
+
05
+

Example: Yappr

+

What you can build today

+
+ + +
+
+

Yappr — decentralized social posting

+ + + +

+ A Twitter/X-like app built entirely on Dash Platform. No backend servers, no databases — just a data contract and a frontend. +

+ +
+
+

How it works

+
    +
  • Register an identity on Platform
  • +
  • Get a username via DPNS
  • +
  • Deploy a data contract with "post" and "like" document types
  • +
  • Create documents (posts) that anyone can query
  • +
  • All data is provably authentic via Merkle proofs
  • +
+
+
+

What makes it special

+
    +
  • No server — queries go directly to masternodes
  • +
  • Censorship resistant — data is on-chain
  • +
  • Cryptographic proofs — every response is verified
  • +
  • Instant finality — posts appear in one block
  • +
  • Rich queries — sort, filter, paginate natively
  • +
+
+
+
+
+ + +
+
+

What can you build today?

+ +
+
+

Social apps

+

Profiles, posts, messaging — all with identity-based ownership

+
+
+

Naming systems

+

Human-readable names for wallets, apps, services (DPNS already exists)

+
+
+

Marketplaces

+

Listings, offers, reviews — with cryptographic proof of authenticity

+
+
+

Credential systems

+

Verifiable claims, attestations, membership proofs

+
+
+

Token platforms

+

Custom tokens with configurable minting, burning, freezing rules

+
+
+

Governance tools

+

Voting, proposals, group-controlled actions with on-chain rules

+
+
+
+
+ + +
+
+

Your path from here

+ +
+
1
+
+

Read the intro at dashplatform.readme.io

+
+
+ +
+
2
+
+

Install prerequisites (Node.js, Docker, Rust)

+
+
+ +
+
3
+
+

Clone the repo and run yarn setup && yarn start

+
+
+ +
+
4
+
+

Try the WASM SDK — connect to testnet, fetch an identity

+
+
+ +
+
5
+
+

Design a data contract for your app idea

+
+
+ +
+
6
+
+

Ask questions on chat.dashdevs.org

+
+
+
+
+ + +
+
+

Discord & getting help

+ + + + + +
+

Tips for getting help

+
    +
  • Include your SDK version and network (testnet/mainnet)
  • +
  • Share error messages and code snippets
  • +
  • Check if there's a GitHub issue first
  • +
  • Platform team is most active during US/EU business hours
  • +
+
+
+
+ + +
+
+

Resources & links

+ +
+
+ + + +
+
+ + + +
+
+
+
+ + +
+ +

Questions?

+

Let's remove friction and build together

+

chat.dashdevs.org • github.com/dashpay/platform

+
+ + +
+
+ + + + + From 5aef054f7f4954b6282bf2a82648e8f8c08d9308 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 3 Apr 2026 18:59:38 +0300 Subject: [PATCH 2/5] chore: remove presentation.html from test PR Co-Authored-By: Claude Opus 4.6 (1M context) --- presentation.html | 1157 --------------------------------------------- 1 file changed, 1157 deletions(-) delete mode 100644 presentation.html diff --git a/presentation.html b/presentation.html deleted file mode 100644 index 3a4b716c3d5..00000000000 --- a/presentation.html +++ /dev/null @@ -1,1157 +0,0 @@ - - - - - -Dash Platform Deep Dive - - - - - -
- -

Developer Deep Dive

-

Finding docs, navigating code, asking questions, and building your first app

-

Quantum Explorer

-
- - -
-
-

What we'll cover

-
-
-

1. Documentation

-

Where to find it, how to navigate it, where to start reading

-
-
-

2. GitHub

-

Where the code lives, how to explore the monorepo

-
-
-

3. How it works

-

GroveDB, Tenderdash, and why every query is provable

-
-
-

4. Developer Flow

-

From zero to running a local network

-
-
-

5. Example: Yappr

-

What you can build today on Dash Platform

-
-
-

6. Discord & Help

-

Where to ask questions, which channels are active

-
-
-
-
- - -
-
-

The problem we're solving

-

- Decentralized apps need to store and query data. Today's blockchains make this surprisingly hard. -

- -
-
-

Ethereum

-
    -
  • Data lives inside smart contracts — no structured queries
  • -
  • Want to list "all posts by user X"? You need an off-chain indexer (The Graph, Alchemy)
  • -
  • Merkle proofs exist but only for single key lookups, not ranges or filters
  • -
  • Light clients can't verify query results — they trust the indexer
  • -
-
-
-

Solana

-
    -
  • Fast writes, but no state proofs at all
  • -
  • Querying account data? You call an RPC node and trust the response
  • -
  • No way for a light client to verify what a node returns
  • -
  • Data schemas are ad-hoc — each program defines its own serialization
  • -
-
-
- -
-

The gap: Every major blockchain forces apps to choose between decentralization and usability. You either trust a centralized indexer for queries, or you run a full node. There's no middle ground — until now.

-
-
-
- - -
-
-

Dash Platform's answer

- -
-
-

What if the chain itself was a database?

-
    -
  • Define your data schema as a data contract (JSON Schema)
  • -
  • The network stores, indexes, and validates your data natively
  • -
  • Query with filters, ranges, sorting, pagination — directly from masternodes
  • -
  • Every response comes with a cryptographic proof
  • -
  • A phone can verify responses with full-node security
  • -
-
-
-

The key insight

-
-

By storing data in a Merkle tree (GroveDB) and having consensus sign the root hash (Tenderdash), we can prove any query result — not just "does this key exist" but "give me all documents where status = active, sorted by date, page 2 of 5."

-
- -

No indexer needed

-

- No The Graph. No Alchemy. No centralized API sitting between your app and the chain. Just a light client, a masternode, and a proof. -

-
-
-
-
- - -
-
01
-

Documentation

-

Where to find it, how to navigate it

-
- - -
-
-

Three documentation sources

- - - - - - -
-
- - -
-
-

Where to start reading

- -
-
1
-
-

New to Dash Platform?
- Start with dashplatform.readme.io → "Introduction" → "What is Dash Platform"

-
-
- -
-
2
-
-

Want to understand the architecture?
- The Platform Book → "Architecture" → "Component Pipeline"

-
-
- -
-
3
-
-

Ready to write code?
- The Platform Book → "Getting Started" (prereqs, setup, first test)

-
-
- -
-
4
-
-

Building a web/JS app?
- Platform Book → "Evo SDK" section (6 chapters + tutorials, just added)

-
-
-
-
- - -
-
-

Platform Book — key chapters

- -
-
-

Architecture & Internals

- - - - - - -
ChapterCovers
Data ContractsSchemas for your app's data
DocumentsUser data storage & querying
State TransitionsClient → chain write path
Drive / Grove OpsGroveDB Merkle tree storage
-
-
-

Evo SDK (JS/TS)

- - - - - - -
ChapterCovers
Getting StartedInstall, connect, first query
State TransitionsWrite identities, docs, tokens
Trusted ModeProofs vs. trusted queries
Wallet UtilitiesKeys, mnemonics, signing
-

+ Tutorials: Token, Car Sales, Card Game

-
-
-
-
- - -
-
02
-

GitHub

-

Where the code lives, how to explore it

-
- - -
-
-

One monorepo, everything inside

- - - -
-
-

Core (Rust)

-
    -
  • rs-dpp — Protocol rules & validation
  • -
  • rs-drive — Storage engine (GroveDB)
  • -
  • rs-drive-abci — Consensus (Tenderdash)
  • -
  • rs-sdk — Rust client SDK
  • -
  • rs-dapi — API server
  • -
-
-
-

Client SDKs

-
    -
  • wasm-sdk — JS/TS (browser + Node)
  • -
  • js-dash-sdk — Legacy JS SDK
  • -
  • swift-sdk — iOS (new in v3.1)
  • -
  • dashmate — Node management CLI
  • -
  • platform-test-suite — E2E tests
  • -
-
-
-
-
- - -
-
-

Files to know about

- -
-
1
-
-

README.md — Architecture overview, comparison table, SDK support matrix

-
-
- -
-
2
-
-

Cargo.toml — Workspace root. Shows all Rust crates and shared dependencies

-
-
- -
-
3
-
-

packages/*/Cargo.toml or package.json — Each package's deps and features

-
-
- -
-
4
-
-

NIGHTLY_STATUS.md — What's passing/failing in CI right now

-
-
- -
-
5
-
-

Data contracts: packages/*-contract/ — DPNS, DashPay, Withdrawals, etc.

-
-
-
-
- - - -
-
-

How GroveDB works

-

- GroveDB is a hierarchical authenticated data structure — a tree of Merkle trees. -

- -
-
-

Structure

-
    -
  • Top level: a tree of subtrees organized by path
  • -
  • Each subtree is a Merk tree (Merkle AVL tree)
  • -
  • Every node stores its hash and its children's hashes
  • -
  • The root hash commits to the entire state
  • -
- -

Why it matters

-
    -
  • Any key-value lookup produces a Merkle proof
  • -
  • Proofs are compact (log N hashes)
  • -
  • Supports range queries with proofs (not just key lookups)
  • -
-
-
-
        Root Hash
-       /         \
-    [contracts]   [identities]
-     /     \          |
- [dpns] [dashpay]  [id:abc...]
-   |                  |
- [domains]         [keys]
-  /    \
-alice  bob
-
-Each level is a Merk tree
-Root hash = hash of everything
-
-
-
-
- - -
-
-

How Tenderdash signs the root

-

- Tenderdash is Dash's BFT consensus engine. It produces blocks and signs the GroveDB root hash. -

- -
-
1
-
-

Propose — A masternode proposes a block containing state transitions

-
-
-
-
2
-
-

Execute — Drive applies the transitions to GroveDB, producing a new root hash

-
-
-
-
3
-
-

Vote — Masternodes verify they get the same root hash, then sign with BLS threshold signatures

-
-
-
-
4
-
-

Commit — Block is finalized instantly (no confirmations needed). The signed root hash is the chain's commitment to the entire platform state.

-
-
- -
-

Result: every block has a quorum-signed root hash that commits to every document, identity, and contract in the system.

-
-
-
- - -
-
-

Why every query is provable

- -

- GroveDB's Merkle structure + Tenderdash's signed root = cryptographic proofs for any query. -

- -
-
-

How a proof works

-
-
1
-

Client sends a query (e.g. "get alice's profile")

-
-
-
2
-

Masternode returns the data + a Merkle proof path

-
-
-
3
-

Client hashes up the proof path to reconstruct the root hash

-
-
-
4
-

Client checks: does my root hash match the quorum-signed root?

-
-
-
-

What this means

-
    -
  • A single masternode can serve your query
  • -
  • You don't need to trust it — the proof is the trust
  • -
  • Light clients get full-node-level security
  • -
  • Works for key lookups, ranges, filters, ordering
  • -
  • No other L1 blockchain does this for structured data queries
  • -
- -
-

Ethereum: Merkle proofs for key-value only
- Solana: No proofs at all
- Dash Platform: Proofs for any query, including ranges and sorts

-
-
-
-
-
- - -
-
04
-

Developer Flow

-

From zero to building

-
- - -
-
-

Prerequisites

- -
-
-

Required

-
    -
  • Node.js v20+ (v24 recommended)
  • -
  • Docker Desktop
  • -
  • Rust (latest stable)
  • -
  • Yarn v4 (via corepack)
  • -
-
-
-

For full builds

-
    -
  • protoc (Protocol Buffers compiler)
  • -
  • wasm-pack
  • -
  • wasm-bindgen-cli (match lockfile version)
  • -
  • wasm32-unknown-unknown target
  • -
-
-
- -
# Install Rust targets and tools
-rustup target add wasm32-unknown-unknown
-cargo install wasm-pack
-cargo install wasm-bindgen-cli@0.2.108  # match Cargo.lock version
- -
-

macOS note: You need LLVM installed via Homebrew (brew install llvm) and configured in your PATH. The Xcode-bundled clang lacks some flags required by RocksDB and other native dependencies. Set LLVM_CONFIG_PATH and update PATH / CC / AR to point to the Homebrew LLVM.

-
-
-
- - -
-
-

Getting started — 3 commands

- -
-
1
-
-

Clone and setup

-
git clone https://github.com/dashpay/platform
-cd platform
-yarn setup   # installs deps, builds JS + WASM packages
-
-
- -
-
2
-
-

Start local network

-
yarn start   # launches dashmate local network (3 masternodes + seed)
-
-
- -
-
3
-
-

Run tests

-
yarn test    # runs the platform test suite against your local network
-
-
-
-
- - -
-
-

Dashmate — your local network tool

- -
yarn dashmate setup local -c 3    # create 3-node local network
-yarn dashmate start                  # start all nodes
-yarn dashmate group status           # see what's running
-yarn dashmate stop                   # stop everything
-yarn dashmate reset --hard           # wipe and start fresh
- -
-

Dashmate manages Docker containers for Core nodes, Tenderdash, Drive, and DAPI. You get a full Platform network locally in minutes.

-
- -
-
-

What it runs

-
    -
  • 3 Dash Core masternodes
  • -
  • 1 seed/miner node
  • -
  • Tenderdash consensus
  • -
  • Drive (GroveDB storage)
  • -
  • DAPI (gRPC API server)
  • -
-
-
-

Useful commands

-
    -
  • dashmate config get ... — inspect config
  • -
  • dashmate group status — node status
  • -
  • dashmate status — single node
  • -
-
-
-
-
- - -
-
-

Choose your SDK

- - - - - - - - - - - - - - - - - - - - - -
SDKLanguageUse caseStatus
JS Evo SDKJS/TSWeb apps, Node.js backendsStable
Rust SDKRustBackend services, tools, CLIStable
Swift SDKSwiftiOS/macOS appsNew (v3.1)
- -
-

Recommendation: Use the JS Evo SDK for new JS/TS projects. It's built from Rust via WebAssembly and gives you the same verification guarantees as the Rust SDK.

-
-
-
- - -
-
-

WASM SDK — quick example

- -
npm install @dashevo/wasm-sdk
- -
// Connect to testnet and query an identity
-import { WasmSdkBuilder } from '@dashevo/wasm-sdk';
-
-const sdk = await new WasmSdkBuilder()
-  .withNetworkTestnet()
-  .build();
-
-// Fetch an identity by name (DPNS)
-const identity = await sdk.getIdentityByName('alice');
-console.log(identity.getId());
-
-// Query documents from a data contract
-const docs = await sdk.getDocuments(
-  contractId,
-  'profile',
-  { where: [['$ownerId', '==', identityId]], limit: 10 }
-);
-
-
-
- - -
-
-

Data contracts — the core concept

- -

- A data contract is a JSON Schema that defines your app's data structure. The network stores, indexes, and enforces it. -

- -
-
-
// Define your contract
-{
-  "note": {
-    "type": "object",
-    "properties": {
-      "message": {
-        "type": "string",
-        "maxLength": 500
-      }
-    },
-    "indices": [{
-      "name": "byOwner",
-      "properties": [
-        {"$ownerId": "asc"}
-      ]
-    }]
-  }
-}
-
-
-

What you get

-
    -
  • Schema validation on every write
  • -
  • Indexed queries with cryptographic proofs
  • -
  • Versioned updates (no data loss)
  • -
  • Per-document ownership and access control
  • -
- -

System contracts

-
    -
  • DPNS — username registration
  • -
  • DashPay — social payments / profiles
  • -
  • Withdrawals — credit → Dash
  • -
-
-
-
-
- - -
-
05
-

Example: Yappr

-

What you can build today

-
- - -
-
-

Yappr — decentralized social posting

- - - -

- A Twitter/X-like app built entirely on Dash Platform. No backend servers, no databases — just a data contract and a frontend. -

- -
-
-

How it works

-
    -
  • Register an identity on Platform
  • -
  • Get a username via DPNS
  • -
  • Deploy a data contract with "post" and "like" document types
  • -
  • Create documents (posts) that anyone can query
  • -
  • All data is provably authentic via Merkle proofs
  • -
-
-
-

What makes it special

-
    -
  • No server — queries go directly to masternodes
  • -
  • Censorship resistant — data is on-chain
  • -
  • Cryptographic proofs — every response is verified
  • -
  • Instant finality — posts appear in one block
  • -
  • Rich queries — sort, filter, paginate natively
  • -
-
-
-
-
- - -
-
-

What can you build today?

- -
-
-

Social apps

-

Profiles, posts, messaging — all with identity-based ownership

-
-
-

Naming systems

-

Human-readable names for wallets, apps, services (DPNS already exists)

-
-
-

Marketplaces

-

Listings, offers, reviews — with cryptographic proof of authenticity

-
-
-

Credential systems

-

Verifiable claims, attestations, membership proofs

-
-
-

Token platforms

-

Custom tokens with configurable minting, burning, freezing rules

-
-
-

Governance tools

-

Voting, proposals, group-controlled actions with on-chain rules

-
-
-
-
- - -
-
-

Your path from here

- -
-
1
-
-

Read the intro at dashplatform.readme.io

-
-
- -
-
2
-
-

Install prerequisites (Node.js, Docker, Rust)

-
-
- -
-
3
-
-

Clone the repo and run yarn setup && yarn start

-
-
- -
-
4
-
-

Try the WASM SDK — connect to testnet, fetch an identity

-
-
- -
-
5
-
-

Design a data contract for your app idea

-
-
- -
-
6
-
-

Ask questions on chat.dashdevs.org

-
-
-
-
- - -
-
-

Discord & getting help

- - - - - -
-

Tips for getting help

-
    -
  • Include your SDK version and network (testnet/mainnet)
  • -
  • Share error messages and code snippets
  • -
  • Check if there's a GitHub issue first
  • -
  • Platform team is most active during US/EU business hours
  • -
-
-
-
- - -
-
-

Resources & links

- -
-
- - - -
-
- - - -
-
-
-
- - -
- -

Questions?

-

Let's remove friction and build together

-

chat.dashdevs.org • github.com/dashpay/platform

-
- - -
-
- - - - - From f96e04821d458b7c3bd12d55754930da593bd69f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 3 Apr 2026 19:05:49 +0300 Subject: [PATCH 3/5] style: cargo fmt --all Co-Authored-By: Claude Opus 4.6 (1M context) --- .../token_configuration_item.rs | 11 +--- .../authorized_action_takers.rs | 5 +- .../identity/identity_public_key/key_type.rs | 15 ++---- .../identity/identity_public_key/purpose.rs | 9 +++- packages/rs-dpp/src/tokens/token_event.rs | 50 +++++-------------- .../src/validation/validation_result.rs | 18 +++---- packages/rs-platform-value/src/display.rs | 25 ++-------- .../rs-platform-value/src/string_encoding.rs | 6 +-- 8 files changed, 38 insertions(+), 101 deletions(-) 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 ff8565db732..ac42e135b11 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 @@ -360,11 +360,7 @@ mod tests { 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 - ); + assert!(indices.contains(&i), "Missing u8_item_index value: {}", i); } } @@ -493,10 +489,7 @@ mod tests { #[test] fn display_main_control_group_none() { - let s = format!( - "{}", - TokenConfigurationChangeItem::MainControlGroup(None) - ); + let s = format!("{}", TokenConfigurationChangeItem::MainControlGroup(None)); assert_eq!(s, "Main Control Group: None"); } 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 a433f35499c..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 @@ -213,10 +213,7 @@ mod tests { Identifier::from([byte; 32]) } - fn make_group( - members: Vec<(Identifier, u32)>, - required_power: u32, - ) -> Group { + fn make_group(members: Vec<(Identifier, u32)>, required_power: u32) -> Group { Group::V0(GroupV0 { members: members.into_iter().collect(), required_power, 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 5e98988a80b..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 @@ -473,10 +473,7 @@ mod tests { #[test] fn test_try_from_u8_ecdsa_secp256k1() { - assert_eq!( - KeyType::try_from(0u8).unwrap(), - KeyType::ECDSA_SECP256K1 - ); + assert_eq!(KeyType::try_from(0u8).unwrap(), KeyType::ECDSA_SECP256K1); } #[test] @@ -491,10 +488,7 @@ mod tests { #[test] fn test_try_from_u8_bip13_script_hash() { - assert_eq!( - KeyType::try_from(3u8).unwrap(), - KeyType::BIP13_SCRIPT_HASH - ); + assert_eq!(KeyType::try_from(3u8).unwrap(), KeyType::BIP13_SCRIPT_HASH); } #[test] @@ -521,10 +515,7 @@ mod tests { #[test] fn test_display_ecdsa_secp256k1() { - assert_eq!( - format!("{}", KeyType::ECDSA_SECP256K1), - "ECDSA_SECP256K1" - ); + assert_eq!(format!("{}", KeyType::ECDSA_SECP256K1), "ECDSA_SECP256K1"); } #[test] diff --git a/packages/rs-dpp/src/identity/identity_public_key/purpose.rs b/packages/rs-dpp/src/identity/identity_public_key/purpose.rs index 84a3680c539..f50418ba992 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/purpose.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/purpose.rs @@ -267,7 +267,14 @@ mod tests { let range = Purpose::full_range(); assert_eq!( range, - [AUTHENTICATION, ENCRYPTION, DECRYPTION, TRANSFER, VOTING, OWNER] + [ + AUTHENTICATION, + ENCRYPTION, + DECRYPTION, + TRANSFER, + VOTING, + OWNER + ] ); } diff --git a/packages/rs-dpp/src/tokens/token_event.rs b/packages/rs-dpp/src/tokens/token_event.rs index 973e593d3a9..014cf06ea49 100644 --- a/packages/rs-dpp/src/tokens/token_event.rs +++ b/packages/rs-dpp/src/tokens/token_event.rs @@ -567,8 +567,7 @@ mod tests { #[test] fn display_destroy_frozen_funds_with_note() { - let event = - TokenEvent::DestroyFrozenFunds(test_id(), 999, Some("destroyed".to_string())); + let event = TokenEvent::DestroyFrozenFunds(test_id(), 999, Some("destroyed".to_string())); let s = format!("{}", event); assert!(s.contains("Destroy 999 frozen from ")); assert!(s.contains("(note: destroyed)")); @@ -584,13 +583,7 @@ mod tests { #[test] fn display_transfer_with_note() { - let event = TokenEvent::Transfer( - test_id(), - Some("payment".to_string()), - None, - None, - 100, - ); + let event = TokenEvent::Transfer(test_id(), Some("payment".to_string()), None, None, 100); let s = format!("{}", event); assert!(s.contains("Transfer 100 to ")); assert!(s.contains("(note: payment)")); @@ -613,10 +606,8 @@ mod tests { #[test] fn display_emergency_action_with_note() { - let event = TokenEvent::EmergencyAction( - TokenEmergencyAction::Resume, - Some("resuming".to_string()), - ); + let event = + TokenEvent::EmergencyAction(TokenEmergencyAction::Resume, Some("resuming".to_string())); let s = format!("{}", event); assert!(s.contains("Emergency action ")); assert!(s.contains("(note: resuming)")); @@ -649,10 +640,8 @@ mod tests { #[test] fn display_change_price_disable_with_note() { - let event = TokenEvent::ChangePriceForDirectPurchase( - None, - Some("no more sales".to_string()), - ); + let event = + TokenEvent::ChangePriceForDirectPurchase(None, Some("no more sales".to_string())); let s = format!("{}", event); assert_eq!(s, "Disable direct purchase (note: no more sales)"); } @@ -788,8 +777,7 @@ mod tests { #[test] fn public_note_destroy_frozen_some() { - let event = - TokenEvent::DestroyFrozenFunds(test_id(), 10, Some("destroy note".to_string())); + let event = TokenEvent::DestroyFrozenFunds(test_id(), 10, Some("destroy note".to_string())); assert_eq!(event.public_note(), Some("destroy note")); } @@ -801,13 +789,7 @@ mod tests { #[test] fn public_note_transfer_some() { - let event = TokenEvent::Transfer( - test_id(), - Some("tx note".to_string()), - None, - None, - 100, - ); + let event = TokenEvent::Transfer(test_id(), Some("tx note".to_string()), None, None, 100); assert_eq!(event.public_note(), Some("tx note")); } @@ -833,10 +815,8 @@ mod tests { #[test] fn public_note_emergency_action_some() { - let event = TokenEvent::EmergencyAction( - TokenEmergencyAction::Pause, - Some("emergency".to_string()), - ); + let event = + TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, Some("emergency".to_string())); assert_eq!(event.public_note(), Some("emergency")); } @@ -866,10 +846,7 @@ mod tests { #[test] fn public_note_change_price_some() { - let event = TokenEvent::ChangePriceForDirectPurchase( - None, - Some("price note".to_string()), - ); + let event = TokenEvent::ChangePriceForDirectPurchase(None, Some("price note".to_string())); assert_eq!(event.public_note(), Some("price note")); } @@ -930,9 +907,6 @@ mod tests { #[test] fn format_note_some_returns_formatted() { - assert_eq!( - format_note(&Some("hello".to_string())), - " (note: hello)" - ); + assert_eq!(format_note(&Some("hello".to_string())), " (note: hello)"); } } diff --git a/packages/rs-dpp/src/validation/validation_result.rs b/packages/rs-dpp/src/validation/validation_result.rs index bc5454d650c..505e65edef4 100644 --- a/packages/rs-dpp/src/validation/validation_result.rs +++ b/packages/rs-dpp/src/validation/validation_result.rs @@ -359,10 +359,8 @@ mod tests { #[test] fn test_map_preserves_errors() { - let result: ValidationResult = ValidationResult::new_with_data_and_errors( - 5, - vec!["err".to_string()], - ); + 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()]); @@ -478,10 +476,8 @@ mod tests { #[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 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"); } @@ -547,8 +543,7 @@ mod tests { #[test] fn test_flatten_merges_data_and_errors() { - let r1: ValidationResult, String> = - ValidationResult::new_with_data(vec![1, 2]); + 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> = @@ -573,8 +568,7 @@ mod tests { 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 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])); diff --git a/packages/rs-platform-value/src/display.rs b/packages/rs-platform-value/src/display.rs index 061cc518e4d..ea45947299b 100644 --- a/packages/rs-platform-value/src/display.rs +++ b/packages/rs-platform-value/src/display.rs @@ -213,10 +213,7 @@ mod tests { #[test] fn display_text_short() { let text = "hello"; - assert_eq!( - format!("{}", Value::Text(text.to_string())), - "string hello" - ); + assert_eq!(format!("{}", Value::Text(text.to_string())), "string hello"); } #[test] @@ -360,10 +357,7 @@ mod tests { #[test] fn non_qualified_i64() { - assert_eq!( - Value::I64(-42).non_qualified_string_representation(), - "-42" - ); + assert_eq!(Value::I64(-42).non_qualified_string_representation(), "-42"); } #[test] @@ -425,18 +419,12 @@ mod tests { #[test] fn non_qualified_i128() { - assert_eq!( - Value::I128(-1).non_qualified_string_representation(), - "-1" - ); + assert_eq!(Value::I128(-1).non_qualified_string_representation(), "-1"); } #[test] fn non_qualified_u32() { - assert_eq!( - Value::U32(100).non_qualified_string_representation(), - "100" - ); + assert_eq!(Value::U32(100).non_qualified_string_representation(), "100"); } #[test] @@ -449,10 +437,7 @@ mod tests { #[test] fn non_qualified_u16() { - assert_eq!( - Value::U16(500).non_qualified_string_representation(), - "500" - ); + assert_eq!(Value::U16(500).non_qualified_string_representation(), "500"); } #[test] diff --git a/packages/rs-platform-value/src/string_encoding.rs b/packages/rs-platform-value/src/string_encoding.rs index 168cb35aeb3..b2f64adf823 100644 --- a/packages/rs-platform-value/src/string_encoding.rs +++ b/packages/rs-platform-value/src/string_encoding.rs @@ -223,11 +223,7 @@ mod tests { for encoding in &ALL_ENCODINGS { let encoded = encode(data, *encoding); let decoded = decode(&encoded, *encoding).unwrap(); - assert_eq!( - decoded, data, - "Round-trip failed for encoding {}", - encoding - ); + assert_eq!(decoded, data, "Round-trip failed for encoding {}", encoding); } } } From 640703b4f9a54bfcaf48fffc429c9c75aa796a7a Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 3 Apr 2026 19:14:32 +0300 Subject: [PATCH 4/5] test: address CodeRabbit review feedback - Add prefix assertion to display_unfreeze_with_note test - Add odd-length hex test documenting panic behavior (#should_panic) Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/rs-dpp/src/tokens/token_event.rs | 1 + packages/rs-dpp/src/util/vec.rs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/packages/rs-dpp/src/tokens/token_event.rs b/packages/rs-dpp/src/tokens/token_event.rs index 014cf06ea49..2e4769a5524 100644 --- a/packages/rs-dpp/src/tokens/token_event.rs +++ b/packages/rs-dpp/src/tokens/token_event.rs @@ -554,6 +554,7 @@ mod tests { fn display_unfreeze_with_note() { let event = TokenEvent::Unfreeze(test_id(), Some("thawed".to_string())); let s = format!("{}", event); + assert!(s.contains("Unfreeze"), "should contain 'Unfreeze'"); assert!(s.contains("(note: thawed)")); } diff --git a/packages/rs-dpp/src/util/vec.rs b/packages/rs-dpp/src/util/vec.rs index 54bd43d0088..c1b78bd11c8 100644 --- a/packages/rs-dpp/src/util/vec.rs +++ b/packages/rs-dpp/src/util/vec.rs @@ -140,6 +140,14 @@ mod tests { 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] From b6d83a08477ca747f75bd39f51104b302f247fa1 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 3 Apr 2026 19:35:22 +0300 Subject: [PATCH 5/5] refactor(test): remove boilerplate tests, add codecov exclusions instead Remove tests for enum TryFrom/Display boilerplate and trivial formatting that should be excluded from coverage rather than tested: - security_level.rs, purpose.rs, gas_fees_paid_by.rs (enum conversions) - display.rs, string_encoding.rs (Value formatting, encoding wrappers) - Display tests from token_event.rs and token_configuration_item.rs Add corresponding .codecov.yml exclusions. Keep tests with real logic: validation_result, encode.rs, fee_result, authorized_action_takers, token_event associated_document_type_name, index uniqueness. Co-Authored-By: Claude Opus 4.6 (1M context) --- .codecov.yml | 8 + .../token_configuration_item.rs | 158 --------- .../identity/identity_public_key/purpose.rs | 222 ------------ .../identity_public_key/security_level.rs | 210 ----------- .../rs-dpp/src/tokens/gas_fees_paid_by.rs | 102 ------ packages/rs-dpp/src/tokens/token_event.rs | 300 ---------------- packages/rs-platform-value/src/display.rs | 330 ------------------ .../rs-platform-value/src/string_encoding.rs | 182 ---------- 8 files changed, 8 insertions(+), 1504 deletions(-) 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 ac42e135b11..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 @@ -400,162 +400,4 @@ mod tests { let variants = all_variants(); assert_eq!(variants.len(), 32); } - - // ---- Display tests for representative variants ---- - - #[test] - fn display_no_change() { - let s = format!( - "{}", - TokenConfigurationChangeItem::TokenConfigurationNoChange - ); - assert_eq!(s, "No Change in Token Configuration"); - } - - #[test] - fn display_max_supply_some() { - let s = format!("{}", TokenConfigurationChangeItem::MaxSupply(Some(1000))); - assert_eq!(s, "Max Supply: 1000"); - } - - #[test] - fn display_max_supply_none() { - let s = format!("{}", TokenConfigurationChangeItem::MaxSupply(None)); - assert_eq!(s, "Max Supply: No Limit"); - } - - #[test] - fn display_minting_allow_choosing_destination_true() { - let s = format!( - "{}", - TokenConfigurationChangeItem::MintingAllowChoosingDestination(true) - ); - assert_eq!(s, "Minting Allow Choosing Destination: true"); - } - - #[test] - fn display_minting_allow_choosing_destination_false() { - let s = format!( - "{}", - TokenConfigurationChangeItem::MintingAllowChoosingDestination(false) - ); - assert_eq!(s, "Minting Allow Choosing Destination: false"); - } - - #[test] - fn display_freeze() { - let s = format!( - "{}", - TokenConfigurationChangeItem::Freeze(AuthorizedActionTakers::ContractOwner) - ); - assert_eq!(s, "Freeze: ContractOwner"); - } - - #[test] - fn display_manual_minting() { - let s = format!( - "{}", - TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::NoOne) - ); - assert_eq!(s, "Manual Minting: NoOne"); - } - - #[test] - fn display_manual_burning() { - let s = format!( - "{}", - TokenConfigurationChangeItem::ManualBurning(AuthorizedActionTakers::ContractOwner) - ); - assert_eq!(s, "Manual Burning: ContractOwner"); - } - - #[test] - fn display_emergency_action() { - let s = format!( - "{}", - TokenConfigurationChangeItem::EmergencyAction(AuthorizedActionTakers::NoOne) - ); - assert_eq!(s, "Emergency Action: NoOne"); - } - - #[test] - fn display_main_control_group_some() { - let s = format!( - "{}", - TokenConfigurationChangeItem::MainControlGroup(Some(42)) - ); - assert_eq!(s, "Main Control Group: 42"); - } - - #[test] - fn display_main_control_group_none() { - let s = format!("{}", TokenConfigurationChangeItem::MainControlGroup(None)); - assert_eq!(s, "Main Control Group: None"); - } - - #[test] - fn display_perpetual_distribution_none() { - let s = format!( - "{}", - TokenConfigurationChangeItem::PerpetualDistribution(None) - ); - assert_eq!(s, "Perpetual Distribution: None"); - } - - #[test] - fn display_new_tokens_destination_identity_none() { - let s = format!( - "{}", - TokenConfigurationChangeItem::NewTokensDestinationIdentity(None) - ); - assert_eq!(s, "New Tokens Destination Identity: None"); - } - - #[test] - fn display_new_tokens_destination_identity_some() { - let id = Identifier::from([3u8; 32]); - let s = format!( - "{}", - TokenConfigurationChangeItem::NewTokensDestinationIdentity(Some(id)) - ); - assert!(s.starts_with("New Tokens Destination Identity: ")); - assert!(!s.contains("None")); - } - - #[test] - fn display_unfreeze() { - let s = format!( - "{}", - TokenConfigurationChangeItem::Unfreeze(AuthorizedActionTakers::NoOne) - ); - assert_eq!(s, "Unfreeze: NoOne"); - } - - #[test] - fn display_destroy_frozen_funds() { - let s = format!( - "{}", - TokenConfigurationChangeItem::DestroyFrozenFunds(AuthorizedActionTakers::ContractOwner) - ); - assert_eq!(s, "Destroy Frozen Funds: ContractOwner"); - } - - #[test] - fn display_marketplace_trade_mode() { - let s = format!( - "{}", - TokenConfigurationChangeItem::MarketplaceTradeMode(TokenTradeMode::NotTradeable) - ); - assert_eq!(s, "Marketplace Trade Mode: NotTradeable"); - } - - // ---- display all variants don't panic ---- - - #[test] - fn display_all_variants_no_panic() { - for v in all_variants() { - let s = format!("{}", v); - assert!(!s.is_empty()); - } - } } diff --git a/packages/rs-dpp/src/identity/identity_public_key/purpose.rs b/packages/rs-dpp/src/identity/identity_public_key/purpose.rs index f50418ba992..fbd5fcb1cb5 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/purpose.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/purpose.rs @@ -129,225 +129,3 @@ impl Purpose { [ENCRYPTION, DECRYPTION] } } - -#[cfg(test)] -mod tests { - use super::*; - - // -- TryFrom valid values -- - - #[test] - fn test_try_from_u8_authentication() { - assert_eq!(Purpose::try_from(0u8).unwrap(), AUTHENTICATION); - } - - #[test] - fn test_try_from_u8_encryption() { - assert_eq!(Purpose::try_from(1u8).unwrap(), ENCRYPTION); - } - - #[test] - fn test_try_from_u8_decryption() { - assert_eq!(Purpose::try_from(2u8).unwrap(), DECRYPTION); - } - - #[test] - fn test_try_from_u8_transfer() { - assert_eq!(Purpose::try_from(3u8).unwrap(), TRANSFER); - } - - #[test] - fn test_try_from_u8_system() { - assert_eq!(Purpose::try_from(4u8).unwrap(), SYSTEM); - } - - #[test] - fn test_try_from_u8_voting() { - assert_eq!(Purpose::try_from(5u8).unwrap(), VOTING); - } - - #[test] - fn test_try_from_u8_owner() { - assert_eq!(Purpose::try_from(6u8).unwrap(), OWNER); - } - - // -- TryFrom invalid values -- - - #[test] - fn test_try_from_u8_invalid_7() { - assert!(Purpose::try_from(7u8).is_err()); - } - - #[test] - fn test_try_from_u8_invalid_255() { - assert!(Purpose::try_from(255u8).is_err()); - } - - // -- TryFrom valid values -- - - #[test] - fn test_try_from_i32_all_valid() { - assert_eq!(Purpose::try_from(0i32).unwrap(), AUTHENTICATION); - assert_eq!(Purpose::try_from(1i32).unwrap(), ENCRYPTION); - assert_eq!(Purpose::try_from(2i32).unwrap(), DECRYPTION); - assert_eq!(Purpose::try_from(3i32).unwrap(), TRANSFER); - assert_eq!(Purpose::try_from(4i32).unwrap(), SYSTEM); - assert_eq!(Purpose::try_from(5i32).unwrap(), VOTING); - assert_eq!(Purpose::try_from(6i32).unwrap(), OWNER); - } - - // -- TryFrom invalid values -- - - #[test] - fn test_try_from_i32_invalid_positive() { - assert!(Purpose::try_from(7i32).is_err()); - } - - #[test] - fn test_try_from_i32_invalid_negative() { - assert!(Purpose::try_from(-1i32).is_err()); - } - - // -- From for [u8; 1] -- - - #[test] - fn test_into_u8_array_all() { - let arr: [u8; 1] = AUTHENTICATION.into(); - assert_eq!(arr, [0]); - let arr: [u8; 1] = ENCRYPTION.into(); - assert_eq!(arr, [1]); - let arr: [u8; 1] = DECRYPTION.into(); - assert_eq!(arr, [2]); - let arr: [u8; 1] = TRANSFER.into(); - assert_eq!(arr, [3]); - let arr: [u8; 1] = SYSTEM.into(); - assert_eq!(arr, [4]); - let arr: [u8; 1] = VOTING.into(); - assert_eq!(arr, [5]); - let arr: [u8; 1] = OWNER.into(); - assert_eq!(arr, [6]); - } - - // -- From for &'static [u8; 1] -- - - #[test] - fn test_into_static_u8_array_ref_all() { - let r: &'static [u8; 1] = AUTHENTICATION.into(); - assert_eq!(r, &[0]); - let r: &'static [u8; 1] = ENCRYPTION.into(); - assert_eq!(r, &[1]); - let r: &'static [u8; 1] = DECRYPTION.into(); - assert_eq!(r, &[2]); - let r: &'static [u8; 1] = TRANSFER.into(); - assert_eq!(r, &[3]); - let r: &'static [u8; 1] = SYSTEM.into(); - assert_eq!(r, &[4]); - let r: &'static [u8; 1] = VOTING.into(); - assert_eq!(r, &[5]); - let r: &'static [u8; 1] = OWNER.into(); - assert_eq!(r, &[6]); - } - - // -- full_range() -- - - #[test] - fn test_full_range_has_six_elements() { - let range = Purpose::full_range(); - assert_eq!(range.len(), 6); - } - - #[test] - fn test_full_range_excludes_system() { - let range = Purpose::full_range(); - assert!(!range.contains(&SYSTEM)); - } - - #[test] - fn test_full_range_contains_expected() { - let range = Purpose::full_range(); - assert_eq!( - range, - [ - AUTHENTICATION, - ENCRYPTION, - DECRYPTION, - TRANSFER, - VOTING, - OWNER - ] - ); - } - - // -- searchable_purposes() -- - - #[test] - fn test_searchable_purposes() { - let purposes = Purpose::searchable_purposes(); - assert_eq!(purposes.len(), 3); - assert_eq!(purposes, [AUTHENTICATION, TRANSFER, VOTING]); - } - - // -- encryption_decryption() -- - - #[test] - fn test_encryption_decryption() { - let purposes = Purpose::encryption_decryption(); - assert_eq!(purposes.len(), 2); - assert_eq!(purposes, [ENCRYPTION, DECRYPTION]); - } - - // -- Display -- - - #[test] - fn test_display_authentication() { - assert_eq!(format!("{}", AUTHENTICATION), "AUTHENTICATION"); - } - - #[test] - fn test_display_encryption() { - assert_eq!(format!("{}", ENCRYPTION), "ENCRYPTION"); - } - - #[test] - fn test_display_decryption() { - assert_eq!(format!("{}", DECRYPTION), "DECRYPTION"); - } - - #[test] - fn test_display_transfer() { - assert_eq!(format!("{}", TRANSFER), "TRANSFER"); - } - - #[test] - fn test_display_system() { - assert_eq!(format!("{}", SYSTEM), "SYSTEM"); - } - - #[test] - fn test_display_voting() { - assert_eq!(format!("{}", VOTING), "VOTING"); - } - - #[test] - fn test_display_owner() { - assert_eq!(format!("{}", OWNER), "OWNER"); - } - - // -- Default -- - - #[test] - fn test_default_is_authentication() { - assert_eq!(Purpose::default(), AUTHENTICATION); - } - - // -- round-trip: u8 -> Purpose -> [u8; 1] -- - - #[test] - fn test_round_trip_all_valid_values() { - for val in 0u8..=6 { - let purpose = Purpose::try_from(val).unwrap(); - let arr: [u8; 1] = purpose.into(); - assert_eq!(arr[0], val); - } - } -} diff --git a/packages/rs-dpp/src/identity/identity_public_key/security_level.rs b/packages/rs-dpp/src/identity/identity_public_key/security_level.rs index 46e30e722e6..4cd9f0d2ed4 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/security_level.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/security_level.rs @@ -111,213 +111,3 @@ impl std::fmt::Display for SecurityLevel { write!(f, "{self:?}") } } - -#[cfg(test)] -mod tests { - use super::*; - - // -- TryFrom valid values -- - - #[test] - fn test_try_from_u8_master() { - assert_eq!(SecurityLevel::try_from(0u8).unwrap(), SecurityLevel::MASTER); - } - - #[test] - fn test_try_from_u8_critical() { - assert_eq!( - SecurityLevel::try_from(1u8).unwrap(), - SecurityLevel::CRITICAL - ); - } - - #[test] - fn test_try_from_u8_high() { - assert_eq!(SecurityLevel::try_from(2u8).unwrap(), SecurityLevel::HIGH); - } - - #[test] - fn test_try_from_u8_medium() { - assert_eq!(SecurityLevel::try_from(3u8).unwrap(), SecurityLevel::MEDIUM); - } - - // -- TryFrom invalid values -- - - #[test] - fn test_try_from_u8_invalid_4() { - assert!(SecurityLevel::try_from(4u8).is_err()); - } - - #[test] - fn test_try_from_u8_invalid_255() { - assert!(SecurityLevel::try_from(255u8).is_err()); - } - - // -- From for [u8; 1] -- - - #[test] - fn test_into_u8_array_master() { - let arr: [u8; 1] = SecurityLevel::MASTER.into(); - assert_eq!(arr, [0]); - } - - #[test] - fn test_into_u8_array_critical() { - let arr: [u8; 1] = SecurityLevel::CRITICAL.into(); - assert_eq!(arr, [1]); - } - - #[test] - fn test_into_u8_array_high() { - let arr: [u8; 1] = SecurityLevel::HIGH.into(); - assert_eq!(arr, [2]); - } - - #[test] - fn test_into_u8_array_medium() { - let arr: [u8; 1] = SecurityLevel::MEDIUM.into(); - assert_eq!(arr, [3]); - } - - // -- From for &'static [u8; 1] -- - - #[test] - fn test_into_static_u8_array_ref() { - let r: &'static [u8; 1] = SecurityLevel::MASTER.into(); - assert_eq!(r, &[0]); - let r: &'static [u8; 1] = SecurityLevel::CRITICAL.into(); - assert_eq!(r, &[1]); - let r: &'static [u8; 1] = SecurityLevel::HIGH.into(); - assert_eq!(r, &[2]); - let r: &'static [u8; 1] = SecurityLevel::MEDIUM.into(); - assert_eq!(r, &[3]); - } - - // -- full_range() -- - - #[test] - fn test_full_range_contains_all_variants() { - let range = SecurityLevel::full_range(); - assert_eq!(range.len(), 4); - assert_eq!(range[0], SecurityLevel::MASTER); - assert_eq!(range[1], SecurityLevel::CRITICAL); - assert_eq!(range[2], SecurityLevel::HIGH); - assert_eq!(range[3], SecurityLevel::MEDIUM); - } - - // -- stronger_security_than -- - - #[test] - fn test_master_stronger_than_critical() { - assert!(SecurityLevel::MASTER.stronger_security_than(SecurityLevel::CRITICAL)); - } - - #[test] - fn test_master_stronger_than_high() { - assert!(SecurityLevel::MASTER.stronger_security_than(SecurityLevel::HIGH)); - } - - #[test] - fn test_master_stronger_than_medium() { - assert!(SecurityLevel::MASTER.stronger_security_than(SecurityLevel::MEDIUM)); - } - - #[test] - fn test_critical_not_stronger_than_master() { - assert!(!SecurityLevel::CRITICAL.stronger_security_than(SecurityLevel::MASTER)); - } - - #[test] - fn test_same_level_not_stronger() { - assert!(!SecurityLevel::HIGH.stronger_security_than(SecurityLevel::HIGH)); - } - - #[test] - fn test_medium_not_stronger_than_high() { - assert!(!SecurityLevel::MEDIUM.stronger_security_than(SecurityLevel::HIGH)); - } - - // -- stronger_or_equal_security_than -- - - #[test] - fn test_master_stronger_or_equal_to_master() { - assert!(SecurityLevel::MASTER.stronger_or_equal_security_than(SecurityLevel::MASTER)); - } - - #[test] - fn test_master_stronger_or_equal_to_medium() { - assert!(SecurityLevel::MASTER.stronger_or_equal_security_than(SecurityLevel::MEDIUM)); - } - - #[test] - fn test_medium_not_stronger_or_equal_to_master() { - assert!(!SecurityLevel::MEDIUM.stronger_or_equal_security_than(SecurityLevel::MASTER)); - } - - #[test] - fn test_high_stronger_or_equal_to_high() { - assert!(SecurityLevel::HIGH.stronger_or_equal_security_than(SecurityLevel::HIGH)); - } - - #[test] - fn test_critical_stronger_or_equal_to_high() { - assert!(SecurityLevel::CRITICAL.stronger_or_equal_security_than(SecurityLevel::HIGH)); - } - - // -- Display -- - - #[test] - fn test_display_master() { - assert_eq!(format!("{}", SecurityLevel::MASTER), "MASTER"); - } - - #[test] - fn test_display_critical() { - assert_eq!(format!("{}", SecurityLevel::CRITICAL), "CRITICAL"); - } - - #[test] - fn test_display_high() { - assert_eq!(format!("{}", SecurityLevel::HIGH), "HIGH"); - } - - #[test] - fn test_display_medium() { - assert_eq!(format!("{}", SecurityLevel::MEDIUM), "MEDIUM"); - } - - // -- last / lowest_level / highest_level -- - - #[test] - fn test_last_is_medium() { - assert_eq!(SecurityLevel::last(), SecurityLevel::MEDIUM); - } - - #[test] - fn test_lowest_level_is_medium() { - assert_eq!(SecurityLevel::lowest_level(), SecurityLevel::MEDIUM); - } - - #[test] - fn test_highest_level_is_master() { - assert_eq!(SecurityLevel::highest_level(), SecurityLevel::MASTER); - } - - // -- Default -- - - #[test] - fn test_default_is_high() { - assert_eq!(SecurityLevel::default(), SecurityLevel::HIGH); - } - - // -- round-trip: u8 -> SecurityLevel -> [u8; 1] -- - - #[test] - fn test_round_trip_all_valid_values() { - for val in 0u8..=3 { - let level = SecurityLevel::try_from(val).unwrap(); - let arr: [u8; 1] = level.into(); - assert_eq!(arr[0], val); - } - } -} diff --git a/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs b/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs index c83fb53e074..cec7564da4f 100644 --- a/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs +++ b/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs @@ -73,105 +73,3 @@ impl TryFrom for GasFeesPaidBy { .try_into() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn from_gas_fees_paid_by_to_u8_document_owner() { - assert_eq!(u8::from(GasFeesPaidBy::DocumentOwner), 0); - } - - #[test] - fn from_gas_fees_paid_by_to_u8_contract_owner() { - assert_eq!(u8::from(GasFeesPaidBy::ContractOwner), 1); - } - - #[test] - fn from_gas_fees_paid_by_to_u8_prefer_contract_owner() { - assert_eq!(u8::from(GasFeesPaidBy::PreferContractOwner), 2); - } - - #[test] - fn try_from_u8_valid_values() { - assert_eq!( - GasFeesPaidBy::try_from(0u8).unwrap(), - GasFeesPaidBy::DocumentOwner - ); - assert_eq!( - GasFeesPaidBy::try_from(1u8).unwrap(), - GasFeesPaidBy::ContractOwner - ); - assert_eq!( - GasFeesPaidBy::try_from(2u8).unwrap(), - GasFeesPaidBy::PreferContractOwner - ); - } - - #[test] - fn try_from_u8_invalid_value_returns_error() { - let result = GasFeesPaidBy::try_from(3u8); - assert!(result.is_err()); - let result = GasFeesPaidBy::try_from(255u8); - assert!(result.is_err()); - } - - #[test] - fn try_from_u64_valid_values() { - assert_eq!( - GasFeesPaidBy::try_from(0u64).unwrap(), - GasFeesPaidBy::DocumentOwner - ); - assert_eq!( - GasFeesPaidBy::try_from(1u64).unwrap(), - GasFeesPaidBy::ContractOwner - ); - assert_eq!( - GasFeesPaidBy::try_from(2u64).unwrap(), - GasFeesPaidBy::PreferContractOwner - ); - } - - #[test] - fn try_from_u64_invalid_small_value_returns_error() { - let result = GasFeesPaidBy::try_from(3u64); - assert!(result.is_err()); - } - - #[test] - fn try_from_u64_large_value_returns_error() { - let result = GasFeesPaidBy::try_from(256u64); - assert!(result.is_err()); - let result = GasFeesPaidBy::try_from(u64::MAX); - assert!(result.is_err()); - } - - #[test] - fn display_variants() { - assert_eq!(format!("{}", GasFeesPaidBy::DocumentOwner), "DocumentOwner"); - assert_eq!(format!("{}", GasFeesPaidBy::ContractOwner), "ContractOwner"); - assert_eq!( - format!("{}", GasFeesPaidBy::PreferContractOwner), - "PreferContractOwner" - ); - } - - #[test] - fn default_is_document_owner() { - assert_eq!(GasFeesPaidBy::default(), GasFeesPaidBy::DocumentOwner); - } - - #[test] - fn u8_round_trip_all_variants() { - for variant in [ - GasFeesPaidBy::DocumentOwner, - GasFeesPaidBy::ContractOwner, - GasFeesPaidBy::PreferContractOwner, - ] { - let byte_val: u8 = variant.into(); - let recovered = GasFeesPaidBy::try_from(byte_val).unwrap(); - assert_eq!(variant, recovered); - } - } -} diff --git a/packages/rs-dpp/src/tokens/token_event.rs b/packages/rs-dpp/src/tokens/token_event.rs index 2e4769a5524..49a63b9b651 100644 --- a/packages/rs-dpp/src/tokens/token_event.rs +++ b/packages/rs-dpp/src/tokens/token_event.rs @@ -492,168 +492,6 @@ mod tests { Identifier::from([2u8; 32]) } - // ---- Display tests ---- - - #[test] - fn display_mint_without_note() { - let event = TokenEvent::Mint(1000, test_id(), None); - let s = format!("{}", event); - assert!(s.starts_with("Mint 1000 to ")); - assert!(!s.contains("(note:")); - } - - #[test] - fn display_mint_with_note() { - let event = TokenEvent::Mint(500, test_id(), Some("test note".to_string())); - let s = format!("{}", event); - assert!(s.starts_with("Mint 500 to ")); - assert!(s.contains("(note: test note)")); - } - - #[test] - fn display_burn_without_note() { - let event = TokenEvent::Burn(200, test_id(), None); - let s = format!("{}", event); - assert!(s.starts_with("Burn 200 from ")); - assert!(!s.contains("(note:")); - } - - #[test] - fn display_burn_with_note() { - let event = TokenEvent::Burn(200, test_id(), Some("burn note".to_string())); - let s = format!("{}", event); - assert!(s.contains("Burn 200 from ")); - assert!(s.contains("(note: burn note)")); - } - - #[test] - fn display_freeze_without_note() { - let event = TokenEvent::Freeze(test_id(), None); - let s = format!("{}", event); - assert!(s.starts_with("Freeze ")); - assert!(!s.contains("(note:")); - } - - #[test] - fn display_freeze_with_note() { - let event = TokenEvent::Freeze(test_id(), Some("frozen".to_string())); - let s = format!("{}", event); - assert!(s.starts_with("Freeze ")); - assert!(s.contains("(note: frozen)")); - } - - #[test] - fn display_unfreeze_without_note() { - let event = TokenEvent::Unfreeze(test_id(), None); - let s = format!("{}", event); - assert!(s.starts_with("Unfreeze ")); - assert!(!s.contains("(note:")); - } - - #[test] - fn display_unfreeze_with_note() { - let event = TokenEvent::Unfreeze(test_id(), Some("thawed".to_string())); - let s = format!("{}", event); - assert!(s.contains("Unfreeze"), "should contain 'Unfreeze'"); - assert!(s.contains("(note: thawed)")); - } - - #[test] - fn display_destroy_frozen_funds_without_note() { - let event = TokenEvent::DestroyFrozenFunds(test_id(), 999, None); - let s = format!("{}", event); - assert!(s.contains("Destroy 999 frozen from ")); - assert!(!s.contains("(note:")); - } - - #[test] - fn display_destroy_frozen_funds_with_note() { - let event = TokenEvent::DestroyFrozenFunds(test_id(), 999, Some("destroyed".to_string())); - let s = format!("{}", event); - assert!(s.contains("Destroy 999 frozen from ")); - assert!(s.contains("(note: destroyed)")); - } - - #[test] - fn display_transfer_without_note() { - let event = TokenEvent::Transfer(test_id(), None, None, None, 100); - let s = format!("{}", event); - assert!(s.contains("Transfer 100 to ")); - assert!(!s.contains("(note:")); - } - - #[test] - fn display_transfer_with_note() { - let event = TokenEvent::Transfer(test_id(), Some("payment".to_string()), None, None, 100); - let s = format!("{}", event); - assert!(s.contains("Transfer 100 to ")); - assert!(s.contains("(note: payment)")); - } - - #[test] - fn display_claim() { - let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id()); - let event = TokenEvent::Claim(recipient, 50, None); - let s = format!("{}", event); - assert!(s.starts_with("Claim 50 by ")); - } - - #[test] - fn display_emergency_action() { - let event = TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None); - let s = format!("{}", event); - assert!(s.starts_with("Emergency action ")); - } - - #[test] - fn display_emergency_action_with_note() { - let event = - TokenEvent::EmergencyAction(TokenEmergencyAction::Resume, Some("resuming".to_string())); - let s = format!("{}", event); - assert!(s.contains("Emergency action ")); - assert!(s.contains("(note: resuming)")); - } - - #[test] - fn display_config_update() { - let event = TokenEvent::ConfigUpdate( - TokenConfigurationChangeItem::TokenConfigurationNoChange, - None, - ); - let s = format!("{}", event); - assert!(s.starts_with("Configuration update ")); - } - - #[test] - fn display_change_price_with_schedule() { - let schedule = TokenPricingSchedule::SinglePrice(1000); - let event = TokenEvent::ChangePriceForDirectPurchase(Some(schedule), None); - let s = format!("{}", event); - assert!(s.starts_with("Change price schedule to ")); - } - - #[test] - fn display_change_price_disable() { - let event = TokenEvent::ChangePriceForDirectPurchase(None, None); - let s = format!("{}", event); - assert!(s.starts_with("Disable direct purchase")); - } - - #[test] - fn display_change_price_disable_with_note() { - let event = - TokenEvent::ChangePriceForDirectPurchase(None, Some("no more sales".to_string())); - let s = format!("{}", event); - assert_eq!(s, "Disable direct purchase (note: no more sales)"); - } - - #[test] - fn display_direct_purchase() { - let event = TokenEvent::DirectPurchase(50, 5000); - let s = format!("{}", event); - assert_eq!(s, "Direct purchase of 50 for 5000 credits"); - } - // ---- associated_document_type_name tests ---- #[test] @@ -726,144 +564,6 @@ mod tests { assert_eq!(event.associated_document_type_name(), "directPricing"); } - // ---- public_note tests ---- - - #[test] - fn public_note_mint_some() { - let event = TokenEvent::Mint(100, test_id(), Some("note".to_string())); - assert_eq!(event.public_note(), Some("note")); - } - - #[test] - fn public_note_mint_none() { - let event = TokenEvent::Mint(100, test_id(), None); - assert_eq!(event.public_note(), None); - } - - #[test] - fn public_note_burn_some() { - let event = TokenEvent::Burn(100, test_id(), Some("burn note".to_string())); - assert_eq!(event.public_note(), Some("burn note")); - } - - #[test] - fn public_note_burn_none() { - let event = TokenEvent::Burn(100, test_id(), None); - assert_eq!(event.public_note(), None); - } - - #[test] - fn public_note_freeze_some() { - let event = TokenEvent::Freeze(test_id(), Some("freeze note".to_string())); - assert_eq!(event.public_note(), Some("freeze note")); - } - - #[test] - fn public_note_freeze_none() { - let event = TokenEvent::Freeze(test_id(), None); - assert_eq!(event.public_note(), None); - } - - #[test] - fn public_note_unfreeze_some() { - let event = TokenEvent::Unfreeze(test_id(), Some("uf note".to_string())); - assert_eq!(event.public_note(), Some("uf note")); - } - - #[test] - fn public_note_unfreeze_none() { - let event = TokenEvent::Unfreeze(test_id(), None); - assert_eq!(event.public_note(), None); - } - - #[test] - fn public_note_destroy_frozen_some() { - let event = TokenEvent::DestroyFrozenFunds(test_id(), 10, Some("destroy note".to_string())); - assert_eq!(event.public_note(), Some("destroy note")); - } - - #[test] - fn public_note_destroy_frozen_none() { - let event = TokenEvent::DestroyFrozenFunds(test_id(), 10, None); - assert_eq!(event.public_note(), None); - } - - #[test] - fn public_note_transfer_some() { - let event = TokenEvent::Transfer(test_id(), Some("tx note".to_string()), None, None, 100); - assert_eq!(event.public_note(), Some("tx note")); - } - - #[test] - fn public_note_transfer_none() { - let event = TokenEvent::Transfer(test_id(), None, None, None, 100); - assert_eq!(event.public_note(), None); - } - - #[test] - fn public_note_claim_some() { - let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id()); - let event = TokenEvent::Claim(recipient, 10, Some("claim note".to_string())); - assert_eq!(event.public_note(), Some("claim note")); - } - - #[test] - fn public_note_claim_none() { - let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id()); - let event = TokenEvent::Claim(recipient, 10, None); - assert_eq!(event.public_note(), None); - } - - #[test] - fn public_note_emergency_action_some() { - let event = - TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, Some("emergency".to_string())); - assert_eq!(event.public_note(), Some("emergency")); - } - - #[test] - fn public_note_emergency_action_none() { - let event = TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None); - assert_eq!(event.public_note(), None); - } - - #[test] - fn public_note_config_update_some() { - let event = TokenEvent::ConfigUpdate( - TokenConfigurationChangeItem::TokenConfigurationNoChange, - Some("config note".to_string()), - ); - assert_eq!(event.public_note(), Some("config note")); - } - - #[test] - fn public_note_config_update_none() { - let event = TokenEvent::ConfigUpdate( - TokenConfigurationChangeItem::TokenConfigurationNoChange, - None, - ); - assert_eq!(event.public_note(), None); - } - - #[test] - fn public_note_change_price_some() { - let event = TokenEvent::ChangePriceForDirectPurchase(None, Some("price note".to_string())); - assert_eq!(event.public_note(), Some("price note")); - } - - #[test] - fn public_note_change_price_none() { - let event = TokenEvent::ChangePriceForDirectPurchase(None, None); - assert_eq!(event.public_note(), None); - } - - #[test] - fn public_note_direct_purchase_returns_none() { - // DirectPurchase has no note field at all - let event = TokenEvent::DirectPurchase(100, 500); - assert_eq!(event.public_note(), None); - } - // ---- all associated_document_type_name values are distinct ---- #[test] diff --git a/packages/rs-platform-value/src/display.rs b/packages/rs-platform-value/src/display.rs index ea45947299b..e7807fbdf63 100644 --- a/packages/rs-platform-value/src/display.rs +++ b/packages/rs-platform-value/src/display.rs @@ -128,333 +128,3 @@ impl Value { } } } - -#[cfg(test)] -mod tests { - use crate::Value; - use base64::Engine; - - // ---- Display (string_representation) tests ---- - - #[test] - fn display_null() { - assert_eq!(format!("{}", Value::Null), "Null"); - } - - #[test] - fn display_bool_true() { - assert_eq!(format!("{}", Value::Bool(true)), "bool true"); - } - - #[test] - fn display_bool_false() { - assert_eq!(format!("{}", Value::Bool(false)), "bool false"); - } - - #[test] - fn display_u128() { - assert_eq!( - format!("{}", Value::U128(340282366920938463463374607431768211455)), - "(u128)340282366920938463463374607431768211455" - ); - } - - #[test] - fn display_i128() { - assert_eq!(format!("{}", Value::I128(-42)), "(i128)-42"); - } - - #[test] - fn display_u64() { - assert_eq!(format!("{}", Value::U64(1000)), "(u64)1000"); - } - - #[test] - fn display_i64() { - assert_eq!(format!("{}", Value::I64(-999)), "(i64)-999"); - } - - #[test] - fn display_u32() { - assert_eq!(format!("{}", Value::U32(42)), "(u32)42"); - } - - #[test] - fn display_i32() { - assert_eq!(format!("{}", Value::I32(-1)), "(i32)-1"); - } - - #[test] - fn display_u16() { - assert_eq!(format!("{}", Value::U16(65535)), "(u16)65535"); - } - - #[test] - fn display_i16() { - assert_eq!(format!("{}", Value::I16(-32768)), "(i16)-32768"); - } - - #[test] - fn display_u8() { - assert_eq!(format!("{}", Value::U8(255)), "(u8)255"); - } - - #[test] - fn display_i8() { - assert_eq!(format!("{}", Value::I8(-128)), "(i8)-128"); - } - - #[test] - fn display_float() { - let s = format!("{}", Value::Float(3.14)); - assert!(s.starts_with("float 3.14")); - } - - #[test] - fn display_text_short() { - let text = "hello"; - assert_eq!(format!("{}", Value::Text(text.to_string())), "string hello"); - } - - #[test] - fn display_text_exactly_20_chars() { - let text = "12345678901234567890"; // exactly 20 - assert_eq!( - format!("{}", Value::Text(text.to_string())), - format!("string {}", text) - ); - } - - #[test] - fn display_text_long_truncated() { - let text = "123456789012345678901"; // 21 chars - let result = format!("{}", Value::Text(text.to_string())); - assert_eq!(result, "string 12345678901234567890[...(21)]"); - } - - #[test] - fn display_text_long_50_chars() { - let text = "a".repeat(50); - let result = format!("{}", Value::Text(text)); - assert_eq!(result, "string aaaaaaaaaaaaaaaaaaaa[...(50)]"); - } - - #[test] - fn display_bytes_empty() { - assert_eq!(format!("{}", Value::Bytes(vec![])), "bytes "); - } - - #[test] - fn display_bytes_non_empty() { - let bytes = vec![0xde, 0xad, 0xbe, 0xef]; - assert_eq!(format!("{}", Value::Bytes(bytes)), "bytes deadbeef"); - } - - #[test] - fn display_bytes20() { - let bytes = [0u8; 20]; - let result = format!("{}", Value::Bytes20(bytes)); - assert!(result.starts_with("bytes20 ")); - // base64 of 20 zero bytes - let expected_b64 = base64::prelude::BASE64_STANDARD.encode(&bytes); - assert_eq!(result, format!("bytes20 {}", expected_b64)); - } - - #[test] - fn display_bytes32() { - let bytes = [1u8; 32]; - let result = format!("{}", Value::Bytes32(bytes)); - let expected_b64 = base64::prelude::BASE64_STANDARD.encode(&bytes); - assert_eq!(result, format!("bytes32 {}", expected_b64)); - } - - #[test] - fn display_bytes36() { - let bytes = [0xffu8; 36]; - let result = format!("{}", Value::Bytes36(bytes)); - let expected_b64 = base64::prelude::BASE64_STANDARD.encode(&bytes); - assert_eq!(result, format!("bytes36 {}", expected_b64)); - } - - #[test] - fn display_identifier() { - let id = [42u8; 32]; - let result = format!("{}", Value::Identifier(id)); - let expected_b58 = bs58::encode(&id).into_string(); - assert_eq!(result, format!("identifier {}", expected_b58)); - } - - #[test] - fn display_array_empty() { - assert_eq!(format!("{}", Value::Array(vec![])), "array of []"); - } - - #[test] - fn display_array_with_elements() { - let arr = vec![Value::U8(1), Value::Bool(true)]; - let result = format!("{}", Value::Array(arr)); - assert_eq!(result, "array of [(u8)1, bool true]"); - } - - #[test] - fn display_map_empty() { - assert_eq!(format!("{}", Value::Map(vec![])), "Map { }"); - } - - #[test] - fn display_map_with_entries() { - let map = vec![(Value::Text("key".to_string()), Value::U32(99))]; - let result = format!("{}", Value::Map(map)); - assert_eq!(result, "Map { string key: (u32)99 }"); - } - - #[test] - fn display_enum_u8() { - assert_eq!(format!("{}", Value::EnumU8(vec![1, 2])), "enum u8"); - } - - #[test] - fn display_enum_string() { - assert_eq!( - format!( - "{}", - Value::EnumString(vec!["a".to_string(), "b".to_string()]) - ), - "enum string" - ); - } - - // ---- non_qualified_string_representation tests ---- - - #[test] - fn non_qualified_null() { - assert_eq!(Value::Null.non_qualified_string_representation(), "Null"); - } - - #[test] - fn non_qualified_bool_true() { - assert_eq!( - Value::Bool(true).non_qualified_string_representation(), - "true" - ); - } - - #[test] - fn non_qualified_bool_false() { - assert_eq!( - Value::Bool(false).non_qualified_string_representation(), - "false" - ); - } - - #[test] - fn non_qualified_u64() { - assert_eq!( - Value::U64(12345).non_qualified_string_representation(), - "12345" - ); - } - - #[test] - fn non_qualified_i64() { - assert_eq!(Value::I64(-42).non_qualified_string_representation(), "-42"); - } - - #[test] - fn non_qualified_float() { - let s = Value::Float(2.5).non_qualified_string_representation(); - assert_eq!(s, "2.5"); - } - - #[test] - fn non_qualified_text_returns_raw_string() { - // non_qualified does NOT prepend "string" or truncate - let text = "a long text that is more than twenty characters"; - assert_eq!( - Value::Text(text.to_string()).non_qualified_string_representation(), - text - ); - } - - #[test] - fn non_qualified_bytes() { - let bytes = vec![0xca, 0xfe]; - assert_eq!( - Value::Bytes(bytes).non_qualified_string_representation(), - "bytes cafe" - ); - } - - #[test] - fn non_qualified_identifier() { - let id = [7u8; 32]; - let expected_b58 = bs58::encode(&id).into_string(); - assert_eq!( - Value::Identifier(id).non_qualified_string_representation(), - format!("identifier {}", expected_b58) - ); - } - - #[test] - fn non_qualified_array() { - let arr = vec![Value::U8(1)]; - let result = Value::Array(arr).non_qualified_string_representation(); - assert_eq!(result, "array of [(u8)1]"); - } - - #[test] - fn non_qualified_map() { - let map = vec![(Value::Text("k".to_string()), Value::Null)]; - let result = Value::Map(map).non_qualified_string_representation(); - assert_eq!(result, "Map { string k: Null }"); - } - - #[test] - fn non_qualified_u128() { - assert_eq!( - Value::U128(999).non_qualified_string_representation(), - "999" - ); - } - - #[test] - fn non_qualified_i128() { - assert_eq!(Value::I128(-1).non_qualified_string_representation(), "-1"); - } - - #[test] - fn non_qualified_u32() { - assert_eq!(Value::U32(100).non_qualified_string_representation(), "100"); - } - - #[test] - fn non_qualified_i32() { - assert_eq!( - Value::I32(-100).non_qualified_string_representation(), - "-100" - ); - } - - #[test] - fn non_qualified_u16() { - assert_eq!(Value::U16(500).non_qualified_string_representation(), "500"); - } - - #[test] - fn non_qualified_i16() { - assert_eq!( - Value::I16(-500).non_qualified_string_representation(), - "-500" - ); - } - - #[test] - fn non_qualified_u8() { - assert_eq!(Value::U8(7).non_qualified_string_representation(), "7"); - } - - #[test] - fn non_qualified_i8() { - assert_eq!(Value::I8(-7).non_qualified_string_representation(), "-7"); - } -} diff --git a/packages/rs-platform-value/src/string_encoding.rs b/packages/rs-platform-value/src/string_encoding.rs index b2f64adf823..bce2c69fbbf 100644 --- a/packages/rs-platform-value/src/string_encoding.rs +++ b/packages/rs-platform-value/src/string_encoding.rs @@ -45,185 +45,3 @@ pub fn encode(value: &[u8], encoding: Encoding) -> String { Encoding::Hex => hex::encode(value), } } - -#[cfg(test)] -mod tests { - use super::*; - - // ---- encode/decode round-trips ---- - - #[test] - fn round_trip_base58_empty() { - let data: &[u8] = &[]; - let encoded = encode(data, Encoding::Base58); - let decoded = decode(&encoded, Encoding::Base58).unwrap(); - assert_eq!(decoded, data); - } - - #[test] - fn round_trip_base58_non_empty() { - let data = b"hello world"; - let encoded = encode(data, Encoding::Base58); - let decoded = decode(&encoded, Encoding::Base58).unwrap(); - assert_eq!(decoded, data); - } - - #[test] - fn round_trip_base58_binary() { - let data: Vec = (0u8..=255).collect(); - let encoded = encode(&data, Encoding::Base58); - let decoded = decode(&encoded, Encoding::Base58).unwrap(); - assert_eq!(decoded, data); - } - - #[test] - fn round_trip_base64_empty() { - let data: &[u8] = &[]; - let encoded = encode(data, Encoding::Base64); - let decoded = decode(&encoded, Encoding::Base64).unwrap(); - assert_eq!(decoded, data); - } - - #[test] - fn round_trip_base64_non_empty() { - let data = b"hello world"; - let encoded = encode(data, Encoding::Base64); - let decoded = decode(&encoded, Encoding::Base64).unwrap(); - assert_eq!(decoded, data); - } - - #[test] - fn round_trip_base64_binary() { - let data: Vec = (0u8..=255).collect(); - let encoded = encode(&data, Encoding::Base64); - let decoded = decode(&encoded, Encoding::Base64).unwrap(); - assert_eq!(decoded, data); - } - - #[test] - fn round_trip_hex_empty() { - let data: &[u8] = &[]; - let encoded = encode(data, Encoding::Hex); - let decoded = decode(&encoded, Encoding::Hex).unwrap(); - assert_eq!(decoded, data); - } - - #[test] - fn round_trip_hex_non_empty() { - let data = b"hello world"; - let encoded = encode(data, Encoding::Hex); - let decoded = decode(&encoded, Encoding::Hex).unwrap(); - assert_eq!(decoded, data); - } - - #[test] - fn round_trip_hex_binary() { - let data: Vec = (0u8..=255).collect(); - let encoded = encode(&data, Encoding::Hex); - let decoded = decode(&encoded, Encoding::Hex).unwrap(); - assert_eq!(decoded, data); - } - - // ---- encode produces expected output ---- - - #[test] - fn encode_hex_known_value() { - assert_eq!(encode(&[0xde, 0xad, 0xbe, 0xef], Encoding::Hex), "deadbeef"); - } - - #[test] - fn encode_base64_known_value() { - // "hello" -> "aGVsbG8=" - assert_eq!(encode(b"hello", Encoding::Base64), "aGVsbG8="); - } - - #[test] - fn encode_base58_known_value() { - // bs58 of "Hello" is "9Ajdvzr" - let encoded = encode(b"Hello", Encoding::Base58); - assert_eq!(encoded, bs58::encode(b"Hello").into_string()); - } - - // ---- Display for Encoding ---- - - #[test] - fn display_encoding_base58() { - assert_eq!(format!("{}", Encoding::Base58), "Base58"); - } - - #[test] - fn display_encoding_base64() { - assert_eq!(format!("{}", Encoding::Base64), "Base64"); - } - - #[test] - fn display_encoding_hex() { - assert_eq!(format!("{}", Encoding::Hex), "Hex"); - } - - // ---- ALL_ENCODINGS covers all variants ---- - - #[test] - fn all_encodings_has_three_entries() { - assert_eq!(ALL_ENCODINGS.len(), 3); - } - - // ---- error cases for invalid encoded strings ---- - - #[test] - fn decode_invalid_hex_returns_error() { - // 'zz' is not valid hex - let result = decode("zzzz", Encoding::Hex); - assert!(result.is_err()); - match result.unwrap_err() { - Error::StringDecodingError(msg) => { - assert!(!msg.is_empty()); - } - other => panic!("Expected StringDecodingError, got {:?}", other), - } - } - - #[test] - fn decode_invalid_hex_odd_length_returns_error() { - let result = decode("abc", Encoding::Hex); - assert!(result.is_err()); - } - - #[test] - fn decode_invalid_base64_returns_error() { - // '!!!' is not valid base64 - let result = decode("!!!", Encoding::Base64); - assert!(result.is_err()); - match result.unwrap_err() { - Error::StringDecodingError(msg) => { - assert!(!msg.is_empty()); - } - other => panic!("Expected StringDecodingError, got {:?}", other), - } - } - - #[test] - fn decode_invalid_base58_returns_error() { - // '0OIl' contains characters invalid in base58 - let result = decode("0OIl", Encoding::Base58); - assert!(result.is_err()); - match result.unwrap_err() { - Error::StringDecodingError(msg) => { - assert!(!msg.is_empty()); - } - other => panic!("Expected StringDecodingError, got {:?}", other), - } - } - - // ---- round-trip through all encodings systematically ---- - - #[test] - fn round_trip_all_encodings_same_data() { - let data = b"The quick brown fox jumps over the lazy dog"; - for encoding in &ALL_ENCODINGS { - let encoded = encode(data, *encoding); - let decoded = decode(&encoded, *encoding).unwrap(); - assert_eq!(decoded, data, "Round-trip failed for encoding {}", encoding); - } - } -}