use crate::error::ValidationError;
use reputation_types::AgentData;
use chrono::Utc;
pub fn validate_agent_data(data: &AgentData) -> Result<(), ValidationError> {
validate_did(&data.did)?;
validate_dates(data)?;
validate_ratings(data)?;
validate_review_counts(data)?;
validate_mcp_level(data.mcp_level)?;
validate_interactions(data)?;
Ok(())
}
fn validate_did(did: &str) -> Result<(), ValidationError> {
if did.is_empty() {
return Err(ValidationError::InvalidDid("DID cannot be empty".to_string()));
}
if did.contains("..") || did.contains("//") {
return Err(ValidationError::InvalidDid(
"DID contains invalid path sequences".to_string()
));
}
if did.contains('<') || did.contains('>') || did.contains("javascript:") {
return Err(ValidationError::InvalidDid(
"DID contains invalid characters".to_string()
));
}
if did.contains('\'') || did.contains('"') || did.contains(';') || did.contains("--") {
return Err(ValidationError::InvalidDid(
"DID contains invalid characters".to_string()
));
}
if did.contains('\0') || did.contains('\n') || did.contains('\r') || did.contains('\t') {
return Err(ValidationError::InvalidDid(
"DID contains invalid control characters".to_string()
));
}
if did.len() > 1000 {
return Err(ValidationError::InvalidDid(
"DID exceeds maximum length".to_string()
));
}
if !did.starts_with("did:") {
return Err(ValidationError::InvalidDid(
"DID must start with 'did:' prefix".to_string()
));
}
let parts: Vec<&str> = did.split(':').collect();
if parts.len() < 3 {
return Err(ValidationError::InvalidDid(
"DID must have format 'did:method:id'".to_string()
));
}
let method = parts[1];
if method.is_empty() {
return Err(ValidationError::InvalidDid(
"DID method name cannot be empty".to_string()
));
}
let identifier = parts[2..].join(":");
if identifier.is_empty() {
return Err(ValidationError::InvalidDid(
"DID method-specific identifier cannot be empty".to_string()
));
}
Ok(())
}
fn validate_dates(data: &AgentData) -> Result<(), ValidationError> {
let now = Utc::now();
if data.created_at > now {
return Err(ValidationError::FutureDate(format!(
"created_at ({}) cannot be in the future",
data.created_at.to_rfc3339()
)));
}
Ok(())
}
fn validate_ratings(data: &AgentData) -> Result<(), ValidationError> {
match (data.average_rating, data.total_reviews) {
(Some(rating), reviews) if reviews > 0 => {
if rating.is_nan() || rating.is_infinite() {
return Err(ValidationError::InvalidRating(rating));
}
if rating < 1.0 || rating > 5.0 {
return Err(ValidationError::InvalidRating(rating));
}
},
(Some(rating), 0) => {
return Err(ValidationError::InvalidField {
field: "average_rating".to_string(),
value: format!("{} (but total_reviews is 0)", rating),
});
},
(None, reviews) if reviews > 0 => {
return Err(ValidationError::InvalidField {
field: "average_rating".to_string(),
value: format!("None (but total_reviews is {})", reviews),
});
},
_ => {}, }
Ok(())
}
fn validate_review_counts(data: &AgentData) -> Result<(), ValidationError> {
let calculated_total = match data.positive_reviews.checked_add(data.negative_reviews) {
Some(total) => total,
None => {
return Err(ValidationError::InconsistentReviews);
}
};
if calculated_total != data.total_reviews {
return Err(ValidationError::InconsistentReviews);
}
Ok(())
}
fn validate_mcp_level(level: Option<u8>) -> Result<(), ValidationError> {
if let Some(mcp) = level {
if mcp > 3 {
return Err(ValidationError::InvalidMcpLevel(mcp));
}
}
Ok(())
}
fn validate_interactions(data: &AgentData) -> Result<(), ValidationError> {
if data.total_reviews > data.total_interactions {
return Err(ValidationError::InvalidField {
field: "total_reviews".to_string(),
value: format!(
"{} (exceeds total_interactions: {})",
data.total_reviews,
data.total_interactions
),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
fn create_valid_agent() -> AgentData {
AgentData {
did: "did:example:123".to_string(),
created_at: Utc::now() - Duration::days(1),
mcp_level: None,
identity_verified: false,
security_audit_passed: false,
open_source: false,
total_interactions: 0,
total_reviews: 0,
average_rating: None,
positive_reviews: 0,
negative_reviews: 0,
}
}
#[test]
fn test_valid_agent_data() {
let agent = create_valid_agent();
assert!(validate_agent_data(&agent).is_ok());
}
#[test]
fn test_valid_did_formats() {
let valid_dids = vec![
"did:example:123",
"did:web:example.com",
"did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH",
"did:ethr:0x1234567890123456789012345678901234567890",
"did:ion:EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg",
"did:test:foo:bar:baz", ];
for did in valid_dids {
assert!(validate_did(did).is_ok(), "DID '{}' should be valid", did);
}
}
#[test]
fn test_invalid_did_formats() {
let test_cases = vec![
("", "DID cannot be empty"),
("example:123", "DID must start with 'did:'"),
("DID:example:123", "DID must start with 'did:'"),
("did:", "DID must have format"),
("did::", "DID method name cannot be empty"),
("did:example", "DID must have format"),
("did:example:", "DID method-specific identifier cannot be empty"),
("did::123", "DID method name cannot be empty"),
];
for (did, expected_msg) in test_cases {
let result = validate_did(did);
assert!(result.is_err(), "DID '{}' should be invalid", did);
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains(expected_msg),
"Error message '{}' should contain '{}'", err_msg, expected_msg);
}
}
#[test]
fn test_very_long_did() {
let long_identifier = "x".repeat(987); let long_did = format!("did:example:{}", long_identifier);
assert!(validate_did(&long_did).is_ok());
let too_long_identifier = "x".repeat(989); let too_long_did = format!("did:example:{}", too_long_identifier);
assert!(matches!(
validate_did(&too_long_did),
Err(ValidationError::InvalidDid(msg)) if msg.contains("exceeds maximum length")
));
}
#[test]
fn test_future_date_validation() {
let mut agent = create_valid_agent();
agent.created_at = Utc::now() + Duration::days(1);
let result = validate_agent_data(&agent);
assert!(matches!(result, Err(ValidationError::FutureDate(_))));
}
#[test]
fn test_date_edge_cases() {
let mut agent = create_valid_agent();
agent.created_at = Utc::now();
assert!(validate_dates(&agent).is_ok());
agent.created_at = Utc::now() - Duration::days(3650); assert!(validate_dates(&agent).is_ok());
}
#[test]
fn test_rating_validation() {
let mut agent = create_valid_agent();
agent.total_reviews = 10;
agent.positive_reviews = 8;
agent.negative_reviews = 2;
agent.total_interactions = 20;
let valid_ratings = vec![1.0, 2.5, 3.0, 4.5, 5.0];
for rating in valid_ratings {
agent.average_rating = Some(rating);
assert!(validate_ratings(&agent).is_ok(),
"Rating {} should be valid", rating);
}
let invalid_ratings = vec![0.0, 0.9, 5.1, 6.0, -1.0, 10.0];
for rating in invalid_ratings {
agent.average_rating = Some(rating);
let result = validate_ratings(&agent);
assert!(matches!(result, Err(ValidationError::InvalidRating(_))),
"Rating {} should be invalid", rating);
}
}
#[test]
fn test_rating_special_values() {
let mut agent = create_valid_agent();
agent.total_reviews = 10;
agent.positive_reviews = 10;
agent.total_interactions = 10;
agent.average_rating = Some(f64::NAN);
let result = validate_ratings(&agent);
assert!(matches!(result, Err(ValidationError::InvalidRating(_))));
agent.average_rating = Some(f64::INFINITY);
let result = validate_ratings(&agent);
assert!(matches!(result, Err(ValidationError::InvalidRating(_))));
agent.average_rating = Some(f64::NEG_INFINITY);
let result = validate_ratings(&agent);
assert!(matches!(result, Err(ValidationError::InvalidRating(_))));
}
#[test]
fn test_rating_consistency() {
let mut agent = create_valid_agent();
agent.total_reviews = 0;
agent.average_rating = Some(4.5);
let result = validate_ratings(&agent);
assert!(matches!(result, Err(ValidationError::InvalidField { field, .. }) if field == "average_rating"));
agent.total_reviews = 10;
agent.positive_reviews = 6;
agent.negative_reviews = 4;
agent.total_interactions = 10;
agent.average_rating = None;
let result = validate_ratings(&agent);
assert!(matches!(result, Err(ValidationError::InvalidField { field, .. }) if field == "average_rating"));
}
#[test]
fn test_review_count_validation() {
let mut agent = create_valid_agent();
agent.positive_reviews = 10;
agent.negative_reviews = 5;
agent.total_reviews = 15;
agent.total_interactions = 20;
assert!(validate_review_counts(&agent).is_ok());
agent.total_reviews = 20;
let result = validate_review_counts(&agent);
assert!(matches!(result, Err(ValidationError::InconsistentReviews)));
}
#[test]
fn test_review_count_edge_cases() {
let mut agent = create_valid_agent();
assert!(validate_review_counts(&agent).is_ok());
agent.positive_reviews = 100;
agent.total_reviews = 100;
agent.total_interactions = 100;
assert!(validate_review_counts(&agent).is_ok());
agent.positive_reviews = 0;
agent.negative_reviews = 50;
agent.total_reviews = 50;
assert!(validate_review_counts(&agent).is_ok());
agent.positive_reviews = 1_000_000;
agent.negative_reviews = 500_000;
agent.total_reviews = 1_500_000;
agent.total_interactions = 2_000_000;
assert!(validate_review_counts(&agent).is_ok());
}
#[test]
fn test_mcp_level_validation() {
for level in 0..=3 {
assert!(validate_mcp_level(Some(level)).is_ok(),
"MCP level {} should be valid", level);
}
assert!(validate_mcp_level(None).is_ok());
for level in 4..=10 {
let result = validate_mcp_level(Some(level));
assert!(matches!(result, Err(ValidationError::InvalidMcpLevel(_))),
"MCP level {} should be invalid", level);
}
}
#[test]
fn test_interaction_validation() {
let mut agent = create_valid_agent();
agent.total_reviews = 50;
agent.positive_reviews = 30;
agent.negative_reviews = 20;
agent.total_interactions = 100;
assert!(validate_interactions(&agent).is_ok());
agent.total_interactions = 50;
assert!(validate_interactions(&agent).is_ok());
agent.total_interactions = 40;
let result = validate_interactions(&agent);
assert!(matches!(result, Err(ValidationError::InvalidField { field, .. }) if field == "total_reviews"));
}
#[test]
fn test_complete_validation_chain() {
let mut agent = create_valid_agent();
agent.did = "did:web:example.com:users:alice".to_string();
agent.created_at = Utc::now() - Duration::days(30);
agent.mcp_level = Some(2);
agent.identity_verified = true;
agent.security_audit_passed = true;
agent.open_source = true;
agent.total_interactions = 1000;
agent.total_reviews = 500;
agent.average_rating = Some(4.2);
agent.positive_reviews = 400;
agent.negative_reviews = 100;
assert!(validate_agent_data(&agent).is_ok());
}
#[test]
fn test_validation_stops_on_first_error() {
let mut agent = create_valid_agent();
agent.did = "not-a-did".to_string();
agent.created_at = Utc::now() + Duration::days(1);
agent.average_rating = Some(10.0);
agent.total_reviews = 10;
agent.positive_reviews = 5;
agent.negative_reviews = 5;
let result = validate_agent_data(&agent);
assert!(matches!(result, Err(ValidationError::InvalidDid(_))));
}
#[test]
fn test_unicode_did_validation() {
let unicode_dids = vec![
"did:测试:123",
"did:тест:456",
"did:テスト:789",
"did:example:用户:alice",
];
for did in unicode_dids {
assert!(validate_did(did).is_ok(),
"Unicode DID '{}' should be valid", did);
}
}
#[test]
fn test_empty_string_fields() {
let mut agent = create_valid_agent();
agent.did = "".to_string();
let result = validate_agent_data(&agent);
assert!(matches!(result, Err(ValidationError::InvalidDid(_))));
}
#[test]
fn test_max_u32_values() {
let mut agent = create_valid_agent();
agent.total_interactions = u32::MAX;
agent.total_reviews = u32::MAX;
agent.positive_reviews = u32::MAX / 2 + 1;
agent.negative_reviews = u32::MAX / 2 + 1;
agent.average_rating = Some(3.5);
let result = validate_review_counts(&agent);
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::InconsistentReviews)));
}
#[test]
fn test_validation_performance() {
use std::time::Instant;
let agent = create_valid_agent();
let start = Instant::now();
for _ in 0..1000 {
let _ = validate_agent_data(&agent);
}
let duration = start.elapsed();
let avg_time = duration.as_micros() as f64 / 1000.0;
assert!(avg_time < 1000.0,
"Validation took {} microseconds on average, should be < 1000", avg_time);
}
}