use std::sync::Arc;
use bytes::Bytes;
use http::header::ALLOW;
use http_body_util::Full;
use hyper::{Method, Response, StatusCode};
use serde::Serialize;
use serde_json::{json, Value};
use tower::ServiceExt;
use hyperlite::{
path_param, path_params as extract_path_params, success, BoxError, PathParams, Router,
};
mod test_helpers;
use test_helpers::*;
#[tokio::test]
async fn test_router_single_route() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/hello",
Method::GET,
Arc::new(|_req, _state| Box::pin(async { Ok(success(StatusCode::OK, "hi")) })),
);
let request = build_request(Method::GET, "/hello", empty_body());
let response = router
.clone()
.oneshot(request)
.await
.expect("router should not fail");
assert_status(&response, StatusCode::OK);
assert_content_type_json(&response);
}
#[tokio::test]
async fn test_router_multiple_routes() {
let state = TestState::new();
let router = Router::new(state.clone())
.route(
"/ping",
Method::GET,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "pong")) })),
)
.route(
"/status",
Method::POST,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::CREATED, "created")) })),
);
let ping_request = build_request(Method::GET, "/ping", empty_body());
let ping_response = router.clone().oneshot(ping_request).await.unwrap();
assert_status(&ping_response, StatusCode::OK);
let status_request = build_request(Method::POST, "/status", empty_body());
let status_response = router.clone().oneshot(status_request).await.unwrap();
assert_status(&status_response, StatusCode::CREATED);
}
#[tokio::test]
async fn test_router_multiple_methods_same_path() {
let state = TestState::new();
let router = Router::new(state.clone())
.route(
"/items",
Method::GET,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "list")) })),
)
.route(
"/items",
Method::POST,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::CREATED, "created")) })),
);
let get_response = router
.clone()
.oneshot(build_request(Method::GET, "/items", empty_body()))
.await
.unwrap();
assert_status(&get_response, StatusCode::OK);
let post_response = router
.clone()
.oneshot(build_request(Method::POST, "/items", empty_body()))
.await
.unwrap();
assert_status(&post_response, StatusCode::CREATED);
}
#[tokio::test]
async fn test_router_route_overwrite() {
let state = TestState::new();
let router = Router::new(state.clone())
.route(
"/overwrite",
Method::GET,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "first")) })),
)
.route(
"/overwrite",
Method::GET,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "second")) })),
);
let response = router
.clone()
.oneshot(build_request(Method::GET, "/overwrite", empty_body()))
.await
.unwrap();
let json = read_body_json(response).await;
assert_eq!(json["data"], "second");
}
#[tokio::test]
async fn test_router_exact_match() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/users",
Method::GET,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "users")) })),
);
let response = router
.clone()
.oneshot(build_request(Method::GET, "/users", empty_body()))
.await
.unwrap();
assert_status(&response, StatusCode::OK);
}
#[tokio::test]
async fn test_router_path_params() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/users/{id}",
Method::GET,
Arc::new(|req, _| {
Box::pin(async move {
let params = req
.extensions()
.get::<PathParams>()
.cloned()
.unwrap_or_default();
Ok(success(StatusCode::OK, params.0))
})
}),
);
let response = router
.clone()
.oneshot({
let req = build_request(Method::GET, "/users/42", empty_body());
println!("[test debug] request uri = {}", req.uri());
req
})
.await
.unwrap();
let json = read_body_json(response).await;
assert_eq!(json["data"].get("id"), Some(&Value::String("42".into())));
}
#[tokio::test]
async fn test_router_multiple_path_params() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/users/{user_id}/posts/{post_id}",
Method::GET,
Arc::new(|req, _| {
Box::pin(async move {
let params = req
.extensions()
.get::<PathParams>()
.cloned()
.unwrap_or_default();
Ok(success(StatusCode::OK, params.0))
})
}),
);
let response = router
.clone()
.oneshot({
let req = build_request(Method::GET, "/users/10/posts/55", empty_body());
println!("[test debug] request uri = {}", req.uri());
req
})
.await
.unwrap();
let json = read_body_json(response).await;
assert_eq!(json["data"]["user_id"], "10");
assert_eq!(json["data"]["post_id"], "55");
}
#[tokio::test]
async fn test_router_nested_paths() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/api/v1/users",
Method::GET,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "nested")) })),
);
let response = router
.clone()
.oneshot(build_request(Method::GET, "/api/v1/users", empty_body()))
.await
.unwrap();
assert_status(&response, StatusCode::OK);
}
#[tokio::test]
async fn test_router_method_get() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/ping",
Method::GET,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "pong")) })),
);
let response = router
.clone()
.oneshot(build_request(Method::GET, "/ping", empty_body()))
.await
.unwrap();
assert_status(&response, StatusCode::OK);
}
#[tokio::test]
async fn test_router_method_post() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/submit",
Method::POST,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::CREATED, "done")) })),
);
let response = router
.clone()
.oneshot(build_request(Method::POST, "/submit", empty_body()))
.await
.unwrap();
assert_status(&response, StatusCode::CREATED);
}
#[tokio::test]
async fn test_router_method_not_allowed() {
let state = TestState::new();
let router = Router::new(state.clone())
.route(
"/items",
Method::GET,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "ok")) })),
)
.route(
"/items",
Method::PUT,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "put")) })),
);
let response = router
.clone()
.oneshot(build_request(Method::POST, "/items", empty_body()))
.await
.unwrap();
assert_status(&response, StatusCode::METHOD_NOT_ALLOWED);
let allow = response.headers().get(ALLOW).unwrap();
assert!(allow.to_str().unwrap().contains("GET"));
assert!(allow.to_str().unwrap().contains("PUT"));
}
#[tokio::test]
async fn test_router_options_method() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/inspect",
Method::OPTIONS,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "options")) })),
);
let response = router
.clone()
.oneshot(build_request(Method::OPTIONS, "/inspect", empty_body()))
.await
.unwrap();
assert_status(&response, StatusCode::OK);
}
#[tokio::test]
async fn test_router_not_found() {
let state = TestState::new();
let router = Router::new(state.clone());
let response = router
.clone()
.oneshot(build_request(Method::GET, "/missing", empty_body()))
.await
.unwrap();
assert_status(&response, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_router_not_found_response_format() {
let state = TestState::new();
let router = Router::new(state.clone());
let response = router
.clone()
.oneshot(build_request(Method::GET, "/missing", empty_body()))
.await
.unwrap();
assert_content_type_json(&response);
let json = read_body_json(response).await;
assert_eq!(json["errors"][0]["code"], "NOT_FOUND");
}
#[tokio::test]
async fn test_router_method_not_allowed_response() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/items",
Method::GET,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "ok")) })),
);
let response = router
.clone()
.oneshot(build_request(Method::POST, "/items", empty_body()))
.await
.unwrap();
let json = read_body_json(response).await;
assert_eq!(json["errors"][0]["code"], "METHOD_NOT_ALLOWED");
}
#[tokio::test]
async fn test_router_method_not_allowed_allow_header() {
let state = TestState::new();
let router = Router::new(state.clone())
.route(
"/items",
Method::GET,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "ok")) })),
)
.route(
"/items",
Method::DELETE,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "deleted")) })),
);
let response = router
.clone()
.oneshot(build_request(Method::POST, "/items", empty_body()))
.await
.unwrap();
let allow = response.headers().get(ALLOW).unwrap().to_str().unwrap();
assert!(allow.contains("GET"));
assert!(allow.contains("DELETE"));
}
#[tokio::test]
async fn test_router_path_params_inserted() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/users/{id}",
Method::GET,
Arc::new(|req, _| {
Box::pin(async move {
let has_params = req.extensions().get::<PathParams>().is_some();
Ok(success(StatusCode::OK, json!({ "has_params": has_params })))
})
}),
);
let response = router
.clone()
.oneshot({
let req = build_request(Method::GET, "/users/123", empty_body());
println!("[test debug] request uri = {}", req.uri());
req
})
.await
.unwrap();
let json = read_body_json(response).await;
assert_eq!(json["data"]["has_params"], true);
}
#[tokio::test]
async fn test_router_path_params_accessible() {
#[derive(Serialize)]
struct Payload {
id: String,
}
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/users/{id}",
Method::GET,
Arc::new(|req, _| {
Box::pin(async move {
let id: String = path_param(&req, "id")?;
Ok(success(StatusCode::OK, Payload { id }))
})
}),
);
let response = router
.clone()
.oneshot({
let req = build_request(Method::GET, "/users/abc", empty_body());
println!("[test debug] request uri = {}", req.uri());
req
})
.await
.unwrap();
let json = read_body_json(response).await;
assert_eq!(json["data"]["id"], "abc");
}
#[tokio::test]
async fn test_router_empty_path_params() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/health",
Method::GET,
Arc::new(|req, _| {
Box::pin(async move {
let params = extract_path_params(&req)?;
Ok(success(StatusCode::OK, params))
})
}),
);
let response = router
.clone()
.oneshot(build_request(Method::GET, "/health", empty_body()))
.await
.unwrap();
let json = read_body_json(response).await;
assert!(json["data"].as_object().unwrap().is_empty());
}
#[tokio::test]
async fn test_router_state_injected() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/state",
Method::GET,
Arc::new(|req, state| {
Box::pin(async move {
let extension_state = req
.extensions()
.get::<Arc<TestState>>()
.cloned()
.expect("state missing from extensions");
extension_state.increment();
state.increment();
Ok(success(StatusCode::OK, state.get()))
})
}),
);
let response = router
.clone()
.oneshot(build_request(Method::GET, "/state", empty_body()))
.await
.unwrap();
let json = read_body_json(response).await;
assert_eq!(json["data"], 2);
}
#[tokio::test]
async fn test_router_state_accessible() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/state",
Method::GET,
Arc::new(|_, state| Box::pin(async move { Ok(success(StatusCode::OK, state.get())) })),
);
state.increment();
let response = router
.clone()
.oneshot(build_request(Method::GET, "/state", empty_body()))
.await
.unwrap();
let json = read_body_json(response).await;
assert_eq!(json["data"], 1);
}
#[tokio::test]
async fn test_router_state_shared() {
let state = TestState::new();
let router = Router::new(state.clone())
.route(
"/one",
Method::GET,
Arc::new(|_, state| {
Box::pin(async move {
state.increment();
Ok(success(StatusCode::OK, state.get()))
})
}),
)
.route(
"/two",
Method::GET,
Arc::new(|_, state| {
Box::pin(async move {
state.increment();
Ok(success(StatusCode::OK, state.get()))
})
}),
);
let first = router
.clone()
.oneshot(build_request(Method::GET, "/one", empty_body()))
.await
.unwrap();
let second = router
.clone()
.oneshot(build_request(Method::GET, "/two", empty_body()))
.await
.unwrap();
let first_json = read_body_json(first).await;
let second_json = read_body_json(second).await;
assert_eq!(first_json["data"], 1);
assert_eq!(second_json["data"], 2);
}
#[tokio::test]
async fn test_router_handler_error() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/error",
Method::GET,
Arc::new(|_, _| {
Box::pin(async move {
let error = std::io::Error::other("handler error");
Err::<Response<Full<Bytes>>, BoxError>(error.into())
})
}),
);
let response = router
.clone()
.oneshot(build_request(Method::GET, "/error", empty_body()))
.await
.unwrap();
assert_status(&response, StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn test_router_handler_error_response_format() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/error",
Method::GET,
Arc::new(|_, _| {
Box::pin(async move {
let error = std::io::Error::other("handler error");
Err::<Response<Full<Bytes>>, BoxError>(error.into())
})
}),
);
let response = router
.clone()
.oneshot(build_request(Method::GET, "/error", empty_body()))
.await
.unwrap();
assert_content_type_json(&response);
let json = read_body_json(response).await;
assert_eq!(json["errors"][0]["code"], "INTERNAL_ERROR");
}
#[tokio::test]
async fn test_router_clone() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/clone",
Method::GET,
Arc::new(|_, _| Box::pin(async { Ok(success(StatusCode::OK, "cloned")) })),
);
let cloned = router.clone();
let response = cloned
.oneshot(build_request(Method::GET, "/clone", empty_body()))
.await
.unwrap();
assert_status(&response, StatusCode::OK);
}
#[tokio::test]
async fn test_router_clone_shares_state() {
let state = TestState::new();
let router = Router::new(state.clone()).route(
"/state",
Method::GET,
Arc::new(|_, state| {
Box::pin(async move {
state.increment();
Ok(success(StatusCode::OK, state.get()))
})
}),
);
let first = router
.clone()
.oneshot(build_request(Method::GET, "/state", empty_body()))
.await
.unwrap();
let second = router
.clone()
.oneshot(build_request(Method::GET, "/state", empty_body()))
.await
.unwrap();
let first_json = read_body_json(first).await;
let second_json = read_body_json(second).await;
assert_eq!(first_json["data"], 1);
assert_eq!(second_json["data"], 2);
}