1use crate::error::ValidationError;
2use reputation_types::AgentData;
3use chrono::Utc;
4
5pub 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
49fn 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 if did.contains("..") || did.contains("//") {
64 return Err(ValidationError::InvalidDid(
65 "DID contains invalid path sequences".to_string()
66 ));
67 }
68
69 if did.contains('<') || did.contains('>') || did.contains("javascript:") {
71 return Err(ValidationError::InvalidDid(
72 "DID contains invalid characters".to_string()
73 ));
74 }
75
76 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 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 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 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 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 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
130fn 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
144fn validate_ratings(data: &AgentData) -> Result<(), ValidationError> {
151 match (data.average_rating, data.total_reviews) {
152 (Some(rating), reviews) if reviews > 0 => {
153 if rating.is_nan() || rating.is_infinite() {
155 return Err(ValidationError::InvalidRating(rating));
156 }
157
158 if rating < 1.0 || rating > 5.0 {
160 return Err(ValidationError::InvalidRating(rating));
161 }
162 },
163 (Some(rating), 0) => {
164 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 return Err(ValidationError::InvalidField {
173 field: "average_rating".to_string(),
174 value: format!("None (but total_reviews is {})", reviews),
175 });
176 },
177 _ => {}, }
179
180 Ok(())
181}
182
183fn validate_review_counts(data: &AgentData) -> Result<(), ValidationError> {
187 let calculated_total = match data.positive_reviews.checked_add(data.negative_reviews) {
189 Some(total) => total,
190 None => {
191 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
203fn 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
214fn validate_interactions(data: &AgentData) -> Result<(), ValidationError> {
220 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", ];
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 let long_identifier = "x".repeat(987); let long_did = format!("did:example:{}", long_identifier);
305 assert!(validate_did(&long_did).is_ok());
306
307 let too_long_identifier = "x".repeat(989); 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 agent.created_at = Utc::now();
331 assert!(validate_dates(&agent).is_ok());
332
333 agent.created_at = Utc::now() - Duration::days(3650); assert!(validate_dates(&agent).is_ok());
336 }
337
338 #[test]
339 fn test_rating_validation() {
340 let mut agent = create_valid_agent();
341
342 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 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 agent.average_rating = Some(f64::NAN);
374 let result = validate_ratings(&agent);
375 assert!(matches!(result, Err(ValidationError::InvalidRating(_))));
376
377 agent.average_rating = Some(f64::INFINITY);
379 let result = validate_ratings(&agent);
380 assert!(matches!(result, Err(ValidationError::InvalidRating(_))));
381
382 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 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 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 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 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 assert!(validate_review_counts(&agent).is_ok());
431
432 agent.positive_reviews = 100;
434 agent.total_reviews = 100;
435 agent.total_interactions = 100;
436 assert!(validate_review_counts(&agent).is_ok());
437
438 agent.positive_reviews = 0;
440 agent.negative_reviews = 50;
441 agent.total_reviews = 50;
442 assert!(validate_review_counts(&agent).is_ok());
443
444 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 for level in 0..=3 {
456 assert!(validate_mcp_level(Some(level)).is_ok(),
457 "MCP level {} should be valid", level);
458 }
459
460 assert!(validate_mcp_level(None).is_ok());
462
463 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 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 agent.total_interactions = 50;
484 assert!(validate_interactions(&agent).is_ok());
485
486 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 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 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 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 agent.positive_reviews = u32::MAX / 2 + 1;
560 agent.negative_reviews = u32::MAX / 2 + 1;
561 agent.average_rating = Some(3.5);
562
563 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 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 assert!(avg_time < 1000.0,
586 "Validation took {} microseconds on average, should be < 1000", avg_time);
587 }
588}