mini-serve 0.13.12

An HTTP server: trie router, middleware, CORS, optional TLS. Built on hyper + tokio.
Documentation
use super::*;
use crate::error::ServeError;
use hyper::Response;
use hyper::body::Bytes;

fn dummy_handler() -> Handler<()> {
	crate::handler::handler(|_, _| async {
		Ok::<_, ServeError>(Response::new(crate::handler::body(Bytes::from("ok"))))
	})
}

#[test]
fn insert_and_match_static() {
	let mut router = Router::new();
	router.insert(Method::GET, "/hello", dummy_handler());
	let (_, _) = router.match_route(&Method::GET, "/hello").unwrap();
}

#[test]
fn match_with_path_param() {
	let mut router = Router::new();
	router.insert(Method::GET, "/users/:id", dummy_handler());
	let (_, params) = router.match_route(&Method::GET, "/users/42").unwrap();
	assert_eq!(params.0.get("id").unwrap(), "42");
}

#[test]
fn match_with_wildcard() {
	let mut router = Router::new();
	router.insert(Method::GET, "/files/*", dummy_handler());
	let (_, params) = router.match_route(&Method::GET, "/files/a/b/c").unwrap();
	assert_eq!(params.0.get("*").unwrap(), "a/b/c");
}

#[test]
fn no_match_for_unregistered_route() {
	let mut router = Router::new();
	router.insert(Method::GET, "/hello", dummy_handler());
	assert!(router.match_route(&Method::GET, "/world").is_none());
}

#[test]
fn method_mismatch_returns_none() {
	let mut router = Router::new();
	router.insert(Method::GET, "/hello", dummy_handler());
	assert!(router.match_route(&Method::POST, "/hello").is_none());
}

#[test]
fn root_path_matches() {
	let mut router = Router::new();
	router.insert(Method::GET, "/", dummy_handler());
	let (_, _) = router.match_route(&Method::GET, "/").unwrap();
}

#[test]
fn method_mismatch_on_static_branch_backtracks_to_param_sibling() {
	let mut router = Router::new();
	router.insert(Method::GET, "/users/:id", dummy_handler());
	router.insert(Method::POST, "/users/new", dummy_handler());

	// The static "new" branch matches the path but has no GET handler;
	// the search must fall back to the ":id" param branch rather than
	// reporting no match.
	let (_, params) = router.match_route(&Method::GET, "/users/new").unwrap();
	assert_eq!(params.0.get("id").unwrap(), "new");
}

#[test]
fn static_branch_with_matching_method_still_wins_over_param_sibling() {
	let mut router = Router::new();
	router.insert(Method::GET, "/users/:id", dummy_handler());
	router.insert(Method::GET, "/users/new", dummy_handler());

	let (_, params) = router.match_route(&Method::GET, "/users/new").unwrap();
	assert!(params.0.is_empty(), "static branch should win, not fall back to :id");
}

#[test]
fn allowed_methods_unions_across_ambiguous_branches() {
	let mut router = Router::new();
	router.insert(Method::GET, "/users/:id", dummy_handler());
	router.insert(Method::POST, "/users/new", dummy_handler());

	let mut allowed = router.allowed_methods("/users/new");
	allowed.sort_by_key(|m| m.to_string());
	assert_eq!(allowed, vec![Method::GET, Method::POST]);
}

#[test]
fn static_path_traversal_borrows_params_without_cloning() {
	let mut router = Router::new();
	// Deep static path: /api/v1/users/profile
	router.insert(Method::GET, "/api/v1/users/profile", dummy_handler());
	let (_, params) = router.match_route(&Method::GET, "/api/v1/users/profile").unwrap();

	// For a pure static path with no params, params map should be empty.
	// More importantly, the optimization ensures we don't clone params
	// while traversing static segments.
	assert!(params.0.is_empty());
}

#[test]
fn param_backtracking_truncates_params_on_failure() {
	let mut router = Router::new();
	// Route with param after static segments
	router.insert(Method::GET, "/users/:id", dummy_handler());
	// Also register a route that forces backtracking
	router.insert(Method::POST, "/users/new", dummy_handler());

	// GET /users/new should match the param route (static branch has no GET)
	let (_, params) = router.match_route(&Method::GET, "/users/new").unwrap();
	assert_eq!(params.0.get("id").unwrap(), "new");

	// Verify no extra params were left behind from the static branch attempt
	assert_eq!(params.0.len(), 1);
}

/// A param branch that matches a segment and then fails deeper must leave nothing
/// behind for the branch tried after it.
///
/// The router had no test for this: its only backtracking test exercised a static
/// branch, which adds no params and so cannot detect a failed undo. If the param
/// branch stopped cleaning up after itself, `x` would still be set when the
/// wildcard wins.
#[test]
fn a_failed_param_branch_leaves_no_params_behind() {
	let mut router: Router<()> = Router::new();
	router.insert(Method::GET, "/a/:x/b", dummy_handler());
	router.insert(Method::GET, "/a/*", dummy_handler());

	// `:x` matches "z", then "b" fails against "c", so the search falls through
	// to the wildcard.
	let (_, params) = router.match_route(&Method::GET, "/a/z/c").unwrap();

	assert_eq!(params.0.get("*").map(String::as_str), Some("z/c"));
	assert_eq!(
		params.0.get("x"),
		None,
		"the abandoned param branch left its capture behind: {params:?}"
	);
	assert_eq!(params.0.len(), 1, "only the wildcard should be captured: {params:?}");
}

/// Nested failure: the undo has to cover pushes made further down the tree, not
/// just the one made at this level.
#[test]
fn a_deeply_failed_branch_unwinds_every_level() {
	let mut router: Router<()> = Router::new();
	router.insert(Method::GET, "/a/:x/:y/deep", dummy_handler());
	router.insert(Method::GET, "/a/*", dummy_handler());

	// `:x` takes "1", `:y` takes "2", then "deep" fails against "no" — two levels
	// of captures to unwind before the wildcard is tried.
	let (_, params) = router.match_route(&Method::GET, "/a/1/2/no").unwrap();

	assert_eq!(params.0.get("*").map(String::as_str), Some("1/2/no"));
	assert_eq!(params.0.len(), 1, "stale captures survived the unwind: {params:?}");
}