nidus-testing 1.0.4

Test application harness, request builders, response helpers, and provider overrides for Nidus.
Documentation
use axum::{
    Json, Router,
    response::IntoResponse,
    routing::{delete, get, head, patch, post, put},
};
use http::{StatusCode, header::HeaderName};
use nidus_http::{controller::Controller, router::RouteDefinition};
use nidus_testing::TestApp;
use serde::Deserialize;
use serde_json::json;

#[derive(Debug, Deserialize, PartialEq, Eq)]
struct UserDto {
    id: u64,
    name: String,
}

#[tokio::test]
async fn route_definition_mounts_controller_prefix_on_axum_router() {
    let router = Controller::new("/users")
        .route(RouteDefinition::get("/:id", || async {
            Json(json!({ "id": 42 }))
        }))
        .into_router();

    let response = TestApp::from_router(router).get("/users/42").send().await;

    response.assert_status(http::StatusCode::OK);
    response.assert_json(json!({ "id": 42 }));
}

#[tokio::test]
async fn test_response_exposes_status_body_and_typed_json() {
    let router = Router::new().route(
        "/users",
        post(|| async { (StatusCode::CREATED, Json(json!({ "id": 7, "name": "Ada" }))) }),
    );

    let response = TestApp::from_router(router)
        .post("/users")
        .json(&json!({ "name": "Ada" }))
        .send()
        .await;

    assert_eq!(response.status(), StatusCode::CREATED);
    assert!(!response.body().is_empty());
    assert_eq!(
        response.json::<UserDto>(),
        UserDto {
            id: 7,
            name: "Ada".to_owned(),
        }
    );
    assert_eq!(
        response.try_json::<UserDto>().unwrap(),
        UserDto {
            id: 7,
            name: "Ada".to_owned(),
        }
    );
}

#[tokio::test]
async fn test_response_exposes_text_and_fallible_json_decode() {
    let router = Router::new().route("/health", get(|| async { "ok" }));

    let response = TestApp::from_router(router).get("/health").send().await;

    assert_eq!(response.text().unwrap(), "ok");
    assert!(response.try_json::<UserDto>().is_err());
}

#[tokio::test]
async fn test_response_exposes_and_asserts_headers() {
    let router = Router::new().route(
        "/health",
        get(|| async {
            ([(HeaderName::from_static("x-request-id"), "req-123")], "ok").into_response()
        }),
    );

    let response = TestApp::from_router(router).get("/health").send().await;

    response.assert_status(StatusCode::OK);
    response.assert_header("x-request-id", "req-123");
    assert_eq!(
        response
            .headers()
            .get("x-request-id")
            .and_then(|value| value.to_str().ok()),
        Some("req-123")
    );
    assert_eq!(
        response
            .header("x-request-id")
            .and_then(|value| value.to_str().ok()),
        Some("req-123")
    );
    assert_eq!(
        response.header_str("x-request-id").unwrap(),
        Some("req-123")
    );
    assert_eq!(response.header_str("x-missing").unwrap(), None);
}

#[tokio::test]
async fn route_definition_mounts_common_mutation_verbs() {
    let router = Controller::new("/users")
        .route(RouteDefinition::put("/:id", || async { "put" }))
        .route(RouteDefinition::patch("/:id", || async { "patch" }))
        .route(RouteDefinition::delete("/:id", || async { "delete" }))
        .into_router();
    let app = TestApp::from_router(router);

    app.put("/users/42").send().await.assert_text("put");
    app.patch("/users/42").send().await.assert_text("patch");
    app.delete("/users/42").send().await.assert_text("delete");
}

#[tokio::test]
async fn test_app_can_wrap_plain_axum_router() {
    let router = Router::new().route("/health", get(|| async { "ok" }));

    let response = TestApp::from_router(router).get("/health").send().await;

    response.assert_status(http::StatusCode::OK);
    response.assert_text("ok");
}

#[tokio::test]
async fn test_app_can_send_arbitrary_http_methods() {
    let router = Router::new().route("/health", head(|| async { "" }));

    let response = TestApp::from_router(router)
        .request(http::Method::HEAD, "/health")
        .send()
        .await;

    response.assert_status(StatusCode::OK);
    assert!(response.body().is_empty());
}

#[tokio::test]
async fn test_app_request_helpers_cover_common_mutation_verbs() {
    let router = Router::new()
        .route("/users/42", put(|| async { "put" }))
        .route("/users/42", patch(|| async { "patch" }))
        .route("/users/42", delete(|| async { "delete" }));
    let app = TestApp::from_router(router);

    app.put("/users/42").send().await.assert_text("put");
    app.patch("/users/42").send().await.assert_text("patch");
    app.delete("/users/42").send().await.assert_text("delete");
}