test(web-gateway): add state, health endpoint, and router integration tests
Adds 10 new tests: - state.rs: AppState creation, empty URL channel handling, broadcast, config wrapping, Clone trait - routes/mod.rs: health check, readiness with/without services, auth gating, public routes Total: 49 tests (up from 39) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,3 +64,93 @@ async fn readiness_check(
|
||||
"ml_training_service": state.ml_training_channel.is_some(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use tower::ServiceExt;
|
||||
|
||||
use crate::config::GatewayConfig;
|
||||
|
||||
async fn test_state() -> AppState {
|
||||
AppState::new(GatewayConfig::default()).await.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_health_returns_ok() {
|
||||
let state = test_state().await;
|
||||
let app = create_router(state);
|
||||
let req = Request::builder()
|
||||
.uri("/health")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ready_returns_service_status() {
|
||||
let state = test_state().await;
|
||||
let app = create_router(state);
|
||||
let req = Request::builder()
|
||||
.uri("/ready")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap();
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(json["status"], "ok");
|
||||
// Default config creates valid channels
|
||||
assert_eq!(json["trading_service"], true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ready_reports_missing_service() {
|
||||
let config = GatewayConfig {
|
||||
trading_service_url: String::new(),
|
||||
..GatewayConfig::default()
|
||||
};
|
||||
let state = AppState::new(config).await.unwrap();
|
||||
let app = create_router(state);
|
||||
let req = Request::builder()
|
||||
.uri("/ready")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.oneshot(req).await.unwrap();
|
||||
let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap();
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(json["trading_service"], false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_protected_route_without_auth_returns_401() {
|
||||
let state = test_state().await;
|
||||
let app = create_router(state);
|
||||
let req = Request::builder()
|
||||
.uri("/api/trading/positions")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_public_auth_route_no_auth_required() {
|
||||
let state = test_state().await;
|
||||
let app = create_router(state);
|
||||
let req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/login")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(r#"{"username":"test","password":"pass"}"#))
|
||||
.unwrap();
|
||||
let resp = app.oneshot(req).await.unwrap();
|
||||
// Will be 500 (no JWT secret) or 200, but NOT 401
|
||||
assert_ne!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,3 +41,57 @@ impl AppState {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_new_creates_state_with_valid_config() {
|
||||
let config = GatewayConfig::default();
|
||||
let state = AppState::new(config).await.unwrap();
|
||||
assert!(state.trading_channel.is_some());
|
||||
assert!(state.backtesting_channel.is_some());
|
||||
assert!(state.ml_training_channel.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_new_with_empty_url_yields_none_channel() {
|
||||
let config = GatewayConfig {
|
||||
trading_service_url: String::new(),
|
||||
..GatewayConfig::default()
|
||||
};
|
||||
let state = AppState::new(config).await.unwrap();
|
||||
assert!(state.trading_channel.is_none());
|
||||
// Other channels should still work
|
||||
assert!(state.backtesting_channel.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_broadcast_channel_sends_and_receives() {
|
||||
let config = GatewayConfig::default();
|
||||
let state = AppState::new(config).await.unwrap();
|
||||
let mut rx = state.ws_broadcast.subscribe();
|
||||
state.ws_broadcast.send("hello".to_owned()).unwrap();
|
||||
let msg = rx.recv().await.unwrap();
|
||||
assert_eq!(msg, "hello");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_config_is_arc_wrapped() {
|
||||
let config = GatewayConfig {
|
||||
jwt_secret: "test-secret".to_owned(),
|
||||
..GatewayConfig::default()
|
||||
};
|
||||
let state = AppState::new(config).await.unwrap();
|
||||
assert_eq!(state.config.jwt_secret, "test-secret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_state_is_clone() {
|
||||
// AppState must be Clone for Axum's State extractor
|
||||
fn assert_clone<T: Clone>() {}
|
||||
assert_clone::<AppState>();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user