Break monolith source files into focused modules: - stemedb-core/types.rs → types/ directory (assertion, source, gold_standard, etc.) - stemedb-storage: audit_store, quota_store, trust_rank_store, vector_index, vote_store → module directories - stemedb-ingest/worker.rs → worker/ with separate test modules - stemedb-query: engine, materializer, query → module directories - stemedb-lens: epoch_aware, skeptic → module directories - stemedb-sim/lib.rs → agent, arenas/, helpers, runner, strategy, types - stemedb-api/tests: integration_tests → http_basic, http_validation, http_epoch, http_pipeline - stemedb-api/tests: e2e_flow_test → e2e_full_pipeline, e2e_lens_resolution - stemedb-query/tests: e2e_pipeline → e2e_pipeline + e2e_decay Also adds new features: gold standard verification, escalation handlers, admin endpoints, concept hierarchy spec, arena roadmap, and Go SDK. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
358 lines
13 KiB
Rust
358 lines
13 KiB
Rust
//! HTTP integration tests for DTO validation.
|
|
//!
|
|
//! Coverage:
|
|
//! - Confidence validation (0.0 to 1.0 range)
|
|
//! - Weight validation (0.0 to 1.0 range)
|
|
//! - Hex string validation (length and character validation)
|
|
//! - Conflict score validation (0.0 to 1.0 range)
|
|
//! - Query parameter validation
|
|
//! - Error response structure validation
|
|
|
|
#![allow(clippy::expect_used)]
|
|
|
|
mod common;
|
|
|
|
use axum::{
|
|
body::Body,
|
|
http::{Request, StatusCode},
|
|
};
|
|
use serde_json::json;
|
|
use tower::ServiceExt;
|
|
|
|
use stemedb_api::create_router;
|
|
|
|
// ============================================================================
|
|
// Confidence Validation Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_dto_confidence_validation() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Test confidence > 1.0
|
|
let invalid_assertion = json!({
|
|
"subject": "Tesla",
|
|
"predicate": "has_revenue",
|
|
"object": {"type": "Number", "value": 100.0},
|
|
"confidence": 1.5,
|
|
"signatures": [{
|
|
"agent_id": "a".repeat(64),
|
|
"signature": "b".repeat(128),
|
|
"timestamp": 1234567890
|
|
}],
|
|
"source_hash": "c".repeat(64)
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&invalid_assertion).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert!(json["error"]
|
|
.as_str()
|
|
.expect("error")
|
|
.contains("Confidence must be between 0.0 and 1.0"));
|
|
|
|
// Test confidence < 0.0
|
|
let invalid_assertion = json!({
|
|
"subject": "Tesla",
|
|
"predicate": "has_revenue",
|
|
"object": {"type": "Number", "value": 100.0},
|
|
"confidence": -0.5,
|
|
"signatures": [{
|
|
"agent_id": "a".repeat(64),
|
|
"signature": "b".repeat(128),
|
|
"timestamp": 1234567890
|
|
}],
|
|
"source_hash": "c".repeat(64)
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&invalid_assertion).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert!(json["error"]
|
|
.as_str()
|
|
.expect("error")
|
|
.contains("Confidence must be between 0.0 and 1.0"));
|
|
}
|
|
|
|
// ============================================================================
|
|
// Weight Validation Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_dto_weight_validation() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Test weight > 1.0
|
|
let invalid_vote = json!({
|
|
"assertion_hash": "a".repeat(64),
|
|
"agent_id": "b".repeat(64),
|
|
"weight": 1.5,
|
|
"signature": "c".repeat(128)
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/vote")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&invalid_vote).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert!(json["error"].as_str().expect("error").contains("Weight must be between 0.0 and 1.0"));
|
|
|
|
// Test weight < 0.0
|
|
let invalid_vote = json!({
|
|
"assertion_hash": "a".repeat(64),
|
|
"agent_id": "b".repeat(64),
|
|
"weight": -0.5,
|
|
"signature": "c".repeat(128)
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/vote")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&invalid_vote).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert!(json["error"].as_str().expect("error").contains("Weight must be between 0.0 and 1.0"));
|
|
}
|
|
|
|
// ============================================================================
|
|
// Hex Validation Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_hex_decode_wrong_length() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Test assertion with wrong-length source_hash
|
|
let invalid_assertion = json!({
|
|
"subject": "Tesla",
|
|
"predicate": "has_revenue",
|
|
"object": {"type": "Number", "value": 100.0},
|
|
"confidence": 0.9,
|
|
"signatures": [{
|
|
"agent_id": "a".repeat(64),
|
|
"signature": "b".repeat(128),
|
|
"timestamp": 1234567890
|
|
}],
|
|
"source_hash": "abc" // Too short
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&invalid_assertion).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
let error_msg = json["error"].as_str().expect("error");
|
|
assert!(error_msg.contains("Expected 64 hex characters"));
|
|
assert!(error_msg.contains("got 3"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hex_decode_valid() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Test assertion with valid hex lengths
|
|
// Note: This will succeed in parsing/validation but may fail in the ingest worker
|
|
// We're primarily testing that hex validation accepts correct-length strings
|
|
let valid_assertion = json!({
|
|
"subject": "Tesla",
|
|
"predicate": "has_revenue",
|
|
"object": {"type": "Number", "value": 100.0},
|
|
"confidence": 0.9,
|
|
"signatures": [{
|
|
"agent_id": "a".repeat(64),
|
|
"signature": "b".repeat(128),
|
|
"timestamp": 1234567890
|
|
}],
|
|
"source_hash": "c".repeat(64)
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&valid_assertion).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
|
|
// Accept either 201 (success) or 500 (ingest worker not running in test)
|
|
// We're primarily testing that the hex validation doesn't reject valid lengths
|
|
assert!(
|
|
response.status() == StatusCode::CREATED
|
|
|| response.status() == StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Expected 201 or 500, got {}",
|
|
response.status()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_dto_to_assertion_invalid_hex() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Test assertion with invalid hex characters
|
|
let invalid_assertion = json!({
|
|
"subject": "Tesla",
|
|
"predicate": "has_revenue",
|
|
"object": {"type": "Number", "value": 100.0},
|
|
"confidence": 0.9,
|
|
"signatures": [{
|
|
"agent_id": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", // 'z' is not a valid hex character (64 z's)
|
|
"signature": "b".repeat(128),
|
|
"timestamp": 1234567890
|
|
}],
|
|
"source_hash": "c".repeat(64)
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&invalid_assertion).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
let error = json["error"].as_str().expect("error");
|
|
// The error could be "Invalid hex encoding" or similar - just check it's a bad request error
|
|
assert!(!error.is_empty(), "Error message should not be empty");
|
|
}
|
|
|
|
// ============================================================================
|
|
// Query Parameter Validation Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_query_empty_results() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/query?subject=nonexistent")
|
|
.method("GET")
|
|
.body(Body::empty())
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
|
|
assert_eq!(json["assertions"], json!([]));
|
|
assert_eq!(json["total_count"], 0);
|
|
assert_eq!(json["has_more"], false);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_query_conflict_score_validation() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Test: min_conflict_score > 1.0 (invalid)
|
|
let request = Request::builder()
|
|
.uri("/v1/query?subject=Test&predicate=test&min_conflict_score=1.5")
|
|
.method("GET")
|
|
.body(Body::empty())
|
|
.expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert!(json["error"]
|
|
.as_str()
|
|
.expect("error")
|
|
.contains("min_conflict_score must be between 0.0 and 1.0"));
|
|
|
|
// Test: min_conflict_score < 0.0 (invalid)
|
|
let request = Request::builder()
|
|
.uri("/v1/query?subject=Test&predicate=test&min_conflict_score=-0.1")
|
|
.method("GET")
|
|
.body(Body::empty())
|
|
.expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert!(json["error"]
|
|
.as_str()
|
|
.expect("error")
|
|
.contains("min_conflict_score must be between 0.0 and 1.0"));
|
|
|
|
// Test: max_conflict_score > 1.0 (invalid)
|
|
let request = Request::builder()
|
|
.uri("/v1/query?subject=Test&predicate=test&max_conflict_score=2.0")
|
|
.method("GET")
|
|
.body(Body::empty())
|
|
.expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert!(json["error"]
|
|
.as_str()
|
|
.expect("error")
|
|
.contains("max_conflict_score must be between 0.0 and 1.0"));
|
|
|
|
// Test: valid conflict scores (should succeed even if no results)
|
|
let request = Request::builder()
|
|
.uri("/v1/query?subject=Test&predicate=test&min_conflict_score=0.3&max_conflict_score=0.7")
|
|
.method("GET")
|
|
.body(Body::empty())
|
|
.expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
}
|