#![cfg(feature = "http-server")]
use std::sync::Arc;
use aletheiadb::AletheiaDB;
use aletheiadb::http::{AppState, ServerConfig, build_test_router};
use autumn_web::test::TestApp;
use axum::http::StatusCode;
use serde_json::Value;
fn test_client() -> autumn_web::test::TestClient {
let db = Arc::new(AletheiaDB::new().unwrap());
let state = AppState::new(db);
let config = ServerConfig::default();
let router = build_test_router(state, &config).expect("router builds");
TestApp::from_router(router)
}
#[tokio::test]
async fn health_endpoint_returns_healthy_status() {
let client = test_client();
let resp = client.get("/status").send().await;
assert_eq!(resp.status, StatusCode::OK);
let body: Value = serde_json::from_slice(&resp.body).expect("response is JSON");
assert_eq!(body.get("status").and_then(Value::as_str), Some("healthy"));
}
#[tokio::test]
async fn health_endpoint_returns_json_content_type() {
let client = test_client();
let resp = client.get("/status").send().await;
let content_type = resp
.headers
.iter()
.find_map(|(k, v)| (k.eq_ignore_ascii_case("content-type")).then_some(v.as_str()))
.expect("Content-Type header present");
assert!(
content_type.contains("application/json"),
"Content-Type should be application/json, got: {content_type}"
);
}
#[tokio::test]
async fn cors_headers_present() {
let db = Arc::new(AletheiaDB::new().unwrap());
let state = AppState::new(db);
let config = ServerConfig::builder()
.cors(aletheiadb::http::CorsConfig::permissive())
.build();
let router = build_test_router(state, &config).unwrap();
let client = TestApp::from_router(router);
let resp = client
.get("/status")
.header("Origin", "http://example.com")
.header("Access-Control-Request-Method", "GET")
.send()
.await;
let has_cors = resp.headers.iter().any(|(k, _)| {
k.eq_ignore_ascii_case("access-control-allow-origin")
|| k.eq_ignore_ascii_case("access-control-allow-methods")
});
assert!(has_cors, "CORS headers should be present in response");
}
#[tokio::test]
async fn server_config_default_port() {
assert_eq!(ServerConfig::default().port(), 1963);
}
#[tokio::test]
async fn server_config_custom_port() {
assert_eq!(ServerConfig::new(3000).port(), 3000);
}
#[tokio::test]
async fn server_config_port_range() {
for port in [0u16, 80, 443, 65535] {
assert_eq!(ServerConfig::new(port).port(), port);
}
}
#[tokio::test]
async fn server_config_builder() {
let config = ServerConfig::builder().port(9000).host("127.0.0.1").build();
assert_eq!(config.port(), 9000);
assert_eq!(config.host(), "127.0.0.1");
}
#[tokio::test]
async fn server_config_default_host() {
assert_eq!(ServerConfig::default().host(), "0.0.0.0");
}
#[tokio::test]
async fn health_endpoint_body_is_healthy() {
let client = test_client();
let resp = client.get("/status").send().await;
assert_eq!(resp.status, StatusCode::OK);
let body: Value = serde_json::from_slice(&resp.body).unwrap();
assert_eq!(body["status"], "healthy");
}
#[tokio::test]
async fn middleware_chain_survives_repeated_requests() {
let client = test_client();
for _ in 0..3 {
let resp = client.get("/status").send().await;
assert_eq!(resp.status, StatusCode::OK);
}
}
#[tokio::test]
async fn unknown_route_returns_404() {
let client = test_client();
let resp = client.get("/nonexistent-route").send().await;
assert_eq!(resp.status, StatusCode::NOT_FOUND);
}
#[cfg(test)]
mod state_tests {
use aletheiadb::AletheiaDB;
use aletheiadb::PropertyMapBuilder;
use aletheiadb::http::AppState;
use std::sync::Arc;
#[test]
fn app_state_creation() {
let db = Arc::new(AletheiaDB::new().unwrap());
let state = AppState::new(db);
assert_eq!(state.db().node_count(), 0);
}
#[test]
fn app_state_is_clone() {
let db = Arc::new(AletheiaDB::new().unwrap());
let state = AppState::new(db);
let state2 = state.clone();
let node_id = state
.db()
.create_node("Test", PropertyMapBuilder::new().build())
.unwrap();
assert_eq!(state2.db().node_count(), 1);
let node = state2.db().get_node(node_id).unwrap();
assert_eq!(node.id, node_id);
}
#[tokio::test]
async fn app_state_concurrent_reads() {
let db = Arc::new(AletheiaDB::new().unwrap());
for i in 0..100i64 {
db.create_node("Test", PropertyMapBuilder::new().insert("id", i).build())
.unwrap();
}
let state = AppState::new(db);
let handles: Vec<_> = (0..10)
.map(|_| {
let state = state.clone();
tokio::spawn(async move {
for _ in 0..100 {
assert_eq!(state.db().node_count(), 100);
}
})
})
.collect();
for handle in handles {
handle.await.expect("task should not panic");
}
}
#[tokio::test]
async fn app_state_concurrent_writes() {
let db = Arc::new(AletheiaDB::new().unwrap());
let state = AppState::new(db);
let handles: Vec<_> = (0..10)
.map(|task_id| {
let state = state.clone();
tokio::spawn(async move {
for iter in 0..10i64 {
state
.db()
.create_node(
"Test",
PropertyMapBuilder::new()
.insert("task", task_id as i64)
.insert("iter", iter)
.build(),
)
.unwrap();
}
})
})
.collect();
for handle in handles {
handle.await.expect("task should not panic");
}
assert_eq!(state.db().node_count(), 100);
}
#[tokio::test]
async fn app_state_mixed_concurrent_operations() {
let db = Arc::new(AletheiaDB::new().unwrap());
let state = AppState::new(db);
let mut handles = Vec::new();
for task_id in 0..5 {
let state = state.clone();
handles.push(tokio::spawn(async move {
for iter in 0..20i64 {
state
.db()
.create_node(
"Writer",
PropertyMapBuilder::new()
.insert("task", task_id as i64)
.insert("iter", iter)
.build(),
)
.unwrap();
}
}));
}
for _ in 0..5 {
let state = state.clone();
handles.push(tokio::spawn(async move {
for _ in 0..50 {
let _ = state.db().node_count();
}
}));
}
for handle in handles {
handle.await.expect("task should not panic");
}
assert_eq!(state.db().node_count(), 100);
}
#[tokio::test]
async fn no_deadlock_under_load() {
let db = Arc::new(AletheiaDB::new().unwrap());
let state = AppState::new(db);
let handles: Vec<_> = (0..20)
.map(|task_id| {
let state = state.clone();
tokio::spawn(async move {
for iter in 0..10i64 {
state
.db()
.create_node(
"Load",
PropertyMapBuilder::new()
.insert("task", task_id as i64)
.insert("iter", iter)
.build(),
)
.unwrap();
let _ = state.db().node_count();
}
})
})
.collect();
let result = tokio::time::timeout(std::time::Duration::from_secs(10), async {
for handle in handles {
handle.await.expect("task should not panic");
}
})
.await;
assert!(result.is_ok(), "tasks should complete without deadlock");
assert_eq!(state.db().node_count(), 200);
}
#[test]
fn app_state_from_arc() {
let db = Arc::new(AletheiaDB::new().unwrap());
let state: AppState = db.into();
assert_eq!(state.db().node_count(), 0);
}
}