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

354 lines
12 KiB
Rust

//! HTTP integration tests for advanced features and edge cases.
//!
//! Coverage:
//! - GET /v1/health - Health check endpoint
//! - POST /v1/admin/decay-trust-ranks - Admin decay operations
//! - Rate limiting via QuotaStore middleware
//! - 404 Not Found responses
//! - Edge cases and error scenarios
#![allow(clippy::expect_used)]
mod common;
use axum::{
body::Body,
http::{Request, StatusCode},
};
use serde_json::json;
use std::sync::Arc;
use tower::ServiceExt;
use stemedb_api::{create_router, create_router_with_meter, AppState};
use stemedb_storage::{GenericEscalationStore, GenericQuotaStore, QuotaStore, SledStore};
use stemedb_wal::Journal;
// ============================================================================
// Health and System Tests
// ============================================================================
#[tokio::test]
async fn test_health_endpoint() {
let env = common::create_test_env().await;
let app = create_router(env.state);
let request =
Request::builder().uri("/v1/health").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["status"], "healthy");
assert!(json.get("version").is_some());
}
#[tokio::test]
async fn test_not_found_endpoint() {
let env = common::create_test_env().await;
let app = create_router(env.state);
let request = Request::builder()
.uri("/v1/nonexistent")
.method("GET")
.body(Body::empty())
.expect("Request");
let response = app.oneshot(request).await.expect("Request");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
// ============================================================================
// POST /v1/admin/decay-trust-ranks - Admin Decay Tests
// ============================================================================
#[tokio::test]
async fn test_decay_trust_ranks_empty_store() {
let env = common::create_test_env().await;
let app = create_router(env.state);
let request = Request::builder()
.uri("/v1/admin/decay-trust-ranks")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&json!({})).expect("JSON")))
.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["decayed_count"], 0);
assert!(json["timestamp_used"].is_number());
assert_eq!(json["status"], "Decay operation completed");
}
#[tokio::test]
async fn test_decay_trust_ranks_with_custom_params() {
let env = common::create_test_env().await;
let app = create_router(env.state);
let request_body = json!({
"now": 1704067200,
"half_life_seconds": 604800
});
let request = Request::builder()
.uri("/v1/admin/decay-trust-ranks")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&request_body).expect("JSON")))
.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["timestamp_used"], 1704067200);
assert_eq!(json["half_life_used"], 604800);
}
#[tokio::test]
async fn test_decay_trust_ranks_response_structure() {
let env = common::create_test_env().await;
let app = create_router(env.state);
let request = Request::builder()
.uri("/v1/admin/decay-trust-ranks")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&json!({})).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 all expected fields are present
assert!(json.get("decayed_count").is_some());
assert!(json.get("timestamp_used").is_some());
assert!(json.get("half_life_used").is_some());
assert!(json.get("status").is_some());
assert_eq!(json.as_object().expect("object").len(), 4);
}
#[tokio::test]
async fn test_decay_trust_ranks_actually_decays() {
use stemedb_storage::{GenericTrustRankStore, TrustRank, TrustRankStore};
let env = common::create_test_env().await;
// Pre-populate TrustRank with an old timestamp
let agent_id = [42u8; 32];
let mut trust_rank = TrustRank::new(agent_id, 1000);
trust_rank.score = 0.8;
let trust_store = GenericTrustRankStore::new((*env.state.store).clone());
trust_store.put_trust_rank(&trust_rank).await.expect("put trust rank");
let app = create_router(env.state.clone());
// Decay with a timestamp far in the future (30+ days)
let thirty_one_days = 31 * 24 * 60 * 60;
let request_body = json!({
"now": 1000 + thirty_one_days,
"half_life_seconds": 30 * 24 * 60 * 60 // 30 days
});
let request = Request::builder()
.uri("/v1/admin/decay-trust-ranks")
.method("POST")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&request_body).expect("JSON")))
.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");
// Should have decayed exactly 1 TrustRank
assert_eq!(json["decayed_count"], 1);
// Verify the score was actually decayed (0.8 * 0.5^(31/30) ≈ 0.37)
let decayed_trust = trust_store.get_trust_rank(&agent_id).await.expect("get trust rank");
assert!(
decayed_trust.score < 0.5,
"Score should be significantly decayed, got {}",
decayed_trust.score
);
}
// ============================================================================
// Rate Limiting Tests (QuotaStore Middleware)
// ============================================================================
#[tokio::test]
async fn test_quota_consumption_with_meter() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let wal_dir = temp_dir.path().join("wal");
let db_dir = temp_dir.path().join("db");
std::fs::create_dir_all(&wal_dir).expect("wal dir");
std::fs::create_dir_all(&db_dir).expect("db dir");
let journal = Journal::open(&wal_dir).expect("journal");
let store = Arc::new(SledStore::open(&db_dir).expect("store"));
// Create AppState manually to share quota_store
let quota_store = Arc::new(GenericQuotaStore::new(store.clone()));
let escalation_store = Arc::new(GenericEscalationStore::new(store.clone()));
let state = AppState {
journal: Arc::new(tokio::sync::Mutex::new(journal)),
store: store.clone(),
quota_store: quota_store.clone(),
escalation_store,
};
let app = create_router_with_meter(state);
let (agent_id, signature) = common::sign_message("test");
let agent_id_hex = hex::encode(agent_id);
// Set a low quota limit for testing
quota_store.set_quota_limit(&agent_id, 50).await.expect("set quota");
let assertion = json!({
"subject": "QuotaTest",
"predicate": "test",
"object": {"type": "Number", "value": 1.0},
"confidence": 0.9,
"signatures": [{
"agent_id": agent_id_hex,
"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")
.header("x-agent-id", &agent_id_hex)
.body(Body::from(serde_json::to_vec(&assertion).expect("JSON")))
.expect("Request");
let response = app.oneshot(request).await.expect("Request");
// Should succeed and include quota headers
assert_eq!(response.status(), StatusCode::CREATED);
let headers = response.headers();
assert!(headers.get("x-quota-remaining").is_some());
assert!(headers.get("x-quota-limit").is_some());
assert!(headers.get("x-quota-reset").is_some());
}
#[tokio::test]
async fn test_quota_exceeded_response() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let wal_dir = temp_dir.path().join("wal");
let db_dir = temp_dir.path().join("db");
std::fs::create_dir_all(&wal_dir).expect("wal dir");
std::fs::create_dir_all(&db_dir).expect("db dir");
let journal = Journal::open(&wal_dir).expect("journal");
let store = Arc::new(SledStore::open(&db_dir).expect("store"));
let quota_store = Arc::new(GenericQuotaStore::new(store.clone()));
let escalation_store = Arc::new(GenericEscalationStore::new(store.clone()));
let state = AppState {
journal: Arc::new(tokio::sync::Mutex::new(journal)),
store: store.clone(),
quota_store: quota_store.clone(),
escalation_store,
};
let app = create_router_with_meter(state);
let (agent_id, _) = common::sign_message("test");
let agent_id_hex = hex::encode(agent_id);
// Set quota to 0 to immediately trigger quota exceeded
quota_store.set_quota_limit(&agent_id, 0).await.expect("set quota");
let request = Request::builder()
.uri("/v1/query?subject=Test")
.method("GET")
.header("x-agent-id", &agent_id_hex)
.body(Body::empty())
.expect("Request");
let response = app.oneshot(request).await.expect("Request");
// Should return 429 Too Many Requests
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
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.get("error").is_some());
assert_eq!(json["code"], "QUOTA_EXCEEDED");
assert!(json.get("remaining").is_some());
assert!(json.get("limit").is_some());
assert!(json.get("reset_at").is_some());
}
#[tokio::test]
async fn test_quota_headers_format() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let wal_dir = temp_dir.path().join("wal");
let db_dir = temp_dir.path().join("db");
std::fs::create_dir_all(&wal_dir).expect("wal dir");
std::fs::create_dir_all(&db_dir).expect("db dir");
let journal = Journal::open(&wal_dir).expect("journal");
let store = Arc::new(SledStore::open(&db_dir).expect("store"));
let quota_store = Arc::new(GenericQuotaStore::new(store.clone()));
let escalation_store = Arc::new(GenericEscalationStore::new(store.clone()));
let state = AppState {
journal: Arc::new(tokio::sync::Mutex::new(journal)),
store: store.clone(),
quota_store: quota_store.clone(),
escalation_store,
};
let app = create_router_with_meter(state);
let (agent_id, _) = common::sign_message("test");
let agent_id_hex = hex::encode(agent_id);
quota_store.set_quota_limit(&agent_id, 100).await.expect("set quota");
let request = Request::builder()
.uri("/v1/query?subject=Test")
.method("GET")
.header("x-agent-id", &agent_id_hex)
.body(Body::empty())
.expect("Request");
let response = app.oneshot(request).await.expect("Request");
assert_eq!(response.status(), StatusCode::OK);
let headers = response.headers();
// Verify quota headers are present and parseable
let remaining = headers.get("x-quota-remaining").expect("remaining header");
let limit = headers.get("x-quota-limit").expect("limit header");
let reset = headers.get("x-quota-reset").expect("reset header");
assert!(remaining.to_str().expect("remaining str").parse::<u64>().is_ok());
assert!(limit.to_str().expect("limit str").parse::<u64>().is_ok());
assert!(reset.to_str().expect("reset str").parse::<u64>().is_ok());
}