sova-core 0.1.11

Core HTTP primitives for Sova (App, Router, Request, Response)
Documentation
use futures_util::stream;
use futures_util::FutureExt;
use http::Method;
use sova_core::extend::IntoHandler;
use sova_core::{App, Error, Html, Json, Request, Response};
use std::convert::Infallible;
use std::panic::AssertUnwindSafe;

#[tokio::test]
async fn error_handler_skips_response_errors() {
    let mut app = App::new();
    app.error_handler(|_err| async move {
        Response::text("should-not-run").status(418)
    });
    app.get(
        "/custom",
        (|_r: Request| async {
            Err::<Response, Error>(Error::custom(409, "nope"))
        })
        .into_handler(),
    );
    let res = app.handle_request(Method::GET, "/custom", "").await;
    assert_eq!(res.status_code().as_u16(), 409);
    assert_eq!(res.body_bytes(), Some(b"nope".as_slice()));
}

#[tokio::test]
async fn error_handler_catches_handler_err() {
    let mut app = App::new();
    app.error_handler(|err| async move {
        Response::text(format!("custom:{err}")).status(418)
    });
    app.get(
        "/deny",
        (|_r: Request| async { Err::<Response, Error>(Error::Unauthorized) }).into_handler(),
    );
    let res = app.handle_request(Method::GET, "/deny", "").await;
    assert_eq!(res.status_code().as_u16(), 418);
    assert_eq!(
        res.body_bytes(),
        Some(b"custom:unauthorized".as_slice())
    );
}

#[tokio::test]
async fn html_json_handler_returns() {
    let mut app = App::new();
    app.get("/h", |_r: Request| async { Html("<p>hi</p>".to_string()) });
    app.get("/j", |_r: Request| async { Json(serde_json::json!({"n": 1})) });

    let h = app.handle_request(Method::GET, "/h", "").await;
    assert_eq!(h.body_bytes(), Some(b"<p>hi</p>".as_slice()));
    let j = app.handle_request(Method::GET, "/j", "").await;
    assert!(j.body_bytes().unwrap().windows(1).any(|w| w == b"1"));
}

#[tokio::test]
async fn sse_streams_events() {
    let res = Response::sse(stream::iter(vec![
        Ok::<_, Infallible>("one".into()),
        Ok("two\nlines".into()),
    ]));
    assert_eq!(
        res.headers()
            .get("content-type")
            .and_then(|v| v.to_str().ok()),
        Some("text/event-stream")
    );
    let mut res = res;
    let bytes = res.take_body().collect().await.unwrap();
    let text = String::from_utf8(bytes.to_vec()).unwrap();
    assert!(text.contains("data: one\n\n"));
    assert!(text.contains("data: two\n"));
    assert!(text.contains("data: lines\n"));
}

#[tokio::test]
async fn panic_in_handler_becomes_500_via_unwind() {
    let mut app = App::new();
    app.get("/boom", |_r: Request| async {
        panic!("boom");
        #[allow(unreachable_code)]
        Ok::<Response, Error>(Response::text("nope"))
    });

    let res = match AssertUnwindSafe(app.handle_request(Method::GET, "/boom", ""))
        .catch_unwind()
        .await
    {
        Ok(res) => res,
        Err(_) => Response::text("Internal Server Error").status(500),
    };
    assert_eq!(res.status_code().as_u16(), 500);
}