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/mod.rs b/rust/signed_doc/src/validator/mod.rs index 6a503a10a5..4d370da1c9 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -6,14 +6,13 @@ pub(crate) mod rules; use std::{ collections::HashMap, sync::{Arc, LazyLock}, - time::{Duration, SystemTime}, }; 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::{ @@ -41,6 +40,8 @@ fn proposal_rule() -> Rules { CATEGORY_PARAMETERS.clone(), ]; Rules { + id: IdRule, + ver: VerRule, content_type: ContentTypeRule { exp: ContentType::Json, }, @@ -75,6 +76,8 @@ fn proposal_comment_rule() -> Rules { CATEGORY_PARAMETERS.clone(), ]; Rules { + id: IdRule, + ver: VerRule, content_type: ContentTypeRule { exp: ContentType::Json, }, @@ -126,6 +129,8 @@ fn proposal_submission_action_rule() -> Rules { .expect("Must be a valid json scheme file"); Rules { + id: IdRule, + ver: VerRule, content_type: ContentTypeRule { exp: ContentType::Json, }, @@ -190,10 +195,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`", @@ -206,106 +207,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. /// @@ -384,82 +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, validate_id_and_ver}, - UuidV7, - }; - - #[test] - 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_valid = validate_id_and_ver(&doc, &provider).unwrap(); - assert!(is_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_valid = validate_id_and_ver(&doc, &provider).unwrap(); - assert!(!is_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_valid = validate_id_and_ver(&doc, &provider).unwrap(); - assert!(!is_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_valid = validate_id_and_ver(&doc, &provider).unwrap(); - assert!(!is_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 new file mode 100644 index 0000000000..0d694e2b94 --- /dev/null +++ b/rust/signed_doc/src/validator/rules/id.rs @@ -0,0 +1,180 @@ +//! 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 + #[allow(clippy::unused_async)] + pub(crate) async 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) + } +} + +#[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( + #[allow(clippy::arithmetic_side_effects)] + |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( + #[allow(clippy::arithmetic_side_effects)] + |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 a2a382044f..be934089ab 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -8,23 +8,31 @@ 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 { + /// 'id' field validation rule + pub(crate) id: IdRule, + /// 'ver' field validation rule + pub(crate) ver: VerRule, /// 'content-type' field validation rule pub(crate) content_type: ContentTypeRule, /// 'content-encoding' field validation rule @@ -54,6 +62,8 @@ impl Rules { Provider: CatalystSignedDocumentProvider, { let rules = [ + self.id.check(doc, provider).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 new file mode 100644 index 0000000000..d67aeef11e --- /dev/null +++ b/rust/signed_doc/src/validator/rules/ver.rs @@ -0,0 +1,359 @@ +//! Validator for Signed Document Version + +use crate::{providers::CatalystSignedDocumentProvider, 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) async 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 Ok(ver) = doc.doc_ver() else { + doc.report().missing_field( + "ver", + "Cannot get the document field during the field validation", + ); + return Ok(false); + }; + + 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; + } 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 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 increasing, 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", "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) + } +} + +#[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, UuidV4, 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( + #[allow(clippy::arithmetic_side_effects)] + |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)) + .with_metadata_field(SupportedField::Type(doc_type.into())) + .build() + } + => true; + "`ver` greater than `id`" + )] + #[test_case( + #[allow(clippy::arithmetic_side_effects)] + |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)) + .with_metadata_field(SupportedField::Type(doc_type.into())) + .build() + } + => false; + "`ver` less than `id`" + )] + #[test_case( + #[allow(clippy::arithmetic_side_effects)] + |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( + #[allow(clippy::arithmetic_side_effects)] + |_| { + 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 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 first version document" + )] + #[test_case( + #[allow(clippy::arithmetic_side_effects)] + |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( + #[allow(clippy::arithmetic_side_effects)] + |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( + #[allow(clippy::arithmetic_side_effects)] + |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() + .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(&mut TestCatalystSignedDocumentProvider) -> CatalystSignedDocument + ) -> bool { + let mut provider = TestCatalystSignedDocumentProvider::default(); + let doc = doc_gen(&mut provider); + + VerRule.check(&doc, &provider).await.unwrap() + } +} 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(),