diff --git a/rust/signed_doc/src/metadata/extra_fields.rs b/rust/signed_doc/src/metadata/extra_fields.rs index d460d60e64..7b9293bf10 100644 --- a/rust/signed_doc/src/metadata/extra_fields.rs +++ b/rust/signed_doc/src/metadata/extra_fields.rs @@ -1,12 +1,11 @@ //! Catalyst Signed Document Extra Fields. -use catalyst_types::{problem_report::ProblemReport, uuid::UuidV4}; +use catalyst_types::problem_report::ProblemReport; use coset::{cbor::Value, Label, ProtectedHeader}; use super::{ - cose_protected_header_find, - utils::{decode_document_field_from_protected_header, CborUuidV4}, - DocumentRef, Section, + cose_protected_header_find, utils::decode_document_field_from_protected_header, DocumentRef, + Section, }; /// `ref` field COSE key value @@ -19,13 +18,13 @@ const REPLY_KEY: &str = "reply"; const SECTION_KEY: &str = "section"; /// `collabs` field COSE key value const COLLABS_KEY: &str = "collabs"; -/// `brand_id` field COSE key value +/// `parameters` field COSE key value +const PARAMETERS_KEY: &str = "parameters"; +/// `brand_id` field COSE key value (alias of the `parameters` field) const BRAND_ID_KEY: &str = "brand_id"; -/// `campaign_id` field COSE key value +/// `campaign_id` field COSE key value (alias of the `parameters` field) const CAMPAIGN_ID_KEY: &str = "campaign_id"; -/// `election_id` field COSE key value -const ELECTION_ID_KEY: &str = "election_id"; -/// `category_id` field COSE key value +/// `category_id` field COSE key value (alias of the `parameters` field) const CATEGORY_ID_KEY: &str = "category_id"; /// Extra Metadata Fields. @@ -48,18 +47,9 @@ pub struct ExtraFields { /// Reference to the document collaborators. Collaborator type is TBD. #[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")] collabs: Vec, - /// Unique identifier for the brand that is running the voting. + /// Reference to the parameters document. #[serde(skip_serializing_if = "Option::is_none")] - brand_id: Option, - /// Unique identifier for the campaign of voting. - #[serde(skip_serializing_if = "Option::is_none")] - campaign_id: Option, - /// Unique identifier for the election. - #[serde(skip_serializing_if = "Option::is_none")] - election_id: Option, - /// Unique identifier for the voting category as a collection of proposals. - #[serde(skip_serializing_if = "Option::is_none")] - category_id: Option, + parameters: Option, } impl ExtraFields { @@ -93,28 +83,10 @@ impl ExtraFields { &self.collabs } - /// Return `brand_id` field. - #[must_use] - pub fn brand_id(&self) -> Option { - self.brand_id - } - - /// Return `campaign_id` field. - #[must_use] - pub fn campaign_id(&self) -> Option { - self.campaign_id - } - - /// Return `election_id` field. + /// Return `parameters` field. #[must_use] - pub fn election_id(&self) -> Option { - self.election_id - } - - /// Return `category_id` field. - #[must_use] - pub fn category_id(&self) -> Option { - self.category_id + pub fn parameters(&self) -> Option { + self.parameters } /// Fill the COSE header `ExtraFields` data into the header builder. @@ -141,26 +113,11 @@ impl ExtraFields { Value::Array(self.collabs.iter().cloned().map(Value::Text).collect()), ); } - if let Some(brand_id) = &self.brand_id { - builder = builder.text_value(BRAND_ID_KEY.to_string(), Value::try_from(*brand_id)?); - } - if let Some(campaign_id) = &self.campaign_id { - builder = - builder.text_value(CAMPAIGN_ID_KEY.to_string(), Value::try_from(*campaign_id)?); + if let Some(parameters) = &self.parameters { + builder = builder.text_value(PARAMETERS_KEY.to_string(), Value::try_from(*parameters)?); } - if let Some(election_id) = &self.election_id { - builder = builder.text_value( - ELECTION_ID_KEY.to_string(), - Value::try_from(CborUuidV4(*election_id))?, - ); - } - - if let Some(category_id) = &self.category_id { - builder = - builder.text_value(CATEGORY_ID_KEY.to_string(), Value::try_from(*category_id)?); - } Ok(builder) } @@ -196,41 +153,47 @@ impl ExtraFields { COSE_DECODING_CONTEXT, error_report, ); - let brand_id = decode_document_field_from_protected_header( - protected, + + // process `parameters` field and all its aliases + let (parameters, count) = [ + PARAMETERS_KEY, BRAND_ID_KEY, - COSE_DECODING_CONTEXT, - error_report, - ); - let campaign_id = decode_document_field_from_protected_header( - protected, CAMPAIGN_ID_KEY, - COSE_DECODING_CONTEXT, - error_report, - ); - let election_id = decode_document_field_from_protected_header::( - protected, - ELECTION_ID_KEY, - COSE_DECODING_CONTEXT, - error_report, - ) - .map(|v| v.0); - let category_id = decode_document_field_from_protected_header( - protected, CATEGORY_ID_KEY, - COSE_DECODING_CONTEXT, - error_report, - ); + ] + .iter() + .map(|field_name| -> Option { + decode_document_field_from_protected_header( + protected, + field_name, + COSE_DECODING_CONTEXT, + error_report, + ) + }) + .fold((None, 0_u32), |(res, count), v| { + ( + res.or(v), + if v.is_some() { + count.saturating_add(1) + } else { + count + }, + ) + }); + if count > 1 { + error_report.duplicate_field( + "brand_id, campaign_id, category_id", + "Only value at the same time is allowed parameters, brand_id, campaign_id, category_id", + "Validation of parameters field aliases" + ); + } let mut extra = ExtraFields { doc_ref, template, reply, section, - brand_id, - campaign_id, - election_id, - category_id, + parameters, ..Default::default() }; diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index ea9c03e03a..0c755bdcb5 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod utils; use std::{ collections::HashMap, + fmt, sync::LazyLock, time::{Duration, SystemTime}, }; @@ -13,18 +14,19 @@ use anyhow::Context; use catalyst_types::{ catalyst_id::{role_index::RoleId, CatalystId}, problem_report::ProblemReport, - uuid::Uuid, + uuid::{Uuid, UuidV4}, }; use coset::{CoseSign, CoseSignature}; use rules::{ - CategoryRule, ContentEncodingRule, ContentRule, ContentSchema, ContentTypeRule, RefRule, + ContentEncodingRule, ContentRule, ContentSchema, ContentTypeRule, ParametersRule, RefRule, ReplyRule, Rules, SectionRule, SignatureKidRule, }; use crate::{ doc_types::{ - COMMENT_DOCUMENT_UUID_TYPE, COMMENT_TEMPLATE_UUID_TYPE, PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, - PROPOSAL_DOCUMENT_UUID_TYPE, PROPOSAL_TEMPLATE_UUID_TYPE, + CATEGORY_DOCUMENT_UUID_TYPE, COMMENT_DOCUMENT_UUID_TYPE, COMMENT_TEMPLATE_UUID_TYPE, + PROPOSAL_ACTION_DOCUMENT_UUID_TYPE, PROPOSAL_DOCUMENT_UUID_TYPE, + PROPOSAL_TEMPLATE_UUID_TYPE, }, providers::{CatalystSignedDocumentProvider, VerifyingKeyProvider}, CatalystSignedDocument, ContentEncoding, ContentType, @@ -33,6 +35,14 @@ use crate::{ /// A table representing a full set or validation rules per document id. static DOCUMENT_RULES: LazyLock> = LazyLock::new(document_rules_init); +/// Returns an [`UuidV4`] from the provided argument, panicking if the argument is +/// invalid. +#[allow(clippy::expect_used)] +fn expect_uuidv4(t: T) -> UuidV4 +where T: TryInto { + t.try_into().expect("Must be a valid UUID V4") +} + /// `DOCUMENT_RULES` initialization function #[allow(clippy::expect_used)] fn document_rules_init() -> HashMap { @@ -47,11 +57,12 @@ fn document_rules_init() -> HashMap { optional: false, }, content: ContentRule::Templated { - exp_template_type: PROPOSAL_TEMPLATE_UUID_TYPE - .try_into() - .expect("Must be a valid UUID V4"), + exp_template_type: expect_uuidv4(PROPOSAL_TEMPLATE_UUID_TYPE), + }, + parameters: ParametersRule::Specified { + exp_parameters_type: expect_uuidv4(CATEGORY_DOCUMENT_UUID_TYPE), + optional: true, }, - category: CategoryRule::Specified { optional: true }, doc_ref: RefRule::NotSpecified, reply: ReplyRule::NotSpecified, section: SectionRule::NotSpecified, @@ -59,6 +70,7 @@ fn document_rules_init() -> HashMap { exp: &[RoleId::Proposer], }, }; + document_rules_map.insert(PROPOSAL_DOCUMENT_UUID_TYPE, proposal_document_rules); let comment_document_rules = Rules { @@ -70,24 +82,18 @@ fn document_rules_init() -> HashMap { optional: false, }, content: ContentRule::Templated { - exp_template_type: COMMENT_TEMPLATE_UUID_TYPE - .try_into() - .expect("Must be a valid UUID V4"), + exp_template_type: expect_uuidv4(COMMENT_TEMPLATE_UUID_TYPE), }, doc_ref: RefRule::Specified { - exp_ref_type: PROPOSAL_DOCUMENT_UUID_TYPE - .try_into() - .expect("Must be a valid UUID V4"), + exp_ref_type: expect_uuidv4(PROPOSAL_DOCUMENT_UUID_TYPE), optional: false, }, reply: ReplyRule::Specified { - exp_reply_type: COMMENT_DOCUMENT_UUID_TYPE - .try_into() - .expect("Must be a valid UUID V4"), + exp_reply_type: expect_uuidv4(COMMENT_DOCUMENT_UUID_TYPE), optional: true, }, section: SectionRule::Specified { optional: true }, - category: CategoryRule::NotSpecified, + parameters: ParametersRule::NotSpecified, kid: SignatureKidRule { exp: &[RoleId::Role0], }, @@ -112,11 +118,12 @@ fn document_rules_init() -> HashMap { optional: false, }, content: ContentRule::Static(ContentSchema::Json(proposal_action_json_schema)), - category: CategoryRule::Specified { optional: true }, + parameters: ParametersRule::Specified { + exp_parameters_type: expect_uuidv4(CATEGORY_DOCUMENT_UUID_TYPE), + optional: true, + }, doc_ref: RefRule::Specified { - exp_ref_type: PROPOSAL_DOCUMENT_UUID_TYPE - .try_into() - .expect("Must be a valid UUID V4"), + exp_ref_type: expect_uuidv4(PROPOSAL_DOCUMENT_UUID_TYPE), optional: false, }, reply: ReplyRule::NotSpecified, diff --git a/rust/signed_doc/src/validator/rules/category.rs b/rust/signed_doc/src/validator/rules/category.rs deleted file mode 100644 index e6db6f32b8..0000000000 --- a/rust/signed_doc/src/validator/rules/category.rs +++ /dev/null @@ -1,180 +0,0 @@ -//! `content-type` rule type impl. - -use super::doc_ref::referenced_doc_check; -use crate::{ - doc_types::CATEGORY_DOCUMENT_UUID_TYPE, providers::CatalystSignedDocumentProvider, - validator::utils::validate_provided_doc, CatalystSignedDocument, -}; - -/// `category_id` field validation rule -#[derive(Clone, Debug, PartialEq)] -pub(crate) enum CategoryRule { - /// Is `category_id` specified - Specified { - /// optional flag for the `category_id` field - optional: bool, - }, - /// `category_id` is not specified - NotSpecified, -} - -impl CategoryRule { - /// Field validation rule - pub(crate) async fn check( - &self, doc: &CatalystSignedDocument, provider: &Provider, - ) -> anyhow::Result - where Provider: CatalystSignedDocumentProvider { - if let Self::Specified { optional } = self { - if let Some(category) = &doc.doc_meta().category_id() { - let category_validator = |category_doc: CatalystSignedDocument| { - referenced_doc_check( - &category_doc, - CATEGORY_DOCUMENT_UUID_TYPE, - "category_id", - doc.report(), - ) - }; - - return validate_provided_doc(category, provider, doc.report(), category_validator) - .await; - } else if !optional { - doc.report() - .missing_field("category_id", "Document must have a category field"); - return Ok(false); - } - } - if &Self::NotSpecified == self { - if let Some(category) = doc.doc_meta().category_id() { - doc.report().unknown_field( - "category_id", - &category.to_string(), - "Document does not expect to have a category field", - ); - return Ok(false); - } - } - - Ok(true) - } -} - -#[cfg(test)] -mod tests { - use catalyst_types::uuid::{UuidV4, UuidV7}; - - use super::*; - use crate::{providers::tests::TestCatalystSignedDocumentProvider, Builder}; - - #[tokio::test] - async fn category_rule_specified_test() { - let mut provider = TestCatalystSignedDocumentProvider::default(); - - let valid_category_doc_id = UuidV7::new(); - let valid_category_doc_ver = UuidV7::new(); - let another_type_category_doc_id = UuidV7::new(); - let another_type_category_doc_ver = UuidV7::new(); - let missing_type_category_doc_id = UuidV7::new(); - let missing_type_category_doc_ver = UuidV7::new(); - - // prepare replied documents - { - let ref_doc = Builder::new() - .with_json_metadata(serde_json::json!({ - "id": valid_category_doc_id.to_string(), - "ver": valid_category_doc_ver.to_string(), - "type": CATEGORY_DOCUMENT_UUID_TYPE.to_string() - })) - .unwrap() - .build(); - provider.add_document(ref_doc).unwrap(); - - // reply doc with other `type` field - let ref_doc = Builder::new() - .with_json_metadata(serde_json::json!({ - "id": another_type_category_doc_id.to_string(), - "ver": another_type_category_doc_ver.to_string(), - "type": UuidV4::new().to_string() - })) - .unwrap() - .build(); - provider.add_document(ref_doc).unwrap(); - - // missing `type` field in the referenced document - let ref_doc = Builder::new() - .with_json_metadata(serde_json::json!({ - "id": missing_type_category_doc_id.to_string(), - "ver": missing_type_category_doc_ver.to_string(), - })) - .unwrap() - .build(); - provider.add_document(ref_doc).unwrap(); - } - - // all correct - let rule = CategoryRule::Specified { optional: false }; - let doc = Builder::new() - .with_json_metadata(serde_json::json!({ - "category_id": {"id": valid_category_doc_id.to_string(), "ver": valid_category_doc_ver } - })) - .unwrap() - .build(); - assert!(rule.check(&doc, &provider).await.unwrap()); - - // all correct, `category_id` field is missing, but its optional - let rule = CategoryRule::Specified { optional: true }; - let doc = Builder::new().build(); - assert!(rule.check(&doc, &provider).await.unwrap()); - - // missing `category_id` field, but its required - let rule = CategoryRule::Specified { optional: false }; - let doc = Builder::new().build(); - assert!(!rule.check(&doc, &provider).await.unwrap()); - - // reference to the document with another `type` field - let doc = Builder::new() - .with_json_metadata(serde_json::json!({ - "category_id": {"id": another_type_category_doc_id.to_string(), "ver": another_type_category_doc_ver.to_string() } - })) - .unwrap() - .build(); - assert!(!rule.check(&doc, &provider).await.unwrap()); - - // missing `type` field in the referenced document - let doc = Builder::new() - .with_json_metadata(serde_json::json!({ - "category_id": {"id": missing_type_category_doc_id.to_string(), "ver": missing_type_category_doc_ver.to_string() } - })) - .unwrap() - .build(); - assert!(!rule.check(&doc, &provider).await.unwrap()); - - // cannot find a referenced document - let doc = Builder::new() - .with_json_metadata(serde_json::json!({ - "category_id": {"id": UuidV7::new().to_string(), "ver": UuidV7::new().to_string() } - })) - .unwrap() - .build(); - assert!(!rule.check(&doc, &provider).await.unwrap()); - } - - #[tokio::test] - async fn category_rule_not_specified_test() { - let provider = TestCatalystSignedDocumentProvider::default(); - - let rule = CategoryRule::NotSpecified; - - 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_json_metadata(serde_json::json!({ - "category_id": {"id": ref_id.to_string(), "ver": ref_ver.to_string(), } - })) - .unwrap() - .build(); - assert!(!rule.check(&doc, &provider).await.unwrap()); - } -} diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index 7f76bad0fd..165dcb043a 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -5,19 +5,19 @@ use futures::FutureExt; use crate::{providers::CatalystSignedDocumentProvider, CatalystSignedDocument}; -mod category; mod content_encoding; mod content_type; mod doc_ref; +mod parameters; mod reply; mod section; mod signature_kid; mod template; -pub(crate) use category::CategoryRule; pub(crate) use content_encoding::ContentEncodingRule; pub(crate) use content_type::ContentTypeRule; pub(crate) use doc_ref::RefRule; +pub(crate) use parameters::ParametersRule; pub(crate) use reply::ReplyRule; pub(crate) use section::SectionRule; pub(crate) use signature_kid::SignatureKidRule; @@ -37,8 +37,8 @@ pub(crate) struct Rules { pub(crate) reply: ReplyRule, /// 'section' field validation rule pub(crate) section: SectionRule, - /// 'category' field validation rule - pub(crate) category: CategoryRule, + /// 'parameters' field validation rule + pub(crate) parameters: ParametersRule, /// `kid` field validation rule pub(crate) kid: SignatureKidRule, } @@ -56,7 +56,7 @@ impl Rules { self.content.check(doc, provider).boxed(), self.reply.check(doc, provider).boxed(), self.section.check(doc).boxed(), - self.category.check(doc, provider).boxed(), + self.parameters.check(doc, provider).boxed(), self.kid.check(doc).boxed(), ]; diff --git a/rust/signed_doc/src/validator/rules/parameters.rs b/rust/signed_doc/src/validator/rules/parameters.rs new file mode 100644 index 0000000000..286394a61b --- /dev/null +++ b/rust/signed_doc/src/validator/rules/parameters.rs @@ -0,0 +1,282 @@ +//! `parameters` rule type impl. + +use catalyst_types::uuid::UuidV4; + +use super::doc_ref::referenced_doc_check; +use crate::{ + providers::CatalystSignedDocumentProvider, validator::utils::validate_provided_doc, + CatalystSignedDocument, +}; + +/// `parameters` field validation rule +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum ParametersRule { + /// Is `parameters` specified + Specified { + /// expected `type` field of the parameter doc + exp_parameters_type: UuidV4, + /// optional flag for the `parameters` field + optional: bool, + }, + /// `parameters` is not specified + NotSpecified, +} + +impl ParametersRule { + /// Field validation rule + pub(crate) async fn check( + &self, doc: &CatalystSignedDocument, provider: &Provider, + ) -> anyhow::Result + where Provider: CatalystSignedDocumentProvider { + if let Self::Specified { + exp_parameters_type, + optional, + } = self + { + if let Some(parameters) = doc.doc_meta().parameters() { + let parameters_validator = |replied_doc: CatalystSignedDocument| { + if !referenced_doc_check( + &replied_doc, + exp_parameters_type.uuid(), + "parameters", + doc.report(), + ) { + return false; + } + let Some(doc_ref) = doc.doc_meta().doc_ref() else { + doc.report() + .missing_field("ref", "Document must have a ref field"); + return false; + }; + + let Some(replied_doc_ref) = replied_doc.doc_meta().doc_ref() else { + doc.report() + .missing_field("ref", "Referenced document must have ref field"); + return false; + }; + + if replied_doc_ref.id != doc_ref.id { + doc.report().invalid_value( + "parameters", + doc_ref.id .to_string().as_str(), + replied_doc_ref.id.to_string().as_str(), + "Invalid referenced document. Document ID should aligned with the replied document.", + ); + return false; + } + + true + }; + return validate_provided_doc( + ¶meters, + provider, + doc.report(), + parameters_validator, + ) + .await; + } else if !optional { + doc.report() + .missing_field("parameters", "Document must have a 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(), + "Document does not expect to have a parameters field", + ); + return Ok(false); + } + } + + Ok(true) + } +} + +#[cfg(test)] +mod tests { + use catalyst_types::uuid::{UuidV4, UuidV7}; + + use super::*; + use crate::{providers::tests::TestCatalystSignedDocumentProvider, Builder}; + + #[allow(clippy::too_many_lines)] + #[tokio::test] + async fn ref_rule_specified_test() { + let mut provider = TestCatalystSignedDocumentProvider::default(); + + let exp_parameters_type = UuidV4::new(); + let common_ref_id = UuidV7::new(); + let common_ref_ver = UuidV7::new(); + + let valid_replied_doc_id = UuidV7::new(); + let valid_replied_doc_ver = UuidV7::new(); + let another_type_replied_doc_ver = UuidV7::new(); + let another_type_replied_doc_id = UuidV7::new(); + let missing_ref_replied_doc_ver = UuidV7::new(); + let missing_ref_replied_doc_id = UuidV7::new(); + let missing_type_replied_doc_ver = UuidV7::new(); + let missing_type_replied_doc_id = UuidV7::new(); + + // prepare replied documents + { + let ref_doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, + "id": valid_replied_doc_id.to_string(), + "ver": valid_replied_doc_ver.to_string(), + "type": exp_parameters_type.to_string() + })) + .unwrap() + .build(); + provider.add_document(ref_doc).unwrap(); + + // parameters doc with other `type` field + let ref_doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, + "id": another_type_replied_doc_id.to_string(), + "ver": another_type_replied_doc_ver.to_string(), + "type": UuidV4::new().to_string() + })) + .unwrap() + .build(); + provider.add_document(ref_doc).unwrap(); + + // missing `ref` field in the referenced document + let ref_doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "id": missing_ref_replied_doc_id.to_string(), + "ver": missing_ref_replied_doc_ver.to_string(), + "type": exp_parameters_type.to_string() + })) + .unwrap() + .build(); + provider.add_document(ref_doc).unwrap(); + + // missing `type` field in the referenced document + let ref_doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, + "id": missing_type_replied_doc_id.to_string(), + "ver": missing_type_replied_doc_ver.to_string(), + })) + .unwrap() + .build(); + provider.add_document(ref_doc).unwrap(); + } + + // all correct + let rule = ParametersRule::Specified { + exp_parameters_type, + optional: false, + }; + let doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, + "parameters": { "id": valid_replied_doc_id.to_string(), "ver": valid_replied_doc_ver.to_string() } + })) + .unwrap() + .build(); + assert!(rule.check(&doc, &provider).await.unwrap()); + + // all correct, `parameters` field is missing, but its optional + let rule = ParametersRule::Specified { + exp_parameters_type, + optional: true, + }; + let doc = Builder::new().build(); + assert!(rule.check(&doc, &provider).await.unwrap()); + + // missing `parameters` field, but its required + let rule = ParametersRule::Specified { + exp_parameters_type, + optional: false, + }; + let doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, + })) + .unwrap() + .build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); + + // missing `ref` field + let doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "parameters": { "id": valid_replied_doc_id.to_string(), "ver": valid_replied_doc_ver.to_string() } + })) + .unwrap() + .build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); + + // reference to the document with another `type` field + let doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, + "parameters": { "id": another_type_replied_doc_id.to_string(), "ver": another_type_replied_doc_ver.to_string() } + })) + .unwrap() + .build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); + + // missing `ref` field in the referenced document + let doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, + "parameters": { "id": missing_ref_replied_doc_id.to_string(), "ver": missing_type_replied_doc_ver.to_string() } + })) + .unwrap() + .build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); + + // missing `type` field in the referenced document + let doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, + "parameters": { "id": missing_type_replied_doc_id.to_string(), "ver": missing_type_replied_doc_ver.to_string() } + })) + .unwrap() + .build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); + + // `ref` field does not align with the referenced document + let doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "ref": { "id": UuidV7::new().to_string(), "ver": UuidV7::new().to_string() }, + "parameters": { "id": valid_replied_doc_id.to_string(), "ver": valid_replied_doc_ver.to_string() } + })) + .unwrap() + .build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); + + // cannot find a referenced document + let doc = Builder::new() + .with_json_metadata(serde_json::json!({ + "ref": { "id": common_ref_id.to_string(), "ver": common_ref_ver.to_string() }, + "parameters": {"id": UuidV7::new().to_string(), "ver": UuidV7::new().to_string() } + })) + .unwrap() + .build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); + } + + #[tokio::test] + async fn parameters_rule_not_specified_test() { + let rule = ParametersRule::NotSpecified; + let provider = TestCatalystSignedDocumentProvider::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_json_metadata(serde_json::json!({"parameters": {"id": ref_id.to_string(), "ver": ref_ver.to_string() } })) + .unwrap() + .build(); + assert!(!rule.check(&doc, &provider).await.unwrap()); + } +} diff --git a/rust/signed_doc/tests/common/mod.rs b/rust/signed_doc/tests/common/mod.rs index 6caa04d937..d7ea84150b 100644 --- a/rust/signed_doc/tests/common/mod.rs +++ b/rust/signed_doc/tests/common/mod.rs @@ -21,10 +21,7 @@ pub fn test_metadata() -> (UuidV7, UuidV4, serde_json::Value) { "template": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, "section": "$".to_string(), "collabs": vec!["Alex1".to_string(), "Alex2".to_string()], - "campaign_id": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, - "election_id": uuid_v4.to_string(), - "brand_id": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, - "category_id": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, + "parameters": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()}, }); (uuid_v7, uuid_v4, metadata_fields) diff --git a/rust/signed_doc/tests/decoding.rs b/rust/signed_doc/tests/decoding.rs index 76c5f93744..38a6615aaf 100644 --- a/rust/signed_doc/tests/decoding.rs +++ b/rust/signed_doc/tests/decoding.rs @@ -3,6 +3,7 @@ use catalyst_signed_doc::*; use catalyst_types::catalyst_id::role_index::RoleId; use common::create_dummy_key_pair; +use coset::TaggedCborSerializable; use ed25519_dalek::ed25519::signature::Signer; mod common; @@ -54,3 +55,113 @@ fn catalyst_signed_doc_cbor_roundtrip_kid_as_id_test() { assert!(doc.problem_report().is_problematic()); } + +#[test] +fn catalyst_signed_doc_parameters_aliases_test() { + let (_, _, metadata_fields) = common::test_metadata(); + + let content = serde_json::to_vec(&serde_json::Value::Null).unwrap(); + + let doc = Builder::new() + .with_json_metadata(metadata_fields.clone()) + .unwrap() + .with_decoded_content(content.clone()) + .build(); + assert!(!doc.problem_report().is_problematic()); + + let parameters_val = doc.doc_meta().parameters().unwrap(); + let parameters_val_cbor: coset::cbor::Value = parameters_val.try_into().unwrap(); + // replace parameters with the alias values `category_id`, `brand_id`, `campaign_id`. + let bytes: Vec = doc.try_into().unwrap(); + let mut cose = coset::CoseSign::from_tagged_slice(bytes.as_slice()).unwrap(); + cose.protected.original_data = None; + cose.protected + .header + .rest + .retain(|(l, _)| l != &coset::Label::Text("parameters".to_string())); + + let doc: CatalystSignedDocument = cose + .clone() + .to_tagged_vec() + .unwrap() + .as_slice() + .try_into() + .unwrap(); + assert!(!doc.problem_report().is_problematic()); + assert!(doc.doc_meta().parameters().is_none()); + + // case: `category_id`. + let mut cose_with_category_id = cose.clone(); + cose_with_category_id.protected.header.rest.push(( + coset::Label::Text("category_id".to_string()), + parameters_val_cbor.clone(), + )); + + let doc: CatalystSignedDocument = cose_with_category_id + .to_tagged_vec() + .unwrap() + .as_slice() + .try_into() + .unwrap(); + assert!(!doc.problem_report().is_problematic()); + assert!(doc.doc_meta().parameters().is_some()); + + // case: `brand_id`. + let mut cose_with_category_id = cose.clone(); + cose_with_category_id.protected.header.rest.push(( + coset::Label::Text("brand_id".to_string()), + parameters_val_cbor.clone(), + )); + + let doc: CatalystSignedDocument = cose_with_category_id + .to_tagged_vec() + .unwrap() + .as_slice() + .try_into() + .unwrap(); + assert!(!doc.problem_report().is_problematic()); + assert!(doc.doc_meta().parameters().is_some()); + + // case: `campaign_id`. + let mut cose_with_category_id = cose.clone(); + cose_with_category_id.protected.header.rest.push(( + coset::Label::Text("campaign_id".to_string()), + parameters_val_cbor.clone(), + )); + + let doc: CatalystSignedDocument = cose_with_category_id + .to_tagged_vec() + .unwrap() + .as_slice() + .try_into() + .unwrap(); + assert!(!doc.problem_report().is_problematic()); + assert!(doc.doc_meta().parameters().is_some()); + + // `parameters` value along with its aliases are not allowed to be present at the + let mut cose_with_category_id = cose.clone(); + cose_with_category_id.protected.header.rest.push(( + coset::Label::Text("parameters".to_string()), + parameters_val_cbor.clone(), + )); + cose_with_category_id.protected.header.rest.push(( + coset::Label::Text("category_id".to_string()), + parameters_val_cbor.clone(), + )); + cose_with_category_id.protected.header.rest.push(( + coset::Label::Text("brand_id".to_string()), + parameters_val_cbor.clone(), + )); + cose_with_category_id.protected.header.rest.push(( + coset::Label::Text("campaign_id".to_string()), + parameters_val_cbor.clone(), + )); + + let doc: CatalystSignedDocument = cose_with_category_id + .to_tagged_vec() + .unwrap() + .as_slice() + .try_into() + .unwrap(); + assert!(doc.problem_report().is_problematic()); +}