Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(rust/signed-doc): Add integration test validating all functionalities against the spec #229

Merged
merged 25 commits into from
Mar 18, 2025
Merged
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/catalyst-types/src/id_uri/mod.rs
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ use role_index::RoleIndex;
///
/// Identity of Catalyst Registration.
/// Optionally also identifies a specific Signed Document Key
#[derive(Debug, Clone, PartialEq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[allow(clippy::module_name_repetitions)]
pub struct IdUri {
/// Username
111 changes: 0 additions & 111 deletions rust/signed_doc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -275,114 +275,3 @@ impl TryFrom<CatalystSignedDocument> for Vec<u8> {
Ok(minicbor::to_vec(value)?)
}
}

#[cfg(test)]
mod tests {
use std::str::FromStr;

use ed25519_dalek::{SigningKey, VerifyingKey};
use metadata::{ContentEncoding, ContentType};
use rand::rngs::OsRng;

use super::*;
use crate::{providers::VerifyingKeyProvider, validator::validate_signatures};

fn test_metadata() -> (UuidV7, UuidV4, serde_json::Value) {
let alg = Algorithm::EdDSA;
let uuid_v7 = UuidV7::new();
let uuid_v4 = UuidV4::new();
let section = "$".to_string();
let collabs = vec!["Alex1".to_string(), "Alex2".to_string()];
let content_type = ContentType::Json;
let content_encoding = ContentEncoding::Brotli;

let metadata_fields = serde_json::json!({
"alg": alg.to_string(),
"content-type": content_type.to_string(),
"content-encoding": content_encoding.to_string(),
"type": uuid_v4.to_string(),
"id": uuid_v7.to_string(),
"ver": uuid_v7.to_string(),
"ref": {"id": uuid_v7.to_string()},
"reply": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()},
"template": {"id": uuid_v7.to_string()},
"section": section,
"collabs": collabs,
"campaign_id": {"id": uuid_v7.to_string()},
"election_id": uuid_v4.to_string(),
"brand_id": {"id": uuid_v7.to_string()},
"category_id": {"id": uuid_v7.to_string()},
});
(uuid_v7, uuid_v4, metadata_fields)
}

#[test]
fn catalyst_signed_doc_cbor_roundtrip_test() {
let (uuid_v7, uuid_v4, metadata_fields) = 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 bytes: Vec<u8> = doc.try_into().unwrap();
let decoded: CatalystSignedDocument = bytes.as_slice().try_into().unwrap();

assert_eq!(decoded.doc_type().unwrap(), uuid_v4);
assert_eq!(decoded.doc_id().unwrap(), uuid_v7);
assert_eq!(decoded.doc_ver().unwrap(), uuid_v7);
assert_eq!(decoded.doc_content().decoded_bytes().unwrap(), &content);
// TODO: after this test will be moved as a crate integration test, enable this
// assertion assert_eq!(decoded.doc_meta(), metadata_fields.extra());
}

struct Provider(anyhow::Result<Option<VerifyingKey>>);
impl VerifyingKeyProvider for Provider {
async fn try_get_key(
&self, _kid: &IdUri,
) -> anyhow::Result<Option<ed25519_dalek::VerifyingKey>> {
let res = self.0.as_ref().map_err(|e| anyhow::anyhow!("{e}"))?;
Ok(*res)
}
}

#[tokio::test]
async fn signature_verification_test() {
let mut csprng = OsRng;
let sk: SigningKey = SigningKey::generate(&mut csprng);
let content = serde_json::to_vec(&serde_json::Value::Null).unwrap();
let pk = sk.verifying_key();

let kid_str = format!(
"id.catalyst://cardano/{}/0/0",
base64_url::encode(pk.as_bytes())
);

let kid = IdUri::from_str(&kid_str).unwrap();
let (_, _, metadata) = test_metadata();
let signed_doc = Builder::new()
.with_decoded_content(content)
.with_json_metadata(metadata)
.unwrap()
.add_signature(sk.to_bytes(), kid.clone())
.unwrap()
.build();
assert!(!signed_doc.problem_report().is_problematic());

assert!(
validate_signatures(&signed_doc, &Provider(Err(anyhow::anyhow!("some error"))))
.await
.is_err()
);
assert!(validate_signatures(&signed_doc, &Provider(Ok(Some(pk))))
.await
.unwrap());
assert!(!validate_signatures(&signed_doc, &Provider(Ok(None)))
.await
.unwrap());
}
}
37 changes: 31 additions & 6 deletions rust/signed_doc/src/providers.rs
Original file line number Diff line number Diff line change
@@ -23,20 +23,28 @@ pub trait CatalystSignedDocumentProvider: Send + Sync {
) -> impl Future<Output = anyhow::Result<Option<CatalystSignedDocument>>> + Send;
}

#[cfg(test)]
pub(crate) mod tests {
pub mod tests {
//! Simple providers implementation just for the testing purposes

use std::collections::HashMap;

use catalyst_types::uuid::Uuid;

use super::*;
use super::{
CatalystSignedDocument, CatalystSignedDocumentProvider, DocumentRef, IdUri, VerifyingKey,
VerifyingKeyProvider,
};

/// Index documents only by `id` field
/// Simple testing implmentation of `CatalystSignedDocumentProvider`
#[derive(Default)]
pub(crate) struct TestCatalystSignedDocumentProvider(HashMap<Uuid, CatalystSignedDocument>);
pub struct TestCatalystSignedDocumentProvider(HashMap<Uuid, CatalystSignedDocument>);

impl TestCatalystSignedDocumentProvider {
pub(crate) fn add_document(&mut self, doc: CatalystSignedDocument) -> anyhow::Result<()> {
/// Inserts document into the `TestCatalystSignedDocumentProvider`
///
/// # Errors
/// - Missing document id
pub fn add_document(&mut self, doc: CatalystSignedDocument) -> anyhow::Result<()> {
self.0.insert(doc.doc_id()?.uuid(), doc);
Ok(())
}
@@ -49,4 +57,21 @@ pub(crate) mod tests {
Ok(self.0.get(&doc_ref.id.uuid()).cloned())
}
}

/// Simple testing implmentation of `VerifyingKeyProvider`
#[derive(Default)]
pub struct TestVerifyingKeyProvider(HashMap<IdUri, VerifyingKey>);

impl TestVerifyingKeyProvider {
/// Inserts public key into the `TestVerifyingKeyProvider`
pub fn add_pk(&mut self, kid: IdUri, pk: VerifyingKey) {
self.0.insert(kid, pk);
}
}

impl VerifyingKeyProvider for TestVerifyingKeyProvider {
async fn try_get_key(&self, kid: &IdUri) -> anyhow::Result<Option<VerifyingKey>> {
Ok(self.0.get(kid).copied())
}
}
}
125 changes: 125 additions & 0 deletions rust/signed_doc/tests/comment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! Integration test for comment document validation part.

use catalyst_signed_doc::{providers::tests::TestCatalystSignedDocumentProvider, *};

mod common;

#[tokio::test]
async fn test_valid_comment_doc() {
let (proposal_doc, proposal_doc_id) =
common::create_dummy_doc(doc_types::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap();
let (template_doc, template_doc_id) =
common::create_dummy_doc(doc_types::COMMENT_TEMPLATE_UUID_TYPE).unwrap();

let uuid_v7 = UuidV7::new();
let (doc, ..) = common::create_dummy_signed_doc(Some(serde_json::json!({
"alg": Algorithm::EdDSA.to_string(),
"content-type": ContentType::Json.to_string(),
"content-encoding": ContentEncoding::Brotli.to_string(),
"type": doc_types::COMMENT_DOCUMENT_UUID_TYPE,
"id": uuid_v7.to_string(),
"ver": uuid_v7.to_string(),
"template": {
"id": template_doc_id
},
"ref": {
"id": proposal_doc_id
}
})))
.unwrap();

let mut provider = TestCatalystSignedDocumentProvider::default();
provider.add_document(template_doc).unwrap();
provider.add_document(proposal_doc).unwrap();

let is_valid = validator::validate(&doc, &provider).await.unwrap();

assert!(is_valid);
}

#[tokio::test]
async fn test_valid_comment_doc_with_reply() {
let empty_json = serde_json::to_vec(&serde_json::json!({})).unwrap();

let (proposal_doc, proposal_doc_id) =
common::create_dummy_doc(doc_types::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap();
let (template_doc, template_doc_id) =
common::create_dummy_doc(doc_types::COMMENT_TEMPLATE_UUID_TYPE).unwrap();

let comment_doc_id = UuidV7::new();
let comment_doc = Builder::new()
.with_json_metadata(serde_json::json!({
"id": comment_doc_id,
"type": doc_types::COMMENT_DOCUMENT_UUID_TYPE,
"content-type": ContentType::Json.to_string(),
"template": { "id": comment_doc_id.to_string() },
"ref": {
"id": proposal_doc_id
},
}))
.unwrap()
.with_decoded_content(empty_json.clone())
.build();

let uuid_v7 = UuidV7::new();
let (doc, ..) = common::create_dummy_signed_doc(Some(serde_json::json!({
"alg": Algorithm::EdDSA.to_string(),
"content-type": ContentType::Json.to_string(),
"content-encoding": ContentEncoding::Brotli.to_string(),
"type": doc_types::COMMENT_DOCUMENT_UUID_TYPE,
"id": uuid_v7.to_string(),
"ver": uuid_v7.to_string(),
"template": {
"id": template_doc_id
},
"ref": {
"id": proposal_doc_id
},
"reply": {
"id": comment_doc_id,
"ver": uuid_v7
}
})))
.unwrap();

let mut provider = TestCatalystSignedDocumentProvider::default();
provider.add_document(template_doc).unwrap();
provider.add_document(proposal_doc).unwrap();
provider.add_document(comment_doc).unwrap();

let is_valid = validator::validate(&doc, &provider).await.unwrap();

assert!(is_valid);
}

#[tokio::test]
async fn test_invalid_comment_doc() {
let (proposal_doc, _) =
common::create_dummy_doc(doc_types::PROPOSAL_DOCUMENT_UUID_TYPE).unwrap();
let (template_doc, template_doc_id) =
common::create_dummy_doc(doc_types::COMMENT_TEMPLATE_UUID_TYPE).unwrap();

let uuid_v7 = UuidV7::new();
let (doc, ..) = common::create_dummy_signed_doc(Some(serde_json::json!({
"alg": Algorithm::EdDSA.to_string(),
"content-type": ContentType::Json.to_string(),
"content-encoding": ContentEncoding::Brotli.to_string(),
"type": doc_types::COMMENT_DOCUMENT_UUID_TYPE,
"id": uuid_v7.to_string(),
"ver": uuid_v7.to_string(),
"template": {
"id": template_doc_id
},
// without ref
"ref": serde_json::Value::Null
})))
.unwrap();

let mut provider = TestCatalystSignedDocumentProvider::default();
provider.add_document(template_doc).unwrap();
provider.add_document(proposal_doc).unwrap();

let is_valid = validator::validate(&doc, &provider).await.unwrap();

assert!(!is_valid);
}
85 changes: 85 additions & 0 deletions rust/signed_doc/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#![allow(dead_code)]

use std::str::FromStr;

use catalyst_signed_doc::*;

pub fn test_metadata() -> (UuidV7, UuidV4, serde_json::Value) {
let uuid_v7 = UuidV7::new();
let uuid_v4 = UuidV4::new();

let metadata_fields = serde_json::json!({
"alg": Algorithm::EdDSA.to_string(),
"content-type": ContentType::Json.to_string(),
"content-encoding": ContentEncoding::Brotli.to_string(),
"type": uuid_v4.to_string(),
"id": uuid_v7.to_string(),
"ver": uuid_v7.to_string(),
"ref": {"id": uuid_v7.to_string()},
"reply": {"id": uuid_v7.to_string(), "ver": uuid_v7.to_string()},
"template": {"id": uuid_v7.to_string()},
"section": "$".to_string(),
"collabs": vec!["Alex1".to_string(), "Alex2".to_string()],
"campaign_id": {"id": uuid_v7.to_string()},
"election_id": uuid_v4.to_string(),
"brand_id": {"id": uuid_v7.to_string()},
"category_id": {"id": uuid_v7.to_string()},
});

(uuid_v7, uuid_v4, metadata_fields)
}

pub fn create_dummy_key_pair() -> anyhow::Result<(
ed25519_dalek::SigningKey,
ed25519_dalek::VerifyingKey,
IdUri,
)> {
let sk = create_signing_key();
let pk = sk.verifying_key();
let kid = IdUri::from_str(&format!(
"id.catalyst://cardano/{}/0/0",
base64_url::encode(pk.as_bytes())
))?;

Ok((sk, pk, kid))
}

pub fn create_dummy_doc(doc_type_id: Uuid) -> anyhow::Result<(CatalystSignedDocument, UuidV7)> {
let empty_json = serde_json::to_vec(&serde_json::json!({}))?;

let doc_id = UuidV7::new();

let doc = Builder::new()
.with_json_metadata(serde_json::json!({
"id": doc_id,
"type": doc_type_id,
"content-type": ContentType::Json.to_string(),
"template": { "id": doc_id.to_string() }
}))?
.with_decoded_content(empty_json.clone())
.build();

Ok((doc, doc_id))
}

pub fn create_signing_key() -> ed25519_dalek::SigningKey {
let mut csprng = rand::rngs::OsRng;
ed25519_dalek::SigningKey::generate(&mut csprng)
}

pub fn create_dummy_signed_doc(
with_metadata: Option<serde_json::Value>,
) -> anyhow::Result<(CatalystSignedDocument, ed25519_dalek::VerifyingKey, IdUri)> {
let (sk, pk, kid) = create_dummy_key_pair()?;

let content = serde_json::to_vec(&serde_json::Value::Null)?;
let (_, _, metadata) = test_metadata();

let signed_doc = Builder::new()
.with_decoded_content(content)
.with_json_metadata(with_metadata.unwrap_or(metadata))?
.add_signature(sk.to_bytes(), kid.clone())?
.build();

Ok((signed_doc, pk, kid))
}
29 changes: 29 additions & 0 deletions rust/signed_doc/tests/decoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! Integration test for COSE decoding part.
use catalyst_signed_doc::*;

mod common;

#[test]
fn catalyst_signed_doc_cbor_roundtrip_test() {
let (uuid_v7, uuid_v4, 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 bytes: Vec<u8> = doc.try_into().unwrap();
let decoded: CatalystSignedDocument = bytes.as_slice().try_into().unwrap();
let extra_fields: ExtraFields = serde_json::from_value(metadata_fields).unwrap();

assert_eq!(decoded.doc_type().unwrap(), uuid_v4);
assert_eq!(decoded.doc_id().unwrap(), uuid_v7);
assert_eq!(decoded.doc_ver().unwrap(), uuid_v7);
assert_eq!(decoded.doc_content().decoded_bytes().unwrap(), &content);
assert_eq!(decoded.doc_meta(), &extra_fields);
}
80 changes: 80 additions & 0 deletions rust/signed_doc/tests/proposal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! Integration test for proposal document validation part.
use catalyst_signed_doc::{providers::tests::TestCatalystSignedDocumentProvider, *};

mod common;

#[tokio::test]
async fn test_valid_proposal_doc() {
let (template_doc, template_doc_id) =
common::create_dummy_doc(doc_types::PROPOSAL_TEMPLATE_UUID_TYPE).unwrap();

let uuid_v7 = UuidV7::new();
let (doc, ..) = common::create_dummy_signed_doc(Some(serde_json::json!({
"alg": Algorithm::EdDSA.to_string(),
"content-type": ContentType::Json.to_string(),
"content-encoding": ContentEncoding::Brotli.to_string(),
"type": doc_types::PROPOSAL_DOCUMENT_UUID_TYPE,
"id": uuid_v7.to_string(),
"ver": uuid_v7.to_string(),
"template": {
"id": template_doc_id
},
})))
.unwrap();

let mut provider = TestCatalystSignedDocumentProvider::default();
provider.add_document(template_doc).unwrap();

let is_valid = validator::validate(&doc, &provider).await.unwrap();

assert!(is_valid);
}

#[tokio::test]
async fn test_valid_proposal_doc_with_empty_provider() {
// dummy template doc to dummy provider
let template_doc_id = UuidV7::new();

let uuid_v7 = UuidV7::new();
let (doc, ..) = common::create_dummy_signed_doc(Some(serde_json::json!({
"alg": Algorithm::EdDSA.to_string(),
"content-type": ContentType::Json.to_string(),
"content-encoding": ContentEncoding::Brotli.to_string(),
"type": doc_types::PROPOSAL_DOCUMENT_UUID_TYPE,
"id": uuid_v7.to_string(),
"ver": uuid_v7.to_string(),
"template": {
"id": template_doc_id
},
})))
.unwrap();

let provider = TestCatalystSignedDocumentProvider::default();

let is_valid = validator::validate(&doc, &provider).await.unwrap();

assert!(!is_valid);
}

#[tokio::test]
async fn test_invalid_proposal_doc() {
let uuid_v7 = UuidV7::new();
let (doc, ..) = common::create_dummy_signed_doc(Some(serde_json::json!({
"alg": Algorithm::EdDSA.to_string(),
"content-type": ContentType::Json.to_string(),
"content-encoding": ContentEncoding::Brotli.to_string(),
"type": doc_types::PROPOSAL_DOCUMENT_UUID_TYPE,
"id": uuid_v7.to_string(),
"ver": uuid_v7.to_string(),
// without specifying template id
"template": serde_json::Value::Null,
})))
.unwrap();

let provider = TestCatalystSignedDocumentProvider::default();

let is_valid = validator::validate(&doc, &provider).await.unwrap();

assert!(!is_valid);
}
78 changes: 78 additions & 0 deletions rust/signed_doc/tests/signature.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Integration test for signature validation part.
use catalyst_signed_doc::{providers::tests::TestVerifyingKeyProvider, *};

mod common;

#[tokio::test]
async fn single_signature_validation_test() {
let (signed_doc, pk, kid) = common::create_dummy_signed_doc(None).unwrap();
assert!(!signed_doc.problem_report().is_problematic());

// case: has key
let mut provider = TestVerifyingKeyProvider::default();
provider.add_pk(kid.clone(), pk);
assert!(validator::validate_signatures(&signed_doc, &provider)
.await
.is_ok_and(|v| v));

// case: empty provider
assert!(
validator::validate_signatures(&signed_doc, &TestVerifyingKeyProvider::default())
.await
.is_ok_and(|v| !v)
);
}

#[tokio::test]
async fn multiple_signatures_validation_test() {
let (sk1, pk1, kid1) = common::create_dummy_key_pair().unwrap();
let (sk2, pk2, kid2) = common::create_dummy_key_pair().unwrap();
let (sk3, pk3, kid3) = common::create_dummy_key_pair().unwrap();
let (_, pk_n, kid_n) = common::create_dummy_key_pair().unwrap();

let signed_doc = Builder::new()
.with_decoded_content(serde_json::to_vec(&serde_json::Value::Null).unwrap())
.with_json_metadata(common::test_metadata().2)
.unwrap()
.add_signature(sk1.to_bytes(), kid1.clone())
.unwrap()
.add_signature(sk2.to_bytes(), kid2.clone())
.unwrap()
.add_signature(sk3.to_bytes(), kid3.clone())
.unwrap()
.build();

assert!(!signed_doc.problem_report().is_problematic());

// case: all signatures valid
let mut provider = TestVerifyingKeyProvider::default();
provider.add_pk(kid1.clone(), pk1);
provider.add_pk(kid2.clone(), pk2);
provider.add_pk(kid3.clone(), pk3);
assert!(validator::validate_signatures(&signed_doc, &provider)
.await
.is_ok_and(|v| v));

// case: partially available signatures
let mut provider = TestVerifyingKeyProvider::default();
provider.add_pk(kid1.clone(), pk1);
provider.add_pk(kid2.clone(), pk2);
assert!(validator::validate_signatures(&signed_doc, &provider)
.await
.is_ok_and(|v| !v));

// case: with unrecognized provider
let mut provider = TestVerifyingKeyProvider::default();
provider.add_pk(kid_n.clone(), pk_n);
assert!(validator::validate_signatures(&signed_doc, &provider)
.await
.is_ok_and(|v| !v));

// case: no valid signatures available
assert!(
validator::validate_signatures(&signed_doc, &TestVerifyingKeyProvider::default())
.await
.is_ok_and(|v| !v)
);
}

Unchanged files with check annotations Beta

DO mdlint-ci+MDLINT_LOCALLY --src=$(echo ${PWD}) --fix=--fix
# clean-spelling-list : Make sure the project dictionary is properly sorted.
clean-spelling-list:

Check failure on line 20 in Earthfile

GitHub Actions / ci / check / .+check-spelling

Error

The command RUN cspell-cli --quiet lint . --dot did not complete successfully. Exit code 1
DO cspell-ci+CLEAN
# check-spelling : Check spelling in this repo inside a container.