Skip to main content

dpp/document/v0/
mod.rs

1//! Documents.
2//!
3//! This module defines the `Document` struct and implements its functions.
4//!
5
6mod accessors;
7#[cfg(feature = "document-cbor-conversion")]
8pub(super) mod cbor_conversion;
9#[cfg(feature = "value-conversion")]
10mod platform_value_conversion;
11pub mod serialize;
12
13use chrono::DateTime;
14use std::collections::BTreeMap;
15use std::fmt;
16
17use platform_value::Value;
18
19use crate::document::document_methods::{
20    DocumentGetRawForContractV0, DocumentGetRawForDocumentTypeV0, DocumentHashV0Method,
21    DocumentIsEqualIgnoringTimestampsV0,
22};
23
24use crate::identity::TimestampMillis;
25use crate::prelude::Revision;
26use crate::prelude::{BlockHeight, CoreBlockHeight, Identifier};
27#[cfg(feature = "json-conversion")]
28use crate::serialization::json_safe_fields;
29
30/// Documents contain the data that goes into data contracts.
31#[cfg_attr(feature = "json-conversion", json_safe_fields)]
32#[derive(Clone, Debug, PartialEq, Default)]
33#[cfg_attr(
34    feature = "serde-conversion",
35    derive(serde::Serialize, serde::Deserialize)
36)]
37pub struct DocumentV0 {
38    /// The unique document ID.
39    #[cfg_attr(feature = "serde-conversion", serde(rename = "$id"))]
40    pub id: Identifier,
41    /// The ID of the document's owner.
42    #[cfg_attr(feature = "serde-conversion", serde(rename = "$ownerId"))]
43    pub owner_id: Identifier,
44    /// The document's properties (data).
45    #[cfg_attr(feature = "serde-conversion", serde(flatten))]
46    pub properties: BTreeMap<String, Value>,
47    /// The document revision, if the document is mutable.
48    #[cfg_attr(feature = "serde-conversion", serde(rename = "$revision", default))]
49    pub revision: Option<Revision>,
50    /// The time in milliseconds that the document was created, if it is set as required by the document type schema.
51    #[cfg_attr(feature = "serde-conversion", serde(rename = "$createdAt", default))]
52    pub created_at: Option<TimestampMillis>,
53    /// The time in milliseconds that the document was last updated, if it is set as required by the document type schema.
54    #[cfg_attr(feature = "serde-conversion", serde(rename = "$updatedAt", default))]
55    pub updated_at: Option<TimestampMillis>,
56    /// The time in milliseconds that the document was last transferred, if it is set as required by the document type schema.
57    #[cfg_attr(
58        feature = "serde-conversion",
59        serde(rename = "$transferredAt", default)
60    )]
61    pub transferred_at: Option<TimestampMillis>,
62    /// The block that the document was created, if it is set as required by the document type schema.
63    #[cfg_attr(
64        feature = "serde-conversion",
65        serde(rename = "$createdAtBlockHeight", default)
66    )]
67    pub created_at_block_height: Option<BlockHeight>,
68    /// The block that the document was last updated, if it is set as required by the document type schema.
69    #[cfg_attr(
70        feature = "serde-conversion",
71        serde(rename = "$updatedAtBlockHeight", default)
72    )]
73    pub updated_at_block_height: Option<BlockHeight>,
74    /// The block that the document was last transferred to a new identity, if it is set as required by the document type schema.
75    #[cfg_attr(
76        feature = "serde-conversion",
77        serde(rename = "$transferredAtBlockHeight", default)
78    )]
79    pub transferred_at_block_height: Option<BlockHeight>,
80    /// The core block that the document was created, if it is set as required by the document type schema.
81    #[cfg_attr(
82        feature = "serde-conversion",
83        serde(rename = "$createdAtCoreBlockHeight", default)
84    )]
85    pub created_at_core_block_height: Option<CoreBlockHeight>,
86    /// The core block that the document was last updated, if it is set as required by the document type schema.
87    #[cfg_attr(
88        feature = "serde-conversion",
89        serde(rename = "$updatedAtCoreBlockHeight", default)
90    )]
91    pub updated_at_core_block_height: Option<CoreBlockHeight>,
92    /// The core block that the document was last transferred to a new identity, if it is set as required by the document type schema.
93    #[cfg_attr(
94        feature = "serde-conversion",
95        serde(rename = "$transferredAtCoreBlockHeight", default)
96    )]
97    pub transferred_at_core_block_height: Option<CoreBlockHeight>,
98    /// The creator id.
99    #[cfg_attr(feature = "serde-conversion", serde(rename = "$creatorId", default))]
100    pub creator_id: Option<Identifier>,
101}
102
103impl DocumentGetRawForContractV0 for DocumentV0 {
104    //automatically done
105}
106
107impl DocumentIsEqualIgnoringTimestampsV0 for DocumentV0 {
108    //automatically done
109}
110
111impl DocumentGetRawForDocumentTypeV0 for DocumentV0 {
112    //automatically done
113}
114
115impl DocumentHashV0Method for DocumentV0 {
116    //automatically done
117}
118
119impl fmt::Display for DocumentV0 {
120    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
121        write!(f, "id:{} ", self.id)?;
122        write!(f, "owner_id:{} ", self.owner_id)?;
123        if let Some(created_at) = self.created_at {
124            let datetime = DateTime::from_timestamp_millis(created_at as i64).unwrap_or_default();
125            write!(f, "created_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
126        }
127        if let Some(updated_at) = self.updated_at {
128            let datetime = DateTime::from_timestamp_millis(updated_at as i64).unwrap_or_default();
129            write!(f, "updated_at:{} ", datetime.format("%Y-%m-%d %H:%M:%S"))?;
130        }
131        if let Some(transferred_at) = self.transferred_at {
132            let datetime =
133                DateTime::from_timestamp_millis(transferred_at as i64).unwrap_or_default();
134            write!(
135                f,
136                "transferred_at:{} ",
137                datetime.format("%Y-%m-%d %H:%M:%S")
138            )?;
139        }
140
141        if let Some(created_at_block_height) = self.created_at_block_height {
142            write!(f, "created_at_block_height:{} ", created_at_block_height)?;
143        }
144        if let Some(updated_at_block_height) = self.updated_at_block_height {
145            write!(f, "updated_at_block_height:{} ", updated_at_block_height)?;
146        }
147        if let Some(transferred_at_block_height) = self.transferred_at_block_height {
148            write!(
149                f,
150                "transferred_at_block_height:{} ",
151                transferred_at_block_height
152            )?;
153        }
154        if let Some(created_at_core_block_height) = self.created_at_core_block_height {
155            write!(
156                f,
157                "created_at_core_block_height:{} ",
158                created_at_core_block_height
159            )?;
160        }
161        if let Some(updated_at_core_block_height) = self.updated_at_core_block_height {
162            write!(
163                f,
164                "updated_at_core_block_height:{} ",
165                updated_at_core_block_height
166            )?;
167        }
168        if let Some(transferred_at_core_block_height) = self.transferred_at_core_block_height {
169            write!(
170                f,
171                "transferred_at_core_block_height:{} ",
172                transferred_at_core_block_height
173            )?;
174        }
175
176        if let Some(creator_id) = self.creator_id {
177            write!(f, "creator_id:{} ", creator_id)?;
178        }
179
180        if self.properties.is_empty() {
181            write!(f, "no properties")?;
182        } else {
183            for (key, value) in self.properties.iter() {
184                write!(f, "{}:{} ", key, value)?
185            }
186        }
187        Ok(())
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::data_contract::accessors::v0::DataContractV0Getters;
195    use crate::document::{DocumentV0Getters, DocumentV0Setters};
196    use platform_value::Identifier;
197
198    fn minimal_doc() -> DocumentV0 {
199        DocumentV0 {
200            id: Identifier::new([1u8; 32]),
201            owner_id: Identifier::new([2u8; 32]),
202            properties: BTreeMap::new(),
203            revision: None,
204            created_at: None,
205            updated_at: None,
206            transferred_at: None,
207            created_at_block_height: None,
208            updated_at_block_height: None,
209            transferred_at_block_height: None,
210            created_at_core_block_height: None,
211            updated_at_core_block_height: None,
212            transferred_at_core_block_height: None,
213            creator_id: None,
214        }
215    }
216
217    // ================================================================
218    //  Display impl: exercise each optional-field branch
219    // ================================================================
220
221    #[test]
222    fn display_minimal_document_has_no_properties_marker() {
223        let doc = minimal_doc();
224        let s = format!("{}", doc);
225        assert!(s.contains("id:"), "should contain id");
226        assert!(s.contains("owner_id:"), "should contain owner_id");
227        assert!(
228            s.contains("no properties"),
229            "empty properties should render as 'no properties', got: {s}"
230        );
231    }
232
233    #[test]
234    fn display_with_properties_formats_key_value_pairs() {
235        let mut doc = minimal_doc();
236        doc.properties
237            .insert("name".to_string(), Value::Text("Bob".to_string()));
238        let s = format!("{}", doc);
239        assert!(!s.contains("no properties"));
240        assert!(s.contains("name:"), "should contain property key");
241    }
242
243    #[test]
244    fn display_formats_all_optional_timestamp_fields() {
245        let mut doc = minimal_doc();
246        // Set every optional field to exercise each branch of Display
247        doc.created_at = Some(1_700_000_000_000);
248        doc.updated_at = Some(1_700_000_100_000);
249        doc.transferred_at = Some(1_700_000_200_000);
250        doc.created_at_block_height = Some(10);
251        doc.updated_at_block_height = Some(20);
252        doc.transferred_at_block_height = Some(30);
253        doc.created_at_core_block_height = Some(1);
254        doc.updated_at_core_block_height = Some(2);
255        doc.transferred_at_core_block_height = Some(3);
256        doc.creator_id = Some(Identifier::new([9u8; 32]));
257
258        let s = format!("{}", doc);
259        // Each branch should emit its labeled prefix
260        assert!(s.contains("created_at:"), "missing created_at: {s}");
261        assert!(s.contains("updated_at:"), "missing updated_at: {s}");
262        assert!(s.contains("transferred_at:"), "missing transferred_at: {s}");
263        assert!(
264            s.contains("created_at_block_height:10"),
265            "missing created_at_block_height: {s}"
266        );
267        assert!(
268            s.contains("updated_at_block_height:20"),
269            "missing updated_at_block_height: {s}"
270        );
271        assert!(
272            s.contains("transferred_at_block_height:30"),
273            "missing transferred_at_block_height: {s}"
274        );
275        assert!(
276            s.contains("created_at_core_block_height:1"),
277            "missing created_at_core_block_height: {s}"
278        );
279        assert!(
280            s.contains("updated_at_core_block_height:2"),
281            "missing updated_at_core_block_height: {s}"
282        );
283        assert!(
284            s.contains("transferred_at_core_block_height:3"),
285            "missing transferred_at_core_block_height: {s}"
286        );
287        assert!(s.contains("creator_id:"), "missing creator_id: {s}");
288    }
289
290    #[test]
291    fn display_invalid_timestamp_uses_default_formatter() {
292        // Timestamps that overflow DateTime should use `.unwrap_or_default()`.
293        // This ensures the "unwrap_or_default()" branch of Display is hit.
294        let mut doc = minimal_doc();
295        // u64::MAX casts to -1i64, which IS inside chrono's range (1 ms before
296        // epoch). Use i64::MAX instead — it exceeds chrono's supported ms
297        // range (~262,000 years) so `from_timestamp_millis` returns None and
298        // the `.unwrap_or_default()` branch is actually exercised.
299        doc.created_at = Some(i64::MAX as u64);
300        let s = format!("{}", doc);
301        // Must not panic and must contain the created_at prefix
302        assert!(s.contains("created_at:"));
303    }
304
305    // ================================================================
306    //  bump_revision: saturating behavior and None pass-through
307    // ================================================================
308
309    #[test]
310    fn bump_revision_increments_when_some() {
311        let mut doc = minimal_doc();
312        doc.set_revision(Some(5));
313        doc.bump_revision();
314        assert_eq!(doc.revision(), Some(6));
315    }
316
317    #[test]
318    fn bump_revision_is_noop_when_none() {
319        let mut doc = minimal_doc();
320        assert_eq!(doc.revision(), None);
321        doc.bump_revision();
322        // None -> None; no panic, no change.
323        assert_eq!(doc.revision(), None);
324    }
325
326    #[test]
327    fn bump_revision_saturates_at_max() {
328        let mut doc = minimal_doc();
329        doc.set_revision(Some(Revision::MAX));
330        doc.bump_revision();
331        // saturating_add should cap at MAX, not wrap
332        assert_eq!(doc.revision(), Some(Revision::MAX));
333    }
334
335    // ================================================================
336    //  Default impl
337    // ================================================================
338
339    #[test]
340    fn default_document_has_zero_identifiers_and_none_fields() {
341        let doc = DocumentV0::default();
342        assert_eq!(doc.id, Identifier::new([0u8; 32]));
343        assert_eq!(doc.owner_id, Identifier::new([0u8; 32]));
344        assert!(doc.properties.is_empty());
345        assert_eq!(doc.revision, None);
346        assert_eq!(doc.created_at, None);
347        assert_eq!(doc.updated_at, None);
348        assert_eq!(doc.transferred_at, None);
349        assert_eq!(doc.creator_id, None);
350    }
351
352    // ================================================================
353    //  PartialEq semantics
354    // ================================================================
355
356    #[test]
357    fn documents_with_different_creator_id_are_not_equal() {
358        let a = minimal_doc();
359        let mut b = minimal_doc();
360        b.creator_id = Some(Identifier::new([7u8; 32]));
361        assert_ne!(a, b);
362    }
363
364    #[test]
365    fn documents_with_equal_fields_are_equal() {
366        let a = minimal_doc();
367        let b = minimal_doc();
368        assert_eq!(a, b);
369    }
370
371    #[test]
372    fn clone_produces_equal_document() {
373        let mut doc = minimal_doc();
374        doc.properties.insert("k".to_string(), Value::U64(42));
375        doc.revision = Some(3);
376        let cloned = doc.clone();
377        assert_eq!(doc, cloned);
378    }
379
380    // ================================================================
381    //  Display impl: properties ordering and mixed fields
382    // ================================================================
383
384    #[test]
385    fn display_writes_properties_in_btreemap_sorted_order() {
386        // BTreeMap iterates in sorted key order. Verify the Display impl
387        // (which delegates to self.properties.iter()) emits the keys in that
388        // order. This exercises the properties-iteration branch of Display
389        // with more than one property.
390        let mut doc = minimal_doc();
391        doc.properties
392            .insert("zebra".to_string(), Value::Text("z".into()));
393        doc.properties
394            .insert("apple".to_string(), Value::Text("a".into()));
395        doc.properties
396            .insert("mango".to_string(), Value::Text("m".into()));
397
398        let s = format!("{}", doc);
399        let apple_idx = s.find("apple:").expect("apple missing");
400        let mango_idx = s.find("mango:").expect("mango missing");
401        let zebra_idx = s.find("zebra:").expect("zebra missing");
402        assert!(
403            apple_idx < mango_idx && mango_idx < zebra_idx,
404            "properties should appear in sorted (BTreeMap) order: {s}"
405        );
406    }
407
408    #[test]
409    fn display_mixes_system_fields_and_user_properties() {
410        // Exercise Display with only some optional system fields set,
411        // plus a property. Different combo than prior tests so we hit
412        // the transition from "system optional Some arm" to "properties
413        // iteration arm".
414        let mut doc = minimal_doc();
415        doc.revision = Some(42);
416        doc.created_at_block_height = Some(7);
417        doc.properties
418            .insert("greeting".to_string(), Value::Text("hi".into()));
419
420        let s = format!("{}", doc);
421        assert!(s.contains("created_at_block_height:7"));
422        assert!(s.contains("greeting:"));
423        // revision is NOT rendered by Display (only system timestamps +
424        // properties are). Verify Display does not add spurious revision text.
425        assert!(!s.contains("revision"));
426    }
427
428    // ================================================================
429    //  Hash method: from the DocumentHashV0Method trait, which is the
430    //  empty impl on DocumentV0 that forwards to hash_v0. Exercises a
431    //  code path not covered by accessor-only tests.
432    // ================================================================
433
434    #[test]
435    fn hash_v0_produces_deterministic_output_for_identical_documents() {
436        use crate::document::document_methods::DocumentHashV0Method;
437        use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0;
438        use crate::tests::json_document::json_document_to_contract;
439        use platform_version::version::PlatformVersion;
440
441        // hash_v0 is the default-method impl on DocumentV0 (via empty impl
442        // block). It requires a contract + document type to hash through.
443        let platform_version = PlatformVersion::first();
444        let contract = json_document_to_contract(
445            "../rs-drive/tests/supporting_files/contract/family/family-contract.json",
446            false,
447            platform_version,
448        )
449        .expect("expected to load family contract");
450        let doc_type = contract
451            .document_type_for_name("person")
452            .expect("expected person type");
453
454        // Build a document that can be serialized under this type.
455        use crate::data_contract::document_type::random_document::CreateRandomDocument;
456        let document = doc_type
457            .random_document(Some(7), platform_version)
458            .expect("random document");
459        let doc_v0 = match &document {
460            crate::document::Document::V0(d) => d.clone(),
461        };
462
463        // Determinism: hashing the same document twice must produce equal bytes.
464        let h1 = doc_v0
465            .hash_v0(&contract, doc_type, platform_version)
466            .expect("hash succeeds");
467        let h2 = doc_v0
468            .hash_v0(&contract, doc_type, platform_version)
469            .expect("hash succeeds");
470        assert_eq!(h1, h2);
471        // The double-SHA256 result is 32 bytes.
472        assert_eq!(h1.len(), 32);
473
474        // And sanity: the hash must differ from the plain serialized bytes
475        // — i.e. the impl actually hashes, it doesn't just forward serialize().
476        let serialized = doc_v0
477            .serialize(doc_type, &contract, platform_version)
478            .expect("serialize");
479        assert_ne!(h1, serialized);
480    }
481
482    #[test]
483    fn hash_v0_differs_between_different_documents() {
484        use crate::document::document_methods::DocumentHashV0Method;
485        use crate::tests::json_document::json_document_to_contract;
486        use platform_version::version::PlatformVersion;
487
488        let platform_version = PlatformVersion::first();
489        let contract = json_document_to_contract(
490            "../rs-drive/tests/supporting_files/contract/family/family-contract.json",
491            false,
492            platform_version,
493        )
494        .expect("family contract");
495        let doc_type = contract
496            .document_type_for_name("person")
497            .expect("person type");
498
499        use crate::data_contract::document_type::random_document::CreateRandomDocument;
500        let crate::document::Document::V0(doc_a) = doc_type
501            .random_document(Some(1), platform_version)
502            .expect("random a");
503        let crate::document::Document::V0(doc_b) = doc_type
504            .random_document(Some(2), platform_version)
505            .expect("random b");
506
507        let h_a = doc_a
508            .hash_v0(&contract, doc_type, platform_version)
509            .expect("hash a");
510        let h_b = doc_b
511            .hash_v0(&contract, doc_type, platform_version)
512            .expect("hash b");
513        assert_ne!(h_a, h_b);
514    }
515
516    // ================================================================
517    //  PartialEq: individually flip each field and assert inequality.
518    //  Exercises the derived PartialEq arm comparisons field-by-field.
519    // ================================================================
520
521    #[test]
522    fn not_equal_when_revision_differs() {
523        let a = minimal_doc();
524        let mut b = minimal_doc();
525        b.revision = Some(1);
526        assert_ne!(a, b);
527    }
528
529    #[test]
530    fn not_equal_when_each_timestamp_differs() {
531        let a = minimal_doc();
532
533        let mut b = minimal_doc();
534        b.created_at = Some(1);
535        assert_ne!(a, b);
536
537        let mut b = minimal_doc();
538        b.updated_at = Some(2);
539        assert_ne!(a, b);
540
541        let mut b = minimal_doc();
542        b.transferred_at = Some(3);
543        assert_ne!(a, b);
544
545        let mut b = minimal_doc();
546        b.created_at_block_height = Some(4);
547        assert_ne!(a, b);
548
549        let mut b = minimal_doc();
550        b.updated_at_block_height = Some(5);
551        assert_ne!(a, b);
552
553        let mut b = minimal_doc();
554        b.transferred_at_block_height = Some(6);
555        assert_ne!(a, b);
556
557        let mut b = minimal_doc();
558        b.created_at_core_block_height = Some(7);
559        assert_ne!(a, b);
560
561        let mut b = minimal_doc();
562        b.updated_at_core_block_height = Some(8);
563        assert_ne!(a, b);
564
565        let mut b = minimal_doc();
566        b.transferred_at_core_block_height = Some(9);
567        assert_ne!(a, b);
568    }
569
570    #[test]
571    fn not_equal_when_properties_differ() {
572        let a = minimal_doc();
573        let mut b = minimal_doc();
574        b.properties.insert("foo".to_string(), Value::U64(1));
575        assert_ne!(a, b);
576    }
577
578    #[test]
579    fn not_equal_when_id_differs() {
580        let a = minimal_doc();
581        let mut b = minimal_doc();
582        b.id = Identifier::new([99u8; 32]);
583        assert_ne!(a, b);
584    }
585
586    #[test]
587    fn not_equal_when_owner_id_differs() {
588        let a = minimal_doc();
589        let mut b = minimal_doc();
590        b.owner_id = Identifier::new([98u8; 32]);
591        assert_ne!(a, b);
592    }
593
594    // ================================================================
595    //  bump_revision: additional edge cases — starting at 0, and at
596    //  MAX-1 → MAX → MAX (saturating).
597    // ================================================================
598
599    #[test]
600    fn bump_revision_from_zero_increments_to_one() {
601        let mut doc = minimal_doc();
602        doc.set_revision(Some(0));
603        doc.bump_revision();
604        assert_eq!(doc.revision(), Some(1));
605    }
606
607    #[test]
608    fn bump_revision_from_max_minus_one_reaches_max_then_saturates() {
609        let mut doc = minimal_doc();
610        doc.set_revision(Some(Revision::MAX - 1));
611        doc.bump_revision();
612        assert_eq!(doc.revision(), Some(Revision::MAX));
613        doc.bump_revision();
614        assert_eq!(doc.revision(), Some(Revision::MAX));
615        // one more to make absolutely sure saturating_add really did saturate.
616        doc.bump_revision();
617        assert_eq!(doc.revision(), Some(Revision::MAX));
618    }
619
620    // ================================================================
621    //  Default + setters: mutate each setter and ensure the getter round-trips.
622    //  Exercises Setter::set_* arms that might otherwise not be executed.
623    // ================================================================
624
625    #[test]
626    fn setters_round_trip_every_field() {
627        use crate::document::{DocumentV0Getters, DocumentV0Setters};
628        let mut doc = DocumentV0::default();
629        doc.set_id(Identifier::new([1u8; 32]));
630        doc.set_owner_id(Identifier::new([2u8; 32]));
631        let mut props = BTreeMap::new();
632        props.insert("a".to_string(), Value::U64(99));
633        doc.set_properties(props.clone());
634        doc.set_revision(Some(4));
635        doc.set_created_at(Some(10));
636        doc.set_updated_at(Some(20));
637        doc.set_transferred_at(Some(30));
638        doc.set_created_at_block_height(Some(100));
639        doc.set_updated_at_block_height(Some(200));
640        doc.set_transferred_at_block_height(Some(300));
641        doc.set_created_at_core_block_height(Some(1));
642        doc.set_updated_at_core_block_height(Some(2));
643        doc.set_transferred_at_core_block_height(Some(3));
644        doc.set_creator_id(Some(Identifier::new([9u8; 32])));
645
646        assert_eq!(doc.id(), Identifier::new([1u8; 32]));
647        assert_eq!(doc.owner_id(), Identifier::new([2u8; 32]));
648        assert_eq!(doc.properties(), &props);
649        assert_eq!(doc.revision(), Some(4));
650        assert_eq!(doc.created_at(), Some(10));
651        assert_eq!(doc.updated_at(), Some(20));
652        assert_eq!(doc.transferred_at(), Some(30));
653        assert_eq!(doc.created_at_block_height(), Some(100));
654        assert_eq!(doc.updated_at_block_height(), Some(200));
655        assert_eq!(doc.transferred_at_block_height(), Some(300));
656        assert_eq!(doc.created_at_core_block_height(), Some(1));
657        assert_eq!(doc.updated_at_core_block_height(), Some(2));
658        assert_eq!(doc.transferred_at_core_block_height(), Some(3));
659        assert_eq!(doc.creator_id(), Some(Identifier::new([9u8; 32])));
660
661        // id_ref, owner_id_ref and properties_consumed exercise separate
662        // methods on DocumentV0Getters.
663        assert_eq!(doc.id_ref(), &Identifier::new([1u8; 32]));
664        assert_eq!(doc.owner_id_ref(), &Identifier::new([2u8; 32]));
665        assert_eq!(doc.clone().properties_consumed(), props);
666    }
667
668    // ================================================================
669    //  properties_mut actually allows mutation (exercises the &mut accessor
670    //  arm, not just the immutable getter).
671    // ================================================================
672
673    #[test]
674    fn properties_mut_allows_inserting_new_key() {
675        use crate::document::DocumentV0Getters;
676        let mut doc = minimal_doc();
677        doc.properties_mut().insert("k".into(), Value::U64(7));
678        assert_eq!(doc.properties().get("k"), Some(&Value::U64(7)));
679    }
680
681    // ================================================================
682    //  Debug impl: should include field names so tracing messages print
683    //  reasonable output (covers the auto-derived Debug arm without
684    //  duplicating other checks).
685    // ================================================================
686
687    #[test]
688    fn debug_format_contains_field_names() {
689        let doc = minimal_doc();
690        let dbg = format!("{:?}", doc);
691        assert!(dbg.contains("DocumentV0"), "expected struct name in Debug");
692        assert!(dbg.contains("id"));
693        assert!(dbg.contains("owner_id"));
694    }
695
696    // ================================================================
697    //  Display with transferred_at_core_block_height and creator_id set
698    //  but other transferred fields None: exercises the "Some(creator_id)
699    //  AFTER several optional system fields that ARE None" path.
700    // ================================================================
701
702    #[test]
703    fn display_with_only_creator_id_and_no_timestamps() {
704        let mut doc = minimal_doc();
705        doc.creator_id = Some(Identifier::new([7u8; 32]));
706        let s = format!("{}", doc);
707        assert!(s.contains("creator_id:"));
708        // No timestamp prefix should be rendered.
709        assert!(!s.contains("created_at:"));
710        assert!(!s.contains("updated_at:"));
711        assert!(!s.contains("transferred_at:"));
712        // With empty properties, the "no properties" trailer kicks in.
713        assert!(s.contains("no properties"));
714    }
715}