sova-core 0.1.2

Core HTTP primitives for Sova (App, Router, Request, Response)
Documentation
use sova_core::{App, CheckKind, Error, TestClient};

#[tokio::test]
async fn healthz_always_ok_without_checks() {
    let mut app = App::new();
    app.with_probes();
    app.register_check("boom", |_s| async {
        Err(Error::Internal("should not run".into()))
    });

    let c = TestClient::tracked(app).await.unwrap();
    let res = c.get("/healthz").await;
    assert_eq!(res.status_code().as_u16(), 200);
    let body = String::from_utf8_lossy(res.body_bytes().unwrap_or(b""));
    assert!(body.contains(r#""status":"ok"#));
}

#[tokio::test]
async fn ready_503_when_ready_check_fails() {
    let mut app = App::new();
    app.register_check("db", |_s| async {
        Err(Error::Internal("connection refused".into()))
    });
    app.register_audit("openapi", |_s| async { Ok(()) });
    app.with_probes();

    let c = TestClient::tracked(app).await.unwrap();
    let res = c.get("/ready").await;
    assert_eq!(res.status_code().as_u16(), 503);
    let body = String::from_utf8_lossy(res.body_bytes().unwrap_or(b""));
    assert!(body.contains(r#""status":"not_ready"#));
    assert!(body.contains("db"));
    assert!(!body.contains("openapi"));
}

#[tokio::test]
async fn ready_ignores_audit_failures() {
    let mut app = App::new();
    app.register_check("db", |_s| async { Ok(()) });
    app.register_audit("openapi", |_s| async {
        Err(Error::Internal("schema drift".into()))
    });
    app.with_probes();

    let c = TestClient::tracked(app).await.unwrap();
    let res = c.get("/ready").await;
    assert_eq!(res.status_code().as_u16(), 200);
    let body = String::from_utf8_lossy(res.body_bytes().unwrap_or(b""));
    assert!(body.contains(r#""status":"ok"#));
    assert!(body.contains("db"));
    assert!(!body.contains("openapi"));
}

#[tokio::test]
async fn run_checks_includes_audit_for_cli() {
    let mut app = App::new();
    app.register_check("db", |_s| async { Ok(()) });
    app.register_audit("openapi", |_s| async {
        Err(Error::Internal("bad".into()))
    });

    let server = app.build().unwrap();
    let results = app
        .run_checks(server.state(), &[CheckKind::Ready, CheckKind::Audit])
        .await;
    assert_eq!(results.len(), 2);
    assert!(results.iter().any(|r| r.name == "db" && r.ok));
    assert!(results.iter().any(|r| r.name == "openapi" && !r.ok));
}

#[tokio::test]
async fn ready_exposes_instance_header() {
    std::env::set_var("SOVA_INSTANCE_ID", "test-instance-1");
    let mut app = App::new();
    app.with_probes();
    let c = TestClient::tracked(app).await.unwrap();
    let res = c.get("/ready").await;
    assert_eq!(res.status_code().as_u16(), 200);
    assert_eq!(
        res.headers().get("x-sova-instance").and_then(|v| v.to_str().ok()),
        Some("test-instance-1")
    );
    std::env::remove_var("SOVA_INSTANCE_ID");
}

#[tokio::test]
async fn route_redirect_helper() {
    let mut app = App::new();
    app.redirect("/old", "/new", 302);
    app.redirect("/gone", "https://example.com/", 301);

    let c = TestClient::tracked(app).await.unwrap();

    let res = c.get("/old").await;
    assert_eq!(res.status_code().as_u16(), 302);
    assert_eq!(res.headers().get("location").unwrap(), "/new");

    let res = c.get("/gone").await;
    assert_eq!(res.status_code().as_u16(), 301);
    assert_eq!(
        res.headers().get("location").unwrap(),
        "https://example.com/"
    );
}