rustapi-core 0.1.450

The core engine of the RustAPI framework. Provides the hyper-based HTTP server, router, extraction logic, and foundational traits.
Documentation
use rustapi_core::health::{HealthCheckBuilder, HealthEndpointConfig, HealthStatus};
use rustapi_core::RustApi;
use std::time::Duration;
use tokio::sync::oneshot;

#[tokio::test]
async fn test_default_health_endpoints_are_available() {
    let app = RustApi::new().health_endpoints();

    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    let port = addr.port();
    drop(listener);

    let (tx, rx) = oneshot::channel();
    let addr_str = format!("127.0.0.1:{}", port);

    let server_handle = tokio::spawn(async move {
        app.run_with_shutdown(&addr_str, async {
            rx.await.ok();
        })
        .await
    });

    tokio::time::sleep(Duration::from_millis(200)).await;
    let client = reqwest::Client::new();
    let base_url = format!("http://127.0.0.1:{}", port);

    for path in ["/health", "/ready", "/live"] {
        let res = client
            .get(format!("{}{}", base_url, path))
            .send()
            .await
            .expect("health endpoint request failed");

        assert_eq!(res.status(), 200, "{} should return 200", path);

        let body: serde_json::Value = res.json().await.unwrap();
        assert!(
            body.get("status").is_some(),
            "{} should include status",
            path
        );
        assert!(
            body.get("timestamp").is_some(),
            "{} should include timestamp",
            path
        );
    }

    tx.send(()).unwrap();
    let _ = tokio::time::timeout(Duration::from_secs(2), server_handle).await;
}

#[tokio::test]
async fn test_unhealthy_readiness_returns_503() {
    let health = HealthCheckBuilder::new(false)
        .add_check("database", || async {
            HealthStatus::unhealthy("database offline")
        })
        .build();

    let app = RustApi::new().with_health_check(health);

    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    let port = addr.port();
    drop(listener);

    let (tx, rx) = oneshot::channel();
    let addr_str = format!("127.0.0.1:{}", port);

    let server_handle = tokio::spawn(async move {
        app.run_with_shutdown(&addr_str, async {
            rx.await.ok();
        })
        .await
    });

    tokio::time::sleep(Duration::from_millis(200)).await;
    let client = reqwest::Client::new();
    let base_url = format!("http://127.0.0.1:{}", port);

    let res = client
        .get(format!("{}/ready", base_url))
        .send()
        .await
        .expect("readiness endpoint request failed");

    assert_eq!(res.status(), 503);

    let body = res.text().await.unwrap();
    assert!(body.contains("database offline"));

    tx.send(()).unwrap();
    let _ = tokio::time::timeout(Duration::from_secs(2), server_handle).await;
}

#[tokio::test]
async fn test_custom_health_endpoint_paths() {
    let config = HealthEndpointConfig::new()
        .health_path("/healthz")
        .readiness_path("/readyz")
        .liveness_path("/livez");

    let app = RustApi::new().health_endpoints_with_config(config);

    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    let port = addr.port();
    drop(listener);

    let (tx, rx) = oneshot::channel();
    let addr_str = format!("127.0.0.1:{}", port);

    let server_handle = tokio::spawn(async move {
        app.run_with_shutdown(&addr_str, async {
            rx.await.ok();
        })
        .await
    });

    tokio::time::sleep(Duration::from_millis(200)).await;
    let client = reqwest::Client::new();
    let base_url = format!("http://127.0.0.1:{}", port);

    for path in ["/healthz", "/readyz", "/livez"] {
        let res = client
            .get(format!("{}{}", base_url, path))
            .send()
            .await
            .expect("custom health endpoint request failed");

        assert_eq!(res.status(), 200, "{} should return 200", path);
    }

    tx.send(()).unwrap();
    let _ = tokio::time::timeout(Duration::from_secs(2), server_handle).await;
}