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