use hyper::StatusCode;
use mini_serve::{RouteBuilder, handler};
#[tokio::test]
async fn method_dispatch_returns_405_with_allow_header() {
let app = RouteBuilder::stateless()
.get("/api/items", handler(|_req, _state| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({"items": []}))
}))
.seal();
let port = app.bind_ephemeral().await.expect("failed to bind");
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let client = reqwest::Client::new();
let resp = client
.post(format!("http://127.0.0.1:{}/api/items", port))
.send()
.await
.expect("request failed");
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let allow_header = resp
.headers()
.get("allow")
.and_then(|h| h.to_str().ok())
.expect("missing Allow header");
assert!(allow_header.contains("GET"), "Allow header should contain GET: {}", allow_header);
assert!(allow_header.contains("HEAD"), "Allow header should contain HEAD: {}", allow_header);
}
#[tokio::test]
async fn multiple_methods_on_same_path() {
let app = RouteBuilder::stateless()
.get("/resource", handler(|_req, _state| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({"method": "GET"}))
}))
.post("/resource", handler(|_req, _state| async {
mini_serve::json(StatusCode::CREATED, &serde_json::json!({"method": "POST"}))
}))
.put("/resource", handler(|_req, _state| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({"method": "PUT"}))
}))
.seal();
let port = app.bind_ephemeral().await.expect("failed to bind");
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let client = reqwest::Client::new();
let get_resp = client
.get(format!("http://127.0.0.1:{}/resource", port))
.send()
.await
.expect("GET failed");
assert_eq!(get_resp.status(), StatusCode::OK);
let post_resp = client
.post(format!("http://127.0.0.1:{}/resource", port))
.send()
.await
.expect("POST failed");
assert_eq!(post_resp.status(), StatusCode::CREATED);
let put_resp = client
.put(format!("http://127.0.0.1:{}/resource", port))
.send()
.await
.expect("PUT failed");
assert_eq!(put_resp.status(), StatusCode::OK);
let delete_resp = client
.delete(format!("http://127.0.0.1:{}/resource", port))
.send()
.await
.expect("DELETE failed");
assert_eq!(delete_resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let allow_header = delete_resp
.headers()
.get("allow")
.and_then(|h| h.to_str().ok())
.expect("missing Allow header");
let methods: Vec<&str> = allow_header.split(", ").collect();
assert!(methods.contains(&"GET"), "Allow should contain GET");
assert!(methods.contains(&"HEAD"), "Allow should contain HEAD");
assert!(methods.contains(&"POST"), "Allow should contain POST");
assert!(methods.contains(&"PUT"), "Allow should contain PUT");
assert!(!methods.contains(&"DELETE"), "Allow should not contain DELETE");
}
#[tokio::test]
async fn unregistered_path_returns_404_without_allow() {
let app = RouteBuilder::stateless()
.get("/exists", handler(|_req, _state| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({}))
}))
.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:{}/does-not-exist", port))
.await
.expect("request failed");
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert!(resp.headers().get("allow").is_none(), "404 should not have Allow header");
}
#[tokio::test]
async fn patch_routes_are_served() {
let app = RouteBuilder::stateless()
.patch(
"/resource",
handler(|_req, _state| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({"patched": true}))
}),
)
.seal();
let port = app.bind_ephemeral().await.unwrap();
let response = reqwest::Client::new()
.patch(format!("http://127.0.0.1:{port}/resource"))
.send()
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body: serde_json::Value = response.json().await.unwrap();
assert_eq!(body["patched"], true);
}
#[tokio::test]
async fn a_wrong_method_on_a_patch_route_reports_patch_in_allow() {
let app = RouteBuilder::stateless()
.patch(
"/resource",
handler(|_req, _state| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({"patched": true}))
}),
)
.seal();
let port = app.bind_ephemeral().await.unwrap();
let response = reqwest::get(format!("http://127.0.0.1:{port}/resource")).await.unwrap();
assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
let allow = response
.headers()
.get("allow")
.expect("a 405 must carry Allow")
.to_str()
.unwrap()
.to_string();
assert!(allow.contains("PATCH"), "Allow omitted PATCH: {allow}");
}
#[tokio::test]
async fn the_allow_header_lists_exactly_the_registered_methods() {
let app = RouteBuilder::stateless()
.get("/r", handler(|_, _| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({}))
}))
.post("/r", handler(|_, _| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({}))
}))
.put("/r", handler(|_, _| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({}))
}))
.patch("/r", handler(|_, _| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({}))
}))
.seal();
let port = app.bind_ephemeral().await.expect("failed to bind");
let resp = reqwest::Client::new()
.request(reqwest::Method::DELETE, format!("http://127.0.0.1:{port}/r"))
.send()
.await
.expect("request failed");
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(
resp.headers().get("allow").and_then(|h| h.to_str().ok()),
Some("GET, HEAD, PATCH, POST, PUT"),
"Allow must list every registered method exactly once, sorted"
);
}
#[tokio::test]
async fn re_registering_a_method_replaces_rather_than_shadows() {
let app = RouteBuilder::stateless()
.get("/r", handler(|_, _| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({"which": "first"}))
}))
.get("/r", handler(|_, _| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({"which": "second"}))
}))
.post("/r", handler(|_, _| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({}))
}))
.seal();
let port = app.bind_ephemeral().await.expect("failed to bind");
let body: serde_json::Value = reqwest::get(&format!("http://127.0.0.1:{port}/r"))
.await
.expect("request failed")
.json()
.await
.expect("failed to parse json");
assert_eq!(
body.get("which").and_then(|v| v.as_str()),
Some("second"),
"the later registration must win, as HashMap::insert did"
);
let resp = reqwest::Client::new()
.request(reqwest::Method::DELETE, format!("http://127.0.0.1:{port}/r"))
.send()
.await
.expect("request failed");
assert_eq!(
resp.headers().get("allow").and_then(|h| h.to_str().ok()),
Some("GET, HEAD, POST"),
"a re-registered method must not appear twice in Allow"
);
}