spikard-http 0.16.0

High-performance HTTP server for Spikard with tower-http middleware stack
Documentation
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::response::IntoResponse;
use axum::{Router, routing::any};
use spikard_http::testing::{MultipartFilePart, TestClient};

async fn echo(req: Request<Body>) -> axum::response::Response {
    let method = req.method().to_string();
    let uri = req.uri().to_string();
    let headers = req
        .headers()
        .iter()
        .fold(serde_json::Map::new(), |mut map, (key, value)| {
            map.insert(
                key.to_string(),
                serde_json::Value::String(value.to_str().unwrap_or("").to_string()),
            );
            map
        });
    let bytes = axum::body::to_bytes(req.into_body(), usize::MAX).await.unwrap();
    let body_text = String::from_utf8_lossy(&bytes).to_string();

    let payload = serde_json::json!({
        "method": method,
        "uri": uri,
        "headers": headers,
        "body": body_text,
    });

    (StatusCode::OK, axum::Json(payload)).into_response()
}

#[tokio::test]
async fn test_client_sends_query_headers_and_bodies() {
    let app = Router::new().route("/{*path}", any(echo));
    let client = TestClient::from_router(app).expect("client");

    let snapshot = client
        .get(
            "/items",
            Some(vec![("q".to_string(), "a b".to_string())]),
            Some(vec![("x-test".to_string(), "1".to_string())]),
        )
        .await
        .expect("get");
    assert_eq!(snapshot.status, 200);
    let json = snapshot.json().expect("json");
    assert_eq!(json["method"], "GET");
    assert!(json["uri"].as_str().unwrap().contains("/items?q=a%20b"));
    assert_eq!(json["headers"]["x-test"], "1");

    let snapshot = client
        .post(
            "/json",
            Some(serde_json::json!({"hello":"world"})),
            None,
            None,
            None,
            None,
        )
        .await
        .expect("post");
    let json = snapshot.json().expect("json");
    assert_eq!(json["method"], "POST");
    assert!(json["body"].as_str().unwrap().contains("\"hello\":\"world\""));

    let snapshot = client
        .post(
            "/form",
            None,
            Some(vec![("a".to_string(), "b".to_string())]),
            None,
            None,
            None,
        )
        .await
        .expect("post");
    let json = snapshot.json().expect("json");
    let body = json["body"].as_str().unwrap();
    assert!(body.contains('a'));
    assert!(body.contains('b'));

    let snapshot = client
        .post(
            "/multipart",
            None,
            None,
            Some((
                vec![("field".to_string(), "value".to_string())],
                vec![MultipartFilePart {
                    field_name: "file".to_string(),
                    filename: "hello.txt".to_string(),
                    content_type: Some("text/plain".to_string()),
                    content: b"hello".to_vec(),
                }],
            )),
            None,
            None,
        )
        .await
        .expect("post");
    let json = snapshot.json().expect("json");
    assert!(
        json["headers"]["content-type"]
            .as_str()
            .unwrap()
            .contains("multipart/form-data")
    );
    assert!(json["body"].as_str().unwrap().contains("hello"));
}

#[tokio::test]
async fn test_client_supports_other_http_methods_and_query_merging() {
    let app = Router::new().route("/{*path}", any(echo));
    let client = TestClient::from_router(app).expect("client");

    let snapshot = client
        .put(
            "/put",
            Some(serde_json::json!({"name":"spikard"})),
            None,
            Some(vec![("x-test".to_string(), "2".to_string())]),
        )
        .await
        .expect("put");
    let json = snapshot.json().expect("json");
    assert_eq!(json["method"], "PUT");
    assert_eq!(json["headers"]["x-test"], "2");
    assert!(json["body"].as_str().unwrap().contains("\"name\":\"spikard\""));

    let snapshot = client.delete("/delete", None, None).await.expect("delete");
    assert_eq!(snapshot.json().expect("json")["method"], "DELETE");

    let snapshot = client.options("/options", None, None).await.expect("options");
    assert_eq!(snapshot.json().expect("json")["method"], "OPTIONS");

    let snapshot = client.head("/head", None, None).await.expect("head");
    assert_eq!(snapshot.status, 200);
    assert!(snapshot.body.is_empty());

    let snapshot = client.trace("/trace", None, None).await.expect("trace");
    assert_eq!(snapshot.json().expect("json")["method"], "TRACE");

    let snapshot = client
        .get("/items?x=1", Some(vec![("y".to_string(), "2".to_string())]), None)
        .await
        .expect("get");
    assert!(
        snapshot.json().expect("json")["uri"]
            .as_str()
            .unwrap()
            .contains("x=1&y=2")
    );
}

#[tokio::test]
async fn test_client_rejects_invalid_header_names() {
    let app = Router::new().route("/{*path}", any(echo));
    let client = TestClient::from_router(app).expect("client");

    let error = client
        .get("/items", None, Some(vec![("bad\n".to_string(), "1".to_string())]))
        .await
        .expect_err("invalid header");
    let message = error.to_string();
    assert!(message.contains("Invalid header name"));
}