This commit includes comprehensive work on Phase 6 features: ## Admission Control (Phase 6 admission middleware) - AdmissionStore implementation backed by TrustRankStore - PoW verification with tier-based difficulty computation - Trust tier progression (Newcomer → Established → Trusted → Authority) - API integration with admission status endpoints ## HLC Recency Lens (Phase 6C) - HlcRecencyLens for distributed system ordering - Hybrid logical clock integration with causality preservation ## Cluster Coordination (Phase 6C) - Multi-node cluster tests (availability, partition tolerance) - CRDT convergence tests for anti-entropy sync - Gateway handler improvements ## Aphoria Code Linter (Phase 2A) - RFC/OWASP corpus builders with network fetching and caching - Concept hierarchy with auto-alias creation on conflict detection - Multiple security extractors (TLS, JWT, CORS, secrets, rate limiting) ## Code Organization - Split large files into modules to comply with 500-line limit - Improved test organization with separate test modules - Fixed rkyv serialization for EigenTrustState (AgentScore struct) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
478 lines
16 KiB
Rust
478 lines
16 KiB
Rust
//! 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(_)));
|
|
}
|