From ddb0b1dce7d890d747fe9d664a48ac91941cdd92 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sat, 27 Sep 2025 21:50:51 +0300 Subject: [PATCH 1/6] wip --- .../signed_doc/src/validator/rules/doc_ref.rs | 631 -------------- .../src/validator/rules/doc_ref/mod.rs | 243 ++++++ .../src/validator/rules/doc_ref/tests.rs | 386 ++++++++ .../src/validator/rules/parameters.rs | 824 ------------------ .../src/validator/rules/parameters/mod.rs | 211 +++++ .../src/validator/rules/parameters/tests.rs | 612 +++++++++++++ rust/signed_doc/src/validator/rules/reply.rs | 437 ---------- .../src/validator/rules/reply/mod.rs | 138 +++ .../src/validator/rules/reply/tests.rs | 298 +++++++ 9 files changed, 1888 insertions(+), 1892 deletions(-) delete mode 100644 rust/signed_doc/src/validator/rules/doc_ref.rs create mode 100644 rust/signed_doc/src/validator/rules/doc_ref/mod.rs create mode 100644 rust/signed_doc/src/validator/rules/doc_ref/tests.rs delete mode 100644 rust/signed_doc/src/validator/rules/parameters.rs create mode 100644 rust/signed_doc/src/validator/rules/parameters/mod.rs create mode 100644 rust/signed_doc/src/validator/rules/parameters/tests.rs delete mode 100644 rust/signed_doc/src/validator/rules/reply.rs create mode 100644 rust/signed_doc/src/validator/rules/reply/mod.rs create mode 100644 rust/signed_doc/src/validator/rules/reply/tests.rs diff --git a/rust/signed_doc/src/validator/rules/doc_ref.rs b/rust/signed_doc/src/validator/rules/doc_ref.rs deleted file mode 100644 index 3fcc5e14ae6..00000000000 --- a/rust/signed_doc/src/validator/rules/doc_ref.rs +++ /dev/null @@ -1,631 +0,0 @@ -//! `ref` rule type impl. - -use catalyst_signed_doc_spec::{is_required::IsRequired, metadata::doc_ref::Ref, DocSpecs}; -use catalyst_types::problem_report::ProblemReport; - -use crate::{ - providers::CatalystSignedDocumentProvider, CatalystSignedDocument, DocType, DocumentRef, - DocumentRefs, -}; - -/// `ref` field validation rule -#[derive(Debug)] -pub(crate) enum RefRule { - /// Is 'ref' specified - Specified { - /// allowed `type` field of the referenced doc - allowed_type: Vec, - /// allows multiple document references or only one - multiple: bool, - /// optional flag for the `ref` field - optional: bool, - }, - /// 'ref' is not specified - NotSpecified, -} -impl RefRule { - /// Generating `RefRule` from specs - pub(crate) fn new( - docs: &DocSpecs, - spec: &Ref, - ) -> anyhow::Result { - let optional = match spec.required { - IsRequired::Yes => false, - IsRequired::Optional => true, - IsRequired::Excluded => { - anyhow::ensure!( - spec.doc_type.is_empty() && spec.multiple.is_none(), - "'type' and 'multiple' fields could not been specified when 'required' is 'excluded' for 'ref' metadata definition" - ); - return Ok(Self::NotSpecified); - }, - }; - - anyhow::ensure!(!spec.doc_type.is_empty(), "'type' field should exists and has at least one entry for the required 'ref' metadata definition"); - - let exp_ref_types = spec.doc_type.iter().try_fold( - Vec::new(), - |mut res, doc_name| -> anyhow::Result<_> { - let docs_spec = docs.get(doc_name).ok_or(anyhow::anyhow!( - "cannot find a document definition {doc_name}" - ))?; - res.push(docs_spec.doc_type.as_str().parse()?); - Ok(res) - }, - )?; - - let multiple = spec.multiple.ok_or(anyhow::anyhow!( - "'multiple' field should exists for the required 'ref' metadata definition" - ))?; - - Ok(Self::Specified { - allowed_type: exp_ref_types, - multiple, - optional, - }) - } - - /// Field validation rule - pub(crate) async fn check( - &self, - doc: &CatalystSignedDocument, - provider: &Provider, - ) -> anyhow::Result - where - Provider: CatalystSignedDocumentProvider, - { - let context: &str = "Ref rule check"; - if let Self::Specified { - allowed_type: exp_ref_types, - multiple, - optional, - } = self - { - if let Some(doc_refs) = doc.doc_meta().doc_ref() { - return doc_refs_check( - doc_refs, - exp_ref_types, - *multiple, - "ref", - provider, - doc.report(), - |_| true, - ) - .await; - } else if !optional { - doc.report() - .missing_field("ref", &format!("{context}, document must have ref field")); - return Ok(false); - } - } - if let Self::NotSpecified = self { - if let Some(doc_ref) = doc.doc_meta().doc_ref() { - doc.report().unknown_field( - "ref", - &doc_ref.to_string(), - &format!("{context}, document does not expect to have a ref field"), - ); - return Ok(false); - } - } - - Ok(true) - } -} - -/// Validate all the document references by the defined validation rules, -/// plus conducting additional validations with the provided `validator`. -/// Document all possible error in doc report (no fail fast) -pub(crate) async fn doc_refs_check( - doc_refs: &DocumentRefs, - exp_ref_types: &[DocType], - multiple: bool, - field_name: &str, - provider: &Provider, - report: &ProblemReport, - validator: Validator, -) -> anyhow::Result -where - Provider: CatalystSignedDocumentProvider, - Validator: Fn(&CatalystSignedDocument) -> bool, -{ - let mut all_valid = true; - - if !multiple && doc_refs.len() > 1 { - report.other( - format!( - "Only ONE document reference is allowed, found {} document references", - doc_refs.len() - ) - .as_str(), - &format!("Referenced document validation for the `{field_name}` field"), - ); - return Ok(false); - } - - for dr in doc_refs.iter() { - if let Some(ref ref_doc) = provider.try_get_doc(dr).await? { - let is_valid = referenced_doc_type_check(ref_doc, exp_ref_types, field_name, report) - && referenced_doc_id_and_ver_check(ref_doc, dr, field_name, report) - && validator(ref_doc); - - if !is_valid { - all_valid = false; - } - } else { - report.functional_validation( - &format!("Cannot retrieve a document {dr}"), - &format!("Referenced document validation for the `{field_name}` field"), - ); - all_valid = false; - } - } - Ok(all_valid) -} - -/// Validation check that the provided `ref_doc` is a correct referenced document found by -/// `original_doc_ref` -fn referenced_doc_id_and_ver_check( - ref_doc: &CatalystSignedDocument, - original_doc_ref: &DocumentRef, - field_name: &str, - report: &ProblemReport, -) -> bool { - let Ok(id) = ref_doc.doc_id() else { - report.missing_field( - "id", - &format!("Referenced document validation for the `{field_name}` field"), - ); - return false; - }; - - let Ok(ver) = ref_doc.doc_ver() else { - report.missing_field( - "ver", - &format!("Referenced document validation for the `{field_name}` field"), - ); - return false; - }; - - // id and version must match the values in ref doc - if &id != original_doc_ref.id() && &ver != original_doc_ref.ver() { - report.invalid_value( - "id and version", - &format!("id: {id}, ver: {ver}"), - &format!( - "id: {}, ver: {}", - original_doc_ref.id(), - original_doc_ref.ver() - ), - &format!("Referenced document validation for the `{field_name}` field"), - ); - return false; - } - - true -} - -/// Validation check that the provided `ref_doc` has an expected `type` field value from -/// the allowed `exp_ref_types` list -fn referenced_doc_type_check( - ref_doc: &CatalystSignedDocument, - exp_ref_types: &[DocType], - field_name: &str, - report: &ProblemReport, -) -> bool { - let Ok(ref_doc_type) = ref_doc.doc_type() else { - report.missing_field( - "type", - &format!("Document reference validation for the `{field_name}` field"), - ); - return false; - }; - - // Check that the type matches one of the expected ones - if exp_ref_types - .iter() - .all(|exp_type| ref_doc_type != exp_type) - { - report.invalid_value( - field_name, - &ref_doc_type.to_string(), - &exp_ref_types - .iter() - .fold(String::new(), |s, v| format!("{s}, {v}")), - &format!("Invalid referenced document type, during validation of {field_name} field"), - ); - return false; - } - true -} - -#[cfg(test)] -#[allow(clippy::too_many_lines)] -mod tests { - use catalyst_types::uuid::{UuidV4, UuidV7}; - use test_case::test_case; - - use super::*; - use crate::{ - builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, - DocLocator, DocumentRef, - }; - - #[test_case( - |exp_types, provider| { - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[0].clone())) - .build(); - provider.add_document(None, &ref_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => true - ; - "valid reference to the one correct document" - )] - #[test_case( - |exp_types, provider| { - let ref_doc_1 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[0].clone())) - .build(); - provider.add_document(None, &ref_doc_1).unwrap(); - let ref_doc_2 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[1].clone())) - .build(); - provider.add_document(None, &ref_doc_2).unwrap(); - let ref_doc_3 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[0].clone())) - .build(); - provider.add_document(None, &ref_doc_3).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - ref_doc_1.doc_id().unwrap(), - ref_doc_1.doc_ver().unwrap(), - DocLocator::default(), - ), - DocumentRef::new( - ref_doc_2.doc_id().unwrap(), - ref_doc_2.doc_ver().unwrap(), - DocLocator::default(), - ), - DocumentRef::new( - ref_doc_3.doc_id().unwrap(), - ref_doc_3.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => true - ; - "valid reference to the multiple documents" - )] - #[test_case( - |exp_types, provider| { - let ref_doc_1 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[0].clone())) - .build(); - provider.add_document(None, &ref_doc_1).unwrap(); - let ref_doc_2 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[1].clone())) - .build(); - provider.add_document(None, &ref_doc_2).unwrap(); - let ref_doc_3 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(UuidV4::new().into())) - .build(); - provider.add_document(None, &ref_doc_3).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - ref_doc_1.doc_id().unwrap(), - ref_doc_1.doc_ver().unwrap(), - DocLocator::default(), - ), - DocumentRef::new( - ref_doc_2.doc_id().unwrap(), - ref_doc_2.doc_ver().unwrap(), - DocLocator::default(), - ), - DocumentRef::new( - ref_doc_3.doc_id().unwrap(), - ref_doc_3.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "valid reference to the multiple documents, one of them invalid `type` field" - )] - #[test_case( - |exp_types, provider| { - let ref_doc_1 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[0].clone())) - .build(); - provider.add_document(None, &ref_doc_1).unwrap(); - let ref_doc_2 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[1].clone())) - .build(); - provider.add_document(None, &ref_doc_2).unwrap(); - let ref_doc_3 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .build(); - provider.add_document(None, &ref_doc_3).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - ref_doc_1.doc_id().unwrap(), - ref_doc_1.doc_ver().unwrap(), - DocLocator::default(), - ), - DocumentRef::new( - ref_doc_2.doc_id().unwrap(), - ref_doc_2.doc_ver().unwrap(), - DocLocator::default(), - ), - DocumentRef::new( - ref_doc_3.doc_id().unwrap(), - ref_doc_3.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "valid reference to the multiple documents, one of them missing `type` field" - )] - #[test_case( - |exp_types, provider| { - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[0].clone())) - .build(); - provider.add_document(Some(DocumentRef::new(UuidV7::new(), UuidV7::new(), DocLocator::default())), &ref_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "invalid reference to the document, which has different id and ver fields as stated in the `ref` field" - )] - #[test_case( - |_, _| { - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ), - ] - .into(), - )) - .build() - } - => false - ; - "valid reference to the missing one document" - )] - #[tokio::test] - async fn ref_multiple_specified_test( - doc_gen: impl FnOnce(&[DocType; 2], &mut TestCatalystProvider) -> CatalystSignedDocument - ) -> bool { - let mut provider = TestCatalystProvider::default(); - - let exp_types: [DocType; 2] = [UuidV4::new().into(), UuidV4::new().into()]; - - let doc = doc_gen(&exp_types, &mut provider); - - let non_optional_res = RefRule::Specified { - allowed_type: exp_types.to_vec(), - multiple: true, - optional: false, - } - .check(&doc, &provider) - .await - .unwrap(); - - let optional_res = RefRule::Specified { - allowed_type: exp_types.to_vec(), - multiple: true, - optional: true, - } - .check(&doc, &provider) - .await - .unwrap(); - - assert_eq!(non_optional_res, optional_res); - non_optional_res - } - - #[test_case( - |exp_types, provider| { - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[0].clone())) - .build(); - provider.add_document(None, &ref_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => true - ; - "valid document with a single reference" - )] - #[test_case( - |exp_types, provider| { - let ref_doc_1 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[0].clone())) - .build(); - provider.add_document(None, &ref_doc_1).unwrap(); - let ref_doc_2 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[1].clone())) - .build(); - provider.add_document(None, &ref_doc_2).unwrap(); - let ref_doc_3 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[0].clone())) - .build(); - provider.add_document(None, &ref_doc_3).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - ref_doc_1.doc_id().unwrap(), - ref_doc_1.doc_ver().unwrap(), - DocLocator::default(), - ), - DocumentRef::new( - ref_doc_2.doc_id().unwrap(), - ref_doc_2.doc_ver().unwrap(), - DocLocator::default(), - ), - DocumentRef::new( - ref_doc_3.doc_id().unwrap(), - ref_doc_3.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "valid document with multiple references" - )] - #[tokio::test] - async fn ref_non_multiple_specified_test( - doc_gen: impl FnOnce(&[DocType; 2], &mut TestCatalystProvider) -> CatalystSignedDocument - ) -> bool { - let mut provider = TestCatalystProvider::default(); - - let exp_types: [DocType; 2] = [UuidV4::new().into(), UuidV4::new().into()]; - - let doc = doc_gen(&exp_types, &mut provider); - - let non_optional_res = RefRule::Specified { - allowed_type: exp_types.to_vec(), - multiple: false, - optional: false, - } - .check(&doc, &provider) - .await - .unwrap(); - - let optional_res = RefRule::Specified { - allowed_type: exp_types.to_vec(), - multiple: false, - optional: true, - } - .check(&doc, &provider) - .await - .unwrap(); - - assert_eq!(non_optional_res, optional_res); - non_optional_res - } - - #[tokio::test] - async fn ref_specified_optional_test() { - let provider = TestCatalystProvider::default(); - let rule = RefRule::Specified { - allowed_type: vec![UuidV4::new().into()], - multiple: true, - optional: true, - }; - - let doc = Builder::new().build(); - assert!(rule.check(&doc, &provider).await.unwrap()); - - let provider = TestCatalystProvider::default(); - let rule = RefRule::Specified { - allowed_type: vec![UuidV4::new().into()], - multiple: true, - optional: false, - }; - - let doc = Builder::new().build(); - assert!(!rule.check(&doc, &provider).await.unwrap()); - } - - #[tokio::test] - async fn ref_rule_not_specified_test() { - let rule = RefRule::NotSpecified; - let provider = TestCatalystProvider::default(); - - let doc = Builder::new().build(); - assert!(rule.check(&doc, &provider).await.unwrap()); - - let ref_id = UuidV7::new(); - let ref_ver = UuidV7::new(); - let doc = Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new(ref_id, ref_ver, DocLocator::default())].into(), - )) - .build(); - assert!(!rule.check(&doc, &provider).await.unwrap()); - } -} diff --git a/rust/signed_doc/src/validator/rules/doc_ref/mod.rs b/rust/signed_doc/src/validator/rules/doc_ref/mod.rs new file mode 100644 index 00000000000..eb868549ef3 --- /dev/null +++ b/rust/signed_doc/src/validator/rules/doc_ref/mod.rs @@ -0,0 +1,243 @@ +//! `ref` rule type impl. + +#[cfg(test)] +mod tests; + +use catalyst_signed_doc_spec::{is_required::IsRequired, metadata::doc_ref::Ref, DocSpecs}; +use catalyst_types::problem_report::ProblemReport; + +use crate::{ + providers::CatalystSignedDocumentProvider, CatalystSignedDocument, DocType, DocumentRef, + DocumentRefs, +}; + +/// `ref` field validation rule +#[derive(Debug)] +pub(crate) enum RefRule { + /// Is 'ref' specified + Specified { + /// allowed `type` field of the referenced doc + allowed_type: Vec, + /// allows multiple document references or only one + multiple: bool, + /// optional flag for the `ref` field + optional: bool, + }, + /// 'ref' is not specified + NotSpecified, +} +impl RefRule { + /// Generating `RefRule` from specs + pub(crate) fn new( + docs: &DocSpecs, + spec: &Ref, + ) -> anyhow::Result { + let optional = match spec.required { + IsRequired::Yes => false, + IsRequired::Optional => true, + IsRequired::Excluded => { + anyhow::ensure!( + spec.doc_type.is_empty() && spec.multiple.is_none(), + "'type' and 'multiple' fields could not been specified when 'required' is 'excluded' for 'ref' metadata definition" + ); + return Ok(Self::NotSpecified); + }, + }; + + anyhow::ensure!(!spec.doc_type.is_empty(), "'type' field should exists and has at least one entry for the required 'ref' metadata definition"); + + let exp_ref_types = spec.doc_type.iter().try_fold( + Vec::new(), + |mut res, doc_name| -> anyhow::Result<_> { + let docs_spec = docs.get(doc_name).ok_or(anyhow::anyhow!( + "cannot find a document definition {doc_name}" + ))?; + res.push(docs_spec.doc_type.as_str().parse()?); + Ok(res) + }, + )?; + + let multiple = spec.multiple.ok_or(anyhow::anyhow!( + "'multiple' field should exists for the required 'ref' metadata definition" + ))?; + + Ok(Self::Specified { + allowed_type: exp_ref_types, + multiple, + optional, + }) + } + + /// Field validation rule + pub(crate) async fn check( + &self, + doc: &CatalystSignedDocument, + provider: &Provider, + ) -> anyhow::Result + where + Provider: CatalystSignedDocumentProvider, + { + let context: &str = "Ref rule check"; + if let Self::Specified { + allowed_type: exp_ref_types, + multiple, + optional, + } = self + { + if let Some(doc_refs) = doc.doc_meta().doc_ref() { + return doc_refs_check( + doc_refs, + exp_ref_types, + *multiple, + "ref", + provider, + doc.report(), + |_| true, + ) + .await; + } else if !optional { + doc.report() + .missing_field("ref", &format!("{context}, document must have ref field")); + return Ok(false); + } + } + if let Self::NotSpecified = self { + if let Some(doc_ref) = doc.doc_meta().doc_ref() { + doc.report().unknown_field( + "ref", + &doc_ref.to_string(), + &format!("{context}, document does not expect to have a ref field"), + ); + return Ok(false); + } + } + + Ok(true) + } +} + +/// Validate all the document references by the defined validation rules, +/// plus conducting additional validations with the provided `validator`. +/// Document all possible error in doc report (no fail fast) +pub(crate) async fn doc_refs_check( + doc_refs: &DocumentRefs, + exp_ref_types: &[DocType], + multiple: bool, + field_name: &str, + provider: &Provider, + report: &ProblemReport, + validator: Validator, +) -> anyhow::Result +where + Provider: CatalystSignedDocumentProvider, + Validator: Fn(&CatalystSignedDocument) -> bool, +{ + let mut all_valid = true; + + if !multiple && doc_refs.len() > 1 { + report.other( + format!( + "Only ONE document reference is allowed, found {} document references", + doc_refs.len() + ) + .as_str(), + &format!("Referenced document validation for the `{field_name}` field"), + ); + return Ok(false); + } + + for dr in doc_refs.iter() { + if let Some(ref ref_doc) = provider.try_get_doc(dr).await? { + let is_valid = referenced_doc_type_check(ref_doc, exp_ref_types, field_name, report) + && referenced_doc_id_and_ver_check(ref_doc, dr, field_name, report) + && validator(ref_doc); + + if !is_valid { + all_valid = false; + } + } else { + report.functional_validation( + &format!("Cannot retrieve a document {dr}"), + &format!("Referenced document validation for the `{field_name}` field"), + ); + all_valid = false; + } + } + Ok(all_valid) +} + +/// Validation check that the provided `ref_doc` is a correct referenced document found by +/// `original_doc_ref` +fn referenced_doc_id_and_ver_check( + ref_doc: &CatalystSignedDocument, + original_doc_ref: &DocumentRef, + field_name: &str, + report: &ProblemReport, +) -> bool { + let Ok(id) = ref_doc.doc_id() else { + report.missing_field( + "id", + &format!("Referenced document validation for the `{field_name}` field"), + ); + return false; + }; + + let Ok(ver) = ref_doc.doc_ver() else { + report.missing_field( + "ver", + &format!("Referenced document validation for the `{field_name}` field"), + ); + return false; + }; + + // id and version must match the values in ref doc + if &id != original_doc_ref.id() && &ver != original_doc_ref.ver() { + report.invalid_value( + "id and version", + &format!("id: {id}, ver: {ver}"), + &format!( + "id: {}, ver: {}", + original_doc_ref.id(), + original_doc_ref.ver() + ), + &format!("Referenced document validation for the `{field_name}` field"), + ); + return false; + } + + true +} + +/// Validation check that the provided `ref_doc` has an expected `type` field value from +/// the allowed `exp_ref_types` list +fn referenced_doc_type_check( + ref_doc: &CatalystSignedDocument, + exp_ref_types: &[DocType], + field_name: &str, + report: &ProblemReport, +) -> bool { + let Ok(ref_doc_type) = ref_doc.doc_type() else { + report.missing_field( + "type", + &format!("Document reference validation for the `{field_name}` field"), + ); + return false; + }; + + // Check that the type matches one of the expected ones + if exp_ref_types + .iter() + .all(|exp_type| ref_doc_type != exp_type) + { + report.invalid_value( + field_name, + &ref_doc_type.to_string(), + &exp_ref_types + .iter() + .fold(String::new(), |s, v| format!("{s}, {v}")), + &format!("Invalid referenced document type, during validation of {field_name} field"), + ); + return false; + } + true +} diff --git a/rust/signed_doc/src/validator/rules/doc_ref/tests.rs b/rust/signed_doc/src/validator/rules/doc_ref/tests.rs new file mode 100644 index 00000000000..3acdf1ef32c --- /dev/null +++ b/rust/signed_doc/src/validator/rules/doc_ref/tests.rs @@ -0,0 +1,386 @@ +use catalyst_types::uuid::{UuidV4, UuidV7}; +use test_case::test_case; + +use super::*; +use crate::{ + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + DocLocator, DocumentRef, +}; + +#[test_case( + |exp_types, provider| { + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[0].clone())) + .build(); + provider.add_document(None, &ref_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => true + ; + "valid reference to the one correct document" +)] +#[test_case( +` |exp_types, provider| { + let ref_doc_1 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[0].clone())) + .build(); + provider.add_document(None, &ref_doc_1).unwrap(); + let ref_doc_2 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[1].clone())) + .build(); + provider.add_document(None, &ref_doc_2).unwrap(); + let ref_doc_3 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[0].clone())) + .build(); + provider.add_document(None, &ref_doc_3).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + ref_doc_1.doc_id().unwrap(), + ref_doc_1.doc_ver().unwrap(), + DocLocator::default(), + ), + DocumentRef::new( + ref_doc_2.doc_id().unwrap(), + ref_doc_2.doc_ver().unwrap(), + DocLocator::default(), + ), + DocumentRef::new( + ref_doc_3.doc_id().unwrap(), + ref_doc_3.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => true + ; + "valid reference to the multiple documents" + )]` +#[test_case( + |exp_types, provider| { + let ref_doc_1 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[0].clone())) + .build(); + provider.add_document(None, &ref_doc_1).unwrap(); + let ref_doc_2 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[1].clone())) + .build(); + provider.add_document(None, &ref_doc_2).unwrap(); + let ref_doc_3 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(UuidV4::new().into())) + .build(); + provider.add_document(None, &ref_doc_3).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + ref_doc_1.doc_id().unwrap(), + ref_doc_1.doc_ver().unwrap(), + DocLocator::default(), + ), + DocumentRef::new( + ref_doc_2.doc_id().unwrap(), + ref_doc_2.doc_ver().unwrap(), + DocLocator::default(), + ), + DocumentRef::new( + ref_doc_3.doc_id().unwrap(), + ref_doc_3.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "valid reference to the multiple documents, one of them invalid `type` field" +)] +#[test_case( + |exp_types, provider| { + let ref_doc_1 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[0].clone())) + .build(); + provider.add_document(None, &ref_doc_1).unwrap(); + let ref_doc_2 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[1].clone())) + .build(); + provider.add_document(None, &ref_doc_2).unwrap(); + let ref_doc_3 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .build(); + provider.add_document(None, &ref_doc_3).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + ref_doc_1.doc_id().unwrap(), + ref_doc_1.doc_ver().unwrap(), + DocLocator::default(), + ), + DocumentRef::new( + ref_doc_2.doc_id().unwrap(), + ref_doc_2.doc_ver().unwrap(), + DocLocator::default(), + ), + DocumentRef::new( + ref_doc_3.doc_id().unwrap(), + ref_doc_3.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "valid reference to the multiple documents, one of them missing `type` field" +)] +#[test_case( + |exp_types, provider| { + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[0].clone())) + .build(); + provider.add_document(Some(DocumentRef::new(UuidV7::new(), UuidV7::new(), DocLocator::default())), &ref_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "invalid reference to the document, which has different id and ver fields as stated in the `ref` field" +)] +#[test_case( + |_, _| { + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ), + ] + .into(), + )) + .build() + } + => false + ; + "valid reference to the missing one document" +)] +#[tokio::test] +async fn ref_multiple_specified_test( + doc_gen: impl FnOnce(&[DocType; 2], &mut TestCatalystProvider) -> CatalystSignedDocument +) -> bool { + let mut provider = TestCatalystProvider::default(); + + let exp_types: [DocType; 2] = [UuidV4::new().into(), UuidV4::new().into()]; + + let doc = doc_gen(&exp_types, &mut provider); + + let non_optional_res = RefRule::Specified { + allowed_type: exp_types.to_vec(), + multiple: true, + optional: false, + } + .check(&doc, &provider) + .await + .unwrap(); + + let optional_res = RefRule::Specified { + allowed_type: exp_types.to_vec(), + multiple: true, + optional: true, + } + .check(&doc, &provider) + .await + .unwrap(); + + assert_eq!(non_optional_res, optional_res); + non_optional_res +} + +#[test_case( + |exp_types, provider| { + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[0].clone())) + .build(); + provider.add_document(None, &ref_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => true + ; + "valid document with a single reference" +)] +#[test_case( + |exp_types, provider| { + let ref_doc_1 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[0].clone())) + .build(); + provider.add_document(None, &ref_doc_1).unwrap(); + let ref_doc_2 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[1].clone())) + .build(); + provider.add_document(None, &ref_doc_2).unwrap(); + let ref_doc_3 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[0].clone())) + .build(); + provider.add_document(None, &ref_doc_3).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + ref_doc_1.doc_id().unwrap(), + ref_doc_1.doc_ver().unwrap(), + DocLocator::default(), + ), + DocumentRef::new( + ref_doc_2.doc_id().unwrap(), + ref_doc_2.doc_ver().unwrap(), + DocLocator::default(), + ), + DocumentRef::new( + ref_doc_3.doc_id().unwrap(), + ref_doc_3.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "valid document with multiple references" +)] +#[tokio::test] +async fn ref_non_multiple_specified_test( + doc_gen: impl FnOnce(&[DocType; 2], &mut TestCatalystProvider) -> CatalystSignedDocument +) -> bool { + let mut provider = TestCatalystProvider::default(); + + let exp_types: [DocType; 2] = [UuidV4::new().into(), UuidV4::new().into()]; + + let doc = doc_gen(&exp_types, &mut provider); + + let non_optional_res = RefRule::Specified { + allowed_type: exp_types.to_vec(), + multiple: false, + optional: false, + } + .check(&doc, &provider) + .await + .unwrap(); + + let optional_res = RefRule::Specified { + allowed_type: exp_types.to_vec(), + multiple: false, + optional: true, + } + .check(&doc, &provider) + .await + .unwrap(); + + assert_eq!(non_optional_res, optional_res); + non_optional_res +} + +#[tokio::test] +async fn ref_specified_optional_test() { + let provider = TestCatalystProvider::default(); + let rule = RefRule::Specified { + allowed_type: vec![UuidV4::new().into()], + multiple: true, + optional: true, + }; + + let doc = Builder::new().build(); + assert!(rule.check(&doc, &provider).await.unwrap()); + + let provider = TestCatalystProvider::default(); + let rule = RefRule::Specified { + allowed_type: vec![UuidV4::new().into()], + multiple: true, + optional: false, + }; + + let doc = Builder::new().build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); +} + +#[tokio::test] +async fn ref_rule_not_specified_test() { + let rule = RefRule::NotSpecified; + let provider = TestCatalystProvider::default(); + + let doc = Builder::new().build(); + assert!(rule.check(&doc, &provider).await.unwrap()); + + let ref_id = UuidV7::new(); + let ref_ver = UuidV7::new(); + let doc = Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new(ref_id, ref_ver, DocLocator::default())].into(), + )) + .build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); +} diff --git a/rust/signed_doc/src/validator/rules/parameters.rs b/rust/signed_doc/src/validator/rules/parameters.rs deleted file mode 100644 index 8bba9e9fc17..00000000000 --- a/rust/signed_doc/src/validator/rules/parameters.rs +++ /dev/null @@ -1,824 +0,0 @@ -//! `parameters` rule type impl. - -use catalyst_signed_doc_spec::{ - is_required::IsRequired, metadata::parameters::Parameters, DocSpecs, -}; -use catalyst_types::problem_report::ProblemReport; -use futures::FutureExt; - -use crate::{ - providers::CatalystSignedDocumentProvider, validator::rules::doc_ref::doc_refs_check, - CatalystSignedDocument, DocType, DocumentRefs, -}; - -/// `parameters` field validation rule -#[derive(Debug)] -pub(crate) enum ParametersRule { - /// Is `parameters` specified - Specified { - /// expected `type` field of the parameter doc - allowed_type: Vec, - /// optional flag for the `parameters` field - optional: bool, - }, - /// `parameters` is not specified - NotSpecified, -} - -impl ParametersRule { - /// Generating `ParametersRule` from specs - pub(crate) fn new( - docs: &DocSpecs, - spec: &Parameters, - ) -> anyhow::Result { - let optional = match spec.required { - IsRequired::Yes => false, - IsRequired::Optional => true, - IsRequired::Excluded => { - anyhow::ensure!( - spec.doc_type.is_empty() && spec.multiple.is_none(), - "'type' and 'multiple' fields could not been specified when 'required' is 'excluded' for 'parameters' metadata definition" - ); - return Ok(Self::NotSpecified); - }, - }; - - anyhow::ensure!(!spec.doc_type.is_empty(), "'type' field should exists and has at least one entry for the required 'parameters' metadata definition"); - anyhow::ensure!( - spec.multiple.is_some_and(|v| !v), - "'multiple' field should be only set to false for the required 'parameters' metadata definition" - ); - - let allowed_type = spec.doc_type.iter().try_fold( - Vec::new(), - |mut res, doc_name| -> anyhow::Result<_> { - let docs_spec = docs.get(doc_name).ok_or(anyhow::anyhow!( - "cannot find a document definition {doc_name}" - ))?; - res.push(docs_spec.doc_type.as_str().parse()?); - Ok(res) - }, - )?; - - Ok(Self::Specified { - allowed_type, - optional, - }) - } - - /// Field validation rule - pub(crate) async fn check( - &self, - doc: &CatalystSignedDocument, - provider: &Provider, - ) -> anyhow::Result - where - Provider: CatalystSignedDocumentProvider, - { - let context: &str = "Parameter rule check"; - if let Self::Specified { - allowed_type: exp_parameters_type, - optional, - } = self - { - if let Some(parameters_ref) = doc.doc_meta().parameters() { - let parameters_check = doc_refs_check( - parameters_ref, - exp_parameters_type, - false, - "parameters", - provider, - doc.report(), - |_| true, - ) - .boxed(); - - let template_link_check = link_check( - doc.doc_meta().template(), - parameters_ref, - "template", - provider, - doc.report(), - ) - .boxed(); - let ref_link_check = link_check( - doc.doc_meta().doc_ref(), - parameters_ref, - "ref", - provider, - doc.report(), - ) - .boxed(); - let reply_link_check = link_check( - doc.doc_meta().reply(), - parameters_ref, - "reply", - provider, - doc.report(), - ) - .boxed(); - - let checks = [ - parameters_check, - template_link_check, - ref_link_check, - reply_link_check, - ]; - let res = futures::future::join_all(checks) - .await - .into_iter() - .collect::>>()? - .iter() - .all(|res| *res); - - return Ok(res); - } else if !optional { - doc.report().missing_field( - "parameters", - &format!("{context}, document must have parameters field"), - ); - return Ok(false); - } - } - if let Self::NotSpecified = self { - if let Some(parameters) = doc.doc_meta().parameters() { - doc.report().unknown_field( - "parameters", - ¶meters.to_string(), - &format!("{context}, document does not expect to have a parameters field"), - ); - return Ok(false); - } - } - - Ok(true) - } -} - -/// Parameter Link reference check -pub(crate) async fn link_check( - ref_field: Option<&DocumentRefs>, - exp_parameters: &DocumentRefs, - field_name: &str, - provider: &Provider, - report: &ProblemReport, -) -> anyhow::Result -where - Provider: CatalystSignedDocumentProvider, -{ - let Some(ref_field) = ref_field else { - return Ok(true); - }; - - let mut all_valid = true; - - for dr in ref_field.iter() { - if let Some(ref ref_doc) = provider.try_get_doc(dr).await? { - let Some(ref_doc_parameters) = ref_doc.doc_meta().parameters() else { - report.missing_field( - "parameters", - &format!( - "Referenced document via {field_name} must have `parameters` field. Referenced Document: {ref_doc}" - ), - ); - all_valid = false; - continue; - }; - - if exp_parameters != ref_doc_parameters { - report.invalid_value( - "parameters", - &format!("Reference doc param: {ref_doc_parameters}",), - &format!("Doc param: {exp_parameters}"), - &format!( - "Referenced document via {field_name} `parameters` field must match. Referenced Document: {ref_doc}" - ), - ); - all_valid = false; - } - } else { - report.functional_validation( - &format!("Cannot retrieve a document {dr}"), - &format!("Referenced document link validation for the `{field_name}` field"), - ); - all_valid = false; - } - } - Ok(all_valid) -} - -#[cfg(test)] -mod tests { - use catalyst_types::uuid::{UuidV4, UuidV7}; - use test_case::test_case; - - use super::*; - use crate::{ - builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, - DocLocator, DocumentRef, - }; - - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Parameters( - vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => true - ; - "valid reference to the valid parameters document" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - let common_parameter_field: DocumentRefs = vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(); - let template_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Parameters(common_parameter_field.clone())) - .build(); - provider.add_document(None, &template_doc).unwrap(); - - - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![DocumentRef::new( - template_doc.doc_id().unwrap(), - template_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(common_parameter_field)) - .build() - } - => true - ; - "valid reference to the valid parameters document, with valid template field" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into())) - .build() - } - => false - ; - "valid reference to the valid parameters document, with missing template doc" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - let template_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .build(); - provider.add_document(None, &template_doc).unwrap(); - - - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![DocumentRef::new( - template_doc.doc_id().unwrap(), - template_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into())) - .build() - } - => false - ; - "valid reference to the valid parameters document, with missing parameters field in template doc" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - let template_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into())) - .build(); - provider.add_document(None, &template_doc).unwrap(); - - - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![DocumentRef::new( - template_doc.doc_id().unwrap(), - template_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into())) - .build() - } - => false - ; - "valid reference to the valid parameters document, with different parameters field in template doc" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - let common_parameter_field: DocumentRefs = vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(); - let replied_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Parameters(common_parameter_field.clone())) - .build(); - provider.add_document(None, &replied_doc).unwrap(); - - - Builder::new() - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new( - replied_doc.doc_id().unwrap(), - replied_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(common_parameter_field)) - .build() - } - => true - ; - "valid reference to the valid parameters document, with valid reply field" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into())) - .build() - } - => false - ; - "valid reference to the valid parameters document, with missing reply doc" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - let reply_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .build(); - provider.add_document(None, &reply_doc).unwrap(); - - - Builder::new() - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new( - reply_doc.doc_id().unwrap(), - reply_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into())) - .build() - } - => false - ; - "valid reference to the valid parameters document, with missing parameters field in replied doc" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - let reply_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into())) - .build(); - provider.add_document(None, &reply_doc).unwrap(); - - - Builder::new() - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new( - reply_doc.doc_id().unwrap(), - reply_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into())) - .build() - } - => false - ; - "valid reference to the valid parameters document, with different parameters field in reply doc" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - let common_parameter_field: DocumentRefs = vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(); - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Parameters(common_parameter_field.clone())) - .build(); - provider.add_document(None, &ref_doc).unwrap(); - - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(common_parameter_field)) - .build() - } - => true - ; - "valid reference to the valid parameters document, with valid ref field" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into())) - .build() - } - => false - ; - "valid reference to the valid parameters document, with missing ref doc" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .build(); - provider.add_document(None, &ref_doc).unwrap(); - - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into())) - .build() - } - => false - ; - "valid reference to the valid parameters document, with missing parameters field in ref doc" - )] - #[test_case( - |exp_param_types, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into())) - .build(); - provider.add_document(None, &ref_doc).unwrap(); - - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into() - )) - .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into())) - .build() - } - => false - ; - "valid reference to the valid parameters document, with different parameters field in ref doc" - )] - #[test_case( - |_, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(UuidV4::new().into())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Parameters( - vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "valid reference to the invalid parameters document, wrong parameters type field value" - )] - #[test_case( - |_, provider| { - let parameter_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .build(); - provider.add_document(None, ¶meter_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Parameters( - vec![DocumentRef::new( - parameter_doc.doc_id().unwrap(), - parameter_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "valid reference to the invalid parameters document, missing type field" - )] - #[test_case( - |_, _| { - Builder::new() - .with_metadata_field(SupportedField::Parameters( - vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "reference to the not known document" - )] - #[tokio::test] - async fn parameter_specified_test( - doc_gen: impl FnOnce(&[DocType; 2], &mut TestCatalystProvider) -> CatalystSignedDocument - ) -> bool { - let mut provider = TestCatalystProvider::default(); - - let exp_param_types: [DocType; 2] = [UuidV4::new().into(), UuidV4::new().into()]; - - let doc = doc_gen(&exp_param_types, &mut provider); - - let non_optional_res = ParametersRule::Specified { - allowed_type: exp_param_types.to_vec(), - optional: false, - } - .check(&doc, &provider) - .await - .unwrap(); - - let optional_res = ParametersRule::Specified { - allowed_type: exp_param_types.to_vec(), - optional: true, - } - .check(&doc, &provider) - .await - .unwrap(); - - assert_eq!(non_optional_res, optional_res); - non_optional_res - } - - #[tokio::test] - async fn ref_specified_optional_test() { - let provider = TestCatalystProvider::default(); - let rule = ParametersRule::Specified { - allowed_type: vec![UuidV4::new().into()], - optional: true, - }; - - let doc = Builder::new().build(); - assert!(rule.check(&doc, &provider).await.unwrap()); - - let provider = TestCatalystProvider::default(); - let rule = ParametersRule::Specified { - allowed_type: vec![UuidV4::new().into()], - optional: false, - }; - - let doc = Builder::new().build(); - assert!(!rule.check(&doc, &provider).await.unwrap()); - } - - #[tokio::test] - async fn parameters_rule_not_specified_test() { - let rule = ParametersRule::NotSpecified; - let provider = TestCatalystProvider::default(); - - let doc = Builder::new().build(); - assert!(rule.check(&doc, &provider).await.unwrap()); - - let ref_id = UuidV7::new(); - let ref_ver = UuidV7::new(); - let doc = Builder::new() - .with_metadata_field(SupportedField::Parameters( - vec![DocumentRef::new(ref_id, ref_ver, DocLocator::default())].into(), - )) - .build(); - assert!(!rule.check(&doc, &provider).await.unwrap()); - } -} diff --git a/rust/signed_doc/src/validator/rules/parameters/mod.rs b/rust/signed_doc/src/validator/rules/parameters/mod.rs new file mode 100644 index 00000000000..a07195048dc --- /dev/null +++ b/rust/signed_doc/src/validator/rules/parameters/mod.rs @@ -0,0 +1,211 @@ +//! `parameters` rule type impl. + +#[cfg(test)] +mod tests; + +use catalyst_signed_doc_spec::{ + is_required::IsRequired, metadata::parameters::Parameters, DocSpecs, +}; +use catalyst_types::problem_report::ProblemReport; +use futures::FutureExt; + +use crate::{ + providers::CatalystSignedDocumentProvider, validator::rules::doc_ref::doc_refs_check, + CatalystSignedDocument, DocType, DocumentRefs, +}; + +/// `parameters` field validation rule +#[derive(Debug)] +pub(crate) enum ParametersRule { + /// Is `parameters` specified + Specified { + /// expected `type` field of the parameter doc + allowed_type: Vec, + /// optional flag for the `parameters` field + optional: bool, + }, + /// `parameters` is not specified + NotSpecified, +} + +impl ParametersRule { + /// Generating `ParametersRule` from specs + pub(crate) fn new( + docs: &DocSpecs, + spec: &Parameters, + ) -> anyhow::Result { + let optional = match spec.required { + IsRequired::Yes => false, + IsRequired::Optional => true, + IsRequired::Excluded => { + anyhow::ensure!( + spec.doc_type.is_empty() && spec.multiple.is_none(), + "'type' and 'multiple' fields could not been specified when 'required' is 'excluded' for 'parameters' metadata definition" + ); + return Ok(Self::NotSpecified); + }, + }; + + anyhow::ensure!(!spec.doc_type.is_empty(), "'type' field should exists and has at least one entry for the required 'parameters' metadata definition"); + anyhow::ensure!( + spec.multiple.is_some_and(|v| !v), + "'multiple' field should be only set to false for the required 'parameters' metadata definition" + ); + + let allowed_type = spec.doc_type.iter().try_fold( + Vec::new(), + |mut res, doc_name| -> anyhow::Result<_> { + let docs_spec = docs.get(doc_name).ok_or(anyhow::anyhow!( + "cannot find a document definition {doc_name}" + ))?; + res.push(docs_spec.doc_type.as_str().parse()?); + Ok(res) + }, + )?; + + Ok(Self::Specified { + allowed_type, + optional, + }) + } + + /// Field validation rule + pub(crate) async fn check( + &self, + doc: &CatalystSignedDocument, + provider: &Provider, + ) -> anyhow::Result + where + Provider: CatalystSignedDocumentProvider, + { + let context: &str = "Parameter rule check"; + if let Self::Specified { + allowed_type: exp_parameters_type, + optional, + } = self + { + if let Some(parameters_ref) = doc.doc_meta().parameters() { + let parameters_check = doc_refs_check( + parameters_ref, + exp_parameters_type, + false, + "parameters", + provider, + doc.report(), + |_| true, + ) + .boxed(); + + let template_link_check = link_check( + doc.doc_meta().template(), + parameters_ref, + "template", + provider, + doc.report(), + ) + .boxed(); + let ref_link_check = link_check( + doc.doc_meta().doc_ref(), + parameters_ref, + "ref", + provider, + doc.report(), + ) + .boxed(); + let reply_link_check = link_check( + doc.doc_meta().reply(), + parameters_ref, + "reply", + provider, + doc.report(), + ) + .boxed(); + + let checks = [ + parameters_check, + template_link_check, + ref_link_check, + reply_link_check, + ]; + let res = futures::future::join_all(checks) + .await + .into_iter() + .collect::>>()? + .iter() + .all(|res| *res); + + return Ok(res); + } else if !optional { + doc.report().missing_field( + "parameters", + &format!("{context}, document must have parameters field"), + ); + return Ok(false); + } + } + if let Self::NotSpecified = self { + if let Some(parameters) = doc.doc_meta().parameters() { + doc.report().unknown_field( + "parameters", + ¶meters.to_string(), + &format!("{context}, document does not expect to have a parameters field"), + ); + return Ok(false); + } + } + + Ok(true) + } +} + +/// Parameter Link reference check +pub(crate) async fn link_check( + ref_field: Option<&DocumentRefs>, + exp_parameters: &DocumentRefs, + field_name: &str, + provider: &Provider, + report: &ProblemReport, +) -> anyhow::Result +where + Provider: CatalystSignedDocumentProvider, +{ + let Some(ref_field) = ref_field else { + return Ok(true); + }; + + let mut all_valid = true; + + for dr in ref_field.iter() { + if let Some(ref ref_doc) = provider.try_get_doc(dr).await? { + let Some(ref_doc_parameters) = ref_doc.doc_meta().parameters() else { + report.missing_field( + "parameters", + &format!( + "Referenced document via {field_name} must have `parameters` field. Referenced Document: {ref_doc}" + ), + ); + all_valid = false; + continue; + }; + + if exp_parameters != ref_doc_parameters { + report.invalid_value( + "parameters", + &format!("Reference doc param: {ref_doc_parameters}",), + &format!("Doc param: {exp_parameters}"), + &format!( + "Referenced document via {field_name} `parameters` field must match. Referenced Document: {ref_doc}" + ), + ); + all_valid = false; + } + } else { + report.functional_validation( + &format!("Cannot retrieve a document {dr}"), + &format!("Referenced document link validation for the `{field_name}` field"), + ); + all_valid = false; + } + } + Ok(all_valid) +} diff --git a/rust/signed_doc/src/validator/rules/parameters/tests.rs b/rust/signed_doc/src/validator/rules/parameters/tests.rs new file mode 100644 index 00000000000..0c71e91d4be --- /dev/null +++ b/rust/signed_doc/src/validator/rules/parameters/tests.rs @@ -0,0 +1,612 @@ +use catalyst_types::uuid::{UuidV4, UuidV7}; +use test_case::test_case; + +use super::*; +use crate::{ + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + DocLocator, DocumentRef, +}; + +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Parameters( + vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => true + ; + "valid reference to the valid parameters document" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + let common_parameter_field: DocumentRefs = vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(); + let template_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Parameters(common_parameter_field.clone())) + .build(); + provider.add_document(None, &template_doc).unwrap(); + + + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![DocumentRef::new( + template_doc.doc_id().unwrap(), + template_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(common_parameter_field)) + .build() + } + => true + ; + "valid reference to the valid parameters document, with valid template field" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into())) + .build() + } + => false + ; + "valid reference to the valid parameters document, with missing template doc" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + let template_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .build(); + provider.add_document(None, &template_doc).unwrap(); + + + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![DocumentRef::new( + template_doc.doc_id().unwrap(), + template_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into())) + .build() + } + => false + ; + "valid reference to the valid parameters document, with missing parameters field in template doc" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + let template_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into())) + .build(); + provider.add_document(None, &template_doc).unwrap(); + + + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![DocumentRef::new( + template_doc.doc_id().unwrap(), + template_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into())) + .build() + } + => false + ; + "valid reference to the valid parameters document, with different parameters field in template doc" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + let common_parameter_field: DocumentRefs = vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(); + let replied_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Parameters(common_parameter_field.clone())) + .build(); + provider.add_document(None, &replied_doc).unwrap(); + + + Builder::new() + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new( + replied_doc.doc_id().unwrap(), + replied_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(common_parameter_field)) + .build() + } + => true + ; + "valid reference to the valid parameters document, with valid reply field" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into())) + .build() + } + => false + ; + "valid reference to the valid parameters document, with missing reply doc" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + let reply_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .build(); + provider.add_document(None, &reply_doc).unwrap(); + + + Builder::new() + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new( + reply_doc.doc_id().unwrap(), + reply_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into())) + .build() + } + => false + ; + "valid reference to the valid parameters document, with missing parameters field in replied doc" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + let reply_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into())) + .build(); + provider.add_document(None, &reply_doc).unwrap(); + + + Builder::new() + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new( + reply_doc.doc_id().unwrap(), + reply_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into())) + .build() + } + => false + ; + "valid reference to the valid parameters document, with different parameters field in reply doc" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + let common_parameter_field: DocumentRefs = vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(); + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Parameters(common_parameter_field.clone())) + .build(); + provider.add_document(None, &ref_doc).unwrap(); + + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(common_parameter_field)) + .build() + } + => true + ; + "valid reference to the valid parameters document, with valid ref field" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into())) + .build() + } + => false + ; + "valid reference to the valid parameters document, with missing ref doc" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .build(); + provider.add_document(None, &ref_doc).unwrap(); + + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into())) + .build() + } + => false + ; + "valid reference to the valid parameters document, with missing parameters field in ref doc" +)] +#[test_case( + |exp_param_types, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_param_types[0].clone())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into())) + .build(); + provider.add_document(None, &ref_doc).unwrap(); + + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into() + )) + .with_metadata_field(SupportedField::Parameters(vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into())) + .build() + } + => false + ; + "valid reference to the valid parameters document, with different parameters field in ref doc" +)] +#[test_case( + |_, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(UuidV4::new().into())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Parameters( + vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "valid reference to the invalid parameters document, wrong parameters type field value" +)] +#[test_case( + |_, provider| { + let parameter_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .build(); + provider.add_document(None, ¶meter_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Parameters( + vec![DocumentRef::new( + parameter_doc.doc_id().unwrap(), + parameter_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "valid reference to the invalid parameters document, missing type field" +)] +#[test_case( + |_, _| { + Builder::new() + .with_metadata_field(SupportedField::Parameters( + vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "reference to the not known document" +)] +#[tokio::test] +async fn parameter_specified_test( + doc_gen: impl FnOnce(&[DocType; 2], &mut TestCatalystProvider) -> CatalystSignedDocument +) -> bool { + let mut provider = TestCatalystProvider::default(); + + let exp_param_types: [DocType; 2] = [UuidV4::new().into(), UuidV4::new().into()]; + + let doc = doc_gen(&exp_param_types, &mut provider); + + let non_optional_res = ParametersRule::Specified { + allowed_type: exp_param_types.to_vec(), + optional: false, + } + .check(&doc, &provider) + .await + .unwrap(); + + let optional_res = ParametersRule::Specified { + allowed_type: exp_param_types.to_vec(), + optional: true, + } + .check(&doc, &provider) + .await + .unwrap(); + + assert_eq!(non_optional_res, optional_res); + non_optional_res +} + +#[tokio::test] +async fn ref_specified_optional_test() { + let provider = TestCatalystProvider::default(); + let rule = ParametersRule::Specified { + allowed_type: vec![UuidV4::new().into()], + optional: true, + }; + + let doc = Builder::new().build(); + assert!(rule.check(&doc, &provider).await.unwrap()); + + let provider = TestCatalystProvider::default(); + let rule = ParametersRule::Specified { + allowed_type: vec![UuidV4::new().into()], + optional: false, + }; + + let doc = Builder::new().build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); +} + +#[tokio::test] +async fn parameters_rule_not_specified_test() { + let rule = ParametersRule::NotSpecified; + let provider = TestCatalystProvider::default(); + + let doc = Builder::new().build(); + assert!(rule.check(&doc, &provider).await.unwrap()); + + let ref_id = UuidV7::new(); + let ref_ver = UuidV7::new(); + let doc = Builder::new() + .with_metadata_field(SupportedField::Parameters( + vec![DocumentRef::new(ref_id, ref_ver, DocLocator::default())].into(), + )) + .build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); +} diff --git a/rust/signed_doc/src/validator/rules/reply.rs b/rust/signed_doc/src/validator/rules/reply.rs deleted file mode 100644 index 0803191643a..00000000000 --- a/rust/signed_doc/src/validator/rules/reply.rs +++ /dev/null @@ -1,437 +0,0 @@ -//! `reply` rule type impl. - -use catalyst_signed_doc_spec::{ - is_required::IsRequired, metadata::reply::Reply, DocSpecs, DocumentName, -}; - -use crate::{ - providers::CatalystSignedDocumentProvider, validator::rules::doc_ref::doc_refs_check, - CatalystSignedDocument, DocType, -}; - -/// `reply` field validation rule -#[derive(Debug)] -pub(crate) enum ReplyRule { - /// Is 'reply' specified - Specified { - /// allowed `type` field of the replied doc - allowed_type: DocType, - /// optional flag for the `ref` field - optional: bool, - }, - /// 'reply' is not specified - NotSpecified, -} - -impl ReplyRule { - /// Generating `ReplyRule` from specs - pub(crate) fn new( - docs: &DocSpecs, - spec: &Reply, - ) -> anyhow::Result { - let optional = match spec.required { - IsRequired::Yes => false, - IsRequired::Optional => true, - IsRequired::Excluded => { - anyhow::ensure!( - spec.doc_type.is_empty() && spec.multiple.is_none(), - "'type' and 'multiple' fields could not been specified when 'required' is 'excluded' for 'reply' metadata definition" - ); - return Ok(Self::NotSpecified); - }, - }; - - anyhow::ensure!( - spec.multiple.is_some_and(|v| !v), - "'multiple' field should be only set to false for the required 'reply' metadata definition" - ); - - let doc_name = &<&[DocumentName; 1]>::try_from(spec.doc_type.as_slice()).map_err(|_| anyhow::anyhow!("'type' field should exists and has only one entry for the required 'reply' metadata definition"))?[0]; - let docs_spec = docs.get(doc_name).ok_or(anyhow::anyhow!( - "cannot find a document definition {doc_name}" - ))?; - let allowed_type = docs_spec.doc_type.as_str().parse()?; - - Ok(Self::Specified { - allowed_type, - optional, - }) - } - - /// Field validation rule - pub(crate) async fn check( - &self, - doc: &CatalystSignedDocument, - provider: &Provider, - ) -> anyhow::Result - where - Provider: CatalystSignedDocumentProvider, - { - let context: &str = "Reply rule check"; - if let Self::Specified { - allowed_type: exp_reply_type, - optional, - } = self - { - if let Some(reply_ref) = doc.doc_meta().reply() { - let reply_validator = |ref_doc: &CatalystSignedDocument| { - // Get `ref` from both the doc and the ref doc - let Some(ref_doc_dr) = ref_doc.doc_meta().doc_ref() else { - doc.report() - .missing_field("Referenced doc `ref` field", context); - return false; - }; - - let Some(doc_dr) = doc.doc_meta().doc_ref() else { - doc.report().missing_field("Document `ref` field", context); - return false; - }; - - // Checking the ref field of ref doc, it should match the ref field of the doc - // If not record the error - if ref_doc_dr != doc_dr { - doc.report().invalid_value( - "ref", - &format!("Reference doc ref: {ref_doc_dr}"), - &format!("Doc ref: {doc_dr}"), - &format!("{context}, ref must be the same"), - ); - return false; - } - true - }; - - return doc_refs_check( - reply_ref, - std::slice::from_ref(exp_reply_type), - false, - "reply", - provider, - doc.report(), - reply_validator, - ) - .await; - } else if !optional { - doc.report().missing_field( - "reply", - &format!("{context}, document must have reply field"), - ); - return Ok(false); - } - } - if let Self::NotSpecified = self { - if let Some(reply) = doc.doc_meta().reply() { - doc.report().unknown_field( - "reply", - &reply.to_string(), - &format!("{context}, document does not expect to have a reply field"), - ); - return Ok(false); - } - } - - Ok(true) - } -} - -#[cfg(test)] -mod tests { - use catalyst_types::uuid::{UuidV4, UuidV7}; - use test_case::test_case; - - use super::*; - use crate::{ - builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, - DocLocator, DocumentRef, DocumentRefs, - }; - - #[test_case( - |exp_type, provider| { - let common_ref: DocumentRefs = vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into(); - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Ref(common_ref.clone())) - .with_metadata_field(SupportedField::Type(exp_type)) - .build(); - provider.add_document(None, &ref_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref(common_ref)) - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => true - ; - "valid reply to the correct document" - )] - #[test_case( - |_, provider| { - let common_ref: DocumentRefs = vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into(); - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Ref(common_ref.clone())) - .with_metadata_field(SupportedField::Type(UuidV4::new().into())) - .build(); - provider.add_document(None, &ref_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref(common_ref)) - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "valid reply to the document, with invalid `type` field" - )] - #[test_case( - |_, provider| { - let common_ref: DocumentRefs = vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into(); - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Ref(common_ref.clone())) - .build(); - provider.add_document(None, &ref_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref(common_ref)) - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "valid reply to the document, with missing `type` field" - )] - #[test_case( - |exp_type, provider| { - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into(), - )) - .with_metadata_field(SupportedField::Type(exp_type)) - .build(); - provider.add_document(None, &ref_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into(), - )) - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "valid reply to the document, with different `ref` field" - )] - #[test_case( - |exp_type, provider| { - let common_ref: DocumentRefs = vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into(); - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_type)) - .build(); - provider.add_document(None, &ref_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Ref(common_ref)) - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "valid reply to the document, with missing `ref` field" - )] - #[test_case( - |_, provider| { - let common_ref: DocumentRefs = vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into(); - let ref_doc = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Ref(common_ref.clone())) - .build(); - provider.add_document(None, &ref_doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new( - ref_doc.doc_id().unwrap(), - ref_doc.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "missing `ref` field and reply to the valid document" - )] - #[test_case( - |_, _| { - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into(), - )) - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => false - ; - "valid reply to the missing document" - )] - #[tokio::test] - async fn reply_specified_test( - doc_gen: impl FnOnce(DocType, &mut TestCatalystProvider) -> CatalystSignedDocument - ) -> bool { - let mut provider = TestCatalystProvider::default(); - - let exp_type: DocType = UuidV4::new().into(); - - let doc = doc_gen(exp_type.clone(), &mut provider); - - let non_optional_res = ReplyRule::Specified { - allowed_type: exp_type.clone(), - optional: false, - } - .check(&doc, &provider) - .await - .unwrap(); - - let optional_res = ReplyRule::Specified { - allowed_type: exp_type.clone(), - optional: true, - } - .check(&doc, &provider) - .await - .unwrap(); - - assert_eq!(non_optional_res, optional_res); - non_optional_res - } - - #[tokio::test] - async fn reply_specified_optional_test() { - let provider = TestCatalystProvider::default(); - let rule = ReplyRule::Specified { - allowed_type: UuidV4::new().into(), - optional: true, - }; - - let doc = Builder::new().build(); - assert!(rule.check(&doc, &provider).await.unwrap()); - - let provider = TestCatalystProvider::default(); - let rule = ReplyRule::Specified { - allowed_type: UuidV4::new().into(), - optional: false, - }; - - let doc = Builder::new().build(); - assert!(!rule.check(&doc, &provider).await.unwrap()); - } - - #[tokio::test] - async fn reply_rule_not_specified_test() { - let rule = ReplyRule::NotSpecified; - let provider = TestCatalystProvider::default(); - - let doc = Builder::new().build(); - assert!(rule.check(&doc, &provider).await.unwrap()); - - let ref_id = UuidV7::new(); - let ref_ver = UuidV7::new(); - let doc = Builder::new() - .with_metadata_field(SupportedField::Reply( - vec![DocumentRef::new(ref_id, ref_ver, DocLocator::default())].into(), - )) - .build(); - assert!(!rule.check(&doc, &provider).await.unwrap()); - } -} diff --git a/rust/signed_doc/src/validator/rules/reply/mod.rs b/rust/signed_doc/src/validator/rules/reply/mod.rs new file mode 100644 index 00000000000..65e09c2e1d1 --- /dev/null +++ b/rust/signed_doc/src/validator/rules/reply/mod.rs @@ -0,0 +1,138 @@ +//! `reply` rule type impl. + +#[cfg(test)] +mod tests; + +use catalyst_signed_doc_spec::{ + is_required::IsRequired, metadata::reply::Reply, DocSpecs, DocumentName, +}; + +use crate::{ + providers::CatalystSignedDocumentProvider, validator::rules::doc_ref::doc_refs_check, + CatalystSignedDocument, DocType, +}; + +/// `reply` field validation rule +#[derive(Debug)] +pub(crate) enum ReplyRule { + /// Is 'reply' specified + Specified { + /// allowed `type` field of the replied doc + allowed_type: DocType, + /// optional flag for the `ref` field + optional: bool, + }, + /// 'reply' is not specified + NotSpecified, +} + +impl ReplyRule { + /// Generating `ReplyRule` from specs + pub(crate) fn new( + docs: &DocSpecs, + spec: &Reply, + ) -> anyhow::Result { + let optional = match spec.required { + IsRequired::Yes => false, + IsRequired::Optional => true, + IsRequired::Excluded => { + anyhow::ensure!( + spec.doc_type.is_empty() && spec.multiple.is_none(), + "'type' and 'multiple' fields could not been specified when 'required' is 'excluded' for 'reply' metadata definition" + ); + return Ok(Self::NotSpecified); + }, + }; + + anyhow::ensure!( + spec.multiple.is_some_and(|v| !v), + "'multiple' field should be only set to false for the required 'reply' metadata definition" + ); + + let doc_name = &<&[DocumentName; 1]>::try_from(spec.doc_type.as_slice()).map_err(|_| anyhow::anyhow!("'type' field should exists and has only one entry for the required 'reply' metadata definition"))?[0]; + let docs_spec = docs.get(doc_name).ok_or(anyhow::anyhow!( + "cannot find a document definition {doc_name}" + ))?; + let allowed_type = docs_spec.doc_type.as_str().parse()?; + + Ok(Self::Specified { + allowed_type, + optional, + }) + } + + /// Field validation rule + pub(crate) async fn check( + &self, + doc: &CatalystSignedDocument, + provider: &Provider, + ) -> anyhow::Result + where + Provider: CatalystSignedDocumentProvider, + { + let context: &str = "Reply rule check"; + if let Self::Specified { + allowed_type: exp_reply_type, + optional, + } = self + { + if let Some(reply_ref) = doc.doc_meta().reply() { + let reply_validator = |ref_doc: &CatalystSignedDocument| { + // Get `ref` from both the doc and the ref doc + let Some(ref_doc_dr) = ref_doc.doc_meta().doc_ref() else { + doc.report() + .missing_field("Referenced doc `ref` field", context); + return false; + }; + + let Some(doc_dr) = doc.doc_meta().doc_ref() else { + doc.report().missing_field("Document `ref` field", context); + return false; + }; + + // Checking the ref field of ref doc, it should match the ref field of the doc + // If not record the error + if ref_doc_dr != doc_dr { + doc.report().invalid_value( + "ref", + &format!("Reference doc ref: {ref_doc_dr}"), + &format!("Doc ref: {doc_dr}"), + &format!("{context}, ref must be the same"), + ); + return false; + } + true + }; + + return doc_refs_check( + reply_ref, + std::slice::from_ref(exp_reply_type), + false, + "reply", + provider, + doc.report(), + reply_validator, + ) + .await; + } else if !optional { + doc.report().missing_field( + "reply", + &format!("{context}, document must have reply field"), + ); + return Ok(false); + } + } + if let Self::NotSpecified = self { + if let Some(reply) = doc.doc_meta().reply() { + doc.report().unknown_field( + "reply", + &reply.to_string(), + &format!("{context}, document does not expect to have a reply field"), + ); + return Ok(false); + } + } + + Ok(true) + } +} diff --git a/rust/signed_doc/src/validator/rules/reply/tests.rs b/rust/signed_doc/src/validator/rules/reply/tests.rs new file mode 100644 index 00000000000..f48d11e1d6b --- /dev/null +++ b/rust/signed_doc/src/validator/rules/reply/tests.rs @@ -0,0 +1,298 @@ +use catalyst_types::uuid::{UuidV4, UuidV7}; +use test_case::test_case; + +use super::*; +use crate::{ + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + DocLocator, DocumentRef, DocumentRefs, +}; + +#[test_case( + |exp_type, provider| { + let common_ref: DocumentRefs = vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into(); + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Ref(common_ref.clone())) + .with_metadata_field(SupportedField::Type(exp_type)) + .build(); + provider.add_document(None, &ref_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref(common_ref)) + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => true + ; + "valid reply to the correct document" +)] +#[test_case( + |_, provider| { + let common_ref: DocumentRefs = vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into(); + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Ref(common_ref.clone())) + .with_metadata_field(SupportedField::Type(UuidV4::new().into())) + .build(); + provider.add_document(None, &ref_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref(common_ref)) + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "valid reply to the document, with invalid `type` field" +)] +#[test_case( + |_, provider| { + let common_ref: DocumentRefs = vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into(); + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Ref(common_ref.clone())) + .build(); + provider.add_document(None, &ref_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref(common_ref)) + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "valid reply to the document, with missing `type` field" +)] +#[test_case( + |exp_type, provider| { + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into(), + )) + .with_metadata_field(SupportedField::Type(exp_type)) + .build(); + provider.add_document(None, &ref_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into(), + )) + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "valid reply to the document, with different `ref` field" +)] +#[test_case( + |exp_type, provider| { + let common_ref: DocumentRefs = vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into(); + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_type)) + .build(); + provider.add_document(None, &ref_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Ref(common_ref)) + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "valid reply to the document, with missing `ref` field" +)] +#[test_case( + |_, provider| { + let common_ref: DocumentRefs = vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into(); + let ref_doc = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Ref(common_ref.clone())) + .build(); + provider.add_document(None, &ref_doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new( + ref_doc.doc_id().unwrap(), + ref_doc.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "missing `ref` field and reply to the valid document" +)] +#[test_case( + |_, _| { + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into(), + )) + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => false + ; + "valid reply to the missing document" +)] +#[tokio::test] +async fn reply_specified_test( + doc_gen: impl FnOnce(DocType, &mut TestCatalystProvider) -> CatalystSignedDocument +) -> bool { + let mut provider = TestCatalystProvider::default(); + + let exp_type: DocType = UuidV4::new().into(); + + let doc = doc_gen(exp_type.clone(), &mut provider); + + let non_optional_res = ReplyRule::Specified { + allowed_type: exp_type.clone(), + optional: false, + } + .check(&doc, &provider) + .await + .unwrap(); + + let optional_res = ReplyRule::Specified { + allowed_type: exp_type.clone(), + optional: true, + } + .check(&doc, &provider) + .await + .unwrap(); + + assert_eq!(non_optional_res, optional_res); + non_optional_res +} + +#[tokio::test] +async fn reply_specified_optional_test() { + let provider = TestCatalystProvider::default(); + let rule = ReplyRule::Specified { + allowed_type: UuidV4::new().into(), + optional: true, + }; + + let doc = Builder::new().build(); + assert!(rule.check(&doc, &provider).await.unwrap()); + + let provider = TestCatalystProvider::default(); + let rule = ReplyRule::Specified { + allowed_type: UuidV4::new().into(), + optional: false, + }; + + let doc = Builder::new().build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); +} + +#[tokio::test] +async fn reply_rule_not_specified_test() { + let rule = ReplyRule::NotSpecified; + let provider = TestCatalystProvider::default(); + + let doc = Builder::new().build(); + assert!(rule.check(&doc, &provider).await.unwrap()); + + let ref_id = UuidV7::new(); + let ref_ver = UuidV7::new(); + let doc = Builder::new() + .with_metadata_field(SupportedField::Reply( + vec![DocumentRef::new(ref_id, ref_ver, DocLocator::default())].into(), + )) + .build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); +} From 3adb60de56e9ccb21c55ee12b68053f124908ea7 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sat, 27 Sep 2025 23:59:03 +0300 Subject: [PATCH 2/6] wip --- .../{content_type.rs => content_type/mod.rs} | 127 +------ .../src/validator/rules/content_type/tests.rs | 120 ++++++ .../rules/{signature.rs => signature/mod.rs} | 0 .../src/validator/rules/signature/tests.rs | 297 +++++++++++++++ .../rules/{template.rs => template/mod.rs} | 3 + .../src/validator/rules/template/tests.rs | 346 ++++++++++++++++++ 6 files changed, 769 insertions(+), 124 deletions(-) rename rust/signed_doc/src/validator/rules/{content_type.rs => content_type/mod.rs} (50%) create mode 100644 rust/signed_doc/src/validator/rules/content_type/tests.rs rename rust/signed_doc/src/validator/rules/{signature.rs => signature/mod.rs} (100%) create mode 100644 rust/signed_doc/src/validator/rules/signature/tests.rs rename rust/signed_doc/src/validator/rules/{template.rs => template/mod.rs} (99%) create mode 100644 rust/signed_doc/src/validator/rules/template/tests.rs diff --git a/rust/signed_doc/src/validator/rules/content_type.rs b/rust/signed_doc/src/validator/rules/content_type/mod.rs similarity index 50% rename from rust/signed_doc/src/validator/rules/content_type.rs rename to rust/signed_doc/src/validator/rules/content_type/mod.rs index b5cf0c1e2b9..f9933da9981 100644 --- a/rust/signed_doc/src/validator/rules/content_type.rs +++ b/rust/signed_doc/src/validator/rules/content_type/mod.rs @@ -1,5 +1,8 @@ //! `content-type` rule type impl. +#[cfg(test)] +mod tests; + use crate::{metadata::ContentType, validator::json_schema::JsonSchema, CatalystSignedDocument}; /// `content-type` field validation rule @@ -130,127 +133,3 @@ fn validate( }, } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{builder::tests::Builder, metadata::SupportedField}; - - #[tokio::test] - async fn cbor_with_trailing_bytes_test() { - // valid cbor: {1: 2} but with trailing 0xff - let mut buf = Vec::new(); - let mut enc = minicbor::Encoder::new(&mut buf); - enc.map(1).unwrap().u8(1).unwrap().u8(2).unwrap(); - buf.push(0xFF); // extra byte - - let content_type = ContentType::Cbor; - let cbor_rule = ContentTypeRule::Specified { exp: content_type }; - - let doc = Builder::new() - .with_metadata_field(SupportedField::ContentType(content_type)) - .with_content(buf) - .build(); - - assert!(matches!(cbor_rule.check(&doc).await, Ok(false))); - } - - #[tokio::test] - async fn malformed_cbor_bytes_test() { - // 0xa2 means a map with 2 key-value pairs, but we only give 1 key - let invalid_bytes = &[0xA2, 0x01]; - - let content_type = ContentType::Cbor; - let cbor_rule = ContentTypeRule::Specified { exp: content_type }; - - let doc = Builder::new() - .with_metadata_field(SupportedField::ContentType(content_type)) - .with_content(invalid_bytes.into()) - .build(); - - assert!(matches!(cbor_rule.check(&doc).await, Ok(false))); - } - - #[tokio::test] - async fn content_type_cbor_rule_test() { - let content_type = ContentType::Cbor; - let cbor_rule = ContentTypeRule::Specified { exp: content_type }; - - // with json bytes - let doc = Builder::new() - .with_metadata_field(SupportedField::ContentType(content_type)) - .with_content(serde_json::to_vec(&serde_json::json!({})).unwrap()) - .build(); - assert!(matches!(cbor_rule.check(&doc).await, Ok(false))); - - // with cbor bytes - let doc = Builder::new() - .with_metadata_field(SupportedField::ContentType(content_type)) - .with_content(minicbor::to_vec(minicbor::data::Token::Null).unwrap()) - .build(); - assert!(matches!(cbor_rule.check(&doc).await, Ok(true))); - - // without content - let doc = Builder::new() - .with_metadata_field(SupportedField::ContentType(content_type)) - .build(); - assert!(matches!(cbor_rule.check(&doc).await, Ok(false))); - - // with empty content - let doc = Builder::new() - .with_metadata_field(SupportedField::ContentType(content_type)) - .build(); - assert!(matches!(cbor_rule.check(&doc).await, Ok(false))); - } - - #[tokio::test] - async fn content_type_json_rule_test() { - let content_type = ContentType::Json; - let json_rule = ContentTypeRule::Specified { - exp: ContentType::Json, - }; - - // with json bytes - let doc = Builder::new() - .with_metadata_field(SupportedField::ContentType(content_type)) - .with_content(serde_json::to_vec(&serde_json::json!({})).unwrap()) - .build(); - assert!(matches!(json_rule.check(&doc).await, Ok(true))); - - // with cbor bytes - let doc = Builder::new() - .with_metadata_field(SupportedField::ContentType(content_type)) - .with_content(minicbor::to_vec(minicbor::data::Token::Null).unwrap()) - .build(); - assert!(matches!(json_rule.check(&doc).await, Ok(false))); - - // without content - let doc = Builder::new() - .with_metadata_field(SupportedField::ContentType(content_type)) - .build(); - assert!(matches!(json_rule.check(&doc).await, Ok(false))); - - // with empty content - let doc = Builder::new() - .with_metadata_field(SupportedField::ContentType(content_type)) - .build(); - assert!(matches!(json_rule.check(&doc).await, Ok(false))); - - let doc = Builder::new().build(); - assert!(matches!(json_rule.check(&doc).await, Ok(false))); - } - - #[tokio::test] - async fn content_type_not_specified_rule_test() { - let content_type = ContentType::Json; - let rule = ContentTypeRule::NotSpecified; - - let doc = Builder::new() - .with_metadata_field(SupportedField::ContentType(content_type)) - .build(); - assert!(!rule.check(&doc).await.unwrap()); - - let doc = Builder::new().build(); - assert!(rule.check(&doc).await.unwrap()); - } -} diff --git a/rust/signed_doc/src/validator/rules/content_type/tests.rs b/rust/signed_doc/src/validator/rules/content_type/tests.rs new file mode 100644 index 00000000000..4bfac569335 --- /dev/null +++ b/rust/signed_doc/src/validator/rules/content_type/tests.rs @@ -0,0 +1,120 @@ +use super::*; +use crate::{builder::tests::Builder, metadata::SupportedField}; + +#[tokio::test] +async fn cbor_with_trailing_bytes_test() { + // valid cbor: {1: 2} but with trailing 0xff + let mut buf = Vec::new(); + let mut enc = minicbor::Encoder::new(&mut buf); + enc.map(1).unwrap().u8(1).unwrap().u8(2).unwrap(); + buf.push(0xFF); // extra byte + + let content_type = ContentType::Cbor; + let cbor_rule = ContentTypeRule::Specified { exp: content_type }; + + let doc = Builder::new() + .with_metadata_field(SupportedField::ContentType(content_type)) + .with_content(buf) + .build(); + + assert!(matches!(cbor_rule.check(&doc).await, Ok(false))); +} + +#[tokio::test] +async fn malformed_cbor_bytes_test() { + // 0xa2 means a map with 2 key-value pairs, but we only give 1 key + let invalid_bytes = &[0xA2, 0x01]; + + let content_type = ContentType::Cbor; + let cbor_rule = ContentTypeRule::Specified { exp: content_type }; + + let doc = Builder::new() + .with_metadata_field(SupportedField::ContentType(content_type)) + .with_content(invalid_bytes.into()) + .build(); + + assert!(matches!(cbor_rule.check(&doc).await, Ok(false))); +} + +#[tokio::test] +async fn content_type_cbor_rule_test() { + let content_type = ContentType::Cbor; + let cbor_rule = ContentTypeRule::Specified { exp: content_type }; + + // with json bytes + let doc = Builder::new() + .with_metadata_field(SupportedField::ContentType(content_type)) + .with_content(serde_json::to_vec(&serde_json::json!({})).unwrap()) + .build(); + assert!(matches!(cbor_rule.check(&doc).await, Ok(false))); + + // with cbor bytes + let doc = Builder::new() + .with_metadata_field(SupportedField::ContentType(content_type)) + .with_content(minicbor::to_vec(minicbor::data::Token::Null).unwrap()) + .build(); + assert!(matches!(cbor_rule.check(&doc).await, Ok(true))); + + // without content + let doc = Builder::new() + .with_metadata_field(SupportedField::ContentType(content_type)) + .build(); + assert!(matches!(cbor_rule.check(&doc).await, Ok(false))); + + // with empty content + let doc = Builder::new() + .with_metadata_field(SupportedField::ContentType(content_type)) + .build(); + assert!(matches!(cbor_rule.check(&doc).await, Ok(false))); +} + +#[tokio::test] +async fn content_type_json_rule_test() { + let content_type = ContentType::Json; + let json_rule = ContentTypeRule::Specified { + exp: ContentType::Json, + }; + + // with json bytes + let doc = Builder::new() + .with_metadata_field(SupportedField::ContentType(content_type)) + .with_content(serde_json::to_vec(&serde_json::json!({})).unwrap()) + .build(); + assert!(matches!(json_rule.check(&doc).await, Ok(true))); + + // with cbor bytes + let doc = Builder::new() + .with_metadata_field(SupportedField::ContentType(content_type)) + .with_content(minicbor::to_vec(minicbor::data::Token::Null).unwrap()) + .build(); + assert!(matches!(json_rule.check(&doc).await, Ok(false))); + + // without content + let doc = Builder::new() + .with_metadata_field(SupportedField::ContentType(content_type)) + .build(); + assert!(matches!(json_rule.check(&doc).await, Ok(false))); + + // with empty content + let doc = Builder::new() + .with_metadata_field(SupportedField::ContentType(content_type)) + .build(); + assert!(matches!(json_rule.check(&doc).await, Ok(false))); + + let doc = Builder::new().build(); + assert!(matches!(json_rule.check(&doc).await, Ok(false))); +} + +#[tokio::test] +async fn content_type_not_specified_rule_test() { + let content_type = ContentType::Json; + let rule = ContentTypeRule::NotSpecified; + + let doc = Builder::new() + .with_metadata_field(SupportedField::ContentType(content_type)) + .build(); + assert!(!rule.check(&doc).await.unwrap()); + + let doc = Builder::new().build(); + assert!(rule.check(&doc).await.unwrap()); +} diff --git a/rust/signed_doc/src/validator/rules/signature.rs b/rust/signed_doc/src/validator/rules/signature/mod.rs similarity index 100% rename from rust/signed_doc/src/validator/rules/signature.rs rename to rust/signed_doc/src/validator/rules/signature/mod.rs diff --git a/rust/signed_doc/src/validator/rules/signature/tests.rs b/rust/signed_doc/src/validator/rules/signature/tests.rs new file mode 100644 index 00000000000..80fd1d3a0df --- /dev/null +++ b/rust/signed_doc/src/validator/rules/signature/tests.rs @@ -0,0 +1,297 @@ +use std::io::Write; + +use catalyst_types::catalyst_id::role_index::RoleId; +use ed25519_dalek::ed25519::signature::Signer; + +use super::*; +use crate::{providers::tests::*, validator::rules::utils::create_dummy_key_pair, *}; + +fn metadata() -> serde_json::Value { + serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "content-encoding": ContentEncoding::Brotli.to_string(), + "type": UuidV4::new(), + "id": UuidV7::new(), + "ver": UuidV7::new(), + "ref": {"id": UuidV7::new(), "ver": UuidV7::new()}, + "reply": {"id": UuidV7::new(), "ver": UuidV7::new()}, + "template": {"id": UuidV7::new(), "ver": UuidV7::new()}, + "section": "$", + "collaborators": vec![ + /* cspell:disable */ + "cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + "id.catalyst://preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3" + /* cspell:enable */ + ], + "parameters": {"id": UuidV7::new(), "ver": UuidV7::new()}, + }) +} + +fn rule(mutlisig: bool) -> SignatureRule { + SignatureRule { mutlisig } +} + +#[tokio::test] +async fn single_signature_validation_test() { + let (sk, pk, kid) = create_dummy_key_pair(RoleId::Role0); + + let signed_doc = Builder::new() + .with_json_metadata(metadata()) + .unwrap() + .with_json_content(&serde_json::Value::Null) + .unwrap() + .add_signature(|m| sk.sign(&m).to_vec(), kid.clone()) + .unwrap() + .build() + .unwrap(); + + assert!(!signed_doc.problem_report().is_problematic()); + + // case: has key + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid.clone(), pk); + assert!( + rule(true).check(&signed_doc, &provider).await.unwrap(), + "{:?}", + signed_doc.problem_report() + ); + + // case: empty provider + assert!(!rule(true) + .check(&signed_doc, &TestCatalystProvider::default()) + .await + .unwrap()); + + // case: signed with different key + let (another_sk, ..) = create_dummy_key_pair(RoleId::Role0); + let invalid_doc = signed_doc + .into_builder() + .unwrap() + .add_signature(|m| another_sk.sign(&m).to_vec(), kid.clone()) + .unwrap() + .build() + .unwrap(); + assert!(!rule(true).check(&invalid_doc, &provider).await.unwrap()); + + // case: missing signatures + let unsigned_doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "content-type": ContentType::Json.to_string(), + "id": UuidV7::new(), + "ver": UuidV7::new(), + "type": UuidV4::new(), + })) + .unwrap() + .with_json_content(&serde_json::json!({})) + .unwrap() + .build() + .unwrap(); + assert!(!rule(true).check(&unsigned_doc, &provider).await.unwrap()); +} + +#[tokio::test] +async fn multiple_signatures_validation_test() { + let (sk1, pk1, kid1) = create_dummy_key_pair(RoleId::Role0); + let (sk2, pk2, kid2) = create_dummy_key_pair(RoleId::Role0); + let (sk3, pk3, kid3) = create_dummy_key_pair(RoleId::Role0); + let (_, pk_n, kid_n) = create_dummy_key_pair(RoleId::Role0); + + let signed_doc = Builder::new() + .with_json_metadata(metadata()) + .unwrap() + .with_json_content(&serde_json::Value::Null) + .unwrap() + .add_signature(|m| sk1.sign(&m).to_vec(), kid1.clone()) + .unwrap() + .add_signature(|m| sk2.sign(&m).to_vec(), kid2.clone()) + .unwrap() + .add_signature(|m| sk3.sign(&m).to_vec(), kid3.clone()) + .unwrap() + .build() + .unwrap(); + + assert!(!signed_doc.problem_report().is_problematic()); + + // case: multi-sig rule disabled + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid1.clone(), pk1); + provider.add_pk(kid2.clone(), pk2); + provider.add_pk(kid3.clone(), pk3); + assert!(!rule(false).check(&signed_doc, &provider).await.unwrap()); + + // case: all signatures valid + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid1.clone(), pk1); + provider.add_pk(kid2.clone(), pk2); + provider.add_pk(kid3.clone(), pk3); + assert!(rule(true).check(&signed_doc, &provider).await.unwrap()); + + // case: partially available signatures + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid1.clone(), pk1); + provider.add_pk(kid2.clone(), pk2); + assert!(!rule(true).check(&signed_doc, &provider).await.unwrap()); + + // case: with unrecognized provider + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid_n.clone(), pk_n); + assert!(!rule(true).check(&signed_doc, &provider).await.unwrap()); + + // case: no valid signatures available + assert!(!rule(true) + .check(&signed_doc, &TestCatalystProvider::default()) + .await + .unwrap()); +} + +fn content( + content_bytes: &[u8], + sk: &ed25519_dalek::SigningKey, + kid: &CatalystId, +) -> anyhow::Result>> { + let mut e = minicbor::Encoder::new(Vec::new()); + e.array(4)?; + // protected headers (empty metadata fields) + let mut m_p_headers = minicbor::Encoder::new(Vec::new()); + m_p_headers.map(0)?; + let m_p_headers = m_p_headers.into_writer(); + e.bytes(m_p_headers.as_slice())?; + // empty unprotected headers + e.map(0)?; + // content + let _ = e.writer_mut().write(content_bytes)?; + // signatures + // one signature + e.array(1)?; + e.array(3)?; + // protected headers (kid field) + let mut s_p_headers = minicbor::Encoder::new(Vec::new()); + s_p_headers + .map(1)? + .u8(4)? + .bytes(Vec::::from(kid).as_slice())?; + let s_p_headers = s_p_headers.into_writer(); + + // [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4) + let mut tbs: minicbor::Encoder> = minicbor::Encoder::new(Vec::new()); + tbs.array(5)?; + tbs.str("Signature")?; + tbs.bytes(&m_p_headers)?; // `body_protected` + tbs.bytes(&s_p_headers)?; // `sign_protected` + tbs.bytes(&[])?; // empty `external_aad` + tbs.writer_mut().write_all(content_bytes)?; // `payload` + + e.bytes(s_p_headers.as_slice())?; + e.map(0)?; + e.bytes(&sk.sign(tbs.writer()).to_bytes())?; + Ok(e) +} + +fn parameters_alias_field( + alias: &str, + sk: &ed25519_dalek::SigningKey, + kid: &CatalystId, +) -> anyhow::Result>> { + let mut e = minicbor::Encoder::new(Vec::new()); + e.array(4)?; + // protected headers (empty metadata fields) + let mut m_p_headers = minicbor::Encoder::new(Vec::new()); + m_p_headers.map(0)?; + let m_p_headers = m_p_headers.into_writer(); + e.bytes(m_p_headers.as_slice())?; + // empty unprotected headers + e.map(1)?; + e.str(alias)?.encode_with( + DocumentRef::new(UuidV7::new(), UuidV7::new(), DocLocator::default()), + &mut (), + )?; + // content (random bytes) + let content = [1, 2, 3]; + e.bytes(&content)?; + // signatures + // one signature + e.array(1)?; + e.array(3)?; + // protected headers (kid field) + let mut s_p_headers = minicbor::Encoder::new(Vec::new()); + s_p_headers + .map(1)? + .u8(4)? + .bytes(Vec::::from(kid).as_slice())?; + let s_p_headers = s_p_headers.into_writer(); + + // [RFC 8152 section 4.4](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4) + let mut tbs: minicbor::Encoder> = minicbor::Encoder::new(Vec::new()); + tbs.array(5)?; + tbs.str("Signature")?; + tbs.bytes(&m_p_headers)?; // `body_protected` + tbs.bytes(&s_p_headers)?; // `sign_protected` + tbs.bytes(&[])?; // empty `external_aad` + tbs.bytes(&content)?; // `payload` + + e.bytes(s_p_headers.as_slice())?; + e.map(0)?; + e.bytes(&sk.sign(tbs.writer()).to_bytes())?; + Ok(e) +} + +type DocBytesGenerator = + dyn Fn(&ed25519_dalek::SigningKey, &CatalystId) -> anyhow::Result>>; + +struct SpecialCborTestCase<'a> { + name: &'static str, + doc_bytes_fn: &'a DocBytesGenerator, +} + +#[tokio::test] +async fn special_cbor_cases() { + let (sk, pk, kid) = create_dummy_key_pair(RoleId::Role0); + let mut provider = TestCatalystProvider::default(); + provider.add_pk(kid.clone(), pk); + + let test_cases: &[SpecialCborTestCase] = &[ + SpecialCborTestCase { + name: "content encoded as cbor null", + doc_bytes_fn: &|sk, kid| { + let mut e = minicbor::Encoder::new(Vec::new()); + content(e.null()?.writer().as_slice(), sk, kid) + }, + }, + SpecialCborTestCase { + name: "content encoded empty bstr e.g. &[]", + doc_bytes_fn: &|sk, kid| { + let mut e = minicbor::Encoder::new(Vec::new()); + content(e.bytes(&[])?.writer().as_slice(), sk, kid) + }, + }, + SpecialCborTestCase { + name: "parameters alias `category_id` field", + doc_bytes_fn: &|sk, kid| parameters_alias_field("category_id", sk, kid), + }, + SpecialCborTestCase { + name: "parameters alias `brand_id` field", + doc_bytes_fn: &|sk, kid| parameters_alias_field("brand_id", sk, kid), + }, + SpecialCborTestCase { + name: "`parameters` alias `campaign_id` field", + doc_bytes_fn: &|sk, kid| parameters_alias_field("campaign_id", sk, kid), + }, + ]; + + for case in test_cases { + let doc = CatalystSignedDocument::try_from( + (case.doc_bytes_fn)(&sk, &kid) + .unwrap() + .into_writer() + .as_slice(), + ) + .unwrap(); + + assert!( + rule(true).check(&doc, &provider).await.unwrap(), + "[case: {}] {:?}", + case.name, + doc.problem_report() + ); + } +} diff --git a/rust/signed_doc/src/validator/rules/template.rs b/rust/signed_doc/src/validator/rules/template/mod.rs similarity index 99% rename from rust/signed_doc/src/validator/rules/template.rs rename to rust/signed_doc/src/validator/rules/template/mod.rs index 40f0397cf31..642470e06cd 100644 --- a/rust/signed_doc/src/validator/rules/template.rs +++ b/rust/signed_doc/src/validator/rules/template/mod.rs @@ -1,5 +1,8 @@ //! `template` rule type impl. +#[cfg(test)] +mod tests; + use catalyst_signed_doc_spec::{ is_required::IsRequired, metadata::template::Template, DocSpecs, DocumentName, }; diff --git a/rust/signed_doc/src/validator/rules/template/tests.rs b/rust/signed_doc/src/validator/rules/template/tests.rs new file mode 100644 index 00000000000..79c8c5ca179 --- /dev/null +++ b/rust/signed_doc/src/validator/rules/template/tests.rs @@ -0,0 +1,346 @@ +use catalyst_types::uuid::{UuidV4, UuidV7}; +use test_case::test_case; + +use super::*; +use crate::{ + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + DocLocator, DocumentRef, +}; + +#[test_case( + |allowed_type, provider| { + let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); + let template_ref = DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(*template_ref.id())) + .with_metadata_field(SupportedField::Ver(*template_ref.ver())) + .with_metadata_field(SupportedField::Type(allowed_type)) + .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) + .with_content(json_schema) + .build(); + provider.add_document(None, &doc).unwrap(); + + let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![template_ref].into(), + )) + .with_content(json_content) + .build() + } + => true + ; + "content is complied with the referenced template json schema" +)] +#[test_case( + |allowed_type, provider| { + let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); + let template_ref = DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(*template_ref.id())) + .with_metadata_field(SupportedField::Ver(*template_ref.ver())) + .with_metadata_field(SupportedField::Type(allowed_type)) + .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) + .with_content(json_schema) + .build(); + provider.add_document(None, &doc).unwrap(); + + let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); + Builder::new() + .with_content(json_content) + .build() + } + => false + ; + "missing template field" +)] +#[test_case( + |allowed_type, provider| { + let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); + let template_ref = DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(*template_ref.id())) + .with_metadata_field(SupportedField::Ver(*template_ref.ver())) + .with_metadata_field(SupportedField::Type(allowed_type)) + .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) + .with_content(json_schema) + .build(); + provider.add_document(None, &doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![template_ref].into(), + )) + .build() + } + => false + ; + "missing content" +)] +#[test_case( + |allowed_type, provider| { + let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); + let template_ref = DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(*template_ref.id())) + .with_metadata_field(SupportedField::Ver(*template_ref.ver())) + .with_metadata_field(SupportedField::Type(allowed_type)) + .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) + .with_content(json_schema) + .build(); + provider.add_document(None, &doc).unwrap(); + + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![template_ref].into(), + )) + .with_content(vec![1, 2, 3,]) + .build() + } + => false + ; + "content is not valid JSON" +)] +#[test_case( + |_, provider| { + let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); + let template_ref = DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(*template_ref.id())) + .with_metadata_field(SupportedField::Ver(*template_ref.ver())) + .with_metadata_field(SupportedField::Type(UuidV4::new().into())) + .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) + .with_content(json_schema) + .build(); + provider.add_document(None, &doc).unwrap(); + + let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![template_ref].into(), + )) + .with_content(json_content) + .build() + } + => false + ; + "wrong 'type' in the referenced template document" +)] +#[test_case( + |_, provider| { + let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); + let template_ref = DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(*template_ref.id())) + .with_metadata_field(SupportedField::Ver(*template_ref.ver())) + .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) + .with_content(json_schema) + .build(); + provider.add_document(None, &doc).unwrap(); + + let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![template_ref].into(), + )) + .with_content(json_content) + .build() + } + => false + ; + "missing 'type' field in the referenced template document" +)] +#[test_case( + |allowed_type, provider| { + let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); + let template_ref = DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(*template_ref.id())) + .with_metadata_field(SupportedField::Ver(*template_ref.ver())) + .with_metadata_field(SupportedField::Type(allowed_type)) + .with_content(json_schema) + .build(); + provider.add_document(None, &doc).unwrap(); + + let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![template_ref].into(), + )) + .with_content(json_content) + .build() + } + => false + ; + "missing 'content-type' field in the referenced template document'" +)] +#[test_case( + |allowed_type, provider| { + let template_ref = DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(*template_ref.id())) + .with_metadata_field(SupportedField::Ver(*template_ref.ver())) + .with_metadata_field(SupportedField::Type(allowed_type)) + .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) + .build(); + provider.add_document(None, &doc).unwrap(); + + let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![template_ref].into(), + )) + .with_content(json_content) + .build() + } + => false + ; + "missing content in the referenced template document" +)] +#[test_case( + |allowed_type, provider| { + let template_ref = DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(*template_ref.id())) + .with_metadata_field(SupportedField::Ver(*template_ref.ver())) + .with_metadata_field(SupportedField::Type(allowed_type)) + .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) + .with_content(vec![1,2 ,3]) + .build(); + provider.add_document(None, &doc).unwrap(); + + let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![template_ref].into(), + )) + .with_content(json_content) + .build() + } + => false + ; + "content is not a JSON schema in the referenced template document" +)] +#[test_case( + |_, _| { + let template_ref = DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ); + + let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![template_ref].into(), + )) + .with_content(json_content) + .build() + } + => false + ; + "referencing to unknown document" +)] +#[tokio::test] +async fn template_specified_test( + doc_gen: impl FnOnce(DocType, &mut TestCatalystProvider) -> CatalystSignedDocument +) -> bool { + let mut provider = TestCatalystProvider::default(); + + let allowed_type: DocType = UuidV4::new().into(); + + let doc = doc_gen(allowed_type.clone(), &mut provider); + + TemplateRule::Specified { allowed_type } + .check(&doc, &provider) + .await + .unwrap() +} + +#[test_case( + |_, _| { + Builder::new() + .build() + } + => true + ; + "missing 'template' field" +)] +#[test_case( + |allowed_type, provider| { + let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); + let template_ref = DocumentRef::new( + UuidV7::new(), + UuidV7::new(), + DocLocator::default(), + ); + let doc = Builder::new() + .with_metadata_field(SupportedField::Id(*template_ref.id())) + .with_metadata_field(SupportedField::Ver(*template_ref.ver())) + .with_metadata_field(SupportedField::Type(allowed_type)) + .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) + .with_content(json_schema) + .build(); + provider.add_document(None, &doc).unwrap(); + + let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); + Builder::new() + .with_metadata_field(SupportedField::Template( + vec![template_ref].into(), + )) + .with_content(json_content) + .build() + } + => false + ; + "content is complied with the referenced template json schema for non specified 'template' field" +)] +#[tokio::test] +async fn reply_rule_not_specified_test( + doc_gen: impl FnOnce(DocType, &mut TestCatalystProvider) -> CatalystSignedDocument +) -> bool { + let allowed_type: DocType = UuidV4::new().into(); + let mut provider = TestCatalystProvider::default(); + + let doc = doc_gen(allowed_type, &mut provider); + TemplateRule::NotSpecified + .check(&doc, &provider) + .await + .unwrap() +} From 0361eab4b8c97ef97945dca8852e953460ba7759 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sun, 28 Sep 2025 00:05:09 +0300 Subject: [PATCH 3/6] wip --- .../src/validator/rules/collaborators.rs | 156 ------------------ .../src/validator/rules/collaborators/mod.rs | 57 +++++++ .../validator/rules/collaborators/tests.rs | 98 +++++++++++ .../rules/{content.rs => content/mod.rs} | 3 + .../src/validator/rules/content/tests.rs | 118 +++++++++++++ .../src/validator/rules/doc_ref/tests.rs | 80 ++++----- 6 files changed, 316 insertions(+), 196 deletions(-) delete mode 100644 rust/signed_doc/src/validator/rules/collaborators.rs create mode 100644 rust/signed_doc/src/validator/rules/collaborators/mod.rs create mode 100644 rust/signed_doc/src/validator/rules/collaborators/tests.rs rename rust/signed_doc/src/validator/rules/{content.rs => content/mod.rs} (99%) create mode 100644 rust/signed_doc/src/validator/rules/content/tests.rs diff --git a/rust/signed_doc/src/validator/rules/collaborators.rs b/rust/signed_doc/src/validator/rules/collaborators.rs deleted file mode 100644 index 25819c4d74a..00000000000 --- a/rust/signed_doc/src/validator/rules/collaborators.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! `collaborators` rule type impl. - -use crate::CatalystSignedDocument; - -/// `collaborators` field validation rule -#[derive(Debug)] -pub(crate) enum CollaboratorsRule { - /// Is 'collaborators' specified - #[allow(dead_code)] - Specified { - /// optional flag for the `collaborators` field - optional: bool, - }, - /// 'collaborators' is not specified - NotSpecified, -} - -impl CollaboratorsRule { - /// Field validation rule - #[allow(clippy::unused_async)] - pub(crate) async fn check( - &self, - doc: &CatalystSignedDocument, - ) -> anyhow::Result { - if let Self::Specified { optional } = self { - if doc.doc_meta().collaborators().is_empty() && !optional { - doc.report().missing_field( - "collaborators", - "Document must have at least one entry in 'collaborators' field", - ); - return Ok(false); - } - } - if let Self::NotSpecified = self { - if !doc.doc_meta().collaborators().is_empty() { - doc.report().unknown_field( - "collaborators", - &format!( - "{:#?}", - doc.doc_meta() - .collaborators() - .iter() - .map(ToString::to_string) - .reduce(|a, b| format!("{a}, {b}")) - ), - "Document does not expect to have a 'collaborators' field", - ); - return Ok(false); - } - } - - Ok(true) - } -} - -#[cfg(test)] -mod tests { - use catalyst_types::catalyst_id::role_index::RoleId; - use test_case::test_case; - - use super::*; - use crate::{ - builder::tests::Builder, metadata::SupportedField, - validator::rules::utils::create_dummy_key_pair, - }; - - #[test_case( - || { - Builder::new() - .with_metadata_field(SupportedField::Collaborators( - vec![create_dummy_key_pair(RoleId::Role0).2].into() - )) - .build() - } - => true - ; - "valid 'collaborators' field present" - )] - #[test_case( - || { - Builder::new().build() - } - => true - ; - "missing 'collaborators' field" - )] - #[tokio::test] - async fn section_rule_specified_optional_test( - doc_gen: impl FnOnce() -> CatalystSignedDocument - ) -> bool { - let rule = CollaboratorsRule::Specified { optional: true }; - - let doc = doc_gen(); - rule.check(&doc).await.unwrap() - } - - #[test_case( - || { - Builder::new() - .with_metadata_field(SupportedField::Collaborators( - vec![create_dummy_key_pair(RoleId::Role0).2].into() - )) - .build() - } - => true - ; - "valid 'collaborators' field present" - )] - #[test_case( - || { - Builder::new().build() - } - => false - ; - "missing 'collaborators' field" - )] - #[tokio::test] - async fn section_rule_specified_not_optional_test( - doc_gen: impl FnOnce() -> CatalystSignedDocument - ) -> bool { - let rule = CollaboratorsRule::Specified { optional: false }; - - let doc = doc_gen(); - rule.check(&doc).await.unwrap() - } - - #[test_case( - || { - Builder::new().build() - } - => true - ; - "missing 'collaborators' field" - )] - #[test_case( - || { - Builder::new() - .with_metadata_field(SupportedField::Collaborators( - vec![create_dummy_key_pair(RoleId::Role0).2].into() - )) - .build() - } - => false - ; - "valid 'collaborators' field present" - )] - #[tokio::test] - async fn section_rule_not_specified_test( - doc_gen: impl FnOnce() -> CatalystSignedDocument - ) -> bool { - let rule = CollaboratorsRule::NotSpecified; - - let doc = doc_gen(); - rule.check(&doc).await.unwrap() - } -} diff --git a/rust/signed_doc/src/validator/rules/collaborators/mod.rs b/rust/signed_doc/src/validator/rules/collaborators/mod.rs new file mode 100644 index 00000000000..42be98b9f25 --- /dev/null +++ b/rust/signed_doc/src/validator/rules/collaborators/mod.rs @@ -0,0 +1,57 @@ +//! `collaborators` rule type impl. + +#[cfg(test)] +mod tests; + +use crate::CatalystSignedDocument; + +/// `collaborators` field validation rule +#[derive(Debug)] +pub(crate) enum CollaboratorsRule { + /// Is 'collaborators' specified + #[allow(dead_code)] + Specified { + /// optional flag for the `collaborators` field + optional: bool, + }, + /// 'collaborators' is not specified + NotSpecified, +} + +impl CollaboratorsRule { + /// Field validation rule + #[allow(clippy::unused_async)] + pub(crate) async fn check( + &self, + doc: &CatalystSignedDocument, + ) -> anyhow::Result { + if let Self::Specified { optional } = self { + if doc.doc_meta().collaborators().is_empty() && !optional { + doc.report().missing_field( + "collaborators", + "Document must have at least one entry in 'collaborators' field", + ); + return Ok(false); + } + } + if let Self::NotSpecified = self { + if !doc.doc_meta().collaborators().is_empty() { + doc.report().unknown_field( + "collaborators", + &format!( + "{:#?}", + doc.doc_meta() + .collaborators() + .iter() + .map(ToString::to_string) + .reduce(|a, b| format!("{a}, {b}")) + ), + "Document does not expect to have a 'collaborators' field", + ); + return Ok(false); + } + } + + Ok(true) + } +} diff --git a/rust/signed_doc/src/validator/rules/collaborators/tests.rs b/rust/signed_doc/src/validator/rules/collaborators/tests.rs new file mode 100644 index 00000000000..f0201a2a87d --- /dev/null +++ b/rust/signed_doc/src/validator/rules/collaborators/tests.rs @@ -0,0 +1,98 @@ +use catalyst_types::catalyst_id::role_index::RoleId; +use test_case::test_case; + +use super::*; +use crate::{ + builder::tests::Builder, metadata::SupportedField, + validator::rules::utils::create_dummy_key_pair, +}; + +#[test_case( + || { + Builder::new() + .with_metadata_field(SupportedField::Collaborators( + vec![create_dummy_key_pair(RoleId::Role0).2].into() + )) + .build() + } + => true + ; + "valid 'collaborators' field present" +)] +#[test_case( + || { + Builder::new().build() + } + => true + ; + "missing 'collaborators' field" +)] +#[tokio::test] +async fn section_rule_specified_optional_test( + doc_gen: impl FnOnce() -> CatalystSignedDocument +) -> bool { + let rule = CollaboratorsRule::Specified { optional: true }; + + let doc = doc_gen(); + rule.check(&doc).await.unwrap() +} + +#[test_case( + || { + Builder::new() + .with_metadata_field(SupportedField::Collaborators( + vec![create_dummy_key_pair(RoleId::Role0).2].into() + )) + .build() + } + => true + ; + "valid 'collaborators' field present" +)] +#[test_case( + || { + Builder::new().build() + } + => false + ; + "missing 'collaborators' field" +)] +#[tokio::test] +async fn section_rule_specified_not_optional_test( + doc_gen: impl FnOnce() -> CatalystSignedDocument +) -> bool { + let rule = CollaboratorsRule::Specified { optional: false }; + + let doc = doc_gen(); + rule.check(&doc).await.unwrap() +} + +#[test_case( + || { + Builder::new().build() + } + => true + ; + "missing 'collaborators' field" +)] +#[test_case( + || { + Builder::new() + .with_metadata_field(SupportedField::Collaborators( + vec![create_dummy_key_pair(RoleId::Role0).2].into() + )) + .build() + } + => false + ; + "valid 'collaborators' field present" +)] +#[tokio::test] +async fn section_rule_not_specified_test( + doc_gen: impl FnOnce() -> CatalystSignedDocument +) -> bool { + let rule = CollaboratorsRule::NotSpecified; + + let doc = doc_gen(); + rule.check(&doc).await.unwrap() +} \ No newline at end of file diff --git a/rust/signed_doc/src/validator/rules/content.rs b/rust/signed_doc/src/validator/rules/content/mod.rs similarity index 99% rename from rust/signed_doc/src/validator/rules/content.rs rename to rust/signed_doc/src/validator/rules/content/mod.rs index 12b99722085..e4e260deb85 100644 --- a/rust/signed_doc/src/validator/rules/content.rs +++ b/rust/signed_doc/src/validator/rules/content/mod.rs @@ -1,5 +1,8 @@ //! `content` rule type impl. +#[cfg(test)] +mod tests; + use std::fmt::Debug; use catalyst_signed_doc_spec::payload::Payload; diff --git a/rust/signed_doc/src/validator/rules/content/tests.rs b/rust/signed_doc/src/validator/rules/content/tests.rs new file mode 100644 index 00000000000..b8fb3394df8 --- /dev/null +++ b/rust/signed_doc/src/validator/rules/content/tests.rs @@ -0,0 +1,118 @@ +use test_case::test_case; + +use super::*; +use crate::builder::tests::Builder; + +#[test_case( + |valid_content| { + Builder::new() + .with_content(valid_content) + .build() + } + => true + ; + "valid content" +)] +#[test_case( + |_| { + Builder::new() + .with_content(vec![1, 2, 3]) + .build() + } + => false + ; + "corrupted content" +)] +#[test_case( + |_| { + Builder::new() + .build() + } + => false + ; + "missing content" +)] +#[tokio::test] +async fn content_rule_specified_test( + doc_gen: impl FnOnce(Vec) -> CatalystSignedDocument +) -> bool { + let schema = json_schema::JsonSchema::try_from(&serde_json::json!({})).unwrap(); + let content_schema = ContentSchema::Json(schema); + let valid_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); + + let rule = ContentRule::StaticSchema(content_schema); + let doc = doc_gen(valid_content); + rule.check(&doc).await.unwrap() +} + +#[test_case( + || { + Builder::new() + .with_content(vec![1, 2, 3]) + .build() + } + => true + ; + "expected not nil content" +)] +#[test_case( + || { + Builder::new() + .with_content(vec![]) + .build() + } + => true + ; + "expected not nil empty content" +)] +#[test_case( + || { + Builder::new() + .build() + } + => false + ; + "not expected nil content" +)] +#[tokio::test] +async fn template_rule_not_nil_test(doc_gen: impl FnOnce() -> CatalystSignedDocument) -> bool { + let rule = ContentRule::NotNil; + let doc = doc_gen(); + rule.check(&doc).await.unwrap() +} + +#[test_case( + || { + Builder::new() + .build() + } + => true + ; + "expected nil content" +)] +#[test_case( + || { + Builder::new() + .with_content(vec![1, 2, 3]) + .build() + } + => false + ; + "non expected not nil content" +)] +#[test_case( + || { + Builder::new() + .with_content(vec![]) + .build() + } + => false + ; + "non expected not nil empty" +)] +#[tokio::test] +async fn template_rule_nil_test(doc_gen: impl FnOnce() -> CatalystSignedDocument) -> bool { + let rule = ContentRule::Nil; + let doc = doc_gen(); + rule.check(&doc).await.unwrap() +} diff --git a/rust/signed_doc/src/validator/rules/doc_ref/tests.rs b/rust/signed_doc/src/validator/rules/doc_ref/tests.rs index 3acdf1ef32c..09856cc9779 100644 --- a/rust/signed_doc/src/validator/rules/doc_ref/tests.rs +++ b/rust/signed_doc/src/validator/rules/doc_ref/tests.rs @@ -32,51 +32,51 @@ use crate::{ "valid reference to the one correct document" )] #[test_case( -` |exp_types, provider| { - let ref_doc_1 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[0].clone())) - .build(); - provider.add_document(None, &ref_doc_1).unwrap(); - let ref_doc_2 = Builder::new() - .with_metadata_field(SupportedField::Id(UuidV7::new())) - .with_metadata_field(SupportedField::Ver(UuidV7::new())) - .with_metadata_field(SupportedField::Type(exp_types[1].clone())) - .build(); - provider.add_document(None, &ref_doc_2).unwrap(); - let ref_doc_3 = Builder::new() + |exp_types, provider| { + let ref_doc_1 = Builder::new() .with_metadata_field(SupportedField::Id(UuidV7::new())) .with_metadata_field(SupportedField::Ver(UuidV7::new())) .with_metadata_field(SupportedField::Type(exp_types[0].clone())) .build(); - provider.add_document(None, &ref_doc_3).unwrap(); + provider.add_document(None, &ref_doc_1).unwrap(); + let ref_doc_2 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[1].clone())) + .build(); + provider.add_document(None, &ref_doc_2).unwrap(); + let ref_doc_3 = Builder::new() + .with_metadata_field(SupportedField::Id(UuidV7::new())) + .with_metadata_field(SupportedField::Ver(UuidV7::new())) + .with_metadata_field(SupportedField::Type(exp_types[0].clone())) + .build(); + provider.add_document(None, &ref_doc_3).unwrap(); - Builder::new() - .with_metadata_field(SupportedField::Ref( - vec![DocumentRef::new( - ref_doc_1.doc_id().unwrap(), - ref_doc_1.doc_ver().unwrap(), - DocLocator::default(), - ), - DocumentRef::new( - ref_doc_2.doc_id().unwrap(), - ref_doc_2.doc_ver().unwrap(), - DocLocator::default(), - ), - DocumentRef::new( - ref_doc_3.doc_id().unwrap(), - ref_doc_3.doc_ver().unwrap(), - DocLocator::default(), - )] - .into(), - )) - .build() - } - => true - ; - "valid reference to the multiple documents" - )]` + Builder::new() + .with_metadata_field(SupportedField::Ref( + vec![DocumentRef::new( + ref_doc_1.doc_id().unwrap(), + ref_doc_1.doc_ver().unwrap(), + DocLocator::default(), + ), + DocumentRef::new( + ref_doc_2.doc_id().unwrap(), + ref_doc_2.doc_ver().unwrap(), + DocLocator::default(), + ), + DocumentRef::new( + ref_doc_3.doc_id().unwrap(), + ref_doc_3.doc_ver().unwrap(), + DocLocator::default(), + )] + .into(), + )) + .build() + } + => true + ; + "valid reference to the multiple documents" +)] #[test_case( |exp_types, provider| { let ref_doc_1 = Builder::new() From 46da6e68c92214712752b7099895dedd21b97a28 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sun, 28 Sep 2025 00:11:07 +0300 Subject: [PATCH 4/6] fix --- .../validator/rules/collaborators/tests.rs | 6 +- .../src/validator/rules/content/mod.rs | 122 ------ .../src/validator/rules/template/mod.rs | 350 ------------------ 3 files changed, 2 insertions(+), 476 deletions(-) diff --git a/rust/signed_doc/src/validator/rules/collaborators/tests.rs b/rust/signed_doc/src/validator/rules/collaborators/tests.rs index f0201a2a87d..abb510d77f9 100644 --- a/rust/signed_doc/src/validator/rules/collaborators/tests.rs +++ b/rust/signed_doc/src/validator/rules/collaborators/tests.rs @@ -88,11 +88,9 @@ async fn section_rule_specified_not_optional_test( "valid 'collaborators' field present" )] #[tokio::test] -async fn section_rule_not_specified_test( - doc_gen: impl FnOnce() -> CatalystSignedDocument -) -> bool { +async fn section_rule_not_specified_test(doc_gen: impl FnOnce() -> CatalystSignedDocument) -> bool { let rule = CollaboratorsRule::NotSpecified; let doc = doc_gen(); rule.check(&doc).await.unwrap() -} \ No newline at end of file +} diff --git a/rust/signed_doc/src/validator/rules/content/mod.rs b/rust/signed_doc/src/validator/rules/content/mod.rs index e4e260deb85..46cb2b4c491 100644 --- a/rust/signed_doc/src/validator/rules/content/mod.rs +++ b/rust/signed_doc/src/validator/rules/content/mod.rs @@ -95,125 +95,3 @@ impl ContentRule { Ok(true) } } - -#[cfg(test)] -mod tests { - use test_case::test_case; - - use super::*; - use crate::builder::tests::Builder; - - #[test_case( - |valid_content| { - Builder::new() - .with_content(valid_content) - .build() - } - => true - ; - "valid content" - )] - #[test_case( - |_| { - Builder::new() - .with_content(vec![1, 2, 3]) - .build() - } - => false - ; - "corrupted content" - )] - #[test_case( - |_| { - Builder::new() - .build() - } - => false - ; - "missing content" - )] - #[tokio::test] - async fn content_rule_specified_test( - doc_gen: impl FnOnce(Vec) -> CatalystSignedDocument - ) -> bool { - let schema = json_schema::JsonSchema::try_from(&serde_json::json!({})).unwrap(); - let content_schema = ContentSchema::Json(schema); - let valid_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); - - let rule = ContentRule::StaticSchema(content_schema); - let doc = doc_gen(valid_content); - rule.check(&doc).await.unwrap() - } - - #[test_case( - || { - Builder::new() - .with_content(vec![1, 2, 3]) - .build() - } - => true - ; - "expected not nil content" - )] - #[test_case( - || { - Builder::new() - .with_content(vec![]) - .build() - } - => true - ; - "expected not nil empty content" - )] - #[test_case( - || { - Builder::new() - .build() - } - => false - ; - "not expected nil content" - )] - #[tokio::test] - async fn template_rule_not_nil_test(doc_gen: impl FnOnce() -> CatalystSignedDocument) -> bool { - let rule = ContentRule::NotNil; - let doc = doc_gen(); - rule.check(&doc).await.unwrap() - } - - #[test_case( - || { - Builder::new() - .build() - } - => true - ; - "expected nil content" - )] - #[test_case( - || { - Builder::new() - .with_content(vec![1, 2, 3]) - .build() - } - => false - ; - "non expected not nil content" - )] - #[test_case( - || { - Builder::new() - .with_content(vec![]) - .build() - } - => false - ; - "non expected not nil empty" - )] - #[tokio::test] - async fn template_rule_nil_test(doc_gen: impl FnOnce() -> CatalystSignedDocument) -> bool { - let rule = ContentRule::Nil; - let doc = doc_gen(); - rule.check(&doc).await.unwrap() - } -} diff --git a/rust/signed_doc/src/validator/rules/template/mod.rs b/rust/signed_doc/src/validator/rules/template/mod.rs index 642470e06cd..39216e4b7af 100644 --- a/rust/signed_doc/src/validator/rules/template/mod.rs +++ b/rust/signed_doc/src/validator/rules/template/mod.rs @@ -161,353 +161,3 @@ fn templated_json_schema_check( content_json_schema_check(doc, &schema) } - -#[cfg(test)] -mod tests { - use catalyst_types::uuid::{UuidV4, UuidV7}; - use test_case::test_case; - - use super::*; - use crate::{ - builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, - DocLocator, DocumentRef, - }; - - #[test_case( - |allowed_type, provider| { - let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); - let template_ref = DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(*template_ref.id())) - .with_metadata_field(SupportedField::Ver(*template_ref.ver())) - .with_metadata_field(SupportedField::Type(allowed_type)) - .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) - .with_content(json_schema) - .build(); - provider.add_document(None, &doc).unwrap(); - - let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![template_ref].into(), - )) - .with_content(json_content) - .build() - } - => true - ; - "content is complied with the referenced template json schema" - )] - #[test_case( - |allowed_type, provider| { - let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); - let template_ref = DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(*template_ref.id())) - .with_metadata_field(SupportedField::Ver(*template_ref.ver())) - .with_metadata_field(SupportedField::Type(allowed_type)) - .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) - .with_content(json_schema) - .build(); - provider.add_document(None, &doc).unwrap(); - - let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); - Builder::new() - .with_content(json_content) - .build() - } - => false - ; - "missing template field" - )] - #[test_case( - |allowed_type, provider| { - let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); - let template_ref = DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(*template_ref.id())) - .with_metadata_field(SupportedField::Ver(*template_ref.ver())) - .with_metadata_field(SupportedField::Type(allowed_type)) - .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) - .with_content(json_schema) - .build(); - provider.add_document(None, &doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![template_ref].into(), - )) - .build() - } - => false - ; - "missing content" - )] - #[test_case( - |allowed_type, provider| { - let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); - let template_ref = DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(*template_ref.id())) - .with_metadata_field(SupportedField::Ver(*template_ref.ver())) - .with_metadata_field(SupportedField::Type(allowed_type)) - .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) - .with_content(json_schema) - .build(); - provider.add_document(None, &doc).unwrap(); - - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![template_ref].into(), - )) - .with_content(vec![1, 2, 3,]) - .build() - } - => false - ; - "content is not valid JSON" - )] - #[test_case( - |_, provider| { - let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); - let template_ref = DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(*template_ref.id())) - .with_metadata_field(SupportedField::Ver(*template_ref.ver())) - .with_metadata_field(SupportedField::Type(UuidV4::new().into())) - .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) - .with_content(json_schema) - .build(); - provider.add_document(None, &doc).unwrap(); - - let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![template_ref].into(), - )) - .with_content(json_content) - .build() - } - => false - ; - "wrong 'type' in the referenced template document" - )] - #[test_case( - |_, provider| { - let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); - let template_ref = DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(*template_ref.id())) - .with_metadata_field(SupportedField::Ver(*template_ref.ver())) - .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) - .with_content(json_schema) - .build(); - provider.add_document(None, &doc).unwrap(); - - let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![template_ref].into(), - )) - .with_content(json_content) - .build() - } - => false - ; - "missing 'type' field in the referenced template document" - )] - #[test_case( - |allowed_type, provider| { - let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); - let template_ref = DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(*template_ref.id())) - .with_metadata_field(SupportedField::Ver(*template_ref.ver())) - .with_metadata_field(SupportedField::Type(allowed_type)) - .with_content(json_schema) - .build(); - provider.add_document(None, &doc).unwrap(); - - let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![template_ref].into(), - )) - .with_content(json_content) - .build() - } - => false - ; - "missing 'content-type' field in the referenced template document'" - )] - #[test_case( - |allowed_type, provider| { - let template_ref = DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(*template_ref.id())) - .with_metadata_field(SupportedField::Ver(*template_ref.ver())) - .with_metadata_field(SupportedField::Type(allowed_type)) - .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) - .build(); - provider.add_document(None, &doc).unwrap(); - - let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![template_ref].into(), - )) - .with_content(json_content) - .build() - } - => false - ; - "missing content in the referenced template document" - )] - #[test_case( - |allowed_type, provider| { - let template_ref = DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(*template_ref.id())) - .with_metadata_field(SupportedField::Ver(*template_ref.ver())) - .with_metadata_field(SupportedField::Type(allowed_type)) - .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) - .with_content(vec![1,2 ,3]) - .build(); - provider.add_document(None, &doc).unwrap(); - - let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![template_ref].into(), - )) - .with_content(json_content) - .build() - } - => false - ; - "content is not a JSON schema in the referenced template document" - )] - #[test_case( - |_, _| { - let template_ref = DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ); - - let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![template_ref].into(), - )) - .with_content(json_content) - .build() - } - => false - ; - "referencing to unknown document" - )] - #[tokio::test] - async fn template_specified_test( - doc_gen: impl FnOnce(DocType, &mut TestCatalystProvider) -> CatalystSignedDocument - ) -> bool { - let mut provider = TestCatalystProvider::default(); - - let allowed_type: DocType = UuidV4::new().into(); - - let doc = doc_gen(allowed_type.clone(), &mut provider); - - TemplateRule::Specified { allowed_type } - .check(&doc, &provider) - .await - .unwrap() - } - - #[test_case( - |_, _| { - Builder::new() - .build() - } - => true - ; - "missing 'template' field" - )] - #[test_case( - |allowed_type, provider| { - let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap(); - let template_ref = DocumentRef::new( - UuidV7::new(), - UuidV7::new(), - DocLocator::default(), - ); - let doc = Builder::new() - .with_metadata_field(SupportedField::Id(*template_ref.id())) - .with_metadata_field(SupportedField::Ver(*template_ref.ver())) - .with_metadata_field(SupportedField::Type(allowed_type)) - .with_metadata_field(SupportedField::ContentType(ContentType::SchemaJson)) - .with_content(json_schema) - .build(); - provider.add_document(None, &doc).unwrap(); - - let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap(); - Builder::new() - .with_metadata_field(SupportedField::Template( - vec![template_ref].into(), - )) - .with_content(json_content) - .build() - } - => false - ; - "content is complied with the referenced template json schema for non specified 'template' field" - )] - #[tokio::test] - async fn reply_rule_not_specified_test( - doc_gen: impl FnOnce(DocType, &mut TestCatalystProvider) -> CatalystSignedDocument - ) -> bool { - let allowed_type: DocType = UuidV4::new().into(); - let mut provider = TestCatalystProvider::default(); - - let doc = doc_gen(allowed_type, &mut provider); - TemplateRule::NotSpecified - .check(&doc, &provider) - .await - .unwrap() - } -} From 3787cb9b6d8d378ae701a14562b3d23617a2377f Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 7 Oct 2025 18:56:26 +0700 Subject: [PATCH 5/6] wip --- rust/signed_doc/src/validator/rules/parameters/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/src/validator/rules/parameters/tests.rs b/rust/signed_doc/src/validator/rules/parameters/tests.rs index 0c71e91d4be..bf9906259e6 100644 --- a/rust/signed_doc/src/validator/rules/parameters/tests.rs +++ b/rust/signed_doc/src/validator/rules/parameters/tests.rs @@ -573,7 +573,7 @@ async fn parameter_specified_test( } #[tokio::test] -async fn ref_specified_optional_test() { +async fn parameters_specified_optional_test() { let provider = TestCatalystProvider::default(); let rule = ParametersRule::Specified { allowed_type: vec![UuidV4::new().into()], From 90e533e776908bc4461c653ede2e94d15b96a470 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Fri, 10 Oct 2025 10:57:20 +0700 Subject: [PATCH 6/6] wip --- .../src/validator/rules/{id.rs => id/mod.rs} | 90 +---- .../src/validator/rules/id/tests.rs | 81 ++++ rust/signed_doc/src/validator/rules/ver.rs | 360 ------------------ .../signed_doc/src/validator/rules/ver/mod.rs | 99 +++++ .../src/validator/rules/ver/tests.rs | 260 +++++++++++++ 5 files changed, 443 insertions(+), 447 deletions(-) rename rust/signed_doc/src/validator/rules/{id.rs => id/mod.rs} (58%) create mode 100644 rust/signed_doc/src/validator/rules/id/tests.rs delete mode 100644 rust/signed_doc/src/validator/rules/ver.rs create mode 100644 rust/signed_doc/src/validator/rules/ver/mod.rs create mode 100644 rust/signed_doc/src/validator/rules/ver/tests.rs diff --git a/rust/signed_doc/src/validator/rules/id.rs b/rust/signed_doc/src/validator/rules/id/mod.rs similarity index 58% rename from rust/signed_doc/src/validator/rules/id.rs rename to rust/signed_doc/src/validator/rules/id/mod.rs index 8eeee742a7e..d094002f877 100644 --- a/rust/signed_doc/src/validator/rules/id.rs +++ b/rust/signed_doc/src/validator/rules/id/mod.rs @@ -1,5 +1,8 @@ //! Validator for Signed Document ID +#[cfg(test)] +mod tests; + use std::time::{Duration, SystemTime}; use anyhow::Context; @@ -92,90 +95,3 @@ 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::TestCatalystProvider, - 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(&TestCatalystProvider) -> CatalystSignedDocument - ) -> bool { - let provider = TestCatalystProvider::default(); - let doc = doc_gen(&provider); - - IdRule.check(&doc, &provider).await.unwrap() - } -} diff --git a/rust/signed_doc/src/validator/rules/id/tests.rs b/rust/signed_doc/src/validator/rules/id/tests.rs new file mode 100644 index 00000000000..864c2e912b7 --- /dev/null +++ b/rust/signed_doc/src/validator/rules/id/tests.rs @@ -0,0 +1,81 @@ +use std::time::SystemTime; + +use test_case::test_case; +use uuid::{Timestamp, Uuid}; + +use super::*; +use crate::{ + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + 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(&TestCatalystProvider) -> CatalystSignedDocument) -> bool { + let provider = TestCatalystProvider::default(); + let doc = doc_gen(&provider); + + IdRule.check(&doc, &provider).await.unwrap() +} diff --git a/rust/signed_doc/src/validator/rules/ver.rs b/rust/signed_doc/src/validator/rules/ver.rs deleted file mode 100644 index 294c53b6dad..00000000000 --- a/rust/signed_doc/src/validator/rules/ver.rs +++ /dev/null @@ -1,360 +0,0 @@ -//! Validator for Signed Document Version - -use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument}; - -/// Signed Document `ver` field validation rule -#[derive(Debug)] -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::TestCatalystProvider, - 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 TestCatalystProvider) -> CatalystSignedDocument - ) -> bool { - let mut provider = TestCatalystProvider::default(); - let doc = doc_gen(&mut provider); - - VerRule.check(&doc, &provider).await.unwrap() - } -} diff --git a/rust/signed_doc/src/validator/rules/ver/mod.rs b/rust/signed_doc/src/validator/rules/ver/mod.rs new file mode 100644 index 00000000000..98dd3fe99b8 --- /dev/null +++ b/rust/signed_doc/src/validator/rules/ver/mod.rs @@ -0,0 +1,99 @@ +//! Validator for Signed Document Version + +#[cfg(test)] +mod tests; + +use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument}; + +/// Signed Document `ver` field validation rule +#[derive(Debug)] +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) + } +} diff --git a/rust/signed_doc/src/validator/rules/ver/tests.rs b/rust/signed_doc/src/validator/rules/ver/tests.rs new file mode 100644 index 00000000000..38ad696b420 --- /dev/null +++ b/rust/signed_doc/src/validator/rules/ver/tests.rs @@ -0,0 +1,260 @@ +use std::time::SystemTime; + +use test_case::test_case; +use uuid::{Timestamp, Uuid}; + +use super::*; +use crate::{ + builder::tests::Builder, metadata::SupportedField, providers::tests::TestCatalystProvider, + 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 TestCatalystProvider) -> CatalystSignedDocument +) -> bool { + let mut provider = TestCatalystProvider::default(); + let doc = doc_gen(&mut provider); + + VerRule.check(&doc, &provider).await.unwrap() +}