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
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -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
Expand Down
111 changes: 0 additions & 111 deletions rust/signed_doc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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 implementation 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(())
}
Expand All @@ -49,4 +57,21 @@ pub(crate) mod tests {
Ok(self.0.get(&doc_ref.id.uuid()).cloned())
}
}

/// Simple testing implementation 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))
}
Loading