diff --git a/INPUT_VALIDATION_IMPLEMENTATION.md b/INPUT_VALIDATION_IMPLEMENTATION.md new file mode 100644 index 0000000..e69de29 diff --git a/dongle-smartcontract/VALIDATION_IMPLEMENTATION.md b/dongle-smartcontract/VALIDATION_IMPLEMENTATION.md new file mode 100644 index 0000000..e69de29 diff --git a/dongle-smartcontract/src/constants.rs b/dongle-smartcontract/src/constants.rs index 0abc2a8..6234473 100644 --- a/dongle-smartcontract/src/constants.rs +++ b/dongle-smartcontract/src/constants.rs @@ -28,8 +28,16 @@ pub const MAX_WEBSITE_LEN: usize = 256; #[allow(dead_code)] pub const MAX_CID_LEN: usize = 128; +/// Minimum length for CID validation. +#[allow(dead_code)] +pub const MIN_CID_LEN: usize = 10; + /// Valid rating range (inclusive). Reviews must be in [RATING_MIN, RATING_MAX]. u32 for Soroban Val. #[allow(dead_code)] pub const RATING_MIN: u32 = 1; #[allow(dead_code)] pub const RATING_MAX: u32 = 5; + +/// Maximum number of items that can be returned in a single pagination request. +#[allow(dead_code)] +pub const MAX_PAGINATION_LIMIT: u32 = 100; diff --git a/dongle-smartcontract/src/lib.rs b/dongle-smartcontract/src/lib.rs index b3f1ca3..34d3fe6 100644 --- a/dongle-smartcontract/src/lib.rs +++ b/dongle-smartcontract/src/lib.rs @@ -11,6 +11,7 @@ pub mod rating_calculator; pub mod review_registry; pub mod storage_keys; pub mod types; +pub mod validation; mod verification_registry; #[cfg(test)] diff --git a/dongle-smartcontract/src/project_registry.rs b/dongle-smartcontract/src/project_registry.rs index ae71194..0dc49f6 100644 --- a/dongle-smartcontract/src/project_registry.rs +++ b/dongle-smartcontract/src/project_registry.rs @@ -3,6 +3,7 @@ use crate::errors::ContractError; use crate::events::{publish_project_registered_event, publish_project_updated_event}; use crate::storage_keys::StorageKey; use crate::types::{Project, ProjectRegistrationParams, ProjectUpdateParams, VerificationStatus}; +use crate::validation; use soroban_sdk::{Address, Env, Vec}; /// Maximum number of items returned per paginated list call. @@ -18,15 +19,13 @@ impl ProjectRegistry { ) -> Result { require_self_auth(¶ms.owner); - if params.name.is_empty() { - panic!("InvalidProjectName"); - } - if params.description.is_empty() { - panic!("InvalidProjectDescription"); - } - if params.category.is_empty() { - panic!("InvalidProjectCategory"); - } + // Validate all inputs + validation::validate_project_name(¶ms.name)?; + validation::validate_description(¶ms.description)?; + validation::validate_category(¶ms.category)?; + validation::validate_website(¶ms.website)?; + validation::validate_cid(¶ms.logo_cid)?; + validation::validate_cid(¶ms.metadata_cid)?; // Check if project name already exists if env @@ -100,23 +99,30 @@ impl ProjectRegistry { require_owner_auth(¶ms.caller, &project.owner)?; - if let Some(value) = params.name { - project.name = value; + // Validate updated fields + if let Some(ref value) = params.name { + validation::validate_project_name(value).ok()?; + project.name = value.clone(); } - if let Some(value) = params.description { - project.description = value; + if let Some(ref value) = params.description { + validation::validate_description(value).ok()?; + project.description = value.clone(); } - if let Some(value) = params.category { - project.category = value; + if let Some(ref value) = params.category { + validation::validate_category(value).ok()?; + project.category = value.clone(); } - if let Some(value) = params.website { - project.website = value; + if let Some(ref value) = params.website { + validation::validate_website(value).ok()?; + project.website = value.clone(); } - if let Some(value) = params.logo_cid { - project.logo_cid = value; + if let Some(ref value) = params.logo_cid { + validation::validate_cid(value).ok()?; + project.logo_cid = value.clone(); } - if let Some(value) = params.metadata_cid { - project.metadata_cid = value; + if let Some(ref value) = params.metadata_cid { + validation::validate_cid(value).ok()?; + project.metadata_cid = value.clone(); } project.updated_at = env.ledger().timestamp(); diff --git a/dongle-smartcontract/src/review_registry.rs b/dongle-smartcontract/src/review_registry.rs index 0886ce8..7ba5dad 100644 --- a/dongle-smartcontract/src/review_registry.rs +++ b/dongle-smartcontract/src/review_registry.rs @@ -7,6 +7,7 @@ use crate::events::publish_review_event; use crate::rating_calculator::RatingCalculator; use crate::storage_keys::StorageKey; use crate::types::{ProjectStats, Review, ReviewAction}; +use crate::validation; use soroban_sdk::{Address, Env, String, Vec}; pub struct ReviewRegistry; @@ -21,9 +22,9 @@ impl ReviewRegistry { ) -> Result<(), ContractError> { require_self_auth(&reviewer); - if !(RATING_MIN..=RATING_MAX).contains(&rating) { - return Err(ContractError::InvalidRating); - } + // Validate inputs + validation::validate_rating(rating)?; + validation::validate_cid(&comment_cid)?; let review_key = StorageKey::Review(project_id, reviewer.clone()); if env.storage().persistent().has(&review_key) { @@ -108,9 +109,9 @@ impl ReviewRegistry { ) -> Result<(), ContractError> { require_self_auth(&reviewer); - if !(RATING_MIN..=RATING_MAX).contains(&rating) { - return Err(ContractError::InvalidRating); - } + // Validate inputs + validation::validate_rating(rating)?; + validation::validate_cid(&comment_cid)?; let review_key = StorageKey::Review(project_id, reviewer.clone()); let mut review: Review = env diff --git a/dongle-smartcontract/src/tests/validation_tests.rs b/dongle-smartcontract/src/tests/validation_tests.rs new file mode 100644 index 0000000..0acfdf3 --- /dev/null +++ b/dongle-smartcontract/src/tests/validation_tests.rs @@ -0,0 +1,396 @@ +//! Integration tests for input validation across all modules + +use crate::errors::ContractError; +use crate::tests::fixtures::{create_test_env, register_test_project}; +use crate::types::ProjectRegistrationParams; +use crate::DongleContract; +use soroban_sdk::{testutils::Address as _, Address, Env, String}; + +// ── Project Registration Validation Tests ── + +#[test] +fn test_register_project_with_valid_inputs() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, "Valid Project"), + description: String::from_str(&env, "A valid project description"), + category: String::from_str(&env, "DeFi"), + website: Some(String::from_str(&env, "https://example.com")), + logo_cid: Some(String::from_str( + &env, + "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG", + )), + metadata_cid: Some(String::from_str( + &env, + "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG", + )), + }; + + let result = DongleContract::register_project(env.clone(), params); + assert!(result.is_ok()); +} + +#[test] +#[should_panic(expected = "InvalidProjectName")] +fn test_register_project_empty_name() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, ""), + description: String::from_str(&env, "Description"), + category: String::from_str(&env, "DeFi"), + website: None, + logo_cid: None, + metadata_cid: None, + }; + + let _ = DongleContract::register_project(env.clone(), params); +} + +#[test] +#[should_panic(expected = "InvalidProjectName")] +fn test_register_project_whitespace_only_name() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, " "), + description: String::from_str(&env, "Description"), + category: String::from_str(&env, "DeFi"), + website: None, + logo_cid: None, + metadata_cid: None, + }; + + let _ = DongleContract::register_project(env.clone(), params); +} + +#[test] +#[should_panic(expected = "ProjectNameTooLong")] +fn test_register_project_name_too_long() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + // 51 characters + let long_name = "ThisProjectNameIsWayTooLongAndExceedsTheFiftyCharL1"; + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, long_name), + description: String::from_str(&env, "Description"), + category: String::from_str(&env, "DeFi"), + website: None, + logo_cid: None, + metadata_cid: None, + }; + + let _ = DongleContract::register_project(env.clone(), params); +} + +#[test] +#[should_panic(expected = "InvalidProjectNameFormat")] +fn test_register_project_name_invalid_characters() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, "Project@Name!"), + description: String::from_str(&env, "Description"), + category: String::from_str(&env, "DeFi"), + website: None, + logo_cid: None, + metadata_cid: None, + }; + + let _ = DongleContract::register_project(env.clone(), params); +} + +#[test] +#[should_panic(expected = "InvalidDescription")] +fn test_register_project_empty_description() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, "ValidName"), + description: String::from_str(&env, ""), + category: String::from_str(&env, "DeFi"), + website: None, + logo_cid: None, + metadata_cid: None, + }; + + let _ = DongleContract::register_project(env.clone(), params); +} + +#[test] +#[should_panic(expected = "InvalidCategory")] +fn test_register_project_empty_category() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, "ValidName"), + description: String::from_str(&env, "Valid description"), + category: String::from_str(&env, ""), + website: None, + logo_cid: None, + metadata_cid: None, + }; + + let _ = DongleContract::register_project(env.clone(), params); +} + +#[test] +#[should_panic(expected = "InvalidWebsiteUrlFormat")] +fn test_register_project_invalid_website_protocol() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, "ValidName"), + description: String::from_str(&env, "Valid description"), + category: String::from_str(&env, "DeFi"), + website: Some(String::from_str(&env, "ftp://example.com")), + logo_cid: None, + metadata_cid: None, + }; + + let _ = DongleContract::register_project(env.clone(), params); +} + +#[test] +#[should_panic(expected = "CidInvalidLength")] +fn test_register_project_invalid_cid_too_short() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, "ValidName"), + description: String::from_str(&env, "Valid description"), + category: String::from_str(&env, "DeFi"), + website: None, + logo_cid: Some(String::from_str(&env, "short")), + metadata_cid: None, + }; + + let _ = DongleContract::register_project(env.clone(), params); +} + +#[test] +#[should_panic(expected = "InvalidCidFormat")] +fn test_register_project_invalid_cid_format() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, "ValidName"), + description: String::from_str(&env, "Valid description"), + category: String::from_str(&env, "DeFi"), + website: None, + logo_cid: Some(String::from_str(&env, "Qm@#$%^&*()_+invalid")), + metadata_cid: None, + }; + + let _ = DongleContract::register_project(env.clone(), params); +} + +// ── Review Validation Tests ── + +#[test] +fn test_add_review_with_valid_rating() { + let (env, admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + let result = DongleContract::add_review( + env.clone(), + project_id, + reviewer.clone(), + 5, + Some(String::from_str( + &env, + "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG", + )), + ); + + assert!(result.is_ok()); +} + +#[test] +fn test_add_review_rating_too_low() { + let (env, admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + let result = DongleContract::add_review(env.clone(), project_id, reviewer.clone(), 0, None); + + assert_eq!(result, Err(ContractError::InvalidRating)); +} + +#[test] +fn test_add_review_rating_too_high() { + let (env, admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + let result = DongleContract::add_review(env.clone(), project_id, reviewer.clone(), 6, None); + + assert_eq!(result, Err(ContractError::InvalidRating)); +} + +#[test] +fn test_add_review_invalid_comment_cid() { + let (env, admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + let result = DongleContract::add_review( + env.clone(), + project_id, + reviewer.clone(), + 5, + Some(String::from_str(&env, "invalid@cid")), + ); + + assert_eq!(result, Err(ContractError::InvalidCidFormat)); +} + +// ── Pagination Validation Tests ── + +#[test] +fn test_list_projects_with_valid_limit() { + let env = Env::default(); + env.mock_all_auths(); + + let projects = DongleContract::list_projects(env.clone(), 1, 10); + // Should not panic, returns empty vec if no projects + assert_eq!(projects.len(), 0); +} + +#[test] +fn test_list_projects_with_zero_limit() { + let env = Env::default(); + env.mock_all_auths(); + + // Should return empty vec due to validation failure + let projects = DongleContract::list_projects(env.clone(), 1, 0); + assert_eq!(projects.len(), 0); +} + +#[test] +fn test_list_projects_with_excessive_limit() { + let env = Env::default(); + env.mock_all_auths(); + + // Should return empty vec due to validation failure (limit > 100) + let projects = DongleContract::list_projects(env.clone(), 1, 101); + assert_eq!(projects.len(), 0); +} + +#[test] +fn test_list_reviews_with_valid_limit() { + let (env, admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviews = DongleContract::list_reviews(env.clone(), project_id, 0, 10); + assert_eq!(reviews.len(), 0); +} + +#[test] +fn test_list_reviews_with_zero_limit() { + let (env, admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviews = DongleContract::list_reviews(env.clone(), project_id, 0, 0); + assert_eq!(reviews.len(), 0); +} + +// ── Verification Evidence CID Tests ── + +#[test] +#[should_panic(expected = "InvalidCid")] +fn test_request_verification_empty_evidence_cid() { + let (env, admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let _ = DongleContract::request_verification( + env.clone(), + project_id, + owner.clone(), + String::from_str(&env, ""), + ); +} + +#[test] +#[should_panic(expected = "CidInvalidLength")] +fn test_request_verification_evidence_cid_too_short() { + let (env, admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let _ = DongleContract::request_verification( + env.clone(), + project_id, + owner.clone(), + String::from_str(&env, "short"), + ); +} + +// ── Edge Case Tests ── + +#[test] +fn test_project_name_at_max_length() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + // Exactly 50 characters + let name = "12345678901234567890123456789012345678901234567890"; + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, name), + description: String::from_str(&env, "Description"), + category: String::from_str(&env, "DeFi"), + website: None, + logo_cid: None, + metadata_cid: None, + }; + + let result = DongleContract::register_project(env.clone(), params); + assert!(result.is_ok()); +} + +#[test] +fn test_all_valid_ratings() { + let (env, admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + // Test all valid ratings (1-5) + for rating in 1..=5 { + let reviewer = Address::generate(&env); + let result = + DongleContract::add_review(env.clone(), project_id, reviewer.clone(), rating, None); + assert!(result.is_ok(), "Rating {} should be valid", rating); + } +} diff --git a/dongle-smartcontract/src/validation.rs b/dongle-smartcontract/src/validation.rs new file mode 100644 index 0000000..29e4278 --- /dev/null +++ b/dongle-smartcontract/src/validation.rs @@ -0,0 +1,481 @@ +//! Input validation module for all user-provided data. +//! Ensures data integrity and prevents abuse through strict validation rules. + +use crate::constants::*; +use crate::errors::ContractError; +use soroban_sdk::String; + +/// Validates project name according to defined rules: +/// - Non-empty after trimming whitespace +/// - Length between MIN_STRING_LEN and MAX_NAME_LEN +/// - Contains only alphanumeric characters, underscores, and hyphens +pub fn validate_project_name(name: &String) -> Result<(), ContractError> { + extern crate alloc; + use alloc::string::ToString; + + let name_str = name.to_string(); + let trimmed = name_str.trim(); + + // Check non-empty + if trimmed.is_empty() { + return Err(ContractError::InvalidProjectName); + } + + // Check length + if trimmed.len() < MIN_STRING_LEN || trimmed.len() > MAX_NAME_LEN { + return Err(ContractError::ProjectNameTooLong); + } + + // Check format: alphanumeric, underscore, hyphen, space + for c in trimmed.chars() { + if !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != ' ' { + return Err(ContractError::InvalidProjectNameFormat); + } + } + + Ok(()) +} + +/// Validates project description: +/// - Non-empty after trimming +/// - Length between MIN_STRING_LEN and MAX_DESCRIPTION_LEN +pub fn validate_description(description: &String) -> Result<(), ContractError> { + extern crate alloc; + use alloc::string::ToString; + + let desc_str = description.to_string(); + let trimmed = desc_str.trim(); + + if trimmed.is_empty() { + return Err(ContractError::InvalidDescription); + } + + if trimmed.len() < MIN_STRING_LEN || trimmed.len() > MAX_DESCRIPTION_LEN { + return Err(ContractError::DescriptionTooLong); + } + + Ok(()) +} + +/// Validates project category: +/// - Non-empty after trimming +/// - Length between MIN_STRING_LEN and MAX_CATEGORY_LEN +/// - Contains only alphanumeric characters, underscores, hyphens, and spaces +pub fn validate_category(category: &String) -> Result<(), ContractError> { + extern crate alloc; + use alloc::string::ToString; + + let cat_str = category.to_string(); + let trimmed = cat_str.trim(); + + if trimmed.is_empty() { + return Err(ContractError::InvalidCategory); + } + + if trimmed.len() < MIN_STRING_LEN || trimmed.len() > MAX_CATEGORY_LEN { + return Err(ContractError::CategoryTooLong); + } + + // Allow alphanumeric, underscore, hyphen, space + for c in trimmed.chars() { + if !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != ' ' { + return Err(ContractError::InvalidCategoryFormat); + } + } + + Ok(()) +} + +/// Validates website URL: +/// - If provided, must be non-empty +/// - Length must not exceed MAX_WEBSITE_LEN +/// - Must start with http:// or https:// +pub fn validate_website(website: &Option) -> Result<(), ContractError> { + extern crate alloc; + use alloc::string::ToString; + + if let Some(url) = website { + let url_str = url.to_string(); + let trimmed = url_str.trim(); + + if trimmed.is_empty() { + return Err(ContractError::InvalidWebsiteUrl); + } + + if trimmed.len() > MAX_WEBSITE_LEN { + return Err(ContractError::WebsiteUrlTooLong); + } + + // Basic URL format check + if !trimmed.starts_with("http://") && !trimmed.starts_with("https://") { + return Err(ContractError::InvalidWebsiteUrlFormat); + } + } + + Ok(()) +} + +/// Validates CID (Content Identifier) for IPFS/similar systems: +/// - If provided, must be non-empty +/// - Length must be between MIN_CID_LEN and MAX_CID_LEN +/// - Contains only alphanumeric characters (base58/base32 encoding) +pub fn validate_cid(cid: &Option) -> Result<(), ContractError> { + extern crate alloc; + use alloc::string::ToString; + + if let Some(cid_val) = cid { + let cid_str = cid_val.to_string(); + let trimmed = cid_str.trim(); + + if trimmed.is_empty() { + return Err(ContractError::InvalidCid); + } + + if trimmed.len() < MIN_CID_LEN || trimmed.len() > MAX_CID_LEN { + return Err(ContractError::CidInvalidLength); + } + + // CIDs are typically base58 or base32 encoded (alphanumeric) + for c in trimmed.chars() { + if !c.is_ascii_alphanumeric() { + return Err(ContractError::InvalidCidFormat); + } + } + } + + Ok(()) +} + +/// Validates rating value: +/// - Must be between RATING_MIN and RATING_MAX (inclusive) +pub fn validate_rating(rating: u32) -> Result<(), ContractError> { + if !(RATING_MIN..=RATING_MAX).contains(&rating) { + return Err(ContractError::InvalidRating); + } + Ok(()) +} + +/// Validates pagination parameters: +/// - Limit must be greater than 0 +/// - Limit must not exceed MAX_PAGINATION_LIMIT +pub fn validate_pagination(limit: u32) -> Result<(), ContractError> { + if limit == 0 { + return Err(ContractError::InvalidPaginationLimit); + } + + if limit > MAX_PAGINATION_LIMIT { + return Err(ContractError::PaginationLimitTooLarge); + } + + Ok(()) +} + +/// Validates evidence CID for verification requests: +/// - Must be non-empty +/// - Length must be between MIN_CID_LEN and MAX_CID_LEN +/// - Contains only alphanumeric characters +pub fn validate_evidence_cid(evidence_cid: &String) -> Result<(), ContractError> { + extern crate alloc; + use alloc::string::ToString; + + let cid_str = evidence_cid.to_string(); + let trimmed = cid_str.trim(); + + if trimmed.is_empty() { + return Err(ContractError::InvalidCid); + } + + if trimmed.len() < MIN_CID_LEN || trimmed.len() > MAX_CID_LEN { + return Err(ContractError::CidInvalidLength); + } + + for c in trimmed.chars() { + if !c.is_ascii_alphanumeric() { + return Err(ContractError::InvalidCidFormat); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::{Env, String}; + + // ── Project Name Tests ── + + #[test] + fn test_valid_project_name() { + let env = Env::default(); + let name = String::from_str(&env, "Valid-Project_Name123"); + assert!(validate_project_name(&name).is_ok()); + } + + #[test] + fn test_project_name_with_spaces() { + let env = Env::default(); + let name = String::from_str(&env, "My Project Name"); + assert!(validate_project_name(&name).is_ok()); + } + + #[test] + fn test_empty_project_name() { + let env = Env::default(); + let name = String::from_str(&env, ""); + assert_eq!( + validate_project_name(&name), + Err(ContractError::InvalidProjectName) + ); + } + + #[test] + fn test_whitespace_only_project_name() { + let env = Env::default(); + let name = String::from_str(&env, " "); + assert_eq!( + validate_project_name(&name), + Err(ContractError::InvalidProjectName) + ); + } + + #[test] + fn test_project_name_too_long() { + let env = Env::default(); + // 51 characters + let name = String::from_str(&env, "ThisProjectNameIsWayTooLongAndExceedsTheFiftyCharL1"); + assert_eq!( + validate_project_name(&name), + Err(ContractError::ProjectNameTooLong) + ); + } + + #[test] + fn test_project_name_invalid_characters() { + let env = Env::default(); + let name = String::from_str(&env, "Project@Name!"); + assert_eq!( + validate_project_name(&name), + Err(ContractError::InvalidProjectNameFormat) + ); + } + + // ── Description Tests ── + + #[test] + fn test_valid_description() { + let env = Env::default(); + let desc = String::from_str(&env, "This is a valid project description."); + assert!(validate_description(&desc).is_ok()); + } + + #[test] + fn test_empty_description() { + let env = Env::default(); + let desc = String::from_str(&env, ""); + assert_eq!( + validate_description(&desc), + Err(ContractError::InvalidDescription) + ); + } + + #[test] + fn test_description_too_long() { + let env = Env::default(); + let long_desc = "a".repeat(MAX_DESCRIPTION_LEN + 1); + let desc = String::from_str(&env, &long_desc); + assert_eq!( + validate_description(&desc), + Err(ContractError::DescriptionTooLong) + ); + } + + // ── Category Tests ── + + #[test] + fn test_valid_category() { + let env = Env::default(); + let cat = String::from_str(&env, "DeFi"); + assert!(validate_category(&cat).is_ok()); + } + + #[test] + fn test_category_with_spaces() { + let env = Env::default(); + let cat = String::from_str(&env, "Decentralized Finance"); + assert!(validate_category(&cat).is_ok()); + } + + #[test] + fn test_empty_category() { + let env = Env::default(); + let cat = String::from_str(&env, ""); + assert_eq!( + validate_category(&cat), + Err(ContractError::InvalidCategory) + ); + } + + #[test] + fn test_category_too_long() { + let env = Env::default(); + let long_cat = "a".repeat(MAX_CATEGORY_LEN + 1); + let cat = String::from_str(&env, &long_cat); + assert_eq!( + validate_category(&cat), + Err(ContractError::CategoryTooLong) + ); + } + + // ── Website Tests ── + + #[test] + fn test_valid_website() { + let env = Env::default(); + let url = Some(String::from_str(&env, "https://example.com")); + assert!(validate_website(&url).is_ok()); + } + + #[test] + fn test_website_http() { + let env = Env::default(); + let url = Some(String::from_str(&env, "http://example.com")); + assert!(validate_website(&url).is_ok()); + } + + #[test] + fn test_website_none() { + assert!(validate_website(&None).is_ok()); + } + + #[test] + fn test_website_invalid_protocol() { + let env = Env::default(); + let url = Some(String::from_str(&env, "ftp://example.com")); + assert_eq!( + validate_website(&url), + Err(ContractError::InvalidWebsiteUrlFormat) + ); + } + + #[test] + fn test_website_too_long() { + let env = Env::default(); + let long_url = format!("https://{}.com", "a".repeat(MAX_WEBSITE_LEN)); + let url = Some(String::from_str(&env, &long_url)); + assert_eq!( + validate_website(&url), + Err(ContractError::WebsiteUrlTooLong) + ); + } + + // ── CID Tests ── + + #[test] + fn test_valid_cid() { + let env = Env::default(); + let cid = Some(String::from_str( + &env, + "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG", + )); + assert!(validate_cid(&cid).is_ok()); + } + + #[test] + fn test_cid_none() { + assert!(validate_cid(&None).is_ok()); + } + + #[test] + fn test_cid_too_short() { + let env = Env::default(); + let cid = Some(String::from_str(&env, "short")); + assert_eq!(validate_cid(&cid), Err(ContractError::CidInvalidLength)); + } + + #[test] + fn test_cid_too_long() { + let env = Env::default(); + let long_cid = "a".repeat(MAX_CID_LEN + 1); + let cid = Some(String::from_str(&env, &long_cid)); + assert_eq!(validate_cid(&cid), Err(ContractError::CidInvalidLength)); + } + + #[test] + fn test_cid_invalid_characters() { + let env = Env::default(); + let cid = Some(String::from_str( + &env, + "Qm@#$%^&*()_+{}|:<>?~`-=[]\\;',./", + )); + assert_eq!(validate_cid(&cid), Err(ContractError::InvalidCidFormat)); + } + + // ── Rating Tests ── + + #[test] + fn test_valid_ratings() { + for rating in RATING_MIN..=RATING_MAX { + assert!(validate_rating(rating).is_ok()); + } + } + + #[test] + fn test_rating_too_low() { + assert_eq!( + validate_rating(RATING_MIN - 1), + Err(ContractError::InvalidRating) + ); + } + + #[test] + fn test_rating_too_high() { + assert_eq!( + validate_rating(RATING_MAX + 1), + Err(ContractError::InvalidRating) + ); + } + + // ── Pagination Tests ── + + #[test] + fn test_valid_pagination() { + assert!(validate_pagination(10).is_ok()); + assert!(validate_pagination(MAX_PAGINATION_LIMIT).is_ok()); + } + + #[test] + fn test_pagination_zero() { + assert_eq!( + validate_pagination(0), + Err(ContractError::InvalidPaginationLimit) + ); + } + + #[test] + fn test_pagination_too_large() { + assert_eq!( + validate_pagination(MAX_PAGINATION_LIMIT + 1), + Err(ContractError::PaginationLimitTooLarge) + ); + } + + // ── Evidence CID Tests ── + + #[test] + fn test_valid_evidence_cid() { + let env = Env::default(); + let cid = String::from_str(&env, "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); + assert!(validate_evidence_cid(&cid).is_ok()); + } + + #[test] + fn test_evidence_cid_empty() { + let env = Env::default(); + let cid = String::from_str(&env, ""); + assert_eq!( + validate_evidence_cid(&cid), + Err(ContractError::InvalidCid) + ); + } +} diff --git a/dongle-smartcontract/src/verification_registry.rs b/dongle-smartcontract/src/verification_registry.rs index 0a60630..6ee8956 100644 --- a/dongle-smartcontract/src/verification_registry.rs +++ b/dongle-smartcontract/src/verification_registry.rs @@ -280,13 +280,6 @@ impl VerificationRegistry { .ok_or(ContractError::VerificationNotFound) } - pub fn validate_evidence_cid(evidence_cid: &String) -> Result<(), ContractError> { - if evidence_cid.is_empty() { - return Err(ContractError::InvalidProjectData); - } - Ok(()) - } - #[allow(dead_code)] pub fn verification_exists(env: &Env, project_id: u64) -> bool { env.storage()