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