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