stemedb/crates/stemedb-api/tests/http_validation.rs
jordan 55349845d0 refactor: Split all files to enforce 500-line max
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>
2026-02-02 01:13:45 -07:00

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);
}