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:
jgrusewski
2026-02-22 08:04:30 +01:00
parent c688d7281f
commit f2340c2ecf
2 changed files with 144 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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>();
}
}