stemedb/crates/stemedb-api/tests/http_advanced.rs
jordan 02ecac9a07 fix: merge upstream 10 commits, fix DashMap deadlock, deterministic sim ingestion
Merged 10 upstream commits (MemTable, read-your-writes tests, feed endpoint,
security hardening, signed assertions, source registry, dashboard enhancements)
and fixed all test failures across the full workspace (2656/2656 passing).

Key fixes:
- fix(cluster): DashMap deadlock in swim.rs suspect_node/fail_node/alive_node
  - DashMap::get_mut RefMut + iter() on same map = non-reentrant write lock deadlock
  - Fix: extract clone in scoped block to drop RefMut before calling update_node_gauges()
  - 6 previously-hanging SWIM tests now pass in <2s
- fix(sim): replace background-task+polling ingestion with synchronous process_pending()
  - smoke_high_volume_simulation was CPU-starved under 2656 parallel tests
  - Removed ingestor.start() + wait_until_ingested() pattern throughout sim
  - All arena functions now call ingestor.process_pending() directly (deterministic)
- fix(test): v2 signature helper used wrong hash (rkyv vs canonical compute_content_hash_v2)
- fix(test): quota test signed "test" but v1 requires "subject:predicate" format
- fix(test): http_validation now accepts 400 for valid-format-but-invalid-crypto hex
- fix(test): scale_adaptive micro tier assertions updated (auto_promote upstream change)
- config: add nextest.toml with slow-timeout for background-task-tests group

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 20:27:32 -07:00

339 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::{HybridStore, QuotaStore};
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 write_journal = Journal::open(&wal_dir).expect("write journal");
let read_journal = Journal::open(&wal_dir).expect("read journal");
let store = Arc::new(HybridStore::open(&db_dir).expect("store"));
let state = AppState::new(write_journal, read_journal, store.clone(), None);
let quota_store = state.quota_store.clone();
let app = create_router_with_meter(state);
// v1 signature: sign "subject:predicate"
let (agent_id, signature) = common::sign_message("QuotaTest: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 write_journal = Journal::open(&wal_dir).expect("write journal");
let read_journal = Journal::open(&wal_dir).expect("read journal");
let store = Arc::new(HybridStore::open(&db_dir).expect("store"));
let state = AppState::new(write_journal, read_journal, store.clone(), None);
let quota_store = state.quota_store.clone();
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 write_journal = Journal::open(&wal_dir).expect("write journal");
let read_journal = Journal::open(&wal_dir).expect("read journal");
let store = Arc::new(HybridStore::open(&db_dir).expect("store"));
let state = AppState::new(write_journal, read_journal, store.clone(), None);
let quota_store = state.quota_store.clone();
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());
}