use hyper::StatusCode;
use mini_serve::{RouteBuilder, handler, path_params};
use serde::Deserialize;
#[derive(Deserialize)]
struct NameParam {
name: String,
}
#[tokio::test]
async fn percent_encoded_path_segment_matches_route() {
let app = RouteBuilder::stateless()
.get("/api/items", handler(|_req, _state| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({"result": "success"}))
}))
.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:{}/api/%69tems", 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("result").and_then(|v| v.as_str()), Some("success"));
}
#[tokio::test]
async fn percent_encoded_path_param_extracted_correctly() {
let app = RouteBuilder::stateless()
.get("/users/:name", handler(|req, _state| async move {
let params: NameParam = path_params(&req)?;
let name = params.name;
mini_serve::json(StatusCode::OK, &serde_json::json!({"name": name}))
}))
.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:{}/users/alice%40example.com", 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("name").and_then(|v| v.as_str()),
Some("alice@example.com")
);
}
#[tokio::test]
async fn plus_decodes_as_space_in_query_string() {
let app = RouteBuilder::stateless()
.get("/search", handler(|req, _state| async move {
let query_str = req
.uri()
.query()
.unwrap_or("");
let mut map = std::collections::HashMap::new();
for pair in query_str.split('&').filter(|s| !s.is_empty()) {
if let Some((key, value)) = pair.split_once('=') {
let key = key.replace('+', " ");
let key = percent_encoding::percent_decode_str(&key)
.decode_utf8_lossy()
.into_owned();
let value = value.replace('+', " ");
let value = percent_encoding::percent_decode_str(&value)
.decode_utf8_lossy()
.into_owned();
map.insert(key, value);
}
}
mini_serve::json(StatusCode::OK, &serde_json::json!({"query": map}))
}))
.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:{}/search?q=hello+world", port))
.await
.expect("request failed");
assert_eq!(resp.status(), StatusCode::OK);
let body: serde_json::Value = resp.json().await.expect("failed to parse json");
let query = body.get("query").and_then(|v| v.as_object()).expect("missing query");
assert_eq!(query.get("q").and_then(|v| v.as_str()), Some("hello world"));
}
#[tokio::test]
async fn percent_encoded_query_param_decoded() {
let app = RouteBuilder::stateless()
.get("/search", handler(|req, _state| async move {
let query_str = req.uri().query().unwrap_or("");
let mut map = std::collections::HashMap::new();
for pair in query_str.split('&').filter(|s| !s.is_empty()) {
if let Some((key, value)) = pair.split_once('=') {
let key = key.replace('+', " ");
let key = percent_encoding::percent_decode_str(&key)
.decode_utf8_lossy()
.into_owned();
let value = value.replace('+', " ");
let value = percent_encoding::percent_decode_str(&value)
.decode_utf8_lossy()
.into_owned();
map.insert(key, value);
}
}
mini_serve::json(StatusCode::OK, &serde_json::json!({"params": map}))
}))
.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:{}/search?data=key%3Dvalue", port))
.await
.expect("request failed");
assert_eq!(resp.status(), StatusCode::OK);
let body: serde_json::Value = resp.json().await.expect("failed to parse json");
let params = body.get("params").and_then(|v| v.as_object()).expect("missing params");
assert_eq!(params.get("data").and_then(|v| v.as_str()), Some("key=value"));
}
#[tokio::test]
async fn combined_percent_encoding_and_plus_in_query() {
let app = RouteBuilder::stateless()
.get("/api/search", handler(|req, _state| async move {
let query_str = req.uri().query().unwrap_or("");
let mut map = std::collections::HashMap::new();
for pair in query_str.split('&').filter(|s| !s.is_empty()) {
if let Some((key, value)) = pair.split_once('=') {
let key = key.replace('+', " ");
let key = percent_encoding::percent_decode_str(&key)
.decode_utf8_lossy()
.into_owned();
let value = value.replace('+', " ");
let value = percent_encoding::percent_decode_str(&value)
.decode_utf8_lossy()
.into_owned();
map.insert(key, value);
}
}
mini_serve::json(StatusCode::OK, &serde_json::json!({"result": map}))
}))
.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:{}/api/search?msg=hello+world+%26+goodbye",
port
))
.await
.expect("request failed");
assert_eq!(resp.status(), StatusCode::OK);
let body: serde_json::Value = resp.json().await.expect("failed to parse json");
let result = body
.get("result")
.and_then(|v| v.as_object())
.expect("missing result");
assert_eq!(
result.get("msg").and_then(|v| v.as_str()),
Some("hello world & goodbye")
);
}