reputation_core/
validation.rs

1use crate::error::ValidationError;
2use reputation_types::AgentData;
3use chrono::Utc;
4
5/// Validates all fields of AgentData to ensure they meet required constraints.
6/// 
7/// This function performs comprehensive validation including:
8/// - DID format validation
9/// - Date validation (no future dates)
10/// - Rating bounds and consistency
11/// - Review count consistency
12/// - MCP level validation
13/// 
14/// # Example
15/// ```
16/// use reputation_core::validation::validate_agent_data;
17/// use reputation_types::AgentData;
18/// use chrono::Utc;
19/// 
20/// let agent = AgentData {
21///     did: "did:example:123".to_string(),
22///     created_at: Utc::now(),
23///     mcp_level: Some(2),
24///     identity_verified: true,
25///     security_audit_passed: false,
26///     open_source: true,
27///     total_interactions: 100,
28///     total_reviews: 50,
29///     average_rating: Some(4.2),
30///     positive_reviews: 40,
31///     negative_reviews: 10,
32/// };
33/// 
34/// match validate_agent_data(&agent) {
35///     Ok(()) => println!("Agent data is valid"),
36///     Err(e) => println!("Validation failed: {}", e),
37/// }
38/// ```
39pub fn validate_agent_data(data: &AgentData) -> Result<(), ValidationError> {
40    validate_did(&data.did)?;
41    validate_dates(data)?;
42    validate_ratings(data)?;
43    validate_review_counts(data)?;
44    validate_mcp_level(data.mcp_level)?;
45    validate_interactions(data)?;
46    Ok(())
47}
48
49/// Validates DID format according to W3C DID specification.
50/// 
51/// DIDs must start with "did:" followed by a method name and method-specific identifier.
52/// Also performs security validation to prevent malicious inputs.
53/// 
54/// # Examples
55/// - Valid: "did:example:123", "did:web:example.com", "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH"
56/// - Invalid: "example:123", "DID:example:123", "", "did:", "did::"
57fn validate_did(did: &str) -> Result<(), ValidationError> {
58    if did.is_empty() {
59        return Err(ValidationError::InvalidDid("DID cannot be empty".to_string()));
60    }
61    
62    // Security: Check for path traversal attempts
63    if did.contains("..") || did.contains("//") {
64        return Err(ValidationError::InvalidDid(
65            "DID contains invalid path sequences".to_string()
66        ));
67    }
68    
69    // Security: Check for script injection attempts
70    if did.contains('<') || did.contains('>') || did.contains("javascript:") {
71        return Err(ValidationError::InvalidDid(
72            "DID contains invalid characters".to_string()
73        ));
74    }
75    
76    // Security: Check for SQL injection patterns
77    if did.contains('\'') || did.contains('"') || did.contains(';') || did.contains("--") {
78        return Err(ValidationError::InvalidDid(
79            "DID contains invalid characters".to_string()
80        ));
81    }
82    
83    // Security: Check for null bytes and control characters
84    if did.contains('\0') || did.contains('\n') || did.contains('\r') || did.contains('\t') {
85        return Err(ValidationError::InvalidDid(
86            "DID contains invalid control characters".to_string()
87        ));
88    }
89    
90    // Security: Reasonable length limit
91    if did.len() > 1000 {
92        return Err(ValidationError::InvalidDid(
93            "DID exceeds maximum length".to_string()
94        ));
95    }
96    
97    if !did.starts_with("did:") {
98        return Err(ValidationError::InvalidDid(
99            "DID must start with 'did:' prefix".to_string()
100        ));
101    }
102    
103    // Check for basic DID structure: did:method:method-specific-id
104    let parts: Vec<&str> = did.split(':').collect();
105    if parts.len() < 3 {
106        return Err(ValidationError::InvalidDid(
107            "DID must have format 'did:method:id'".to_string()
108        ));
109    }
110    
111    // Validate method name (second part)
112    let method = parts[1];
113    if method.is_empty() {
114        return Err(ValidationError::InvalidDid(
115            "DID method name cannot be empty".to_string()
116        ));
117    }
118    
119    // Validate method-specific identifier (third part and beyond)
120    let identifier = parts[2..].join(":");
121    if identifier.is_empty() {
122        return Err(ValidationError::InvalidDid(
123            "DID method-specific identifier cannot be empty".to_string()
124        ));
125    }
126    
127    Ok(())
128}
129
130/// Validates date fields to ensure they are not in the future.
131fn validate_dates(data: &AgentData) -> Result<(), ValidationError> {
132    let now = Utc::now();
133    
134    if data.created_at > now {
135        return Err(ValidationError::FutureDate(format!(
136            "created_at ({}) cannot be in the future", 
137            data.created_at.to_rfc3339()
138        )));
139    }
140    
141    Ok(())
142}
143
144/// Validates rating values and consistency.
145/// 
146/// Ensures:
147/// - Average rating is between 1.0 and 5.0 (inclusive) when reviews exist
148/// - Average rating is None when there are no reviews
149/// - Rating values are not NaN or infinite
150fn validate_ratings(data: &AgentData) -> Result<(), ValidationError> {
151    match (data.average_rating, data.total_reviews) {
152        (Some(rating), reviews) if reviews > 0 => {
153            // Check for NaN or infinite values
154            if rating.is_nan() || rating.is_infinite() {
155                return Err(ValidationError::InvalidRating(rating));
156            }
157            
158            // Check rating bounds
159            if rating < 1.0 || rating > 5.0 {
160                return Err(ValidationError::InvalidRating(rating));
161            }
162        },
163        (Some(rating), 0) => {
164            // If there are no reviews, there shouldn't be an average rating
165            return Err(ValidationError::InvalidField {
166                field: "average_rating".to_string(),
167                value: format!("{} (but total_reviews is 0)", rating),
168            });
169        },
170        (None, reviews) if reviews > 0 => {
171            // If there are reviews, there should be an average rating
172            return Err(ValidationError::InvalidField {
173                field: "average_rating".to_string(),
174                value: format!("None (but total_reviews is {})", reviews),
175            });
176        },
177        _ => {}, // None rating with 0 reviews is valid
178    }
179    
180    Ok(())
181}
182
183/// Validates review count consistency.
184/// 
185/// Ensures positive_reviews + negative_reviews = total_reviews
186fn validate_review_counts(data: &AgentData) -> Result<(), ValidationError> {
187    // Use checked_add to prevent overflow
188    let calculated_total = match data.positive_reviews.checked_add(data.negative_reviews) {
189        Some(total) => total,
190        None => {
191            // Overflow occurred - this is definitely inconsistent
192            return Err(ValidationError::InconsistentReviews);
193        }
194    };
195    
196    if calculated_total != data.total_reviews {
197        return Err(ValidationError::InconsistentReviews);
198    }
199    
200    Ok(())
201}
202
203/// Validates MCP level is within allowed range (0-3).
204fn validate_mcp_level(level: Option<u8>) -> Result<(), ValidationError> {
205    if let Some(mcp) = level {
206        if mcp > 3 {
207            return Err(ValidationError::InvalidMcpLevel(mcp));
208        }
209    }
210    
211    Ok(())
212}
213
214/// Validates interaction counts and their relationships.
215/// 
216/// Ensures:
217/// - Total reviews doesn't exceed total interactions
218/// - Values are logically consistent
219fn validate_interactions(data: &AgentData) -> Result<(), ValidationError> {
220    // Reviews can't exceed total interactions
221    if data.total_reviews > data.total_interactions {
222        return Err(ValidationError::InvalidField {
223            field: "total_reviews".to_string(),
224            value: format!(
225                "{} (exceeds total_interactions: {})", 
226                data.total_reviews, 
227                data.total_interactions
228            ),
229        });
230    }
231    
232    Ok(())
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use chrono::Duration;
239
240    fn create_valid_agent() -> AgentData {
241        AgentData {
242            did: "did:example:123".to_string(),
243            created_at: Utc::now() - Duration::days(1),
244            mcp_level: None,
245            identity_verified: false,
246            security_audit_passed: false,
247            open_source: false,
248            total_interactions: 0,
249            total_reviews: 0,
250            average_rating: None,
251            positive_reviews: 0,
252            negative_reviews: 0,
253        }
254    }
255
256    #[test]
257    fn test_valid_agent_data() {
258        let agent = create_valid_agent();
259        assert!(validate_agent_data(&agent).is_ok());
260    }
261
262    #[test]
263    fn test_valid_did_formats() {
264        let valid_dids = vec![
265            "did:example:123",
266            "did:web:example.com",
267            "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH",
268            "did:ethr:0x1234567890123456789012345678901234567890",
269            "did:ion:EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg",
270            "did:test:foo:bar:baz", // Multiple colons in identifier
271        ];
272
273        for did in valid_dids {
274            assert!(validate_did(did).is_ok(), "DID '{}' should be valid", did);
275        }
276    }
277
278    #[test]
279    fn test_invalid_did_formats() {
280        let test_cases = vec![
281            ("", "DID cannot be empty"),
282            ("example:123", "DID must start with 'did:'"),
283            ("DID:example:123", "DID must start with 'did:'"),
284            ("did:", "DID must have format"),
285            ("did::", "DID method name cannot be empty"),
286            ("did:example", "DID must have format"),
287            ("did:example:", "DID method-specific identifier cannot be empty"),
288            ("did::123", "DID method name cannot be empty"),
289        ];
290
291        for (did, expected_msg) in test_cases {
292            let result = validate_did(did);
293            assert!(result.is_err(), "DID '{}' should be invalid", did);
294            let err_msg = result.unwrap_err().to_string();
295            assert!(err_msg.contains(expected_msg), 
296                "Error message '{}' should contain '{}'", err_msg, expected_msg);
297        }
298    }
299
300    #[test]
301    fn test_very_long_did() {
302        // Test that DIDs up to the limit are accepted
303        let long_identifier = "x".repeat(987); // "did:example:" is 12 chars, so 987 + 12 = 999 < 1000
304        let long_did = format!("did:example:{}", long_identifier);
305        assert!(validate_did(&long_did).is_ok());
306        
307        // Test that DIDs over the limit are rejected
308        let too_long_identifier = "x".repeat(989); // 989 + 12 = 1001 > 1000
309        let too_long_did = format!("did:example:{}", too_long_identifier);
310        assert!(matches!(
311            validate_did(&too_long_did),
312            Err(ValidationError::InvalidDid(msg)) if msg.contains("exceeds maximum length")
313        ));
314    }
315
316    #[test]
317    fn test_future_date_validation() {
318        let mut agent = create_valid_agent();
319        agent.created_at = Utc::now() + Duration::days(1);
320        
321        let result = validate_agent_data(&agent);
322        assert!(matches!(result, Err(ValidationError::FutureDate(_))));
323    }
324
325    #[test]
326    fn test_date_edge_cases() {
327        let mut agent = create_valid_agent();
328        
329        // Exactly now should be valid
330        agent.created_at = Utc::now();
331        assert!(validate_dates(&agent).is_ok());
332        
333        // Far past should be valid
334        agent.created_at = Utc::now() - Duration::days(3650); // 10 years ago
335        assert!(validate_dates(&agent).is_ok());
336    }
337
338    #[test]
339    fn test_rating_validation() {
340        let mut agent = create_valid_agent();
341        
342        // Valid ratings with reviews
343        agent.total_reviews = 10;
344        agent.positive_reviews = 8;
345        agent.negative_reviews = 2;
346        agent.total_interactions = 20;
347        
348        let valid_ratings = vec![1.0, 2.5, 3.0, 4.5, 5.0];
349        for rating in valid_ratings {
350            agent.average_rating = Some(rating);
351            assert!(validate_ratings(&agent).is_ok(), 
352                "Rating {} should be valid", rating);
353        }
354        
355        // Invalid ratings
356        let invalid_ratings = vec![0.0, 0.9, 5.1, 6.0, -1.0, 10.0];
357        for rating in invalid_ratings {
358            agent.average_rating = Some(rating);
359            let result = validate_ratings(&agent);
360            assert!(matches!(result, Err(ValidationError::InvalidRating(_))),
361                "Rating {} should be invalid", rating);
362        }
363    }
364
365    #[test]
366    fn test_rating_special_values() {
367        let mut agent = create_valid_agent();
368        agent.total_reviews = 10;
369        agent.positive_reviews = 10;
370        agent.total_interactions = 10;
371        
372        // NaN
373        agent.average_rating = Some(f64::NAN);
374        let result = validate_ratings(&agent);
375        assert!(matches!(result, Err(ValidationError::InvalidRating(_))));
376        
377        // Positive infinity
378        agent.average_rating = Some(f64::INFINITY);
379        let result = validate_ratings(&agent);
380        assert!(matches!(result, Err(ValidationError::InvalidRating(_))));
381        
382        // Negative infinity
383        agent.average_rating = Some(f64::NEG_INFINITY);
384        let result = validate_ratings(&agent);
385        assert!(matches!(result, Err(ValidationError::InvalidRating(_))));
386    }
387
388    #[test]
389    fn test_rating_consistency() {
390        let mut agent = create_valid_agent();
391        
392        // No reviews but has rating
393        agent.total_reviews = 0;
394        agent.average_rating = Some(4.5);
395        let result = validate_ratings(&agent);
396        assert!(matches!(result, Err(ValidationError::InvalidField { field, .. }) if field == "average_rating"));
397        
398        // Has reviews but no rating
399        agent.total_reviews = 10;
400        agent.positive_reviews = 6;
401        agent.negative_reviews = 4;
402        agent.total_interactions = 10;
403        agent.average_rating = None;
404        let result = validate_ratings(&agent);
405        assert!(matches!(result, Err(ValidationError::InvalidField { field, .. }) if field == "average_rating"));
406    }
407
408    #[test]
409    fn test_review_count_validation() {
410        let mut agent = create_valid_agent();
411        
412        // Valid: positive + negative = total
413        agent.positive_reviews = 10;
414        agent.negative_reviews = 5;
415        agent.total_reviews = 15;
416        agent.total_interactions = 20;
417        assert!(validate_review_counts(&agent).is_ok());
418        
419        // Invalid: doesn't add up
420        agent.total_reviews = 20;
421        let result = validate_review_counts(&agent);
422        assert!(matches!(result, Err(ValidationError::InconsistentReviews)));
423    }
424
425    #[test]
426    fn test_review_count_edge_cases() {
427        let mut agent = create_valid_agent();
428        
429        // All zeros
430        assert!(validate_review_counts(&agent).is_ok());
431        
432        // Only positive reviews
433        agent.positive_reviews = 100;
434        agent.total_reviews = 100;
435        agent.total_interactions = 100;
436        assert!(validate_review_counts(&agent).is_ok());
437        
438        // Only negative reviews
439        agent.positive_reviews = 0;
440        agent.negative_reviews = 50;
441        agent.total_reviews = 50;
442        assert!(validate_review_counts(&agent).is_ok());
443        
444        // Large numbers
445        agent.positive_reviews = 1_000_000;
446        agent.negative_reviews = 500_000;
447        agent.total_reviews = 1_500_000;
448        agent.total_interactions = 2_000_000;
449        assert!(validate_review_counts(&agent).is_ok());
450    }
451
452    #[test]
453    fn test_mcp_level_validation() {
454        // Valid levels
455        for level in 0..=3 {
456            assert!(validate_mcp_level(Some(level)).is_ok(), 
457                "MCP level {} should be valid", level);
458        }
459        
460        // None is valid
461        assert!(validate_mcp_level(None).is_ok());
462        
463        // Invalid levels
464        for level in 4..=10 {
465            let result = validate_mcp_level(Some(level));
466            assert!(matches!(result, Err(ValidationError::InvalidMcpLevel(_))),
467                "MCP level {} should be invalid", level);
468        }
469    }
470
471    #[test]
472    fn test_interaction_validation() {
473        let mut agent = create_valid_agent();
474        
475        // Valid: reviews <= interactions
476        agent.total_reviews = 50;
477        agent.positive_reviews = 30;
478        agent.negative_reviews = 20;
479        agent.total_interactions = 100;
480        assert!(validate_interactions(&agent).is_ok());
481        
482        // Valid: reviews = interactions
483        agent.total_interactions = 50;
484        assert!(validate_interactions(&agent).is_ok());
485        
486        // Invalid: reviews > interactions
487        agent.total_interactions = 40;
488        let result = validate_interactions(&agent);
489        assert!(matches!(result, Err(ValidationError::InvalidField { field, .. }) if field == "total_reviews"));
490    }
491
492    #[test]
493    fn test_complete_validation_chain() {
494        let mut agent = create_valid_agent();
495        
496        // Set up a complex valid scenario
497        agent.did = "did:web:example.com:users:alice".to_string();
498        agent.created_at = Utc::now() - Duration::days(30);
499        agent.mcp_level = Some(2);
500        agent.identity_verified = true;
501        agent.security_audit_passed = true;
502        agent.open_source = true;
503        agent.total_interactions = 1000;
504        agent.total_reviews = 500;
505        agent.average_rating = Some(4.2);
506        agent.positive_reviews = 400;
507        agent.negative_reviews = 100;
508        
509        assert!(validate_agent_data(&agent).is_ok());
510    }
511
512    #[test]
513    fn test_validation_stops_on_first_error() {
514        let mut agent = create_valid_agent();
515        
516        // Multiple errors: bad DID, future date, invalid rating
517        agent.did = "not-a-did".to_string();
518        agent.created_at = Utc::now() + Duration::days(1);
519        agent.average_rating = Some(10.0);
520        agent.total_reviews = 10;
521        agent.positive_reviews = 5;
522        agent.negative_reviews = 5;
523        
524        let result = validate_agent_data(&agent);
525        // Should get DID error first
526        assert!(matches!(result, Err(ValidationError::InvalidDid(_))));
527    }
528
529    #[test]
530    fn test_unicode_did_validation() {
531        let unicode_dids = vec![
532            "did:测试:123",
533            "did:тест:456",
534            "did:テスト:789",
535            "did:example:用户:alice",
536        ];
537        
538        for did in unicode_dids {
539            assert!(validate_did(did).is_ok(), 
540                "Unicode DID '{}' should be valid", did);
541        }
542    }
543
544    #[test]
545    fn test_empty_string_fields() {
546        let mut agent = create_valid_agent();
547        agent.did = "".to_string();
548        
549        let result = validate_agent_data(&agent);
550        assert!(matches!(result, Err(ValidationError::InvalidDid(_))));
551    }
552
553    #[test]
554    fn test_max_u32_values() {
555        let mut agent = create_valid_agent();
556        agent.total_interactions = u32::MAX;
557        agent.total_reviews = u32::MAX;
558        // These values will cause the sum to overflow when added
559        agent.positive_reviews = u32::MAX / 2 + 1;
560        agent.negative_reviews = u32::MAX / 2 + 1;
561        agent.average_rating = Some(3.5);
562        
563        // This will fail because positive + negative != total_reviews
564        let result = validate_review_counts(&agent);
565        assert!(result.is_err());
566        assert!(matches!(result, Err(ValidationError::InconsistentReviews)));
567    }
568
569    #[test]
570    fn test_validation_performance() {
571        use std::time::Instant;
572        
573        let agent = create_valid_agent();
574        let start = Instant::now();
575        
576        // Run validation 1000 times
577        for _ in 0..1000 {
578            let _ = validate_agent_data(&agent);
579        }
580        
581        let duration = start.elapsed();
582        let avg_time = duration.as_micros() as f64 / 1000.0;
583        
584        // Should be well under 1ms per validation
585        assert!(avg_time < 1000.0, 
586            "Validation took {} microseconds on average, should be < 1000", avg_time);
587    }
588}