use hyper::StatusCode;
use mini_serve::{RouteBuilder, handler, path_params, query_params};
use serde::Deserialize;
#[derive(Deserialize)]
struct ItemParams {
id: u64,
}
#[tokio::test]
async fn typed_path_param_extracts_parsed_numeric_value() {
let app = RouteBuilder::stateless()
.get("/items/:id", handler(|req, _state| async move {
let params: ItemParams = path_params(&req)?;
mini_serve::json(StatusCode::OK, &serde_json::json!({"id": params.id}))
}))
.seal();
let port = app.bind_ephemeral().await.expect("failed to bind");
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let resp = reqwest::get(&format!("http://127.0.0.1:{}/items/42", port))
.await
.expect("request failed");
assert_eq!(resp.status(), StatusCode::OK);
let body: serde_json::Value = resp.json().await.expect("failed to parse json");
assert_eq!(body.get("id").and_then(|v| v.as_u64()), Some(42));
}
#[tokio::test]
async fn unparseable_path_param_returns_400_with_documented_message() {
let app = RouteBuilder::stateless()
.get("/items/:id", handler(|req, _state| async move {
let params: ItemParams = path_params(&req)?;
mini_serve::json(StatusCode::OK, &serde_json::json!({"id": params.id}))
}))
.seal();
let port = app.bind_ephemeral().await.expect("failed to bind");
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let resp = reqwest::get(&format!("http://127.0.0.1:{}/items/not-a-number", port))
.await
.expect("request failed");
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body: serde_json::Value = resp.json().await.expect("failed to parse json");
assert_eq!(
body.get("message").and_then(|v| v.as_str()),
Some("invalid path parameters")
);
}
#[tokio::test]
async fn a_param_less_route_asking_for_params_is_a_400_not_a_500() {
let app = RouteBuilder::stateless()
.get("/health", handler(|req, _state| async move {
let params: ItemParams = path_params(&req)?;
mini_serve::json(StatusCode::OK, &serde_json::json!({"id": params.id}))
}))
.seal();
let port = app.bind_ephemeral().await.expect("failed to bind");
let resp = reqwest::get(&format!("http://127.0.0.1:{port}/health"))
.await
.expect("request failed");
assert_eq!(
resp.status(),
StatusCode::BAD_REQUEST,
"a route that captures nothing cannot answer for its params; that is the \
caller's error, not a server fault"
);
}
#[tokio::test]
async fn query_params_reads_absence_as_emptiness() {
let app = RouteBuilder::stateless()
.get("/search", handler(|req, _state| async move {
let q = query_params(&req);
mini_serve::json(StatusCode::OK, &serde_json::json!({
"count": q.0.len(),
"term": q.0.get("term").cloned(),
}))
}))
.seal();
let port = app.bind_ephemeral().await.expect("failed to bind");
for (url, expected_count, expected_term) in [
("/search", 0, None),
("/search?", 0, None),
("/search?term=rust", 1, Some("rust")),
] {
let resp = reqwest::get(&format!("http://127.0.0.1:{port}{url}"))
.await
.expect("request failed");
assert_eq!(resp.status(), StatusCode::OK, "{url}");
let body: serde_json::Value = resp.json().await.expect("failed to parse json");
assert_eq!(
body.get("count").and_then(|v| v.as_u64()),
Some(expected_count),
"{url} must present a map whether or not one was inserted"
);
assert_eq!(
body.get("term").and_then(|v| v.as_str()),
expected_term,
"{url}"
);
}
}
#[tokio::test]
async fn a_guarded_query_parse_still_parses_the_same_way() {
let app = RouteBuilder::stateless()
.get("/q", handler(|req, _state| async move {
let q = query_params(&req);
let mut pairs: Vec<String> =
q.0.iter().map(|(k, v)| format!("{k}={v}")).collect();
pairs.sort();
mini_serve::json(StatusCode::OK, &serde_json::json!({"pairs": pairs}))
}))
.seal();
let port = app.bind_ephemeral().await.expect("failed to bind");
for (query, expected) in [
("?a=1&a=2", vec!["a=2"]),
("?a=", vec!["a="]),
("?flag", vec!["flag="]),
("?name=a+b", vec!["name=a b"]),
("?name=%69tem", vec!["name=item"]),
("?a=1&b=2", vec!["a=1", "b=2"]),
] {
let body: serde_json::Value =
reqwest::get(&format!("http://127.0.0.1:{port}/q{query}"))
.await
.expect("request failed")
.json()
.await
.expect("failed to parse json");
let pairs: Vec<&str> = body["pairs"]
.as_array()
.expect("pairs")
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert_eq!(pairs, expected, "query {query}");
}
}