From 2ab601e81eb434f35ee17bc3211fc15dc4114727 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 8 Apr 2026 10:52:10 +0300 Subject: [PATCH] test(dpp): improve coverage for document module with 76 new tests Add comprehensive unit tests covering real logic in document serialization, conversion, validation, and comparison across 7 files that previously had zero or minimal test coverage. - extended_document/v0: construction, property access, JSON/value conversion, serialization round-trip, validation, document_type lookup, set/get paths - v0/cbor_conversion: CBOR round-trip, from_map field extraction, DocumentForCbor conversion, version prefix, error handling for invalid CBOR - v0/json_conversion: to_json/from_json_value round-trip, all timestamp fields, creator_id parsing, to_json_with_identifiers_using_bytes - platform_value_conversion: Document <-> Value round-trip, to_map_value, to_object, into_map_value, from_platform_value with minimal data - get_raw_for_document_type: system field extraction ($id, $ownerId, $creatorId), timestamp encoding, block height encoding, owner_id override, user properties - is_equal_ignoring_timestamps: equal ignoring time fields, different properties, different ids/owners/revisions, also_ignore_fields filtering - document/mod.rs: Display impl edge cases, increment_revision, version dispatch for is_equal_ignoring_time_based_fields, get_raw_for_contract, hash Co-Authored-By: Claude Opus 4.6 (1M context) --- .../get_raw_for_document_type/v0/mod.rs | 339 ++++++++++++ .../is_equal_ignoring_timestamps/v0/mod.rs | 179 ++++++ .../src/document/extended_document/v0/mod.rs | 520 ++++++++++++++++++ packages/rs-dpp/src/document/mod.rs | 272 +++++++++ .../platform_value_conversion/mod.rs | 189 +++++++ .../rs-dpp/src/document/v0/cbor_conversion.rs | 338 ++++++++++++ .../rs-dpp/src/document/v0/json_conversion.rs | 334 +++++++++++ 7 files changed, 2171 insertions(+) diff --git a/packages/rs-dpp/src/document/document_methods/get_raw_for_document_type/v0/mod.rs b/packages/rs-dpp/src/document/document_methods/get_raw_for_document_type/v0/mod.rs index 1a2918d368d..f09744a2af6 100644 --- a/packages/rs-dpp/src/document/document_methods/get_raw_for_document_type/v0/mod.rs +++ b/packages/rs-dpp/src/document/document_methods/get_raw_for_document_type/v0/mod.rs @@ -82,3 +82,342 @@ pub trait DocumentGetRawForDocumentTypeV0: DocumentV0Getters { .transpose() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::accessors::v0::DataContractV0Getters; + use crate::data_contract::document_type::random_document::CreateRandomDocument; + use crate::document::DocumentV0; + use crate::tests::json_document::json_document_to_contract; + use platform_value::Identifier; + use platform_version::version::PlatformVersion; + use std::collections::BTreeMap; + + fn make_document_with_known_ids() -> DocumentV0 { + DocumentV0 { + id: Identifier::new([0xAA; 32]), + owner_id: Identifier::new([0xBB; 32]), + properties: BTreeMap::new(), + revision: None, + created_at: Some(1_700_000_000_000), + updated_at: Some(1_700_000_100_000), + transferred_at: Some(1_700_000_200_000), + created_at_block_height: Some(100), + updated_at_block_height: Some(200), + transferred_at_block_height: Some(300), + created_at_core_block_height: Some(50), + updated_at_core_block_height: Some(60), + transferred_at_core_block_height: Some(70), + creator_id: Some(Identifier::new([0xCC; 32])), + } + } + + // ================================================================ + // System field extraction: $id, $ownerId, $creatorId + // ================================================================ + + #[test] + fn get_raw_returns_id_for_dollar_id() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let doc = make_document_with_known_ids(); + let raw = doc + .get_raw_for_document_type_v0("$id", document_type, None, platform_version) + .expect("should succeed"); + assert_eq!( + raw, + Some(doc.id.to_vec()), + "$id should return the document id bytes" + ); + } + + #[test] + fn get_raw_returns_owner_id_for_dollar_owner_id() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let doc = make_document_with_known_ids(); + let raw = doc + .get_raw_for_document_type_v0("$ownerId", document_type, None, platform_version) + .expect("should succeed"); + assert_eq!(raw, Some(doc.owner_id.to_vec())); + } + + #[test] + fn get_raw_returns_override_owner_id_when_provided() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let doc = make_document_with_known_ids(); + let override_owner = [0xFF; 32]; + let raw = doc + .get_raw_for_document_type_v0( + "$ownerId", + document_type, + Some(override_owner), + platform_version, + ) + .expect("should succeed"); + assert_eq!( + raw, + Some(Vec::from(override_owner)), + "explicit owner_id should override the document's owner_id" + ); + } + + #[test] + fn get_raw_returns_creator_id() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let doc = make_document_with_known_ids(); + let raw = doc + .get_raw_for_document_type_v0("$creatorId", document_type, None, platform_version) + .expect("should succeed"); + assert_eq!(raw, Some(Identifier::new([0xCC; 32]).to_vec())); + } + + // ================================================================ + // Timestamp fields + // ================================================================ + + #[test] + fn get_raw_returns_encoded_created_at() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let doc = make_document_with_known_ids(); + let raw = doc + .get_raw_for_document_type_v0("$createdAt", document_type, None, platform_version) + .expect("should succeed"); + assert!(raw.is_some(), "$createdAt should produce bytes"); + let expected = DocumentPropertyType::encode_date_timestamp(1_700_000_000_000); + assert_eq!(raw.unwrap(), expected); + } + + #[test] + fn get_raw_returns_encoded_updated_at() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let doc = make_document_with_known_ids(); + let raw = doc + .get_raw_for_document_type_v0("$updatedAt", document_type, None, platform_version) + .expect("should succeed"); + assert!(raw.is_some()); + let expected = DocumentPropertyType::encode_date_timestamp(1_700_000_100_000); + assert_eq!(raw.unwrap(), expected); + } + + #[test] + fn get_raw_returns_encoded_block_heights() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let doc = make_document_with_known_ids(); + + // $createdAtBlockHeight -> encode_u64(100) + let raw = doc + .get_raw_for_document_type_v0( + "$createdAtBlockHeight", + document_type, + None, + platform_version, + ) + .expect("should succeed"); + assert_eq!(raw, Some(DocumentPropertyType::encode_u64(100))); + + // $updatedAtBlockHeight -> encode_u64(200) + let raw = doc + .get_raw_for_document_type_v0( + "$updatedAtBlockHeight", + document_type, + None, + platform_version, + ) + .expect("should succeed"); + assert_eq!(raw, Some(DocumentPropertyType::encode_u64(200))); + + // $createdAtCoreBlockHeight -> encode_u32(50) + let raw = doc + .get_raw_for_document_type_v0( + "$createdAtCoreBlockHeight", + document_type, + None, + platform_version, + ) + .expect("should succeed"); + assert_eq!(raw, Some(DocumentPropertyType::encode_u32(50))); + + // $updatedAtCoreBlockHeight -> encode_u32(60) + let raw = doc + .get_raw_for_document_type_v0( + "$updatedAtCoreBlockHeight", + document_type, + None, + platform_version, + ) + .expect("should succeed"); + assert_eq!(raw, Some(DocumentPropertyType::encode_u32(60))); + } + + #[test] + fn get_raw_returns_encoded_transferred_fields() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let doc = make_document_with_known_ids(); + + let raw = doc + .get_raw_for_document_type_v0("$transferredAt", document_type, None, platform_version) + .expect("should succeed"); + assert_eq!( + raw, + Some(DocumentPropertyType::encode_date_timestamp( + 1_700_000_200_000 + )) + ); + + let raw = doc + .get_raw_for_document_type_v0( + "$transferredAtBlockHeight", + document_type, + None, + platform_version, + ) + .expect("should succeed"); + assert_eq!(raw, Some(DocumentPropertyType::encode_u64(300))); + + let raw = doc + .get_raw_for_document_type_v0( + "$transferredAtCoreBlockHeight", + document_type, + None, + platform_version, + ) + .expect("should succeed"); + assert_eq!(raw, Some(DocumentPropertyType::encode_u32(70))); + } + + // ================================================================ + // Non-existent property returns None + // ================================================================ + + #[test] + fn get_raw_returns_none_for_missing_property() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let doc = make_document_with_known_ids(); + let raw = doc + .get_raw_for_document_type_v0("nonExistentField", document_type, None, platform_version) + .expect("should succeed"); + assert_eq!(raw, None); + } + + // ================================================================ + // User-defined property serialization + // ================================================================ + + #[test] + fn get_raw_serializes_user_defined_property() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let document = document_type + .random_document(Some(42), platform_version) + .expect("expected random document"); + + let doc_v0 = match &document { + crate::document::Document::V0(d) => d, + }; + + // "displayName" is a required string property in dashpay profile + let raw = doc_v0 + .get_raw_for_document_type_v0("displayName", document_type, None, platform_version) + .expect("should succeed"); + assert!(raw.is_some(), "displayName should produce serialized bytes"); + } +} diff --git a/packages/rs-dpp/src/document/document_methods/is_equal_ignoring_timestamps/v0/mod.rs b/packages/rs-dpp/src/document/document_methods/is_equal_ignoring_timestamps/v0/mod.rs index 2bb4066ae59..14ea66b1ee3 100644 --- a/packages/rs-dpp/src/document/document_methods/is_equal_ignoring_timestamps/v0/mod.rs +++ b/packages/rs-dpp/src/document/document_methods/is_equal_ignoring_timestamps/v0/mod.rs @@ -42,3 +42,182 @@ pub trait DocumentIsEqualIgnoringTimestampsV0: && self.revision() == rhs.revision() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::document::DocumentV0; + use platform_value::Identifier; + use std::collections::BTreeMap; + + fn make_base_document() -> DocumentV0 { + let mut properties = BTreeMap::new(); + properties.insert("name".to_string(), Value::Text("Alice".to_string())); + properties.insert("score".to_string(), Value::U64(100)); + + DocumentV0 { + id: Identifier::new([1u8; 32]), + owner_id: Identifier::new([2u8; 32]), + properties, + revision: Some(1), + created_at: Some(1_000_000), + updated_at: Some(2_000_000), + transferred_at: Some(3_000_000), + created_at_block_height: Some(10), + updated_at_block_height: Some(20), + transferred_at_block_height: Some(30), + created_at_core_block_height: Some(5), + updated_at_core_block_height: Some(6), + transferred_at_core_block_height: Some(7), + creator_id: None, + } + } + + // ================================================================ + // Documents with same data but different timestamps are equal + // ================================================================ + + #[test] + fn equal_documents_with_different_timestamps_returns_true() { + let doc1 = make_base_document(); + let mut doc2 = make_base_document(); + + // Change all time-based fields + doc2.created_at = Some(9_999_999); + doc2.updated_at = Some(8_888_888); + doc2.transferred_at = Some(7_777_777); + doc2.created_at_block_height = Some(999); + doc2.updated_at_block_height = Some(888); + doc2.transferred_at_block_height = Some(777); + doc2.created_at_core_block_height = Some(99); + doc2.updated_at_core_block_height = Some(88); + doc2.transferred_at_core_block_height = Some(77); + + assert!( + doc1.is_equal_ignoring_time_based_fields_v0(&doc2, None), + "documents with same id/owner/revision/properties but different timestamps should be equal" + ); + } + + // ================================================================ + // Documents with different properties are not equal + // ================================================================ + + #[test] + fn documents_with_different_properties_returns_false() { + let doc1 = make_base_document(); + let mut doc2 = make_base_document(); + + doc2.properties + .insert("name".to_string(), Value::Text("Bob".to_string())); + + assert!( + !doc1.is_equal_ignoring_time_based_fields_v0(&doc2, None), + "documents with different properties should not be equal" + ); + } + + // ================================================================ + // Documents with different IDs are not equal + // ================================================================ + + #[test] + fn documents_with_different_ids_returns_false() { + let doc1 = make_base_document(); + let mut doc2 = make_base_document(); + doc2.id = Identifier::new([99u8; 32]); + + assert!( + !doc1.is_equal_ignoring_time_based_fields_v0(&doc2, None), + "documents with different IDs should not be equal" + ); + } + + // ================================================================ + // Documents with different owner IDs are not equal + // ================================================================ + + #[test] + fn documents_with_different_owner_ids_returns_false() { + let doc1 = make_base_document(); + let mut doc2 = make_base_document(); + doc2.owner_id = Identifier::new([99u8; 32]); + + assert!( + !doc1.is_equal_ignoring_time_based_fields_v0(&doc2, None), + "documents with different owner IDs should not be equal" + ); + } + + // ================================================================ + // Documents with different revisions are not equal + // ================================================================ + + #[test] + fn documents_with_different_revisions_returns_false() { + let doc1 = make_base_document(); + let mut doc2 = make_base_document(); + doc2.revision = Some(99); + + assert!( + !doc1.is_equal_ignoring_time_based_fields_v0(&doc2, None), + "documents with different revisions should not be equal" + ); + } + + // ================================================================ + // also_ignore_fields filters additional properties + // ================================================================ + + #[test] + fn also_ignore_fields_excludes_specified_properties() { + let doc1 = make_base_document(); + let mut doc2 = make_base_document(); + // Change a property that we will explicitly ignore + doc2.properties.insert("score".to_string(), Value::U64(999)); + + // Without ignoring, they should differ + assert!( + !doc1.is_equal_ignoring_time_based_fields_v0(&doc2, None), + "should differ when score is changed" + ); + + // With "score" ignored, they should be equal + assert!( + doc1.is_equal_ignoring_time_based_fields_v0(&doc2, Some(vec!["score"])), + "should be equal when score is in the ignore list" + ); + } + + #[test] + fn also_ignore_fields_with_multiple_fields() { + let doc1 = make_base_document(); + let mut doc2 = make_base_document(); + doc2.properties + .insert("name".to_string(), Value::Text("Bob".to_string())); + doc2.properties.insert("score".to_string(), Value::U64(999)); + + assert!( + doc1.is_equal_ignoring_time_based_fields_v0(&doc2, Some(vec!["name", "score"])), + "should be equal when all differing fields are ignored" + ); + } + + // ================================================================ + // Empty properties case + // ================================================================ + + #[test] + fn documents_with_empty_properties_are_equal() { + let mut doc1 = make_base_document(); + let mut doc2 = make_base_document(); + doc1.properties.clear(); + doc2.properties.clear(); + doc2.created_at = Some(9_999_999); + + assert!( + doc1.is_equal_ignoring_time_based_fields_v0(&doc2, None), + "empty-property documents with same ids should be equal ignoring timestamps" + ); + } +} diff --git a/packages/rs-dpp/src/document/extended_document/v0/mod.rs b/packages/rs-dpp/src/document/extended_document/v0/mod.rs index 0951c3b77a0..e527a7c471e 100644 --- a/packages/rs-dpp/src/document/extended_document/v0/mod.rs +++ b/packages/rs-dpp/src/document/extended_document/v0/mod.rs @@ -540,3 +540,523 @@ impl ExtendedDocumentV0 { ) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::accessors::v0::DataContractV0Getters; + use crate::data_contract::document_type::random_document::CreateRandomDocument; + use crate::document::serialization_traits::ExtendedDocumentPlatformConversionMethodsV0; + use crate::document::DocumentV0Getters; + use crate::tests::json_document::json_document_to_contract; + use platform_version::version::PlatformVersion; + + fn load_dashpay_contract(platform_version: &PlatformVersion) -> crate::prelude::DataContract { + json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected to load dashpay contract") + } + + fn make_extended_document( + platform_version: &PlatformVersion, + ) -> (ExtendedDocumentV0, crate::prelude::DataContract) { + let contract = load_dashpay_contract(platform_version); + let document_type = contract + .document_type_for_name("profile") + .expect("expected profile document type"); + let document = document_type + .random_document(Some(42), platform_version) + .expect("expected random document"); + let ext_doc = ExtendedDocumentV0::from_document_with_additional_info( + document, + contract.clone(), + "profile".to_string(), + None, + ); + (ext_doc, contract) + } + + // ================================================================ + // Construction: from_document_with_additional_info + // ================================================================ + + #[test] + fn from_document_with_additional_info_sets_fields_correctly() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, contract) = make_extended_document(platform_version); + + assert_eq!(ext_doc.document_type_name, "profile"); + assert_eq!(ext_doc.data_contract_id, contract.id()); + assert!(ext_doc.metadata.is_none()); + assert_eq!(ext_doc.entropy, Bytes32::default()); + assert!(ext_doc.token_payment_info.is_none()); + } + + // ================================================================ + // Property access methods + // ================================================================ + + #[test] + fn get_optional_value_returns_existing_property() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + // Random dashpay profile documents have "displayName" + let display_name = ext_doc.get_optional_value("displayName"); + assert!( + display_name.is_some(), + "displayName should exist in random profile document" + ); + } + + #[test] + fn get_optional_value_returns_none_for_missing_key() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + let missing = ext_doc.get_optional_value("nonExistentField"); + assert!(missing.is_none()); + } + + #[test] + fn properties_returns_the_document_properties() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + let props = ext_doc.properties(); + assert!( + !props.is_empty(), + "random profile document should have properties" + ); + } + + #[test] + fn properties_as_mut_allows_modification() { + let platform_version = PlatformVersion::latest(); + let (mut ext_doc, _) = make_extended_document(platform_version); + + ext_doc + .properties_as_mut() + .insert("newField".to_string(), Value::Text("newValue".to_string())); + assert_eq!( + ext_doc.get_optional_value("newField"), + Some(&Value::Text("newValue".to_string())) + ); + } + + // ================================================================ + // ID delegation methods + // ================================================================ + + #[test] + fn id_and_owner_id_delegate_to_inner_document() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + assert_eq!(ext_doc.id(), ext_doc.document.id()); + assert_eq!(ext_doc.owner_id(), ext_doc.document.owner_id()); + } + + // ================================================================ + // document_type lookup + // ================================================================ + + #[test] + fn document_type_returns_correct_type_ref() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + let doc_type = ext_doc + .document_type() + .expect("document_type should succeed for valid profile"); + assert_eq!(doc_type.name(), "profile"); + } + + #[test] + fn document_type_fails_for_invalid_type_name() { + let platform_version = PlatformVersion::latest(); + let contract = load_dashpay_contract(platform_version); + let document_type = contract + .document_type_for_name("profile") + .expect("expected profile type"); + let document = document_type + .random_document(Some(1), platform_version) + .expect("random document"); + + let ext_doc = ExtendedDocumentV0 { + document_type_name: "nonExistentType".to_string(), + data_contract_id: contract.id(), + document, + data_contract: contract, + metadata: None, + entropy: Default::default(), + token_payment_info: None, + }; + + let result = ext_doc.document_type(); + assert!( + result.is_err(), + "document_type should fail for unknown type name" + ); + } + + // ================================================================ + // can_be_modified and requires_revision + // ================================================================ + + #[test] + fn can_be_modified_returns_value_from_document_type() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + // The profile document type in dashpay is mutable + let can_modify = ext_doc + .can_be_modified() + .expect("can_be_modified should succeed"); + assert!(can_modify, "dashpay profile should be mutable"); + } + + #[test] + fn requires_revision_returns_value_from_document_type() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + // Mutable documents require revision + let requires_rev = ext_doc + .requires_revision() + .expect("requires_revision should succeed"); + assert!( + requires_rev, + "mutable dashpay profile should require revision" + ); + } + + // ================================================================ + // Timestamp delegation methods + // ================================================================ + + #[test] + fn timestamp_methods_delegate_to_inner_document() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + assert_eq!(ext_doc.created_at(), ext_doc.document.created_at()); + assert_eq!(ext_doc.updated_at(), ext_doc.document.updated_at()); + assert_eq!(ext_doc.revision(), ext_doc.document.revision()); + assert_eq!( + ext_doc.created_at_block_height(), + ext_doc.document.created_at_block_height() + ); + assert_eq!( + ext_doc.updated_at_block_height(), + ext_doc.document.updated_at_block_height() + ); + assert_eq!( + ext_doc.created_at_core_block_height(), + ext_doc.document.created_at_core_block_height() + ); + assert_eq!( + ext_doc.updated_at_core_block_height(), + ext_doc.document.updated_at_core_block_height() + ); + } + + // ================================================================ + // set and get for path-based property access + // ================================================================ + + #[test] + fn set_and_get_inserts_and_retrieves_value() { + let platform_version = PlatformVersion::latest(); + let (mut ext_doc, _) = make_extended_document(platform_version); + + ext_doc + .set("customPath", Value::U64(999)) + .expect("set should succeed"); + let val = ext_doc.get("customPath"); + assert_eq!(val, Some(&Value::U64(999))); + } + + #[test] + fn get_returns_none_for_nonexistent_path() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + assert!(ext_doc.get("no.such.path").is_none()); + } + + // ================================================================ + // to_map_value and into_map_value + // ================================================================ + + #[test] + fn to_map_value_contains_type_and_contract_id() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, contract) = make_extended_document(platform_version); + + let map = ext_doc.to_map_value().expect("to_map_value should succeed"); + assert_eq!( + map.get(property_names::DOCUMENT_TYPE_NAME), + Some(&Value::Text("profile".to_string())) + ); + assert_eq!( + map.get(property_names::DATA_CONTRACT_ID), + Some(&Value::Identifier(contract.id().to_buffer())) + ); + } + + #[test] + fn into_map_value_contains_feature_version() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + let map = ext_doc + .into_map_value() + .expect("into_map_value should succeed"); + assert_eq!( + map.get(property_names::FEATURE_VERSION), + Some(&Value::U16(0)) + ); + } + + // ================================================================ + // to_value and into_value + // ================================================================ + + #[test] + fn to_value_produces_a_map_value() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + let val = ext_doc.to_value().expect("to_value should succeed"); + assert!(val.is_map(), "to_value should produce a map Value"); + } + + #[test] + fn into_value_produces_a_map_value() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + let val = ext_doc.into_value().expect("into_value should succeed"); + assert!(val.is_map(), "into_value should produce a map Value"); + } + + // ================================================================ + // properties_as_json_data + // ================================================================ + + #[test] + fn properties_as_json_data_returns_json_with_properties() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + let json_data = ext_doc + .properties_as_json_data() + .expect("properties_as_json_data should succeed"); + assert!( + json_data.is_object(), + "properties_as_json_data should return a JSON object" + ); + } + + // ================================================================ + // to_json_object_for_validation + // ================================================================ + + #[test] + fn to_json_object_for_validation_returns_json_object() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + let json_obj = ext_doc + .to_json_object_for_validation() + .expect("to_json_object_for_validation should succeed"); + assert!( + json_obj.is_object(), + "should return a JSON object for validation" + ); + } + + // ================================================================ + // to_pretty_json + // ================================================================ + + #[test] + fn to_pretty_json_includes_type_and_contract_id() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, contract) = make_extended_document(platform_version); + + let pretty = ext_doc + .to_pretty_json(platform_version) + .expect("to_pretty_json should succeed"); + let obj = pretty.as_object().expect("should be a JSON object"); + assert!( + obj.contains_key(property_names::DOCUMENT_TYPE_NAME), + "pretty JSON should contain $type" + ); + assert!( + obj.contains_key(property_names::DATA_CONTRACT_ID), + "pretty JSON should contain $dataContractId" + ); + // Verify the contract id is base58-encoded + let contract_id_str = obj[property_names::DATA_CONTRACT_ID] + .as_str() + .expect("$dataContractId should be a string"); + let expected_b58 = bs58::encode(contract.id().to_buffer()).into_string(); + assert_eq!(contract_id_str, expected_b58); + } + + // ================================================================ + // hash + // ================================================================ + + #[test] + fn hash_produces_consistent_output() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + let hash1 = ext_doc.hash(platform_version).expect("hash should succeed"); + let hash2 = ext_doc.hash(platform_version).expect("hash should succeed"); + assert_eq!(hash1, hash2, "hash should be deterministic"); + assert!(!hash1.is_empty(), "hash should not be empty"); + } + + // ================================================================ + // from_json_string + // ================================================================ + + #[test] + fn from_json_string_parses_valid_json() { + let platform_version = PlatformVersion::latest(); + let contract = load_dashpay_contract(platform_version); + let contract_id = contract.id(); + + // Build a minimal valid JSON string for a "profile" document + let json_str = format!( + r#"{{ + "$type": "profile", + "$dataContractId": "{}", + "$id": "{}", + "$ownerId": "{}", + "$revision": 1, + "displayName": "TestUser", + "publicMessage": "Hello", + "avatarUrl": "https://example.com/avatar.png" + }}"#, + bs58::encode(contract_id.to_buffer()).into_string(), + bs58::encode([1u8; 32]).into_string(), + bs58::encode([2u8; 32]).into_string(), + ); + + let ext_doc = ExtendedDocumentV0::from_json_string(&json_str, contract, platform_version) + .expect("from_json_string should succeed"); + + assert_eq!(ext_doc.document_type_name, "profile"); + assert!(ext_doc.get_optional_value("displayName").is_some()); + } + + #[test] + fn from_json_string_rejects_invalid_json() { + let platform_version = PlatformVersion::latest(); + let contract = load_dashpay_contract(platform_version); + + let result = + ExtendedDocumentV0::from_json_string("not valid json {{{", contract, platform_version); + assert!( + result.is_err(), + "from_json_string should fail on invalid JSON" + ); + } + + // ================================================================ + // Serialization round-trip + // ================================================================ + + #[test] + fn extended_document_serialize_deserialize_round_trip() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + let serialized = ext_doc + .serialize_to_bytes(platform_version) + .expect("serialize_to_bytes should succeed"); + + let recovered = ExtendedDocumentV0::from_bytes(&serialized, platform_version) + .expect("from_bytes should succeed"); + + assert_eq!(ext_doc.document_type_name, recovered.document_type_name); + assert_eq!(ext_doc.data_contract_id, recovered.data_contract_id); + assert_eq!(ext_doc.document.id(), recovered.document.id()); + assert_eq!(ext_doc.document.owner_id(), recovered.document.owner_id()); + } + + // ================================================================ + // validate + // ================================================================ + + #[cfg(feature = "validation")] + #[test] + fn validate_returns_result_without_error() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, _) = make_extended_document(platform_version); + + // validate() should not return a ProtocolError (it may return + // validation errors for random data, but should not panic) + let result = ext_doc.validate(platform_version); + assert!(result.is_ok(), "validate should not return a ProtocolError"); + } + + // ================================================================ + // from_trusted_platform_value + // ================================================================ + + #[test] + fn from_trusted_platform_value_round_trip() { + let platform_version = PlatformVersion::latest(); + let (ext_doc, contract) = make_extended_document(platform_version); + + let map_val = ext_doc.to_map_value().expect("to_map_value should succeed"); + let platform_val: Value = map_val.into(); + + let recovered = ExtendedDocumentV0::from_trusted_platform_value( + platform_val, + contract, + platform_version, + ) + .expect("from_trusted_platform_value should succeed"); + + assert_eq!(recovered.document_type_name, "profile"); + assert_eq!(recovered.document.id(), ext_doc.document.id()); + } + + // ================================================================ + // from_raw_json_document + // ================================================================ + + #[test] + fn from_raw_json_document_parses_json_value() { + let platform_version = PlatformVersion::latest(); + let contract = load_dashpay_contract(platform_version); + let contract_id = contract.id(); + + let json_val: JsonValue = serde_json::json!({ + "$type": "profile", + "$dataContractId": bs58::encode(contract_id.to_buffer()).into_string(), + "$id": bs58::encode([1u8; 32]).into_string(), + "$ownerId": bs58::encode([2u8; 32]).into_string(), + "$revision": 1, + "displayName": "Bob", + "publicMessage": "Hi", + "avatarUrl": "https://example.com/bob.png" + }); + + let ext_doc = + ExtendedDocumentV0::from_raw_json_document(json_val, contract, platform_version) + .expect("from_raw_json_document should succeed"); + + assert_eq!(ext_doc.document_type_name, "profile"); + } +} diff --git a/packages/rs-dpp/src/document/mod.rs b/packages/rs-dpp/src/document/mod.rs index df07589695d..4a1309555c2 100644 --- a/packages/rs-dpp/src/document/mod.rs +++ b/packages/rs-dpp/src/document/mod.rs @@ -332,4 +332,276 @@ mod tests { .expect("expected to deserialize domain document"); } } + + // ================================================================ + // Display impl tests for Document + // ================================================================ + + #[test] + fn display_document_with_no_properties() { + let doc = Document::V0(DocumentV0 { + id: platform_value::Identifier::new([0xAA; 32]), + owner_id: platform_value::Identifier::new([0xBB; 32]), + properties: Default::default(), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }); + + let s = format!("{}", doc); + assert!( + s.contains("no properties"), + "should say 'no properties' when the BTreeMap is empty, got: {}", + s + ); + } + + #[test] + fn display_document_shows_transferred_at_fields() { + let doc = Document::V0(DocumentV0 { + id: platform_value::Identifier::new([1u8; 32]), + owner_id: platform_value::Identifier::new([2u8; 32]), + properties: Default::default(), + revision: None, + created_at: None, + updated_at: None, + transferred_at: Some(1_700_000_000_000), + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: Some(500), + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: Some(42), + creator_id: None, + }); + + let s = format!("{}", doc); + assert!( + s.contains("transferred_at:"), + "should contain transferred_at, got: {}", + s + ); + assert!( + s.contains("transferred_at_block_height:500"), + "should contain transferred_at_block_height:500, got: {}", + s + ); + assert!( + s.contains("transferred_at_core_block_height:42"), + "should contain transferred_at_core_block_height:42, got: {}", + s + ); + } + + #[test] + fn display_document_shows_creator_id() { + let creator = platform_value::Identifier::new([0xCC; 32]); + let doc = Document::V0(DocumentV0 { + id: platform_value::Identifier::new([1u8; 32]), + owner_id: platform_value::Identifier::new([2u8; 32]), + properties: Default::default(), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: Some(creator), + }); + + let s = format!("{}", doc); + assert!( + s.contains("creator_id:"), + "should contain creator_id, got: {}", + s + ); + } + + #[test] + fn display_document_shows_block_height_fields() { + let doc = Document::V0(DocumentV0 { + id: platform_value::Identifier::new([1u8; 32]), + owner_id: platform_value::Identifier::new([2u8; 32]), + properties: Default::default(), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: Some(100), + updated_at_block_height: Some(200), + transferred_at_block_height: None, + created_at_core_block_height: Some(50), + updated_at_core_block_height: Some(60), + transferred_at_core_block_height: None, + creator_id: None, + }); + + let s = format!("{}", doc); + assert!(s.contains("created_at_block_height:100"), "got: {}", s); + assert!(s.contains("updated_at_block_height:200"), "got: {}", s); + assert!(s.contains("created_at_core_block_height:50"), "got: {}", s); + assert!(s.contains("updated_at_core_block_height:60"), "got: {}", s); + } + + // ================================================================ + // Version dispatch: increment_revision + // ================================================================ + + #[test] + fn increment_revision_works_on_mutable_document() { + let mut doc = Document::V0(DocumentV0 { + id: platform_value::Identifier::new([1u8; 32]), + owner_id: platform_value::Identifier::new([2u8; 32]), + properties: Default::default(), + revision: Some(1), + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }); + + doc.increment_revision() + .expect("increment_revision should succeed"); + assert_eq!(doc.revision(), Some(2)); + } + + #[test] + fn increment_revision_fails_when_no_revision() { + let mut doc = Document::V0(DocumentV0 { + id: platform_value::Identifier::new([1u8; 32]), + owner_id: platform_value::Identifier::new([2u8; 32]), + properties: Default::default(), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }); + + let result = doc.increment_revision(); + assert!( + result.is_err(), + "increment_revision should fail when revision is None" + ); + } + + // ================================================================ + // Version dispatch: is_equal_ignoring_time_based_fields + // ================================================================ + + #[test] + fn is_equal_ignoring_time_based_fields_dispatches_correctly() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected to get contract"); + + let document_type = contract + .document_type_for_name("profile") + .expect("expected to get profile document type"); + + let doc1 = document_type + .random_document(Some(42), platform_version) + .expect("expected random document"); + + let mut doc2 = doc1.clone(); + // Change timestamps + doc2.set_created_at(Some(9_999_999)); + doc2.set_updated_at(Some(8_888_888)); + + let result = doc1 + .is_equal_ignoring_time_based_fields(&doc2, None, platform_version) + .expect("should succeed"); + assert!( + result, + "same document with different timestamps should be equal ignoring time fields" + ); + } + + // ================================================================ + // Version dispatch: get_raw_for_contract + // ================================================================ + + #[test] + fn get_raw_for_contract_dispatches_to_v0() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected to get contract"); + + let document_type = contract + .document_type_for_name("profile") + .expect("expected to get profile document type"); + + let document = document_type + .random_document(Some(7), platform_version) + .expect("expected random document"); + + let raw_id = document + .get_raw_for_contract("$id", "profile", &contract, None, platform_version) + .expect("should succeed"); + assert_eq!(raw_id, Some(document.id().to_vec())); + } + + // ================================================================ + // Version dispatch: hash + // ================================================================ + + #[test] + fn document_hash_is_deterministic() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected to get contract"); + + let document_type = contract + .document_type_for_name("profile") + .expect("expected to get profile document type"); + + let document = document_type + .random_document(Some(42), platform_version) + .expect("expected random document"); + + let hash1 = document + .hash(&contract, document_type, platform_version) + .expect("hash should succeed"); + let hash2 = document + .hash(&contract, document_type, platform_version) + .expect("hash should succeed"); + assert_eq!(hash1, hash2, "hash should be deterministic"); + assert!(!hash1.is_empty(), "hash should not be empty"); + } } diff --git a/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/mod.rs b/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/mod.rs index 315d766493f..80cdd018b63 100644 --- a/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/mod.rs +++ b/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/mod.rs @@ -57,3 +57,192 @@ impl DocumentPlatformValueMethodsV0<'_> for Document { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::accessors::v0::DataContractV0Getters; + use crate::data_contract::document_type::random_document::CreateRandomDocument; + use crate::document::DocumentV0Getters; + use crate::tests::json_document::json_document_to_contract; + use platform_value::Identifier; + use platform_version::version::PlatformVersion; + + // ================================================================ + // Round-trip: Document -> Value -> Document + // ================================================================ + + #[test] + fn round_trip_document_to_value_and_back() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected to load dashpay contract"); + + let document_type = contract + .document_type_for_name("profile") + .expect("expected profile document type"); + + for seed in 0..10u64 { + let document = document_type + .random_document(Some(seed), platform_version) + .expect("expected random document"); + + let value = Document::into_value(document.clone()).expect("into_value should succeed"); + + let recovered = Document::from_platform_value(value, platform_version) + .expect("from_platform_value should succeed"); + + assert_eq!(document.id(), recovered.id(), "id mismatch for seed {seed}"); + assert_eq!( + document.owner_id(), + recovered.owner_id(), + "owner_id mismatch for seed {seed}" + ); + assert_eq!( + document.revision(), + recovered.revision(), + "revision mismatch for seed {seed}" + ); + assert_eq!( + document.properties(), + recovered.properties(), + "properties mismatch for seed {seed}" + ); + } + } + + // ================================================================ + // to_map_value preserves all fields + // ================================================================ + + #[test] + fn to_map_value_contains_id_and_owner_id() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected to load dashpay contract"); + + let document_type = contract + .document_type_for_name("profile") + .expect("expected profile document type"); + + let document = document_type + .random_document(Some(42), platform_version) + .expect("expected random document"); + + let map = document + .to_map_value() + .expect("to_map_value should succeed"); + assert!(map.contains_key("$id"), "map should contain $id"); + assert!(map.contains_key("$ownerId"), "map should contain $ownerId"); + } + + // ================================================================ + // to_object returns a Value + // ================================================================ + + #[test] + fn to_object_returns_map_value() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected to load dashpay contract"); + + let document_type = contract + .document_type_for_name("profile") + .expect("expected profile document type"); + + let document = document_type + .random_document(Some(7), platform_version) + .expect("expected random document"); + + let obj = document.to_object().expect("to_object should succeed"); + assert!(obj.is_map(), "to_object should return a Map value"); + } + + // ================================================================ + // into_map_value consumes document + // ================================================================ + + #[test] + fn into_map_value_consumes_and_returns_correct_data() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected to load dashpay contract"); + + let document_type = contract + .document_type_for_name("profile") + .expect("expected profile document type"); + + let document = document_type + .random_document(Some(55), platform_version) + .expect("expected random document"); + + let original_id = document.id(); + let map = document + .into_map_value() + .expect("into_map_value should succeed"); + + // The map should contain the id + let id_val = map.get("$id").expect("should have $id"); + match id_val { + Value::Identifier(bytes) => { + assert_eq!( + Identifier::new(*bytes), + original_id, + "id in map should match original" + ); + } + _ => panic!("$id should be an Identifier value"), + } + } + + // ================================================================ + // from_platform_value with minimal document + // ================================================================ + + #[test] + fn from_platform_value_with_minimal_data() { + let platform_version = PlatformVersion::latest(); + let id = Identifier::new([1u8; 32]); + let owner_id = Identifier::new([2u8; 32]); + + let doc_v0 = DocumentV0 { + id, + owner_id, + properties: std::collections::BTreeMap::new(), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }; + + let value = DocumentV0::into_value(doc_v0).expect("into_value should succeed"); + let recovered = Document::from_platform_value(value, platform_version) + .expect("from_platform_value should succeed"); + + assert_eq!(recovered.id(), id); + assert_eq!(recovered.owner_id(), owner_id); + } +} diff --git a/packages/rs-dpp/src/document/v0/cbor_conversion.rs b/packages/rs-dpp/src/document/v0/cbor_conversion.rs index 76e1f05676e..957cd0ec101 100644 --- a/packages/rs-dpp/src/document/v0/cbor_conversion.rs +++ b/packages/rs-dpp/src/document/v0/cbor_conversion.rs @@ -208,3 +208,341 @@ impl DocumentCborMethodsV0 for DocumentV0 { Ok(buffer) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::accessors::v0::DataContractV0Getters; + use crate::data_contract::document_type::random_document::CreateRandomDocument; + use crate::document::serialization_traits::DocumentCborMethodsV0; + use crate::document::DocumentV0Getters; + use crate::tests::json_document::json_document_to_contract; + use platform_version::version::PlatformVersion; + + fn make_document_v0_with_timestamps() -> DocumentV0 { + let id = Identifier::new([1u8; 32]); + let owner_id = Identifier::new([2u8; 32]); + let mut properties = BTreeMap::new(); + properties.insert("name".to_string(), Value::Text("Alice".to_string())); + properties.insert("age".to_string(), Value::U64(30)); + DocumentV0 { + id, + owner_id, + properties, + revision: Some(1), + created_at: Some(1_700_000_000_000), + updated_at: Some(1_700_000_100_000), + transferred_at: None, + created_at_block_height: Some(100), + updated_at_block_height: Some(200), + transferred_at_block_height: None, + created_at_core_block_height: Some(50), + updated_at_core_block_height: Some(60), + transferred_at_core_block_height: None, + creator_id: None, + } + } + + // ================================================================ + // Round-trip: to_cbor -> from_cbor preserves document data + // ================================================================ + + #[test] + fn cbor_round_trip_with_random_dashpay_profile() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected to load dashpay contract"); + + let document_type = contract + .document_type_for_name("profile") + .expect("expected profile document type"); + + for seed in 0..10u64 { + let document = document_type + .random_document(Some(seed), platform_version) + .expect("expected random document"); + + // Use Document-level from_cbor which handles the version prefix + let cbor_bytes = document.to_cbor().expect("to_cbor should succeed"); + let recovered = + crate::document::Document::from_cbor(&cbor_bytes, None, None, platform_version) + .expect("from_cbor should succeed"); + + assert_eq!(document.id(), recovered.id(), "id mismatch for seed {seed}"); + assert_eq!( + document.owner_id(), + recovered.owner_id(), + "owner_id mismatch for seed {seed}" + ); + assert_eq!( + document.revision(), + recovered.revision(), + "revision mismatch for seed {seed}" + ); + assert_eq!( + document.properties(), + recovered.properties(), + "properties mismatch for seed {seed}" + ); + } + } + + #[test] + fn cbor_round_trip_with_explicit_ids_overrides_embedded_ids() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected to load dashpay contract"); + + let document_type = contract + .document_type_for_name("profile") + .expect("expected profile document type"); + + let document = document_type + .random_document(Some(42), platform_version) + .expect("expected random document"); + + let cbor_bytes = document.to_cbor().expect("to_cbor should succeed"); + + let override_id = [0xAA; 32]; + let override_owner = [0xBB; 32]; + + let recovered = crate::document::Document::from_cbor( + &cbor_bytes, + Some(override_id), + Some(override_owner), + platform_version, + ) + .expect("from_cbor with explicit ids should succeed"); + + assert_eq!( + recovered.id(), + Identifier::new(override_id), + "explicit document_id should override the one in CBOR" + ); + assert_eq!( + recovered.owner_id(), + Identifier::new(override_owner), + "explicit owner_id should override the one in CBOR" + ); + } + + // ================================================================ + // to_cbor_value produces a valid CborValue + // ================================================================ + + #[test] + fn to_cbor_value_returns_map_for_document_with_properties() { + let doc = make_document_v0_with_timestamps(); + let cbor_val = doc.to_cbor_value().expect("to_cbor_value should succeed"); + // CborValue should be a Map at the top level + assert!( + cbor_val.is_map(), + "CBOR value of a document should be a Map, got {:?}", + cbor_val + ); + } + + // ================================================================ + // to_cbor output starts with varint-encoded version prefix (0) + // ================================================================ + + #[test] + fn to_cbor_starts_with_version_zero_varint() { + let doc = make_document_v0_with_timestamps(); + let cbor_bytes = doc.to_cbor().expect("to_cbor should succeed"); + // The first byte should be the varint encoding of 0 + assert!(!cbor_bytes.is_empty(), "CBOR output should not be empty"); + assert_eq!( + cbor_bytes[0], 0, + "first byte should be varint(0) for version" + ); + } + + // ================================================================ + // from_cbor rejects invalid CBOR data + // ================================================================ + + #[test] + fn from_cbor_rejects_invalid_cbor_bytes() { + let platform_version = PlatformVersion::latest(); + let garbage = vec![0xFF, 0xFE, 0xFD, 0x00, 0x01]; + let result = DocumentV0::from_cbor(&garbage, None, None, platform_version); + assert!( + result.is_err(), + "from_cbor should fail on invalid CBOR bytes" + ); + } + + // ================================================================ + // DocumentForCbor TryFrom preserves all timestamp fields + // ================================================================ + + #[test] + fn document_for_cbor_preserves_all_fields() { + let doc = make_document_v0_with_timestamps(); + let cbor_doc = DocumentForCbor::try_from(doc.clone()).expect("TryFrom should succeed"); + assert_eq!(cbor_doc.id, doc.id.to_buffer()); + assert_eq!(cbor_doc.owner_id, doc.owner_id.to_buffer()); + assert_eq!(cbor_doc.revision, doc.revision); + assert_eq!(cbor_doc.created_at, doc.created_at); + assert_eq!(cbor_doc.updated_at, doc.updated_at); + assert_eq!(cbor_doc.transferred_at, doc.transferred_at); + assert_eq!( + cbor_doc.created_at_block_height, + doc.created_at_block_height + ); + assert_eq!( + cbor_doc.updated_at_block_height, + doc.updated_at_block_height + ); + assert_eq!( + cbor_doc.transferred_at_block_height, + doc.transferred_at_block_height + ); + assert_eq!( + cbor_doc.created_at_core_block_height, + doc.created_at_core_block_height + ); + assert_eq!( + cbor_doc.updated_at_core_block_height, + doc.updated_at_core_block_height + ); + assert_eq!( + cbor_doc.transferred_at_core_block_height, + doc.transferred_at_core_block_height + ); + } + + // ================================================================ + // from_map populates fields correctly from a BTreeMap + // ================================================================ + + #[test] + fn from_map_extracts_system_fields_and_leaves_properties() { + let id_bytes = [3u8; 32]; + let owner_bytes = [4u8; 32]; + + let mut map = BTreeMap::new(); + map.insert(property_names::ID.to_string(), Value::Bytes32(id_bytes)); + map.insert( + property_names::OWNER_ID.to_string(), + Value::Bytes32(owner_bytes), + ); + map.insert(property_names::REVISION.to_string(), Value::U64(5)); + map.insert( + property_names::CREATED_AT.to_string(), + Value::U64(1_000_000), + ); + map.insert( + property_names::UPDATED_AT.to_string(), + Value::U64(2_000_000), + ); + map.insert("customField".to_string(), Value::Text("hello".to_string())); + + let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed"); + + assert_eq!(doc.id, Identifier::new(id_bytes)); + assert_eq!(doc.owner_id, Identifier::new(owner_bytes)); + assert_eq!(doc.revision, Some(5)); + assert_eq!(doc.created_at, Some(1_000_000)); + assert_eq!(doc.updated_at, Some(2_000_000)); + // The custom field should remain in properties + assert_eq!( + doc.properties.get("customField"), + Some(&Value::Text("hello".to_string())) + ); + // System fields should NOT be in properties + assert!(!doc.properties.contains_key(property_names::ID)); + assert!(!doc.properties.contains_key(property_names::OWNER_ID)); + assert!(!doc.properties.contains_key(property_names::REVISION)); + } + + #[test] + fn from_map_with_explicit_ids_overrides_map_ids() { + let map_id = [10u8; 32]; + let map_owner = [11u8; 32]; + let override_id = [20u8; 32]; + let override_owner = [21u8; 32]; + + let mut map = BTreeMap::new(); + map.insert(property_names::ID.to_string(), Value::Bytes32(map_id)); + map.insert( + property_names::OWNER_ID.to_string(), + Value::Bytes32(map_owner), + ); + + let doc = DocumentV0::from_map(map, Some(override_id), Some(override_owner)) + .expect("from_map should succeed"); + + assert_eq!( + doc.id, + Identifier::new(override_id), + "explicit document_id should take precedence" + ); + assert_eq!( + doc.owner_id, + Identifier::new(override_owner), + "explicit owner_id should take precedence" + ); + } + + // ================================================================ + // Round-trip via from_map: construct map, parse, verify + // ================================================================ + + #[test] + fn from_map_with_all_timestamp_variants() { + let mut map = BTreeMap::new(); + map.insert(property_names::ID.to_string(), Value::Bytes32([5u8; 32])); + map.insert( + property_names::OWNER_ID.to_string(), + Value::Bytes32([6u8; 32]), + ); + map.insert( + property_names::CREATED_AT_BLOCK_HEIGHT.to_string(), + Value::U64(100), + ); + map.insert( + property_names::UPDATED_AT_BLOCK_HEIGHT.to_string(), + Value::U64(200), + ); + map.insert( + property_names::TRANSFERRED_AT.to_string(), + Value::U64(3_000_000), + ); + map.insert( + property_names::TRANSFERRED_AT_BLOCK_HEIGHT.to_string(), + Value::U64(300), + ); + map.insert( + property_names::CREATED_AT_CORE_BLOCK_HEIGHT.to_string(), + Value::U32(50), + ); + map.insert( + property_names::UPDATED_AT_CORE_BLOCK_HEIGHT.to_string(), + Value::U32(60), + ); + map.insert( + property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT.to_string(), + Value::U32(70), + ); + + let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed"); + + assert_eq!(doc.created_at_block_height, Some(100)); + assert_eq!(doc.updated_at_block_height, Some(200)); + assert_eq!(doc.transferred_at, Some(3_000_000)); + assert_eq!(doc.transferred_at_block_height, Some(300)); + assert_eq!(doc.created_at_core_block_height, Some(50)); + assert_eq!(doc.updated_at_core_block_height, Some(60)); + assert_eq!(doc.transferred_at_core_block_height, Some(70)); + } +} diff --git a/packages/rs-dpp/src/document/v0/json_conversion.rs b/packages/rs-dpp/src/document/v0/json_conversion.rs index 0ade5bbead0..692a5e59310 100644 --- a/packages/rs-dpp/src/document/v0/json_conversion.rs +++ b/packages/rs-dpp/src/document/v0/json_conversion.rs @@ -173,3 +173,337 @@ impl DocumentJsonMethodsV0<'_> for DocumentV0 { Ok(document) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::accessors::v0::DataContractV0Getters; + use crate::data_contract::document_type::random_document::CreateRandomDocument; + use crate::document::serialization_traits::DocumentJsonMethodsV0; + use crate::tests::json_document::json_document_to_contract; + use platform_version::version::PlatformVersion; + use std::collections::BTreeMap; + + fn make_document_v0_with_all_timestamps() -> DocumentV0 { + let mut properties = BTreeMap::new(); + properties.insert("label".to_string(), Value::Text("test-label".to_string())); + DocumentV0 { + id: Identifier::new([1u8; 32]), + owner_id: Identifier::new([2u8; 32]), + properties, + revision: Some(3), + created_at: Some(1_700_000_000_000), + updated_at: Some(1_700_000_100_000), + transferred_at: Some(1_700_000_200_000), + created_at_block_height: Some(100), + updated_at_block_height: Some(200), + transferred_at_block_height: Some(300), + created_at_core_block_height: Some(50), + updated_at_core_block_height: Some(60), + transferred_at_core_block_height: Some(70), + creator_id: Some(Identifier::new([9u8; 32])), + } + } + + fn make_minimal_document_v0() -> DocumentV0 { + DocumentV0 { + id: Identifier::new([0xAA; 32]), + owner_id: Identifier::new([0xBB; 32]), + properties: BTreeMap::new(), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + } + } + + // ================================================================ + // to_json produces a JsonValue containing all set fields + // ================================================================ + + #[test] + fn to_json_includes_id_and_owner_id() { + let platform_version = PlatformVersion::latest(); + let doc = make_minimal_document_v0(); + let json = doc + .to_json(platform_version) + .expect("to_json should succeed"); + let obj = json.as_object().expect("should be an object"); + assert!( + obj.contains_key(property_names::ID), + "JSON should contain $id" + ); + assert!( + obj.contains_key(property_names::OWNER_ID), + "JSON should contain $ownerId" + ); + } + + #[test] + fn to_json_represents_none_timestamps_as_null() { + let platform_version = PlatformVersion::latest(); + let doc = make_minimal_document_v0(); + let json = doc + .to_json(platform_version) + .expect("to_json should succeed"); + let obj = json.as_object().expect("should be an object"); + + // to_json serializes via serde, so None fields appear as null + if let Some(val) = obj.get(property_names::CREATED_AT) { + assert!( + val.is_null(), + "$createdAt should be null when None, got: {:?}", + val + ); + } + if let Some(val) = obj.get(property_names::UPDATED_AT) { + assert!( + val.is_null(), + "$updatedAt should be null when None, got: {:?}", + val + ); + } + if let Some(val) = obj.get(property_names::REVISION) { + assert!( + val.is_null(), + "$revision should be null when None, got: {:?}", + val + ); + } + } + + // ================================================================ + // to_json_with_identifiers_using_bytes includes all timestamps + // ================================================================ + + #[test] + fn to_json_with_identifiers_using_bytes_includes_all_timestamp_fields() { + let platform_version = PlatformVersion::latest(); + let doc = make_document_v0_with_all_timestamps(); + let json = doc + .to_json_with_identifiers_using_bytes(platform_version) + .expect("to_json_with_identifiers_using_bytes should succeed"); + let obj = json.as_object().expect("should be an object"); + + assert!(obj.contains_key(property_names::ID)); + assert!(obj.contains_key(property_names::OWNER_ID)); + assert!(obj.contains_key(property_names::REVISION)); + assert!(obj.contains_key(property_names::CREATED_AT)); + assert!(obj.contains_key(property_names::UPDATED_AT)); + assert!(obj.contains_key(property_names::TRANSFERRED_AT)); + assert!(obj.contains_key(property_names::CREATED_AT_BLOCK_HEIGHT)); + assert!(obj.contains_key(property_names::UPDATED_AT_BLOCK_HEIGHT)); + assert!(obj.contains_key(property_names::TRANSFERRED_AT_BLOCK_HEIGHT)); + assert!(obj.contains_key(property_names::CREATED_AT_CORE_BLOCK_HEIGHT)); + assert!(obj.contains_key(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT)); + assert!(obj.contains_key(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT)); + assert!(obj.contains_key(property_names::CREATOR_ID)); + + // Verify numeric values + assert_eq!(obj[property_names::REVISION].as_u64(), Some(3)); + assert_eq!( + obj[property_names::CREATED_AT].as_u64(), + Some(1_700_000_000_000) + ); + assert_eq!( + obj[property_names::UPDATED_AT].as_u64(), + Some(1_700_000_100_000) + ); + assert_eq!( + obj[property_names::TRANSFERRED_AT].as_u64(), + Some(1_700_000_200_000) + ); + assert_eq!( + obj[property_names::CREATED_AT_BLOCK_HEIGHT].as_u64(), + Some(100) + ); + assert_eq!( + obj[property_names::UPDATED_AT_BLOCK_HEIGHT].as_u64(), + Some(200) + ); + assert_eq!( + obj[property_names::TRANSFERRED_AT_BLOCK_HEIGHT].as_u64(), + Some(300) + ); + assert_eq!( + obj[property_names::CREATED_AT_CORE_BLOCK_HEIGHT].as_u64(), + Some(50) + ); + assert_eq!( + obj[property_names::UPDATED_AT_CORE_BLOCK_HEIGHT].as_u64(), + Some(60) + ); + assert_eq!( + obj[property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT].as_u64(), + Some(70) + ); + } + + #[test] + fn to_json_with_identifiers_using_bytes_includes_custom_properties() { + let platform_version = PlatformVersion::latest(); + let doc = make_document_v0_with_all_timestamps(); + let json = doc + .to_json_with_identifiers_using_bytes(platform_version) + .expect("should succeed"); + let obj = json.as_object().expect("should be an object"); + assert_eq!( + obj.get("label").and_then(|v| v.as_str()), + Some("test-label") + ); + } + + // ================================================================ + // from_json_value round-trip: to_json -> from_json_value + // Uses String as the identifier deserialization type since + // to_json produces base58 string identifiers. + // ================================================================ + + #[test] + fn json_round_trip_with_random_dashpay_profile() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected to load dashpay contract"); + + let document_type = contract + .document_type_for_name("profile") + .expect("expected profile document type"); + + for seed in 0..5u64 { + let document = document_type + .random_document(Some(seed), platform_version) + .expect("expected random document"); + + let doc_v0 = match &document { + crate::document::Document::V0(d) => d, + }; + + let json_val = doc_v0 + .to_json(platform_version) + .expect("to_json should succeed"); + + let recovered = DocumentV0::from_json_value::(json_val, platform_version) + .expect("from_json_value should succeed"); + + assert_eq!(doc_v0.id, recovered.id, "id mismatch for seed {seed}"); + assert_eq!( + doc_v0.owner_id, recovered.owner_id, + "owner_id mismatch for seed {seed}" + ); + assert_eq!( + doc_v0.revision, recovered.revision, + "revision mismatch for seed {seed}" + ); + } + } + + // ================================================================ + // from_json_value extracts all system fields correctly + // ================================================================ + + #[test] + fn from_json_value_extracts_timestamps_and_revision() { + let platform_version = PlatformVersion::latest(); + let id = Identifier::new([1u8; 32]); + let owner = Identifier::new([2u8; 32]); + let creator = Identifier::new([9u8; 32]); + + let json_val = json!({ + "$id": bs58::encode(id.to_buffer()).into_string(), + "$ownerId": bs58::encode(owner.to_buffer()).into_string(), + "$revision": 5, + "$createdAt": 1_000_000u64, + "$updatedAt": 2_000_000u64, + "$createdAtBlockHeight": 100u64, + "$updatedAtBlockHeight": 200u64, + "$createdAtCoreBlockHeight": 50u32, + "$updatedAtCoreBlockHeight": 60u32, + "$transferredAt": 3_000_000u64, + "$transferredAtBlockHeight": 300u64, + "$transferredAtCoreBlockHeight": 70u32, + "$creatorId": bs58::encode(creator.to_buffer()).into_string(), + "customProp": "hello" + }); + + let doc = DocumentV0::from_json_value::(json_val, platform_version) + .expect("from_json_value should succeed"); + + assert_eq!(doc.id, id); + assert_eq!(doc.owner_id, owner); + assert_eq!(doc.revision, Some(5)); + assert_eq!(doc.created_at, Some(1_000_000)); + assert_eq!(doc.updated_at, Some(2_000_000)); + assert_eq!(doc.created_at_block_height, Some(100)); + assert_eq!(doc.updated_at_block_height, Some(200)); + assert_eq!(doc.created_at_core_block_height, Some(50)); + assert_eq!(doc.updated_at_core_block_height, Some(60)); + assert_eq!(doc.transferred_at, Some(3_000_000)); + assert_eq!(doc.transferred_at_block_height, Some(300)); + assert_eq!(doc.transferred_at_core_block_height, Some(70)); + assert_eq!(doc.creator_id, Some(creator)); + // Custom property should be in properties map + assert_eq!( + doc.properties.get("customProp"), + Some(&Value::Text("hello".to_string())) + ); + } + + #[test] + fn from_json_value_handles_missing_optional_fields() { + let platform_version = PlatformVersion::latest(); + let id = Identifier::new([3u8; 32]); + let owner = Identifier::new([4u8; 32]); + let json_val = json!({ + "$id": bs58::encode(id.to_buffer()).into_string(), + "$ownerId": bs58::encode(owner.to_buffer()).into_string(), + }); + + let doc = DocumentV0::from_json_value::(json_val, platform_version) + .expect("from_json_value should succeed with minimal fields"); + + assert_eq!(doc.id, id); + assert_eq!(doc.owner_id, owner); + assert_eq!(doc.revision, None); + assert_eq!(doc.created_at, None); + assert_eq!(doc.updated_at, None); + assert_eq!(doc.transferred_at, None); + assert_eq!(doc.created_at_block_height, None); + assert_eq!(doc.updated_at_block_height, None); + assert_eq!(doc.transferred_at_block_height, None); + assert_eq!(doc.created_at_core_block_height, None); + assert_eq!(doc.updated_at_core_block_height, None); + assert_eq!(doc.transferred_at_core_block_height, None); + assert_eq!(doc.creator_id, None); + } + + // ================================================================ + // from_json_value with creator_id + // ================================================================ + + #[test] + fn from_json_value_parses_creator_id() { + let platform_version = PlatformVersion::latest(); + let creator = Identifier::new([0xCC; 32]); + let json_val = json!({ + "$id": bs58::encode([1u8; 32]).into_string(), + "$ownerId": bs58::encode([2u8; 32]).into_string(), + "$creatorId": bs58::encode(creator.to_buffer()).into_string(), + }); + + let doc = DocumentV0::from_json_value::(json_val, platform_version) + .expect("from_json_value with creator_id should succeed"); + + assert_eq!(doc.creator_id, Some(creator)); + } +}