- Add Layered() method to Go SDK for per-source-class consensus queries - Add LayeredQueryParams, LayeredResult, TierResolution types to Go SDK - Create conflict example demonstrating Skeptic and Layered endpoints - Update quickstart.md with sections 6 (conflict detection) and 7 (authority tiers) - Remove tracked Go binary and add data/ to .gitignore The new quickstart sections demonstrate Episteme's differentiating features: - Skeptic endpoint shows "Trust but Verify" conflict analysis - Layered endpoint shows per-tier resolution (Clinical vs Anecdotal) Note: Pre-existing large files flagged by pre-commit hook (technical debt from prior sessions) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
645 lines
22 KiB
Rust
645 lines
22 KiB
Rust
//! Comprehensive HTTP integration tests for the StemeDB API.
|
|
//!
|
|
//! These tests verify the full HTTP layer without background workers,
|
|
//! focusing on request validation, error handling, and response structure.
|
|
//!
|
|
//! Coverage:
|
|
//! - POST /v1/assert - Assertion creation
|
|
//! - POST /v1/vote - Vote submission
|
|
//! - GET /v1/query - Query with lens parameter
|
|
//! - Error responses (400, 500)
|
|
//! - Rate limiting via QuotaStore (when enabled)
|
|
|
|
#![allow(clippy::expect_used)]
|
|
|
|
use axum::{
|
|
body::Body,
|
|
http::{Request, StatusCode},
|
|
};
|
|
use ed25519_dalek::{Signer, SigningKey};
|
|
use rand::rngs::OsRng;
|
|
use serde_json::json;
|
|
use std::sync::Arc;
|
|
use tower::ServiceExt;
|
|
|
|
use stemedb_api::{create_router, create_router_with_meter, AppState};
|
|
use stemedb_storage::{GenericQuotaStore, QuotaStore, SledStore};
|
|
use stemedb_wal::Journal;
|
|
|
|
// ============================================================================
|
|
// Test Environment Setup
|
|
// ============================================================================
|
|
|
|
/// Test environment that keeps temp directories alive for the test duration.
|
|
struct TestEnvironment {
|
|
_temp_dir: tempfile::TempDir,
|
|
state: AppState,
|
|
}
|
|
|
|
/// Helper to create a test environment with temporary directories.
|
|
async fn create_test_env() -> TestEnvironment {
|
|
let temp_dir = tempfile::tempdir().expect("failed to create 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("failed to create wal dir");
|
|
std::fs::create_dir_all(&db_dir).expect("failed to create db dir");
|
|
|
|
let journal = Journal::open(&wal_dir).expect("failed to open journal");
|
|
let store = SledStore::open(&db_dir).expect("failed to open store");
|
|
|
|
let state = AppState::new(journal, store);
|
|
|
|
TestEnvironment { _temp_dir: temp_dir, state }
|
|
}
|
|
|
|
/// Helper to sign a message using Ed25519.
|
|
fn sign_message(message: &str) -> ([u8; 32], [u8; 64]) {
|
|
let mut csprng = OsRng;
|
|
let signing_key = SigningKey::generate(&mut csprng);
|
|
let verifying_key = signing_key.verifying_key();
|
|
|
|
let signature = signing_key.sign(message.as_bytes());
|
|
|
|
(verifying_key.to_bytes(), signature.to_bytes())
|
|
}
|
|
|
|
// ============================================================================
|
|
// POST /v1/assert - Create Assertion Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_assert_valid_creation() {
|
|
let env = 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) = 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 = create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = 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 = 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"));
|
|
}
|
|
|
|
// ============================================================================
|
|
// POST /v1/vote - Submit Vote Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_vote_valid_submission() {
|
|
let env = create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = 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 = create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = 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 = create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = 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"));
|
|
}
|
|
|
|
// ============================================================================
|
|
// GET /v1/query - Query Assertions Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_query_basic_with_subject_predicate() {
|
|
let env = create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/query?subject=Test_Entity&predicate=test_property")
|
|
.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");
|
|
|
|
// Verify response structure (empty since DB is empty)
|
|
assert!(json.get("assertions").is_some());
|
|
assert!(json.get("total_count").is_some());
|
|
assert!(json.get("has_more").is_some());
|
|
assert_eq!(json["assertions"], json!([]));
|
|
assert_eq!(json["total_count"], 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_query_with_lens_recency() {
|
|
let env = create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/query?subject=Test&predicate=prop&lens=Recency")
|
|
.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");
|
|
|
|
// When a lens is applied (even with empty results), conflict_score and
|
|
// resolution_confidence should be None (empty result set)
|
|
assert_eq!(json["assertions"], json!([]));
|
|
assert_eq!(json["total_count"], 0);
|
|
assert!(json.get("conflict_score").is_none() || json["conflict_score"].is_null());
|
|
assert!(json.get("resolution_confidence").is_none() || json["resolution_confidence"].is_null());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_query_lifecycle_filtering() {
|
|
let env = create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/query?subject=Test&lifecycle=Approved")
|
|
.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);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_query_with_limit() {
|
|
let env = create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/query?subject=Test&limit=10")
|
|
.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");
|
|
|
|
// Verify query executes successfully (limit parameter is accepted)
|
|
assert!(json.get("assertions").is_some());
|
|
}
|
|
|
|
// ============================================================================
|
|
// Error Response Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_400_bad_request_invalid_input() {
|
|
let env = 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);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_error_message_format() {
|
|
let env = create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Send assertion with invalid confidence
|
|
let (agent_id, signature) = 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 = create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let (agent_id, signature) = 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"));
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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 state = AppState {
|
|
journal: Arc::new(tokio::sync::Mutex::new(journal)),
|
|
store: store.clone(),
|
|
quota_store: quota_store.clone(),
|
|
};
|
|
|
|
let app = create_router_with_meter(state);
|
|
|
|
let (agent_id, signature) = 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 state = AppState {
|
|
journal: Arc::new(tokio::sync::Mutex::new(journal)),
|
|
store: store.clone(),
|
|
quota_store: quota_store.clone(),
|
|
};
|
|
|
|
let app = create_router_with_meter(state);
|
|
|
|
let (agent_id, _) = 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());
|
|
}
|
|
|
|
// ============================================================================
|
|
// Additional Edge Cases
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_health_endpoint() {
|
|
let env = 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 = 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);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_query_with_all_lenses() {
|
|
let env = create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Test that all lens values are accepted
|
|
let lenses = vec!["Recency", "Consensus", "Confidence", "Authority", "VoteAwareConsensus"];
|
|
|
|
for lens in lenses {
|
|
let request = Request::builder()
|
|
.uri(format!("/v1/query?subject=Test&predicate=prop&lens={}", lens))
|
|
.method("GET")
|
|
.body(Body::empty())
|
|
.expect("Request");
|
|
|
|
let response = app.clone().oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::OK, "Lens {} should be accepted", lens);
|
|
}
|
|
}
|