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>
491 lines
17 KiB
Rust
491 lines
17 KiB
Rust
//! HTTP integration tests for CRUD operations.
|
|
//!
|
|
//! Coverage:
|
|
//! - POST /v1/assert - Assertion creation with signatures
|
|
//! - POST /v1/vote - Vote submission with weight validation
|
|
//! - Error responses (400 Bad Request)
|
|
//! - 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;
|
|
|
|
// ============================================================================
|
|
// POST /v1/assert - Create Assertion Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_assert_valid_creation() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let subject = "Test_Entity";
|
|
let predicate = "test_property";
|
|
let message = format!("{}:{}", subject, predicate);
|
|
let (agent_id, signature) = common::sign_message(&message);
|
|
|
|
let assertion = json!({
|
|
"subject": subject,
|
|
"predicate": predicate,
|
|
"object": {"type": "Number", "value": 42.0},
|
|
"confidence": 0.95,
|
|
"signatures": [{
|
|
"agent_id": hex::encode(agent_id),
|
|
"signature": hex::encode(signature),
|
|
"timestamp": 1234567890
|
|
}],
|
|
"source_hash": hex::encode([0u8; 32])
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&assertion).expect("JSON serialization")))
|
|
.expect("Failed to build request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request failed");
|
|
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
|
|
let body =
|
|
axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Failed to read body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("Failed to parse JSON");
|
|
|
|
assert_eq!(json["status"], "created");
|
|
assert!(json["hash"].is_string());
|
|
assert_eq!(json["hash"].as_str().expect("hash").len(), 64); // BLAKE3 = 32 bytes = 64 hex
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_assert_response_includes_hash() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = common::sign_message("Test:test");
|
|
|
|
let assertion = json!({
|
|
"subject": "Test",
|
|
"predicate": "test",
|
|
"object": {"type": "Text", "value": "test_value"},
|
|
"confidence": 0.8,
|
|
"signatures": [{
|
|
"agent_id": hex::encode(agent_id),
|
|
"signature": hex::encode(signature),
|
|
"timestamp": 1000000000
|
|
}],
|
|
"source_hash": hex::encode([1u8; 32])
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&assertion).expect("JSON")))
|
|
.expect("Request build");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
|
|
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 parse");
|
|
|
|
// Verify hash is present and correctly formatted
|
|
let hash = json["hash"].as_str().expect("hash should be present");
|
|
assert_eq!(hash.len(), 64);
|
|
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_assert_missing_signature_fails() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let assertion = json!({
|
|
"subject": "Test",
|
|
"predicate": "test",
|
|
"object": {"type": "Number", "value": 1.0},
|
|
"confidence": 0.9,
|
|
"signatures": [], // Empty signatures
|
|
"source_hash": hex::encode([0u8; 32])
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&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 message")
|
|
.contains("At least one signature is required"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_assert_invalid_confidence_fails() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = common::sign_message("test");
|
|
|
|
let assertion = json!({
|
|
"subject": "Test",
|
|
"predicate": "test",
|
|
"object": {"type": "Number", "value": 1.0},
|
|
"confidence": 2.0, // Invalid: > 1.0
|
|
"signatures": [{
|
|
"agent_id": hex::encode(agent_id),
|
|
"signature": hex::encode(signature),
|
|
"timestamp": 1000
|
|
}],
|
|
"source_hash": hex::encode([0u8; 32])
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&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");
|
|
|
|
// Verify error response structure matches ErrorResponse DTO
|
|
assert!(json.get("error").is_some());
|
|
assert!(json.get("code").is_some());
|
|
assert!(json["error"].as_str().expect("error message").contains("Confidence"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hex_validation_wrong_length() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = common::sign_message("test");
|
|
|
|
let assertion = json!({
|
|
"subject": "Test",
|
|
"predicate": "test",
|
|
"object": {"type": "Number", "value": 1.0},
|
|
"confidence": 0.9,
|
|
"signatures": [{
|
|
"agent_id": hex::encode(agent_id),
|
|
"signature": hex::encode(signature),
|
|
"timestamp": 1000
|
|
}],
|
|
"source_hash": "abc" // Too short - should be 64 hex chars
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&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("Expected 64 hex characters"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_invalid_json_payload() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Invalid JSON payload
|
|
let invalid_json = b"{invalid json}";
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/assert")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(invalid_json.to_vec()))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
}
|
|
|
|
// ============================================================================
|
|
// POST /v1/vote - Submit Vote Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_vote_valid_submission() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = common::sign_message("vote_message");
|
|
|
|
let vote = json!({
|
|
"assertion_hash": hex::encode([0u8; 32]),
|
|
"agent_id": hex::encode(agent_id),
|
|
"weight": 0.75,
|
|
"signature": hex::encode(signature)
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/vote")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&vote).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
|
|
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["status"], "created");
|
|
assert!(json["hash"].is_string());
|
|
assert_eq!(json["hash"].as_str().expect("hash").len(), 64);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_vote_response_structure() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = common::sign_message("test");
|
|
|
|
let vote = json!({
|
|
"assertion_hash": hex::encode([1u8; 32]),
|
|
"agent_id": hex::encode(agent_id),
|
|
"weight": 1.0,
|
|
"signature": hex::encode(signature)
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/vote")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&vote).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("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");
|
|
|
|
// Verify response structure matches CreateResponse
|
|
assert!(json.get("hash").is_some());
|
|
assert!(json.get("status").is_some());
|
|
assert_eq!(json.as_object().expect("object").len(), 2);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_vote_invalid_weight_fails() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = common::sign_message("test");
|
|
|
|
// Test weight > 1.0
|
|
let vote = json!({
|
|
"assertion_hash": hex::encode([0u8; 32]),
|
|
"agent_id": hex::encode(agent_id),
|
|
"weight": 1.5,
|
|
"signature": hex::encode(signature)
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/vote")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&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"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_vote_with_provenance_fields() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = common::sign_message("vote_with_provenance");
|
|
|
|
let vote = json!({
|
|
"assertion_hash": hex::encode([0u8; 32]),
|
|
"agent_id": hex::encode(agent_id),
|
|
"weight": 0.9,
|
|
"signature": hex::encode(signature),
|
|
"source_url": "https://example.com/article",
|
|
"observed_context": "The study found that coffee consumption..."
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/vote")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&vote).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
|
|
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["status"], "created");
|
|
assert!(json["hash"].is_string());
|
|
assert_eq!(json["hash"].as_str().expect("hash").len(), 64);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_vote_without_provenance_fields_backward_compat() {
|
|
// Ensure votes without provenance fields still work (backward compatibility)
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = common::sign_message("vote_no_provenance");
|
|
|
|
let vote = json!({
|
|
"assertion_hash": hex::encode([0u8; 32]),
|
|
"agent_id": hex::encode(agent_id),
|
|
"weight": 0.8,
|
|
"signature": hex::encode(signature)
|
|
// No source_url or observed_context
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/vote")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&vote).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
|
|
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["status"], "created");
|
|
assert!(json["hash"].is_string());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_vote_source_url_too_long_fails() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = common::sign_message("test");
|
|
|
|
// Test source_url > 2048 characters
|
|
let long_url = format!("https://example.com/{}", "x".repeat(2049));
|
|
let vote = json!({
|
|
"assertion_hash": hex::encode([0u8; 32]),
|
|
"agent_id": hex::encode(agent_id),
|
|
"weight": 0.8,
|
|
"signature": hex::encode(signature),
|
|
"source_url": long_url
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/vote")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&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("2048"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_vote_source_url_empty_fails() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = common::sign_message("test");
|
|
|
|
// Test empty source_url
|
|
let vote = json!({
|
|
"assertion_hash": hex::encode([0u8; 32]),
|
|
"agent_id": hex::encode(agent_id),
|
|
"weight": 0.8,
|
|
"signature": hex::encode(signature),
|
|
"source_url": ""
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/vote")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&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("empty"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_vote_observed_context_too_large_fails() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = common::sign_message("test");
|
|
|
|
// Test observed_context > 64KB
|
|
let large_context = "c".repeat(65537);
|
|
let vote = json!({
|
|
"assertion_hash": hex::encode([0u8; 32]),
|
|
"agent_id": hex::encode(agent_id),
|
|
"weight": 0.8,
|
|
"signature": hex::encode(signature),
|
|
"observed_context": large_context
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/vote")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&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("64KB"));
|
|
}
|