stemedb/crates/stemedb-api/tests/http_crud.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

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