mini-serve 0.6.0

An HTTP server: trie router, middleware, CORS, optional TLS. Built on hyper + tokio.
Documentation
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");
}