From 172559d7aca4905fee975b9d2229464bd2d76dc8 Mon Sep 17 00:00:00 2001 From: Apisit Ritruengroj Date: Thu, 21 Aug 2025 19:46:22 +0700 Subject: [PATCH 01/14] feat: ver --- rust/signed_doc/src/validator/mod.rs | 13 ++++-- rust/signed_doc/src/validator/rules/mod.rs | 6 ++- rust/signed_doc/src/validator/ver.rs | 53 ++++++++++++++++++++++ 3 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 rust/signed_doc/src/validator/ver.rs diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index 4a03fbfc8c..be157a3a1d 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -1,5 +1,6 @@ //! Catalyst Signed Documents validation logic +pub(crate) mod ver; pub(crate) mod json_schema; pub(crate) mod rules; pub(crate) mod utils; @@ -21,11 +22,7 @@ use crate::{ doc_types::{ BRAND_PARAMETERS, CAMPAIGN_PARAMETERS, CATEGORY_PARAMETERS, PROPOSAL, PROPOSAL_COMMENT, PROPOSAL_COMMENT_FORM_TEMPLATE, PROPOSAL_FORM_TEMPLATE, PROPOSAL_SUBMISSION_ACTION, - }, - metadata::DocType, - providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, - signature::{tbs_data, Signature}, - CatalystSignedDocument, ContentEncoding, ContentType, + }, metadata::DocType, providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, signature::{tbs_data, Signature}, CatalystSignedDocument, ContentEncoding, ContentType }; /// A table representing a full set or validation rules per document id. @@ -42,6 +39,8 @@ fn proposal_rule() -> Rules { CATEGORY_PARAMETERS.clone(), ]; Rules { + id: None, + ver: None, content_type: ContentTypeRule { exp: ContentType::Json, }, @@ -76,6 +75,8 @@ fn proposal_comment_rule() -> Rules { CATEGORY_PARAMETERS.clone(), ]; Rules { + id: None, + ver: None, content_type: ContentTypeRule { exp: ContentType::Json, }, @@ -127,6 +128,8 @@ fn proposal_submission_action_rule() -> Rules { .expect("Must be a valid json scheme file"); Rules { + id: None, + ver: None, content_type: ContentTypeRule { exp: ContentType::Json, }, diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index a2a382044f..cbd3946c35 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -3,7 +3,7 @@ use futures::FutureExt; -use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument}; +use crate::{providers::CatalystSignedDocumentProvider, validator::id_and_ver::{IdRule, VerRule}, CatalystSignedDocument}; mod content_encoding; mod content_type; @@ -25,6 +25,10 @@ pub(crate) use template::{ContentRule, ContentSchema}; /// Struct represented a full collection of rules for all fields pub(crate) struct Rules { + /// 'id' field validation rule + pub(crate) id: Option, + /// 'ver' field validation rule + pub(crate) ver: Option, /// 'content-type' field validation rule pub(crate) content_type: ContentTypeRule, /// 'content-encoding' field validation rule diff --git a/rust/signed_doc/src/validator/ver.rs b/rust/signed_doc/src/validator/ver.rs new file mode 100644 index 0000000000..0eb251e32d --- /dev/null +++ b/rust/signed_doc/src/validator/ver.rs @@ -0,0 +1,53 @@ +//! Validator for Signed Document Version + +use std::time::{Duration, SystemTime}; + +use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument}; + +pub(crate) struct VerRule; + +impl VerRule { + pub(crate) fn check( + &self, + doc: &CatalystSignedDocument, + _provider: &Provider, + ) -> anyhow::Result + where + Provider: CatalystSignedDocumentProvider, + { + let id = doc.doc_id().ok(); + let ver = doc.doc_ver().ok(); + + if id.is_none() { + doc.report().missing_field( + "id", + "Can't get a document id during the validation process", + ); + } + if ver.is_none() { + doc.report().missing_field( + "ver", + "Can't get a document ver during the validation process", + ); + } + + match (id, ver) { + (Some(id), Some(ver)) => { + if ver < id { + doc.report().invalid_value( + "ver", + &ver.to_string(), + "ver < id", + &format!( + "Document Version {ver} cannot be smaller than Document ID {id}" + ), + ); + Ok(false) + } else { + Ok(true) + } + } + _ => Ok(false), + } + } +} From 93644b3b29e3b9a095606b3f97e0b43bb5b7f845 Mon Sep 17 00:00:00 2001 From: Apisit Ritruengroj Date: Fri, 22 Aug 2025 21:14:54 +0700 Subject: [PATCH 02/14] chore: check version --- rust/signed_doc/src/validator/id.rs | 1 + rust/signed_doc/src/validator/mod.rs | 9 +++- rust/signed_doc/src/validator/rules/mod.rs | 6 ++- rust/signed_doc/src/validator/ver.rs | 53 ++++++++-------------- 4 files changed, 33 insertions(+), 36 deletions(-) create mode 100644 rust/signed_doc/src/validator/id.rs diff --git a/rust/signed_doc/src/validator/id.rs b/rust/signed_doc/src/validator/id.rs new file mode 100644 index 0000000000..3b1febc6ce --- /dev/null +++ b/rust/signed_doc/src/validator/id.rs @@ -0,0 +1 @@ +pub(crate) struct IdRule; diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index be157a3a1d..66a9392d3e 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -1,9 +1,10 @@ //! Catalyst Signed Documents validation logic -pub(crate) mod ver; +pub(crate) mod id; pub(crate) mod json_schema; pub(crate) mod rules; pub(crate) mod utils; +pub(crate) mod ver; use std::{ collections::HashMap, @@ -22,7 +23,11 @@ use crate::{ doc_types::{ BRAND_PARAMETERS, CAMPAIGN_PARAMETERS, CATEGORY_PARAMETERS, PROPOSAL, PROPOSAL_COMMENT, PROPOSAL_COMMENT_FORM_TEMPLATE, PROPOSAL_FORM_TEMPLATE, PROPOSAL_SUBMISSION_ACTION, - }, metadata::DocType, providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, signature::{tbs_data, Signature}, CatalystSignedDocument, ContentEncoding, ContentType + }, + metadata::DocType, + providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, + signature::{tbs_data, Signature}, + CatalystSignedDocument, ContentEncoding, ContentType, }; /// A table representing a full set or validation rules per document id. diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index cbd3946c35..da85e5ca23 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -3,7 +3,11 @@ use futures::FutureExt; -use crate::{providers::CatalystSignedDocumentProvider, validator::id_and_ver::{IdRule, VerRule}, CatalystSignedDocument}; +use crate::{ + providers::CatalystSignedDocumentProvider, + validator::{id::IdRule, ver::VerRule}, + CatalystSignedDocument, +}; mod content_encoding; mod content_type; diff --git a/rust/signed_doc/src/validator/ver.rs b/rust/signed_doc/src/validator/ver.rs index 0eb251e32d..c62b799cab 100644 --- a/rust/signed_doc/src/validator/ver.rs +++ b/rust/signed_doc/src/validator/ver.rs @@ -1,53 +1,40 @@ //! Validator for Signed Document Version -use std::time::{Duration, SystemTime}; - -use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument}; +use crate::CatalystSignedDocument; pub(crate) struct VerRule; impl VerRule { - pub(crate) fn check( + pub(crate) fn check( &self, doc: &CatalystSignedDocument, - _provider: &Provider, - ) -> anyhow::Result - where - Provider: CatalystSignedDocumentProvider, - { - let id = doc.doc_id().ok(); - let ver = doc.doc_ver().ok(); - - if id.is_none() { + ) -> anyhow::Result { + let Ok(id) = doc.doc_id() else { doc.report().missing_field( "id", "Can't get a document id during the validation process", ); - } - if ver.is_none() { + return Ok(false); + }; + + let Ok(ver) = doc.doc_ver() else { doc.report().missing_field( "ver", "Can't get a document ver during the validation process", ); - } + return Ok(false); + }; - match (id, ver) { - (Some(id), Some(ver)) => { - if ver < id { - doc.report().invalid_value( - "ver", - &ver.to_string(), - "ver < id", - &format!( - "Document Version {ver} cannot be smaller than Document ID {id}" - ), - ); - Ok(false) - } else { - Ok(true) - } - } - _ => Ok(false), + if ver < id { + doc.report().invalid_value( + "ver", + &ver.to_string(), + "ver < id", + &format!("Document Version {ver} cannot be smaller than Document ID {id}"), + ); + Ok(false) + } else { + Ok(true) } } } From 384776b863cdb706e4c87d3abaaf122e72b9deeb Mon Sep 17 00:00:00 2001 From: Apisit Ritruengroj Date: Fri, 22 Aug 2025 21:30:37 +0700 Subject: [PATCH 03/14] feat: id --- rust/signed_doc/src/validator/id.rs | 91 ++++++++++++++++++++++++++++ rust/signed_doc/src/validator/ver.rs | 14 +++-- 2 files changed, 99 insertions(+), 6 deletions(-) diff --git a/rust/signed_doc/src/validator/id.rs b/rust/signed_doc/src/validator/id.rs index 3b1febc6ce..d739103011 100644 --- a/rust/signed_doc/src/validator/id.rs +++ b/rust/signed_doc/src/validator/id.rs @@ -1 +1,92 @@ +//! Validator for Signed Document ID + +use std::time::{Duration, SystemTime}; + +use anyhow::Context; + +use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument}; + +/// Signed Document `id` field validation rule pub(crate) struct IdRule; + +impl IdRule { + /// Validates document `id` field on the timestamps: + /// 1. If `provider.future_threshold()` not `None`, document `id` cannot be too far in + /// the future (`future_threshold` arg) from `SystemTime::now()` based on the + /// provide threshold + /// 2. If `provider.future_threshold()` not `None`, document `id` cannot be too far + /// behind (`past_threshold` arg) from `SystemTime::now()` based on the provide + /// threshold + pub(crate) fn check( + self, + doc: &CatalystSignedDocument, + provider: &Provider, + ) -> anyhow::Result + where + Provider: CatalystSignedDocumentProvider, + { + let Ok(id) = doc.doc_id() else { + doc.report().missing_field( + "id", + "Cannot get the document field during the field validation", + ); + return Ok(false); + }; + + let mut is_valid = true; + + let (id_time_secs, id_time_nanos) = id + .uuid() + .get_timestamp() + .ok_or(anyhow::anyhow!("Document id field must be a UUIDv7"))? + .to_unix(); + + let Some(id_time) = + SystemTime::UNIX_EPOCH.checked_add(Duration::new(id_time_secs, id_time_nanos)) + else { + doc.report().invalid_value( + "id", + &id.to_string(), + "Must a valid duration since `UNIX_EPOCH`", + "Cannot instantiate a valid `SystemTime` value from the provided `id` field timestamp.", + ); + return Ok(false); + }; + + let now = SystemTime::now(); + + if let Ok(id_age) = id_time.duration_since(now) { + // `now` is earlier than `id_time` + if let Some(future_threshold) = provider.future_threshold() { + if id_age > future_threshold { + doc.report().invalid_value( + "id", + &id.to_string(), + "id < now + future_threshold", + &format!("Document Version timestamp {id} cannot be too far in future (threshold: {future_threshold:?}) from now: {now:?}"), + ); + is_valid = false; + } + } + } else { + // `id_time` is earlier than `now` + let id_age = now + .duration_since(id_time) + .context("BUG! `id_time` must be earlier than `now` at this place")?; + + if let Some(past_threshold) = provider.past_threshold() { + if id_age > past_threshold { + doc.report().invalid_value( + "id", + &id.to_string(), + "id > now - past_threshold", + &format!("Document Version timestamp {id} cannot be too far behind (threshold: {past_threshold:?}) from now: {now:?}",), + ); + is_valid = false; + } + } + } + + Ok(is_valid) + } +} diff --git a/rust/signed_doc/src/validator/ver.rs b/rust/signed_doc/src/validator/ver.rs index c62b799cab..0fb1f60c5b 100644 --- a/rust/signed_doc/src/validator/ver.rs +++ b/rust/signed_doc/src/validator/ver.rs @@ -2,9 +2,12 @@ use crate::CatalystSignedDocument; +/// Signed Document `ver` field validation rule pub(crate) struct VerRule; impl VerRule { + /// Validates document `ver` field on the timestamps: + /// 1. document `ver` cannot be smaller than document `id` field pub(crate) fn check( &self, doc: &CatalystSignedDocument, @@ -12,15 +15,14 @@ impl VerRule { let Ok(id) = doc.doc_id() else { doc.report().missing_field( "id", - "Can't get a document id during the validation process", + "Cannot get the document field during the field validation", ); return Ok(false); }; - let Ok(ver) = doc.doc_ver() else { doc.report().missing_field( "ver", - "Can't get a document ver during the validation process", + "Cannot get the document field during the field validation", ); return Ok(false); }; @@ -32,9 +34,9 @@ impl VerRule { "ver < id", &format!("Document Version {ver} cannot be smaller than Document ID {id}"), ); - Ok(false) - } else { - Ok(true) + return Ok(false); } + + Ok(true) } } From 17ac954801ebbd6ec59b2a1e8e38d831f1a33220 Mon Sep 17 00:00:00 2001 From: Apisit Ritruengroj Date: Fri, 22 Aug 2025 22:18:09 +0700 Subject: [PATCH 04/14] finalize --- rust/signed_doc/src/validator/id.rs | 7 +- rust/signed_doc/src/validator/mod.rs | 134 +++------------------ rust/signed_doc/src/validator/rules/mod.rs | 12 ++ rust/signed_doc/src/validator/ver.rs | 3 +- 4 files changed, 36 insertions(+), 120 deletions(-) diff --git a/rust/signed_doc/src/validator/id.rs b/rust/signed_doc/src/validator/id.rs index d739103011..8cbeb5fe75 100644 --- a/rust/signed_doc/src/validator/id.rs +++ b/rust/signed_doc/src/validator/id.rs @@ -17,8 +17,9 @@ impl IdRule { /// 2. If `provider.future_threshold()` not `None`, document `id` cannot be too far /// behind (`past_threshold` arg) from `SystemTime::now()` based on the provide /// threshold - pub(crate) fn check( - self, + #[allow(clippy::unused_async)] + pub(crate) async fn check( + &self, doc: &CatalystSignedDocument, provider: &Provider, ) -> anyhow::Result @@ -38,7 +39,7 @@ impl IdRule { let (id_time_secs, id_time_nanos) = id .uuid() .get_timestamp() - .ok_or(anyhow::anyhow!("Document id field must be a UUIDv7"))? + .ok_or(anyhow::anyhow!("Document `id` field must be a UUIDv7"))? .to_unix(); let Some(id_time) = diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index 66a9392d3e..206e162097 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -9,7 +9,6 @@ pub(crate) mod ver; use std::{ collections::HashMap, sync::{Arc, LazyLock}, - time::{Duration, SystemTime}, }; use anyhow::Context; @@ -199,10 +198,6 @@ where return Ok(false); }; - if !validate_id_and_ver(doc, provider)? { - return Ok(false); - } - let Some(rules) = DOCUMENT_RULES.get(doc_type) else { doc.report().invalid_value( "`type`", @@ -215,106 +210,6 @@ where rules.check(doc, provider).await } -/// Validates document `id` and `ver` fields on the timestamps: -/// 1. document `ver` cannot be smaller than document id field -/// 2. If `provider.future_threshold()` not `None`, document `id` cannot be too far in the -/// future (`future_threshold` arg) from `SystemTime::now()` based on the provide -/// threshold -/// 3. If `provider.future_threshold()` not `None`, document `id` cannot be too far behind -/// (`past_threshold` arg) from `SystemTime::now()` based on the provide threshold -fn validate_id_and_ver( - doc: &CatalystSignedDocument, - provider: &Provider, -) -> anyhow::Result -where - Provider: CatalystSignedDocumentProvider, -{ - let id = doc.doc_id().ok(); - let ver = doc.doc_ver().ok(); - if id.is_none() { - doc.report().missing_field( - "id", - "Can't get a document id during the validation process", - ); - } - if ver.is_none() { - doc.report().missing_field( - "ver", - "Can't get a document ver during the validation process", - ); - } - match (id, ver) { - (Some(id), Some(ver)) => { - let mut is_valid = true; - if ver < id { - doc.report().invalid_value( - "ver", - &ver.to_string(), - "ver < id", - &format!("Document Version {ver} cannot be smaller than Document ID {id}"), - ); - is_valid = false; - } - - let (ver_time_secs, ver_time_nanos) = ver - .uuid() - .get_timestamp() - .ok_or(anyhow::anyhow!("Document ver field must be a UUIDv7"))? - .to_unix(); - - let Some(ver_time) = - SystemTime::UNIX_EPOCH.checked_add(Duration::new(ver_time_secs, ver_time_nanos)) - else { - doc.report().invalid_value( - "ver", - &ver.to_string(), - "Must a valid duration since `UNIX_EPOCH`", - "Cannot instantiate a valid `SystemTime` value from the provided `ver` field timestamp.", - ); - return Ok(false); - }; - - let now = SystemTime::now(); - - if let Ok(version_age) = ver_time.duration_since(now) { - // `now` is earlier than `ver_time` - if let Some(future_threshold) = provider.future_threshold() { - if version_age > future_threshold { - doc.report().invalid_value( - "ver", - &ver.to_string(), - "ver < now + future_threshold", - &format!("Document Version timestamp {id} cannot be too far in future (threshold: {future_threshold:?}) from now: {now:?}"), - ); - is_valid = false; - } - } - } else { - // `ver_time` is earlier than `now` - let version_age = now - .duration_since(ver_time) - .context("BUG! `ver_time` must be earlier than `now` at this place")?; - - if let Some(past_threshold) = provider.past_threshold() { - if version_age > past_threshold { - doc.report().invalid_value( - "ver", - &ver.to_string(), - "ver > now - past_threshold", - &format!("Document Version timestamp {id} cannot be too far behind (threshold: {past_threshold:?}) from now: {now:?}",), - ); - is_valid = false; - } - } - } - - Ok(is_valid) - }, - - _ => Ok(false), - } -} - /// Verify document signatures. /// Return true if all signatures are valid, otherwise return false. /// @@ -401,12 +296,12 @@ mod tests { builder::tests::Builder, metadata::SupportedField, providers::{tests::TestCatalystSignedDocumentProvider, CatalystSignedDocumentProvider}, - validator::{document_rules_init, validate_id_and_ver}, + validator::{document_rules_init, id::IdRule, ver::VerRule}, UuidV7, }; - #[test] - fn document_id_and_ver_test() { + #[tokio::test] + async fn document_id_and_ver_test() { let provider = TestCatalystSignedDocumentProvider::default(); let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -419,8 +314,9 @@ mod tests { .with_metadata_field(SupportedField::Ver(uuid_v7)) .build(); - let is_valid = validate_id_and_ver(&doc, &provider).unwrap(); - assert!(is_valid); + let is_id_valid = IdRule.check(&doc, &provider).await.unwrap(); + let is_ver_valid = VerRule.check(&doc).await.unwrap(); + assert!(is_id_valid && is_ver_valid); let ver = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) .try_into() @@ -434,8 +330,10 @@ mod tests { .with_metadata_field(SupportedField::Ver(ver)) .build(); - let is_valid = validate_id_and_ver(&doc, &provider).unwrap(); - assert!(!is_valid); + let is_id_valid = IdRule.check(&doc, &provider).await.unwrap(); + let is_ver_valid = VerRule.check(&doc).await.unwrap(); + assert!(is_id_valid); + assert!(!is_ver_valid); let to_far_in_past = Uuid::new_v7(Timestamp::from_unix_time( now - provider.past_threshold().unwrap().as_secs() - 1, @@ -450,8 +348,10 @@ mod tests { .with_metadata_field(SupportedField::Ver(to_far_in_past)) .build(); - let is_valid = validate_id_and_ver(&doc, &provider).unwrap(); - assert!(!is_valid); + let is_id_valid = IdRule.check(&doc, &provider).await.unwrap(); + let is_ver_valid = VerRule.check(&doc).await.unwrap(); + assert!(!is_id_valid); + assert!(is_ver_valid); let to_far_in_future = Uuid::new_v7(Timestamp::from_unix_time( now + provider.future_threshold().unwrap().as_secs() + 1, @@ -466,8 +366,10 @@ mod tests { .with_metadata_field(SupportedField::Ver(to_far_in_future)) .build(); - let is_valid = validate_id_and_ver(&doc, &provider).unwrap(); - assert!(!is_valid); + let is_id_valid = IdRule.check(&doc, &provider).await.unwrap(); + let is_ver_valid = VerRule.check(&doc).await.unwrap(); + assert!(!is_id_valid); + assert!(is_ver_valid); } #[test] diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index da85e5ca23..9fb35a17c7 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -62,6 +62,12 @@ impl Rules { Provider: CatalystSignedDocumentProvider, { let rules = [ + self.id + .as_ref() + .map_or_else(|| pass().boxed(), |rule| rule.check(doc, provider).boxed()), + self.ver + .as_ref() + .map_or_else(|| pass().boxed(), |rule| rule.check(doc).boxed()), self.content_type.check(doc).boxed(), self.content_encoding.check(doc).boxed(), self.content.check(doc, provider).boxed(), @@ -81,3 +87,9 @@ impl Rules { Ok(res) } } + +/// An async no-op function to pass the rule validation. +#[allow(clippy::unused_async)] +pub async fn pass() -> anyhow::Result { + Ok(true) +} diff --git a/rust/signed_doc/src/validator/ver.rs b/rust/signed_doc/src/validator/ver.rs index 0fb1f60c5b..24b8ea521d 100644 --- a/rust/signed_doc/src/validator/ver.rs +++ b/rust/signed_doc/src/validator/ver.rs @@ -8,7 +8,8 @@ pub(crate) struct VerRule; impl VerRule { /// Validates document `ver` field on the timestamps: /// 1. document `ver` cannot be smaller than document `id` field - pub(crate) fn check( + #[allow(clippy::unused_async)] + pub(crate) async fn check( &self, doc: &CatalystSignedDocument, ) -> anyhow::Result { From e1a0043892e591f670c47a89529e9cb3dd32b400 Mon Sep 17 00:00:00 2001 From: Apisit Ritruengroj Date: Fri, 22 Aug 2025 22:21:13 +0700 Subject: [PATCH 05/14] feat: apply rules --- rust/signed_doc/src/validator/mod.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index 206e162097..0bc07ab43a 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -22,11 +22,7 @@ use crate::{ doc_types::{ BRAND_PARAMETERS, CAMPAIGN_PARAMETERS, CATEGORY_PARAMETERS, PROPOSAL, PROPOSAL_COMMENT, PROPOSAL_COMMENT_FORM_TEMPLATE, PROPOSAL_FORM_TEMPLATE, PROPOSAL_SUBMISSION_ACTION, - }, - metadata::DocType, - providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, - signature::{tbs_data, Signature}, - CatalystSignedDocument, ContentEncoding, ContentType, + }, metadata::DocType, providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, signature::{tbs_data, Signature}, validator::{id::IdRule, ver::VerRule}, CatalystSignedDocument, ContentEncoding, ContentType }; /// A table representing a full set or validation rules per document id. @@ -43,8 +39,8 @@ fn proposal_rule() -> Rules { CATEGORY_PARAMETERS.clone(), ]; Rules { - id: None, - ver: None, + id: Some(IdRule), + ver: Some(VerRule), content_type: ContentTypeRule { exp: ContentType::Json, }, @@ -79,8 +75,8 @@ fn proposal_comment_rule() -> Rules { CATEGORY_PARAMETERS.clone(), ]; Rules { - id: None, - ver: None, + id: Some(IdRule), + ver: Some(VerRule), content_type: ContentTypeRule { exp: ContentType::Json, }, @@ -132,8 +128,8 @@ fn proposal_submission_action_rule() -> Rules { .expect("Must be a valid json scheme file"); Rules { - id: None, - ver: None, + id: Some(IdRule), + ver: Some(VerRule), content_type: ContentTypeRule { exp: ContentType::Json, }, From bf994393f0dab3b6a1c6fd974641c739c0e2adb6 Mon Sep 17 00:00:00 2001 From: Apisit Ritruengroj Date: Fri, 22 Aug 2025 22:21:35 +0700 Subject: [PATCH 06/14] chore: fmtfix --- rust/signed_doc/src/validator/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index 0bc07ab43a..b38f41ae99 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -22,7 +22,12 @@ use crate::{ doc_types::{ BRAND_PARAMETERS, CAMPAIGN_PARAMETERS, CATEGORY_PARAMETERS, PROPOSAL, PROPOSAL_COMMENT, PROPOSAL_COMMENT_FORM_TEMPLATE, PROPOSAL_FORM_TEMPLATE, PROPOSAL_SUBMISSION_ACTION, - }, metadata::DocType, providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, signature::{tbs_data, Signature}, validator::{id::IdRule, ver::VerRule}, CatalystSignedDocument, ContentEncoding, ContentType + }, + metadata::DocType, + providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, + signature::{tbs_data, Signature}, + validator::{id::IdRule, ver::VerRule}, + CatalystSignedDocument, ContentEncoding, ContentType, }; /// A table representing a full set or validation rules per document id. From 3e698d43770ff9df7f351e92731d9e5d918ac542 Mon Sep 17 00:00:00 2001 From: Apisit Ritruengroj Date: Fri, 22 Aug 2025 22:24:03 +0700 Subject: [PATCH 07/14] refactor: move to rules --- rust/signed_doc/src/validator/mod.rs | 12 ++++++------ rust/signed_doc/src/validator/{ => rules}/id.rs | 0 rust/signed_doc/src/validator/rules/mod.rs | 10 +++++----- rust/signed_doc/src/validator/{ => rules}/ver.rs | 0 4 files changed, 11 insertions(+), 11 deletions(-) rename rust/signed_doc/src/validator/{ => rules}/id.rs (100%) rename rust/signed_doc/src/validator/{ => rules}/ver.rs (100%) diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index b38f41ae99..adc2061f84 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -1,10 +1,8 @@ //! Catalyst Signed Documents validation logic -pub(crate) mod id; pub(crate) mod json_schema; pub(crate) mod rules; pub(crate) mod utils; -pub(crate) mod ver; use std::{ collections::HashMap, @@ -14,8 +12,8 @@ use std::{ use anyhow::Context; use catalyst_types::{catalyst_id::role_index::RoleId, problem_report::ProblemReport}; use rules::{ - ContentEncodingRule, ContentRule, ContentSchema, ContentTypeRule, ParametersRule, RefRule, - ReplyRule, Rules, SectionRule, SignatureKidRule, + ContentEncodingRule, ContentRule, ContentSchema, ContentTypeRule, IdRule, ParametersRule, + RefRule, ReplyRule, Rules, SectionRule, SignatureKidRule, VerRule, }; use crate::{ @@ -26,7 +24,6 @@ use crate::{ metadata::DocType, providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, signature::{tbs_data, Signature}, - validator::{id::IdRule, ver::VerRule}, CatalystSignedDocument, ContentEncoding, ContentType, }; @@ -297,7 +294,10 @@ mod tests { builder::tests::Builder, metadata::SupportedField, providers::{tests::TestCatalystSignedDocumentProvider, CatalystSignedDocumentProvider}, - validator::{document_rules_init, id::IdRule, ver::VerRule}, + validator::{ + document_rules_init, + rules::{IdRule, VerRule}, + }, UuidV7, }; diff --git a/rust/signed_doc/src/validator/id.rs b/rust/signed_doc/src/validator/rules/id.rs similarity index 100% rename from rust/signed_doc/src/validator/id.rs rename to rust/signed_doc/src/validator/rules/id.rs diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index 9fb35a17c7..680a77a378 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -3,29 +3,29 @@ use futures::FutureExt; -use crate::{ - providers::CatalystSignedDocumentProvider, - validator::{id::IdRule, ver::VerRule}, - CatalystSignedDocument, -}; +use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument}; mod content_encoding; mod content_type; mod doc_ref; +mod id; mod parameters; mod reply; mod section; mod signature_kid; mod template; +mod ver; pub(crate) use content_encoding::ContentEncodingRule; pub(crate) use content_type::ContentTypeRule; pub(crate) use doc_ref::RefRule; +pub(crate) use id::IdRule; pub(crate) use parameters::ParametersRule; pub(crate) use reply::ReplyRule; pub(crate) use section::SectionRule; pub(crate) use signature_kid::SignatureKidRule; pub(crate) use template::{ContentRule, ContentSchema}; +pub(crate) use ver::VerRule; /// Struct represented a full collection of rules for all fields pub(crate) struct Rules { diff --git a/rust/signed_doc/src/validator/ver.rs b/rust/signed_doc/src/validator/rules/ver.rs similarity index 100% rename from rust/signed_doc/src/validator/ver.rs rename to rust/signed_doc/src/validator/rules/ver.rs From a0277fbc30e11f7a7e813ebf63a705b69bd9dc7e Mon Sep 17 00:00:00 2001 From: Apisit Ritruengroj Date: Fri, 22 Aug 2025 22:29:38 +0700 Subject: [PATCH 08/14] chore: minor doc --- rust/signed_doc/src/validator/rules/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index 680a77a378..fca33ae142 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -88,7 +88,7 @@ impl Rules { } } -/// An async no-op function to pass the rule validation. +/// An async no-op function to pass a rule validation. #[allow(clippy::unused_async)] pub async fn pass() -> anyhow::Result { Ok(true) From 210a21e029517629dae55204c70a35d8b7724944 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 27 Aug 2025 13:01:07 +0400 Subject: [PATCH 09/14] move unit tests for IdRule and VerRule --- rust/signed_doc/src/validator/mod.rs | 99 ++-------------------- rust/signed_doc/src/validator/rules/id.rs | 85 +++++++++++++++++++ rust/signed_doc/src/validator/rules/mod.rs | 18 +--- rust/signed_doc/src/validator/rules/ver.rs | 87 +++++++++++++++++++ 4 files changed, 183 insertions(+), 106 deletions(-) diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index a1eebc37e1..4d370da1c9 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -40,8 +40,8 @@ fn proposal_rule() -> Rules { CATEGORY_PARAMETERS.clone(), ]; Rules { - id: Some(IdRule), - ver: Some(VerRule), + id: IdRule, + ver: VerRule, content_type: ContentTypeRule { exp: ContentType::Json, }, @@ -76,8 +76,8 @@ fn proposal_comment_rule() -> Rules { CATEGORY_PARAMETERS.clone(), ]; Rules { - id: Some(IdRule), - ver: Some(VerRule), + id: IdRule, + ver: VerRule, content_type: ContentTypeRule { exp: ContentType::Json, }, @@ -129,8 +129,8 @@ fn proposal_submission_action_rule() -> Rules { .expect("Must be a valid json scheme file"); Rules { - id: Some(IdRule), - ver: Some(VerRule), + id: IdRule, + ver: VerRule, content_type: ContentTypeRule { exp: ContentType::Json, }, @@ -285,92 +285,7 @@ where #[cfg(test)] mod tests { - use std::time::SystemTime; - - use uuid::{Timestamp, Uuid}; - - use crate::{ - builder::tests::Builder, - metadata::SupportedField, - providers::{tests::TestCatalystSignedDocumentProvider, CatalystSignedDocumentProvider}, - validator::{ - document_rules_init, - rules::{IdRule, VerRule}, - }, - UuidV7, - }; - - #[tokio::test] - async fn document_id_and_ver_test() { - let provider = TestCatalystSignedDocumentProvider::default(); - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(); - - let uuid_v7 = UuidV7::new(); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(uuid_v7)) - .with_metadata_field(SupportedField::Ver(uuid_v7)) - .build(); - - let is_id_valid = IdRule.check(&doc, &provider).await.unwrap(); - let is_ver_valid = VerRule.check(&doc).await.unwrap(); - assert!(is_id_valid && is_ver_valid); - - let ver = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) - .try_into() - .unwrap(); - let id = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) - .try_into() - .unwrap(); - assert!(ver < id); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(id)) - .with_metadata_field(SupportedField::Ver(ver)) - .build(); - - let is_id_valid = IdRule.check(&doc, &provider).await.unwrap(); - let is_ver_valid = VerRule.check(&doc).await.unwrap(); - assert!(is_id_valid); - assert!(!is_ver_valid); - - let to_far_in_past = Uuid::new_v7(Timestamp::from_unix_time( - now - provider.past_threshold().unwrap().as_secs() - 1, - 0, - 0, - 0, - )) - .try_into() - .unwrap(); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(to_far_in_past)) - .with_metadata_field(SupportedField::Ver(to_far_in_past)) - .build(); - - let is_id_valid = IdRule.check(&doc, &provider).await.unwrap(); - let is_ver_valid = VerRule.check(&doc).await.unwrap(); - assert!(!is_id_valid); - assert!(is_ver_valid); - - let to_far_in_future = Uuid::new_v7(Timestamp::from_unix_time( - now + provider.future_threshold().unwrap().as_secs() + 1, - 0, - 0, - 0, - )) - .try_into() - .unwrap(); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(to_far_in_future)) - .with_metadata_field(SupportedField::Ver(to_far_in_future)) - .build(); - - let is_id_valid = IdRule.check(&doc, &provider).await.unwrap(); - let is_ver_valid = VerRule.check(&doc).await.unwrap(); - assert!(!is_id_valid); - assert!(is_ver_valid); - } + use crate::validator::document_rules_init; #[test] fn document_rules_init_test() { diff --git a/rust/signed_doc/src/validator/rules/id.rs b/rust/signed_doc/src/validator/rules/id.rs index 8cbeb5fe75..55e5be4632 100644 --- a/rust/signed_doc/src/validator/rules/id.rs +++ b/rust/signed_doc/src/validator/rules/id.rs @@ -91,3 +91,88 @@ impl IdRule { Ok(is_valid) } } + +#[cfg(test)] +mod tests { + use std::time::SystemTime; + + use test_case::test_case; + use uuid::{Timestamp, Uuid}; + + use super::*; + use crate::{ + builder::tests::Builder, metadata::SupportedField, + providers::tests::TestCatalystSignedDocumentProvider, UuidV7, + }; + + #[test_case( + |_| { + let uuid_v7 = UuidV7::new(); + Builder::new() + .with_metadata_field(SupportedField::Id(uuid_v7)) + .build() + } + => true; + "valid id" + )] + #[test_case( + |provider| { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let to_far_in_past = Uuid::new_v7(Timestamp::from_unix_time( + now - provider.past_threshold().unwrap().as_secs() - 1, + 0, + 0, + 0, + )) + .try_into() + .unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Id(to_far_in_past)) + .build() + } + => false; + "`id` to far in past" + )] + #[test_case( + |provider| { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let to_far_in_future = Uuid::new_v7(Timestamp::from_unix_time( + now + provider.future_threshold().unwrap().as_secs() + 1, + 0, + 0, + 0, + )) + .try_into() + .unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Id(to_far_in_future)) + .build() + } + => false; + "`id` to far in future" + )] + #[test_case( + |_| { + Builder::new() + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .build() + } + => false; + "missing `id` field" + )] + #[tokio::test] + async fn id_test( + doc_gen: impl FnOnce(&TestCatalystSignedDocumentProvider) -> CatalystSignedDocument + ) -> bool { + let provider = TestCatalystSignedDocumentProvider::default(); + let doc = doc_gen(&provider); + + IdRule.check(&doc, &provider).await.unwrap() + } +} diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index fca33ae142..65822eee79 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -30,9 +30,9 @@ pub(crate) use ver::VerRule; /// Struct represented a full collection of rules for all fields pub(crate) struct Rules { /// 'id' field validation rule - pub(crate) id: Option, + pub(crate) id: IdRule, /// 'ver' field validation rule - pub(crate) ver: Option, + pub(crate) ver: VerRule, /// 'content-type' field validation rule pub(crate) content_type: ContentTypeRule, /// 'content-encoding' field validation rule @@ -62,12 +62,8 @@ impl Rules { Provider: CatalystSignedDocumentProvider, { let rules = [ - self.id - .as_ref() - .map_or_else(|| pass().boxed(), |rule| rule.check(doc, provider).boxed()), - self.ver - .as_ref() - .map_or_else(|| pass().boxed(), |rule| rule.check(doc).boxed()), + self.id.check(doc, provider).boxed(), + self.ver.check(doc).boxed(), self.content_type.check(doc).boxed(), self.content_encoding.check(doc).boxed(), self.content.check(doc, provider).boxed(), @@ -87,9 +83,3 @@ impl Rules { Ok(res) } } - -/// An async no-op function to pass a rule validation. -#[allow(clippy::unused_async)] -pub async fn pass() -> anyhow::Result { - Ok(true) -} diff --git a/rust/signed_doc/src/validator/rules/ver.rs b/rust/signed_doc/src/validator/rules/ver.rs index 24b8ea521d..905040aa7b 100644 --- a/rust/signed_doc/src/validator/rules/ver.rs +++ b/rust/signed_doc/src/validator/rules/ver.rs @@ -41,3 +41,90 @@ impl VerRule { Ok(true) } } + +#[cfg(test)] +mod tests { + use std::time::SystemTime; + + use test_case::test_case; + use uuid::{Timestamp, Uuid}; + + use super::*; + use crate::{builder::tests::Builder, metadata::SupportedField, UuidV7}; + + #[test_case( + || { + let uuid_v7 = UuidV7::new(); + Builder::new() + .with_metadata_field(SupportedField::Id(uuid_v7)) + .with_metadata_field(SupportedField::Ver(uuid_v7)) + .build() + } + => true; + "`ver` and `id` are equal" + )] + #[test_case( + || { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let ver = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) + .try_into() + .unwrap(); + let id = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) + .try_into() + .unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(ver)) + .build() + } + => true; + "`ver` greater than `id` are equal" + )] + #[test_case( + || { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let ver = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) + .try_into() + .unwrap(); + let id = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) + .try_into() + .unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(ver)) + .build() + } + => false; + "`ver` less than `id` are equal" + )] + #[test_case( + || { + Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .build() + } + => false; + "missing `ver` field" + )] + #[test_case( + || { + Builder::new() + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .build() + } + => false; + "missing `id` field" + )] + #[tokio::test] + async fn ver_test(doc_gen: impl FnOnce() -> CatalystSignedDocument) -> bool { + let doc = doc_gen(); + + VerRule.check(&doc).await.unwrap() + } +} From 053412d6c7622a255530d99e7fecce54376c4165 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 27 Aug 2025 13:19:35 +0400 Subject: [PATCH 10/14] update VerRule validation --- rust/signed_doc/src/validator/rules/mod.rs | 2 +- rust/signed_doc/src/validator/rules/ver.rs | 95 +++++++++++++++++----- 2 files changed, 77 insertions(+), 20 deletions(-) diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index 65822eee79..be934089ab 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -63,7 +63,7 @@ impl Rules { { let rules = [ self.id.check(doc, provider).boxed(), - self.ver.check(doc).boxed(), + self.ver.check(doc, provider).boxed(), self.content_type.check(doc).boxed(), self.content_encoding.check(doc).boxed(), self.content.check(doc, provider).boxed(), diff --git a/rust/signed_doc/src/validator/rules/ver.rs b/rust/signed_doc/src/validator/rules/ver.rs index 905040aa7b..484b3e94f8 100644 --- a/rust/signed_doc/src/validator/rules/ver.rs +++ b/rust/signed_doc/src/validator/rules/ver.rs @@ -1,6 +1,8 @@ //! Validator for Signed Document Version -use crate::CatalystSignedDocument; +use crate::{ + providers::CatalystSignedDocumentProvider, CatalystSignedDocument, DocLocator, DocumentRef, +}; /// Signed Document `ver` field validation rule pub(crate) struct VerRule; @@ -9,10 +11,14 @@ impl VerRule { /// Validates document `ver` field on the timestamps: /// 1. document `ver` cannot be smaller than document `id` field #[allow(clippy::unused_async)] - pub(crate) async fn check( + pub(crate) async fn check( &self, doc: &CatalystSignedDocument, - ) -> anyhow::Result { + provider: &Provider, + ) -> anyhow::Result + where + Provider: CatalystSignedDocumentProvider, + { let Ok(id) = doc.doc_id() else { doc.report().missing_field( "id", @@ -28,6 +34,8 @@ impl VerRule { return Ok(false); }; + let mut is_valid = true; + if ver < id { doc.report().invalid_value( "ver", @@ -35,10 +43,21 @@ impl VerRule { "ver < id", &format!("Document Version {ver} cannot be smaller than Document ID {id}"), ); - return Ok(false); + is_valid = false; + } + + if ver != id { + let first_submited_doc = DocumentRef::new(id, id, DocLocator::default()); + if provider.try_get_doc(&first_submited_doc).await?.is_none() { + doc.report().functional_validation( + &format!("`ver` and `id` are not equal, ver: {ver}, id: {id}. Document with `id` and `ver` being equal MUST exist"), + "Cannot get a first version document from the provider, document for which `id` and `ver` are equal.", + ); + is_valid = false; + } } - Ok(true) + Ok(is_valid) } } @@ -50,10 +69,13 @@ mod tests { use uuid::{Timestamp, Uuid}; use super::*; - use crate::{builder::tests::Builder, metadata::SupportedField, UuidV7}; + use crate::{ + builder::tests::Builder, metadata::SupportedField, + providers::tests::TestCatalystSignedDocumentProvider, UuidV7, + }; #[test_case( - || { + |_| { let uuid_v7 = UuidV7::new(); Builder::new() .with_metadata_field(SupportedField::Id(uuid_v7)) @@ -64,15 +86,21 @@ mod tests { "`ver` and `id` are equal" )] #[test_case( - || { + |provider| { let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs(); - let ver = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) + let id = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) .try_into() .unwrap(); - let id = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) + let first_doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .build(); + provider.add_document(None, &first_doc).unwrap(); + + let ver = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) .try_into() .unwrap(); Builder::new() @@ -84,15 +112,41 @@ mod tests { "`ver` greater than `id` are equal" )] #[test_case( - || { + |provider| { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let id = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) + .try_into() + .unwrap(); + let first_doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .build(); + provider.add_document(None, &first_doc).unwrap(); + + let ver = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) + .try_into() + .unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(ver)) + .build() + } + => false; + "`ver` less than `id` are equal" + )] + #[test_case( + |_| { let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs(); - let ver = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) + let id = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) .try_into() .unwrap(); - let id = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) + let ver = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) .try_into() .unwrap(); Builder::new() @@ -101,10 +155,10 @@ mod tests { .build() } => false; - "`ver` less than `id` are equal" + "missing first version document" )] #[test_case( - || { + |_| { Builder::new() .with_metadata_field(SupportedField::Id(UuidV7::new())) .build() @@ -113,7 +167,7 @@ mod tests { "missing `ver` field" )] #[test_case( - || { + |_| { Builder::new() .with_metadata_field(SupportedField::Ver(UuidV7::new())) .build() @@ -122,9 +176,12 @@ mod tests { "missing `id` field" )] #[tokio::test] - async fn ver_test(doc_gen: impl FnOnce() -> CatalystSignedDocument) -> bool { - let doc = doc_gen(); + async fn ver_test( + doc_gen: impl FnOnce(&mut TestCatalystSignedDocumentProvider) -> CatalystSignedDocument + ) -> bool { + let mut provider = TestCatalystSignedDocumentProvider::default(); + let doc = doc_gen(&mut provider); - VerRule.check(&doc).await.unwrap() + VerRule.check(&doc, &provider).await.unwrap() } } From 28adeb9ec0a54665c32d3b994a5364deef1555e1 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 27 Aug 2025 15:31:29 +0400 Subject: [PATCH 11/14] fix tests and spelling --- rust/signed_doc/src/validator/rules/ver.rs | 11 +++++----- rust/signed_doc/tests/comment.rs | 25 +++++++++++++--------- rust/signed_doc/tests/proposal.rs | 20 ++++++++++------- rust/signed_doc/tests/submission.rs | 25 +++++++++++++--------- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/rust/signed_doc/src/validator/rules/ver.rs b/rust/signed_doc/src/validator/rules/ver.rs index 484b3e94f8..20ccb109fa 100644 --- a/rust/signed_doc/src/validator/rules/ver.rs +++ b/rust/signed_doc/src/validator/rules/ver.rs @@ -10,7 +10,6 @@ pub(crate) struct VerRule; impl VerRule { /// Validates document `ver` field on the timestamps: /// 1. document `ver` cannot be smaller than document `id` field - #[allow(clippy::unused_async)] pub(crate) async fn check( &self, doc: &CatalystSignedDocument, @@ -47,8 +46,8 @@ impl VerRule { } if ver != id { - let first_submited_doc = DocumentRef::new(id, id, DocLocator::default()); - if provider.try_get_doc(&first_submited_doc).await?.is_none() { + let first_submitted_doc = DocumentRef::new(id, id, DocLocator::default()); + if provider.try_get_doc(&first_submitted_doc).await?.is_none() { doc.report().functional_validation( &format!("`ver` and `id` are not equal, ver: {ver}, id: {id}. Document with `id` and `ver` being equal MUST exist"), "Cannot get a first version document from the provider, document for which `id` and `ver` are equal.", @@ -109,7 +108,7 @@ mod tests { .build() } => true; - "`ver` greater than `id` are equal" + "`ver` greater than `id`" )] #[test_case( |provider| { @@ -117,7 +116,7 @@ mod tests { .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs(); - let id = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) + let id = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) .try_into() .unwrap(); let first_doc = Builder::new() @@ -135,7 +134,7 @@ mod tests { .build() } => false; - "`ver` less than `id` are equal" + "`ver` less than `id`" )] #[test_case( |_| { diff --git a/rust/signed_doc/tests/comment.rs b/rust/signed_doc/tests/comment.rs index 394323977d..d0f5358911 100644 --- a/rust/signed_doc/tests/comment.rs +++ b/rust/signed_doc/tests/comment.rs @@ -126,13 +126,14 @@ async fn test_valid_comment_doc() { // Create a main comment doc, contain all fields mention in the document (except // revocations and section) + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL_COMMENT.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, "ref": { "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), @@ -180,13 +181,14 @@ async fn test_invalid_comment_doc_wrong_role() { // Create a main comment doc, contain all fields mention in the document (except // revocations and section) + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL_COMMENT.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, "ref": { "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), @@ -224,13 +226,14 @@ async fn test_invalid_comment_doc_wrong_role() { #[tokio::test] async fn test_invalid_comment_doc_missing_parameters() { + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL_COMMENT.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, "ref": { "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), @@ -266,13 +269,14 @@ async fn test_invalid_comment_doc_missing_parameters() { #[tokio::test] async fn test_invalid_comment_doc_missing_template() { + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL_COMMENT.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, "ref": { "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), @@ -308,13 +312,14 @@ async fn test_invalid_comment_doc_missing_template() { #[tokio::test] async fn test_invalid_comment_doc_missing_ref() { + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL_COMMENT.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, // "ref": { // "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), // "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), diff --git a/rust/signed_doc/tests/proposal.rs b/rust/signed_doc/tests/proposal.rs index 03f4d60864..99019a4cbf 100644 --- a/rust/signed_doc/tests/proposal.rs +++ b/rust/signed_doc/tests/proposal.rs @@ -73,13 +73,14 @@ async fn test_valid_proposal_doc() { // Create a main proposal doc, contain all fields mention in the document (except // collaborations and revocations) + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, "template": { "id": PROPOSAL_TEMPLATE_DOC.doc_id().unwrap(), "ver": PROPOSAL_TEMPLATE_DOC.doc_ver().unwrap(), @@ -118,13 +119,14 @@ async fn test_invalid_proposal_doc_wrong_role() { // Create a main proposal doc, contain all fields mention in the document (except // collaborations and revocations) + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, "template": { "id": PROPOSAL_TEMPLATE_DOC.doc_id().unwrap(), "ver": PROPOSAL_TEMPLATE_DOC.doc_ver().unwrap(), @@ -153,13 +155,14 @@ async fn test_invalid_proposal_doc_wrong_role() { #[tokio::test] async fn test_invalid_proposal_doc_missing_template() { + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, // "template": { // "id": PROPOSAL_TEMPLATE_DOC.doc_id().unwrap(), // "ver": PROPOSAL_TEMPLATE_DOC.doc_ver().unwrap(), @@ -186,13 +189,14 @@ async fn test_invalid_proposal_doc_missing_template() { #[tokio::test] async fn test_invalid_proposal_doc_missing_parameters() { + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, "template": { "id": PROPOSAL_TEMPLATE_DOC.doc_id().unwrap(), "ver": PROPOSAL_TEMPLATE_DOC.doc_ver().unwrap(), diff --git a/rust/signed_doc/tests/submission.rs b/rust/signed_doc/tests/submission.rs index 6436181064..9d72045bc5 100644 --- a/rust/signed_doc/tests/submission.rs +++ b/rust/signed_doc/tests/submission.rs @@ -65,13 +65,14 @@ async fn test_valid_submission_action() { key_provider.add_pk(kid.clone(), pk); // Create a main proposal submission doc, contain all fields mention in the document + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL_SUBMISSION_ACTION.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, "ref": { "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), @@ -111,13 +112,14 @@ async fn test_invalid_submission_action_wrong_role() { let (sk, _pk, kid) = create_dummy_key_pair(RoleId::Role0).unwrap(); // Create a main proposal submission doc, contain all fields mention in the document + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL_SUBMISSION_ACTION.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, "ref": { "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), @@ -148,13 +150,14 @@ async fn test_invalid_submission_action_wrong_role() { #[tokio::test] async fn test_invalid_submission_action_corrupted_json() { + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL_SUBMISSION_ACTION.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, "ref": { "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), @@ -181,13 +184,14 @@ async fn test_invalid_submission_action_corrupted_json() { #[tokio::test] async fn test_invalid_submission_action_missing_ref() { + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL_SUBMISSION_ACTION.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, // "ref": { // "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), // "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), @@ -216,13 +220,14 @@ async fn test_invalid_submission_action_missing_ref() { #[tokio::test] async fn test_invalid_submission_action_missing_parameters() { + let id = UuidV7::new(); let doc = Builder::new() .with_json_metadata(serde_json::json!({ "content-type": ContentType::Json.to_string(), "content-encoding": ContentEncoding::Brotli.to_string(), "type": doc_types::PROPOSAL_SUBMISSION_ACTION.clone(), - "id": UuidV7::new(), - "ver": UuidV7::new(), + "id": id, + "ver": id, "ref": { "id": DUMMY_PROPOSAL_DOC.doc_id().unwrap(), "ver": DUMMY_PROPOSAL_DOC.doc_ver().unwrap(), From 61ea015925fef28ed7b709e8fa83cdfe5353e8bf Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 28 Aug 2025 11:19:26 +0400 Subject: [PATCH 12/14] wip --- rust/signed_doc/src/providers.rs | 24 ++- rust/signed_doc/src/validator/rules/ver.rs | 193 +++++++++++++++++++-- 2 files changed, 201 insertions(+), 16 deletions(-) diff --git a/rust/signed_doc/src/providers.rs b/rust/signed_doc/src/providers.rs index aee610a6c5..e99b2eddd1 100644 --- a/rust/signed_doc/src/providers.rs +++ b/rust/signed_doc/src/providers.rs @@ -2,7 +2,7 @@ use std::{future::Future, time::Duration}; -use catalyst_types::catalyst_id::CatalystId; +use catalyst_types::{catalyst_id::CatalystId, uuid::UuidV7}; use ed25519_dalek::VerifyingKey; use crate::{CatalystSignedDocument, DocumentRef}; @@ -18,12 +18,19 @@ pub trait VerifyingKeyProvider { /// `CatalystSignedDocument` Provider trait pub trait CatalystSignedDocumentProvider: Send + Sync { - /// Try to get `CatalystSignedDocument`from document reference + /// Try to get `CatalystSignedDocument` from document reference fn try_get_doc( &self, doc_ref: &DocumentRef, ) -> impl Future>> + Send; + /// Try to get the last known version of the `CatalystSignedDocument`, same + /// `id` and the highest known `ver`. + fn try_get_last_doc( + &self, + id: UuidV7, + ) -> impl Future>> + Send; + /// Returns a future threshold value, which is used in the validation of the `ver` /// field that it is not too far in the future. /// If `None` is returned, skips "too far in the future" validation. @@ -48,7 +55,6 @@ pub mod tests { /// Simple testing implementation of `CatalystSignedDocumentProvider` #[derive(Default, Debug)] - pub struct TestCatalystSignedDocumentProvider(HashMap); impl TestCatalystSignedDocumentProvider { @@ -82,6 +88,18 @@ pub mod tests { Ok(self.0.get(doc_ref).cloned()) } + async fn try_get_last_doc( + &self, + id: catalyst_types::uuid::UuidV7, + ) -> anyhow::Result> { + Ok(self + .0 + .iter() + .filter(|(doc_ref, _)| doc_ref.id() == &id) + .max_by_key(|(doc_ref, _)| doc_ref.ver().uuid()) + .map(|(_, doc)| doc.clone())) + } + fn future_threshold(&self) -> Option { Some(Duration::from_secs(5)) } diff --git a/rust/signed_doc/src/validator/rules/ver.rs b/rust/signed_doc/src/validator/rules/ver.rs index 20ccb109fa..d8a748166f 100644 --- a/rust/signed_doc/src/validator/rules/ver.rs +++ b/rust/signed_doc/src/validator/rules/ver.rs @@ -1,8 +1,6 @@ //! Validator for Signed Document Version -use crate::{ - providers::CatalystSignedDocumentProvider, CatalystSignedDocument, DocLocator, DocumentRef, -}; +use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument}; /// Signed Document `ver` field validation rule pub(crate) struct VerRule; @@ -43,17 +41,54 @@ impl VerRule { &format!("Document Version {ver} cannot be smaller than Document ID {id}"), ); is_valid = false; - } + } else if let Some(last_doc) = provider.try_get_last_doc(id).await? { + let Ok(last_doc_ver) = last_doc.doc_ver() else { + doc.report().missing_field( + "ver", + &format!( + "Missing `ver` field in the latest known document, for the the id {id}" + ), + ); + return Ok(false); + }; - if ver != id { - let first_submitted_doc = DocumentRef::new(id, id, DocLocator::default()); - if provider.try_get_doc(&first_submitted_doc).await?.is_none() { + if last_doc_ver >= ver { doc.report().functional_validation( - &format!("`ver` and `id` are not equal, ver: {ver}, id: {id}. Document with `id` and `ver` being equal MUST exist"), - "Cannot get a first version document from the provider, document for which `id` and `ver` are equal.", + &format!("New document ver should be greater that the submitted latest known. New document ver: {ver}, latest known ver: {last_doc_ver}"), + &format!("Document's `ver` field should continuously incrising, for the the id {id}"), ); is_valid = false; } + + let Ok(last_doc_type) = last_doc.doc_type() else { + doc.report().missing_field( + "type", + &format!( + "Missing `type` field in the latest known document. Last known document id: {id}, ver: {last_doc_ver}." + ), + ); + return Ok(false); + }; + + let Ok(doc_type) = doc.doc_type() else { + doc.report() + .missing_field("type", &format!("Missing `type` field.")); + return Ok(false); + }; + + if last_doc_type != doc_type { + doc.report().functional_validation( + &format!("New document type should be the same that the submitted latest known. New document type: {doc_type}, latest known ver: {last_doc_type}"), + &format!("Document's type should be the same for all documents with the same id {id}"), + ); + is_valid = false; + } + } else if ver != id { + doc.report().functional_validation( + &format!("`ver` and `id` are not equal, ver: {ver}, id: {id}. Document with `id` and `ver` being equal MUST exist"), + "Cannot get a first version document from the provider, document for which `id` and `ver` are equal.", + ); + is_valid = false; } Ok(is_valid) @@ -70,7 +105,7 @@ mod tests { use super::*; use crate::{ builder::tests::Builder, metadata::SupportedField, - providers::tests::TestCatalystSignedDocumentProvider, UuidV7, + providers::tests::TestCatalystSignedDocumentProvider, UuidV4, UuidV7, }; #[test_case( @@ -86,6 +121,7 @@ mod tests { )] #[test_case( |provider| { + let doc_type = UuidV4::new(); let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() @@ -96,6 +132,7 @@ mod tests { let first_doc = Builder::new() .with_metadata_field(SupportedField::Id(id)) .with_metadata_field(SupportedField::Ver(id)) + .with_metadata_field(SupportedField::Type(doc_type.into())) .build(); provider.add_document(None, &first_doc).unwrap(); @@ -105,6 +142,7 @@ mod tests { Builder::new() .with_metadata_field(SupportedField::Id(id)) .with_metadata_field(SupportedField::Ver(ver)) + .with_metadata_field(SupportedField::Type(doc_type.into())) .build() } => true; @@ -112,16 +150,18 @@ mod tests { )] #[test_case( |provider| { + let doc_type = UuidV4::new(); let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(); + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); let id = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) .try_into() .unwrap(); let first_doc = Builder::new() .with_metadata_field(SupportedField::Id(id)) .with_metadata_field(SupportedField::Ver(id)) + .with_metadata_field(SupportedField::Type(doc_type.into())) .build(); provider.add_document(None, &first_doc).unwrap(); @@ -131,13 +171,55 @@ mod tests { Builder::new() .with_metadata_field(SupportedField::Id(id)) .with_metadata_field(SupportedField::Ver(ver)) + .with_metadata_field(SupportedField::Type(doc_type.into())) .build() } => false; "`ver` less than `id`" )] + #[test_case( + |provider| { + let doc_type = UuidV4::new(); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let id = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) + .try_into() + .unwrap(); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .with_metadata_field(SupportedField::Type(doc_type.into())) + .build(); + provider.add_document(None, &doc).unwrap(); + + + let ver = Uuid::new_v7(Timestamp::from_unix_time(now + 3, 0, 0, 0)) + .try_into() + .unwrap(); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(ver)) + .with_metadata_field(SupportedField::Type(doc_type.into())) + .build(); + provider.add_document(None, &doc).unwrap(); + + let ver = Uuid::new_v7(Timestamp::from_unix_time(now + 2, 0, 0, 0)) + .try_into() + .unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(ver)) + .with_metadata_field(SupportedField::Type(doc_type.into())) + .build() + } + => false; + "`ver` less than `ver` field for of the latest known document" + )] #[test_case( |_| { + let doc_type = UuidV4::new(); let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() @@ -151,11 +233,96 @@ mod tests { Builder::new() .with_metadata_field(SupportedField::Id(id)) .with_metadata_field(SupportedField::Ver(ver)) + .with_metadata_field(SupportedField::Type(doc_type.into())) .build() } => false; "missing first version document" )] + #[test_case( + |provider| { + let doc_type = UuidV4::new(); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let id = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) + .try_into() + .unwrap(); + let first_doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .with_metadata_field(SupportedField::Type(doc_type.into())) + .build(); + provider.add_document(None, &first_doc).unwrap(); + + let ver = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) + .try_into() + .unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(ver)) + .build() + } + => false; + "missing `type` field" + )] + #[test_case( + |provider| { + let doc_type = UuidV4::new(); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let id = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) + .try_into() + .unwrap(); + let first_doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .build(); + provider.add_document(None, &first_doc).unwrap(); + + let ver = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) + .try_into() + .unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(ver)) + .with_metadata_field(SupportedField::Type(doc_type.into())) + .build() + } + => false; + "missing `type` field for the latest known document" + )] + #[test_case( + |provider| { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let id = Uuid::new_v7(Timestamp::from_unix_time(now - 1, 0, 0, 0)) + .try_into() + .unwrap(); + let first_doc = Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(id)) + .with_metadata_field(SupportedField::Type(UuidV4::new().into())) + .build(); + provider.add_document(None, &first_doc).unwrap(); + + let ver = Uuid::new_v7(Timestamp::from_unix_time(now + 1, 0, 0, 0)) + .try_into() + .unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Id(id)) + .with_metadata_field(SupportedField::Ver(ver)) + .with_metadata_field(SupportedField::Type(UuidV4::new().into())) + .build() + } + => false; + "diverge `type` field with the latest known document" + )] #[test_case( |_| { Builder::new() From 90dc1d17c6196634d3f3810912bc7fbe7ef4d1d2 Mon Sep 17 00:00:00 2001 From: Apisit Ritruengroj Date: Thu, 28 Aug 2025 14:43:39 +0700 Subject: [PATCH 13/14] chore: cspellfix --- rust/signed_doc/src/validator/rules/ver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/src/validator/rules/ver.rs b/rust/signed_doc/src/validator/rules/ver.rs index d8a748166f..6d2dd88a2f 100644 --- a/rust/signed_doc/src/validator/rules/ver.rs +++ b/rust/signed_doc/src/validator/rules/ver.rs @@ -55,7 +55,7 @@ impl VerRule { if last_doc_ver >= ver { doc.report().functional_validation( &format!("New document ver should be greater that the submitted latest known. New document ver: {ver}, latest known ver: {last_doc_ver}"), - &format!("Document's `ver` field should continuously incrising, for the the id {id}"), + &format!("Document's `ver` field should continuously increasing, for the the id {id}"), ); is_valid = false; } From 8136bd70bc6ab0de30ebff2332d3247e538a2168 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 28 Aug 2025 12:53:10 +0400 Subject: [PATCH 14/14] fix clippy --- rust/signed_doc/src/validator/rules/id.rs | 2 ++ rust/signed_doc/src/validator/rules/ver.rs | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/rust/signed_doc/src/validator/rules/id.rs b/rust/signed_doc/src/validator/rules/id.rs index 55e5be4632..0d694e2b94 100644 --- a/rust/signed_doc/src/validator/rules/id.rs +++ b/rust/signed_doc/src/validator/rules/id.rs @@ -116,6 +116,7 @@ mod tests { "valid id" )] #[test_case( + #[allow(clippy::arithmetic_side_effects)] |provider| { let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -137,6 +138,7 @@ mod tests { "`id` to far in past" )] #[test_case( + #[allow(clippy::arithmetic_side_effects)] |provider| { let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) diff --git a/rust/signed_doc/src/validator/rules/ver.rs b/rust/signed_doc/src/validator/rules/ver.rs index 6d2dd88a2f..d67aeef11e 100644 --- a/rust/signed_doc/src/validator/rules/ver.rs +++ b/rust/signed_doc/src/validator/rules/ver.rs @@ -71,8 +71,7 @@ impl VerRule { }; let Ok(doc_type) = doc.doc_type() else { - doc.report() - .missing_field("type", &format!("Missing `type` field.")); + doc.report().missing_field("type", "Missing `type` field."); return Ok(false); }; @@ -120,6 +119,7 @@ mod tests { "`ver` and `id` are equal" )] #[test_case( + #[allow(clippy::arithmetic_side_effects)] |provider| { let doc_type = UuidV4::new(); let now = SystemTime::now() @@ -149,6 +149,7 @@ mod tests { "`ver` greater than `id`" )] #[test_case( + #[allow(clippy::arithmetic_side_effects)] |provider| { let doc_type = UuidV4::new(); let now = SystemTime::now() @@ -178,6 +179,7 @@ mod tests { "`ver` less than `id`" )] #[test_case( + #[allow(clippy::arithmetic_side_effects)] |provider| { let doc_type = UuidV4::new(); let now = SystemTime::now() @@ -218,6 +220,7 @@ mod tests { "`ver` less than `ver` field for of the latest known document" )] #[test_case( + #[allow(clippy::arithmetic_side_effects)] |_| { let doc_type = UuidV4::new(); let now = SystemTime::now() @@ -240,6 +243,7 @@ mod tests { "missing first version document" )] #[test_case( + #[allow(clippy::arithmetic_side_effects)] |provider| { let doc_type = UuidV4::new(); let now = SystemTime::now() @@ -268,6 +272,7 @@ mod tests { "missing `type` field" )] #[test_case( + #[allow(clippy::arithmetic_side_effects)] |provider| { let doc_type = UuidV4::new(); let now = SystemTime::now() @@ -296,6 +301,7 @@ mod tests { "missing `type` field for the latest known document" )] #[test_case( + #[allow(clippy::arithmetic_side_effects)] |provider| { let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH)