//! Input validation tests. //! //! Tests for confidence bounds, vote weight validation, subject/predicate //! size limits, and timestamp validation. use super::*; use crate::error::IngestError; /// Test: Assertions with confidence > 1.0 are rejected. #[tokio::test] async fn test_rejects_high_confidence() { let dir = tempdir().expect("Failed to create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); let mut csprng = OsRng; let signing_key = SigningKey::generate(&mut csprng); let verifying_key = signing_key.verifying_key(); let message = "Test:high"; let signature = signing_key.sign(message.as_bytes()); let assertion = Assertion { subject: "Test".to_string(), predicate: "high".to_string(), object: ObjectValue::Text("test".to_string()), parent_hash: None, source_hash: [0u8; 32], source_class: SourceClass::Expert, visual_hash: None, epoch: None, source_metadata: None, lifecycle: LifecycleStage::Proposed, signatures: vec![SignatureEntry { version: 1, agent_id: verifying_key.to_bytes(), signature: signature.to_bytes(), timestamp: 1000, }], confidence: 1.5, // Invalid: > 1.0 timestamp: 1000, hlc_timestamp: HlcTimestamp::default(), vector: None, }; let mut journal = Journal::open(&wal_dir).expect("Failed to open journal"); let store = HybridStore::open(&db_dir).expect("Failed to open store"); journal.append(serialize_assertion(&assertion).expect("ser")).expect("append"); let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(store); let mut worker = IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker"); let result = worker.step().await; assert!(result.is_err(), "Should reject confidence > 1.0"); let err = result.unwrap_err(); assert!( matches!(err, IngestError::InputValidation(_)), "Error should be InputValidation, got: {:?}", err ); assert!(err.to_string().contains("confidence")); } /// Test: Assertions with negative confidence are rejected. #[tokio::test] async fn test_rejects_negative_confidence() { let dir = tempdir().expect("Failed to create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); let mut csprng = OsRng; let signing_key = SigningKey::generate(&mut csprng); let verifying_key = signing_key.verifying_key(); let message = "Test:negative"; let signature = signing_key.sign(message.as_bytes()); let assertion = Assertion { subject: "Test".to_string(), predicate: "negative".to_string(), object: ObjectValue::Text("test".to_string()), parent_hash: None, source_hash: [0u8; 32], source_class: SourceClass::Expert, visual_hash: None, epoch: None, source_metadata: None, lifecycle: LifecycleStage::Proposed, signatures: vec![SignatureEntry { version: 1, agent_id: verifying_key.to_bytes(), signature: signature.to_bytes(), timestamp: 1000, }], confidence: -0.5, // Invalid: < 0.0 timestamp: 1000, hlc_timestamp: HlcTimestamp::default(), vector: None, }; let mut journal = Journal::open(&wal_dir).expect("Failed to open journal"); let store = HybridStore::open(&db_dir).expect("Failed to open store"); journal.append(serialize_assertion(&assertion).expect("ser")).expect("append"); let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(store); let mut worker = IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker"); let result = worker.step().await; assert!(result.is_err(), "Should reject confidence < 0.0"); let err = result.unwrap_err(); assert!(matches!(err, IngestError::InputValidation(_))); } /// Test: Votes with out-of-range weight are rejected. #[tokio::test] async fn test_rejects_invalid_vote_weight() { let dir = tempdir().expect("Failed to create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); // Create vote with weight > 1.0 let vote = Vote { assertion_hash: [1u8; 32], agent_id: [2u8; 32], weight: 1.5, // Invalid: > 1.0 signature: [3u8; 64], timestamp: 1000, source_url: None, observed_context: None, }; let mut journal = Journal::open(&wal_dir).expect("Failed to open journal"); let store = HybridStore::open(&db_dir).expect("Failed to open store"); journal.append(serialize_vote(&vote).expect("ser")).expect("append"); let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(store); let mut worker = IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker"); let result = worker.step().await; assert!(result.is_err(), "Should reject vote weight > 1.0"); let err = result.unwrap_err(); assert!( matches!(err, IngestError::InputValidation(_)), "Error should be InputValidation, got: {:?}", err ); assert!(err.to_string().contains("weight")); } /// Test: Votes with negative weight are rejected. #[tokio::test] async fn test_rejects_negative_vote_weight() { let dir = tempdir().expect("Failed to create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); let vote = Vote { assertion_hash: [1u8; 32], agent_id: [2u8; 32], weight: -0.5, // Invalid: < 0.0 signature: [3u8; 64], timestamp: 1000, source_url: None, observed_context: None, }; let mut journal = Journal::open(&wal_dir).expect("Failed to open journal"); let store = HybridStore::open(&db_dir).expect("Failed to open store"); journal.append(serialize_vote(&vote).expect("ser")).expect("append"); let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(store); let mut worker = IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker"); let result = worker.step().await; assert!(result.is_err(), "Should reject vote weight < 0.0"); let err = result.unwrap_err(); assert!(matches!(err, IngestError::InputValidation(_))); } /// Test: Assertions with oversized subject are rejected. #[tokio::test] async fn test_rejects_oversized_subject() { let dir = tempdir().expect("Failed to create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); // Create a subject that exceeds MAX_SUBJECT_LEN (1024 bytes) let oversized_subject = "x".repeat(1025); let mut csprng = OsRng; let signing_key = SigningKey::generate(&mut csprng); let verifying_key = signing_key.verifying_key(); let message = format!("{}:pred", oversized_subject); let signature = signing_key.sign(message.as_bytes()); let assertion = Assertion { subject: oversized_subject, predicate: "pred".to_string(), object: ObjectValue::Text("test".to_string()), parent_hash: None, source_hash: [0u8; 32], source_class: SourceClass::Expert, visual_hash: None, epoch: None, source_metadata: None, lifecycle: LifecycleStage::Proposed, signatures: vec![SignatureEntry { version: 1, agent_id: verifying_key.to_bytes(), signature: signature.to_bytes(), timestamp: 1000, }], confidence: 0.9, timestamp: 1000, hlc_timestamp: HlcTimestamp::default(), vector: None, }; let mut journal = Journal::open(&wal_dir).expect("Failed to open journal"); let store = HybridStore::open(&db_dir).expect("Failed to open store"); journal.append(serialize_assertion(&assertion).expect("ser")).expect("append"); let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(store); let mut worker = IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker"); let result = worker.step().await; assert!(result.is_err(), "Should reject oversized subject"); let err = result.unwrap_err(); assert!( matches!(err, IngestError::InputValidation(_)), "Error should be InputValidation, got: {:?}", err ); assert!(err.to_string().contains("subject")); } /// Test: Assertions with oversized predicate are rejected. #[tokio::test] async fn test_rejects_oversized_predicate() { let dir = tempdir().expect("Failed to create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); // Create a predicate that exceeds MAX_PREDICATE_LEN (256 bytes) let oversized_predicate = "y".repeat(257); let mut csprng = OsRng; let signing_key = SigningKey::generate(&mut csprng); let verifying_key = signing_key.verifying_key(); let message = format!("subj:{}", oversized_predicate); let signature = signing_key.sign(message.as_bytes()); let assertion = Assertion { subject: "subj".to_string(), predicate: oversized_predicate, object: ObjectValue::Text("test".to_string()), parent_hash: None, source_hash: [0u8; 32], source_class: SourceClass::Expert, visual_hash: None, epoch: None, source_metadata: None, lifecycle: LifecycleStage::Proposed, signatures: vec![SignatureEntry { version: 1, agent_id: verifying_key.to_bytes(), signature: signature.to_bytes(), timestamp: 1000, }], confidence: 0.9, timestamp: 1000, hlc_timestamp: HlcTimestamp::default(), vector: None, }; let mut journal = Journal::open(&wal_dir).expect("Failed to open journal"); let store = HybridStore::open(&db_dir).expect("Failed to open store"); journal.append(serialize_assertion(&assertion).expect("ser")).expect("append"); let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(store); let mut worker = IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker"); let result = worker.step().await; assert!(result.is_err(), "Should reject oversized predicate"); let err = result.unwrap_err(); assert!( matches!(err, IngestError::InputValidation(_)), "Error should be InputValidation, got: {:?}", err ); assert!(err.to_string().contains("predicate")); } /// Test: Assertions with exactly MAX_SUBJECT_LEN bytes are accepted. /// This boundary test verifies that the validation uses `>` not `>=`, /// catching off-by-one errors in the comparison. #[tokio::test] async fn test_accepts_exact_max_subject_length() { let dir = tempdir().expect("Failed to create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); // Create a subject with exactly MAX_SUBJECT_LEN (1024 bytes) let exact_max_subject = "x".repeat(1024); let mut csprng = OsRng; let signing_key = SigningKey::generate(&mut csprng); let verifying_key = signing_key.verifying_key(); let message = format!("{}:pred", exact_max_subject); let signature = signing_key.sign(message.as_bytes()); let assertion = Assertion { subject: exact_max_subject, predicate: "pred".to_string(), object: ObjectValue::Text("test".to_string()), parent_hash: None, source_hash: [0u8; 32], source_class: SourceClass::Expert, visual_hash: None, epoch: None, source_metadata: None, lifecycle: LifecycleStage::Proposed, signatures: vec![SignatureEntry { version: 1, agent_id: verifying_key.to_bytes(), signature: signature.to_bytes(), timestamp: 1000, }], confidence: 0.9, timestamp: 1000, hlc_timestamp: HlcTimestamp::default(), vector: None, }; let mut journal = Journal::open(&wal_dir).expect("Failed to open journal"); let store = HybridStore::open(&db_dir).expect("Failed to open store"); journal.append(serialize_assertion(&assertion).expect("ser")).expect("append"); let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(store); let mut worker = IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker"); let result = worker.step().await; assert!( result.is_ok(), "Should accept subject with exactly 1024 bytes, got error: {:?}", result ); } /// Test: Assertions with exactly MAX_PREDICATE_LEN bytes are accepted. /// This boundary test verifies that the validation uses `>` not `>=`, /// catching off-by-one errors in the comparison. #[tokio::test] async fn test_accepts_exact_max_predicate_length() { let dir = tempdir().expect("Failed to create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); // Create a predicate with exactly MAX_PREDICATE_LEN (256 bytes) let exact_max_predicate = "y".repeat(256); let mut csprng = OsRng; let signing_key = SigningKey::generate(&mut csprng); let verifying_key = signing_key.verifying_key(); let message = format!("subj:{}", exact_max_predicate); let signature = signing_key.sign(message.as_bytes()); let assertion = Assertion { subject: "subj".to_string(), predicate: exact_max_predicate, object: ObjectValue::Text("test".to_string()), parent_hash: None, source_hash: [0u8; 32], source_class: SourceClass::Expert, visual_hash: None, epoch: None, source_metadata: None, lifecycle: LifecycleStage::Proposed, signatures: vec![SignatureEntry { version: 1, agent_id: verifying_key.to_bytes(), signature: signature.to_bytes(), timestamp: 1000, }], confidence: 0.9, timestamp: 1000, hlc_timestamp: HlcTimestamp::default(), vector: None, }; let mut journal = Journal::open(&wal_dir).expect("Failed to open journal"); let store = HybridStore::open(&db_dir).expect("Failed to open store"); journal.append(serialize_assertion(&assertion).expect("ser")).expect("append"); let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(store); let mut worker = IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker"); let result = worker.step().await; assert!( result.is_ok(), "Should accept predicate with exactly 256 bytes, got error: {:?}", result ); } /// Test: Assertions with NaN confidence are rejected. #[tokio::test] async fn test_rejects_nan_confidence() { let dir = tempdir().expect("Failed to create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); let mut csprng = OsRng; let signing_key = SigningKey::generate(&mut csprng); let verifying_key = signing_key.verifying_key(); let message = "Test:nan"; let signature = signing_key.sign(message.as_bytes()); let assertion = Assertion { subject: "Test".to_string(), predicate: "nan".to_string(), object: ObjectValue::Text("test".to_string()), parent_hash: None, source_hash: [0u8; 32], source_class: SourceClass::Expert, visual_hash: None, epoch: None, source_metadata: None, lifecycle: LifecycleStage::Proposed, signatures: vec![SignatureEntry { version: 1, agent_id: verifying_key.to_bytes(), signature: signature.to_bytes(), timestamp: 1000, }], confidence: f32::NAN, timestamp: 1000, hlc_timestamp: HlcTimestamp::default(), vector: None, }; let mut journal = Journal::open(&wal_dir).expect("Failed to open journal"); let store = HybridStore::open(&db_dir).expect("Failed to open store"); journal.append(serialize_assertion(&assertion).expect("ser")).expect("append"); let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(store); let mut worker = IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker"); let result = worker.step().await; assert!(result.is_err(), "Should reject NaN confidence"); let err = result.unwrap_err(); assert!(matches!(err, IngestError::InputValidation(_))); }