use crate::router::{
convert_path_params, delete, get, normalize_path_for_comparison, normalize_prefix, patch, post,
put, MethodRouter, RouteMatch, Router,
};
use http::Method;
use proptest::prelude::*;
use std::panic::{catch_unwind, AssertUnwindSafe};
#[test]
fn test_convert_path_params() {
assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
assert_eq!(
convert_path_params("/users/{user_id}/posts/{post_id}"),
"/users/:user_id/posts/:post_id"
);
assert_eq!(convert_path_params("/static/path"), "/static/path");
}
#[test]
fn test_normalize_path_for_comparison() {
assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
assert_eq!(
normalize_path_for_comparison("/users/:user_id"),
"/users/:_"
);
assert_eq!(
normalize_path_for_comparison("/users/:id/posts/:post_id"),
"/users/:_/posts/:_"
);
assert_eq!(
normalize_path_for_comparison("/static/path"),
"/static/path"
);
}
#[test]
fn test_normalize_prefix() {
assert_eq!(normalize_prefix("api"), "/api");
assert_eq!(normalize_prefix("/api"), "/api");
assert_eq!(normalize_prefix("/api/"), "/api");
assert_eq!(normalize_prefix("api/"), "/api");
assert_eq!(normalize_prefix("api/v1"), "/api/v1");
assert_eq!(normalize_prefix("/api/v1"), "/api/v1");
assert_eq!(normalize_prefix("/api/v1/"), "/api/v1");
assert_eq!(normalize_prefix(""), "/");
assert_eq!(normalize_prefix("/"), "/");
assert_eq!(normalize_prefix("//api"), "/api");
assert_eq!(normalize_prefix("api//v1"), "/api/v1");
assert_eq!(normalize_prefix("//api//v1//"), "/api/v1");
assert_eq!(normalize_prefix("///"), "/");
}
#[test]
#[should_panic(expected = "ROUTE CONFLICT DETECTED")]
fn test_route_conflict_detection() {
async fn handler1() -> &'static str {
"handler1"
}
async fn handler2() -> &'static str {
"handler2"
}
let _router = Router::new()
.route("/users/{id}", get(handler1))
.route("/users/{user_id}", get(handler2)); }
#[test]
fn test_no_conflict_different_paths() {
async fn handler1() -> &'static str {
"handler1"
}
async fn handler2() -> &'static str {
"handler2"
}
let router = Router::new()
.route("/users/{id}", get(handler1))
.route("/users/{id}/profile", get(handler2));
assert_eq!(router.registered_routes().len(), 2);
}
#[test]
fn test_route_info_tracking() {
async fn handler() -> &'static str {
"handler"
}
let router = Router::new().route("/users/{id}", get(handler));
let routes = router.registered_routes();
assert_eq!(routes.len(), 1);
let info = routes.get("/users/:id").unwrap();
assert_eq!(info.path, "/users/{id}");
assert_eq!(info.methods.len(), 1);
assert_eq!(info.methods[0], Method::GET);
}
#[test]
fn test_basic_router_nesting() {
async fn list_users() -> &'static str {
"list users"
}
async fn get_user() -> &'static str {
"get user"
}
let users_router = Router::new()
.route("/", get(list_users))
.route("/{id}", get(get_user));
let app = Router::new().nest("/api/users", users_router);
let routes = app.registered_routes();
assert_eq!(routes.len(), 2);
assert!(routes.contains_key("/api/users"));
assert!(routes.contains_key("/api/users/:id"));
let list_info = routes.get("/api/users").unwrap();
assert_eq!(list_info.path, "/api/users");
let get_info = routes.get("/api/users/:id").unwrap();
assert_eq!(get_info.path, "/api/users/{id}");
}
#[test]
fn test_nested_route_matching() {
async fn handler() -> &'static str {
"handler"
}
let users_router = Router::new().route("/{id}", get(handler));
let app = Router::new().nest("/api/users", users_router);
match app.match_route("/api/users/123", &Method::GET) {
RouteMatch::Found { params, .. } => {
assert_eq!(params.get("id"), Some(&"123".to_string()));
}
_ => panic!("Route should be found"),
}
}
#[test]
fn test_nested_route_matching_multiple_params() {
async fn handler() -> &'static str {
"handler"
}
let posts_router = Router::new().route("/{user_id}/posts/{post_id}", get(handler));
let app = Router::new().nest("/api", posts_router);
match app.match_route("/api/42/posts/100", &Method::GET) {
RouteMatch::Found { params, .. } => {
assert_eq!(params.get("user_id"), Some(&"42".to_string()));
assert_eq!(params.get("post_id"), Some(&"100".to_string()));
}
_ => panic!("Route should be found"),
}
}
#[test]
fn test_nested_route_matching_static_path() {
async fn handler() -> &'static str {
"handler"
}
let health_router = Router::new().route("/health", get(handler));
let app = Router::new().nest("/api/v1", health_router);
match app.match_route("/api/v1/health", &Method::GET) {
RouteMatch::Found { params, .. } => {
assert!(params.is_empty(), "Static path should have no params");
}
_ => panic!("Route should be found"),
}
}
#[test]
fn test_nested_route_not_found() {
async fn handler() -> &'static str {
"handler"
}
let users_router = Router::new().route("/users", get(handler));
let app = Router::new().nest("/api", users_router);
match app.match_route("/api/posts", &Method::GET) {
RouteMatch::NotFound => {
}
_ => panic!("Route should not be found"),
}
match app.match_route("/v2/users", &Method::GET) {
RouteMatch::NotFound => {
}
_ => panic!("Route with wrong prefix should not be found"),
}
}
#[test]
fn test_nested_route_method_not_allowed() {
async fn handler() -> &'static str {
"handler"
}
let users_router = Router::new().route("/users", get(handler));
let app = Router::new().nest("/api", users_router);
match app.match_route("/api/users", &Method::POST) {
RouteMatch::MethodNotAllowed { allowed } => {
assert!(allowed.contains(&Method::GET));
assert!(!allowed.contains(&Method::POST));
}
_ => panic!("Should return MethodNotAllowed"),
}
}
#[test]
fn test_nested_route_multiple_methods() {
async fn get_handler() -> &'static str {
"get"
}
async fn post_handler() -> &'static str {
"post"
}
let get_router = get(get_handler);
let post_router = post(post_handler);
let mut combined = MethodRouter::new();
for (method, handler) in get_router.handlers {
combined.handlers.insert(method, handler);
}
for (method, handler) in post_router.handlers {
combined.handlers.insert(method, handler);
}
let users_router = Router::new().route("/users", combined);
let app = Router::new().nest("/api", users_router);
match app.match_route("/api/users", &Method::GET) {
RouteMatch::Found { .. } => {}
_ => panic!("GET should be found"),
}
match app.match_route("/api/users", &Method::POST) {
RouteMatch::Found { .. } => {}
_ => panic!("POST should be found"),
}
match app.match_route("/api/users", &Method::DELETE) {
RouteMatch::MethodNotAllowed { allowed } => {
assert!(allowed.contains(&Method::GET));
assert!(allowed.contains(&Method::POST));
}
_ => panic!("DELETE should return MethodNotAllowed"),
}
}
#[test]
fn test_nested_router_prefix_normalization() {
async fn handler() -> &'static str {
"handler"
}
let router1 = Router::new().route("/test", get(handler));
let app1 = Router::new().nest("api", router1);
assert!(app1.registered_routes().contains_key("/api/test"));
let router2 = Router::new().route("/test", get(handler));
let app2 = Router::new().nest("/api/", router2);
assert!(app2.registered_routes().contains_key("/api/test"));
let router3 = Router::new().route("/test", get(handler));
let app3 = Router::new().nest("//api//", router3);
assert!(app3.registered_routes().contains_key("/api/test"));
}
#[test]
fn test_state_tracking() {
#[derive(Clone)]
struct MyState(#[allow(dead_code)] String);
let router = Router::new().state(MyState("test".to_string()));
assert!(router.has_state::<MyState>());
assert!(!router.has_state::<String>());
}
#[test]
fn test_state_merge_nested_only() {
#[derive(Clone, PartialEq, Debug)]
struct NestedState(String);
async fn handler() -> &'static str {
"handler"
}
let state_source = Router::new().state(NestedState("nested".to_string()));
let nested = Router::new().route("/test", get(handler));
let parent = Router::new()
.nest("/api", nested)
.merge_state::<NestedState>(&state_source);
assert!(parent.has_state::<NestedState>());
let state = parent.state.get::<NestedState>().unwrap();
assert_eq!(state.0, "nested");
}
#[test]
fn test_state_merge_parent_wins() {
#[derive(Clone, PartialEq, Debug)]
struct SharedState(String);
async fn handler() -> &'static str {
"handler"
}
let state_source = Router::new().state(SharedState("nested".to_string()));
let nested = Router::new().route("/test", get(handler));
let parent = Router::new()
.state(SharedState("parent".to_string()))
.nest("/api", nested)
.merge_state::<SharedState>(&state_source);
assert!(parent.has_state::<SharedState>());
let state = parent.state.get::<SharedState>().unwrap();
assert_eq!(state.0, "parent");
}
#[test]
fn test_state_type_ids_merged_on_nest() {
#[derive(Clone)]
struct NestedState(#[allow(dead_code)] String);
async fn handler() -> &'static str {
"handler"
}
let nested = Router::new()
.route("/test", get(handler))
.state(NestedState("nested".to_string()));
let parent = Router::new().nest("/api", nested);
assert!(parent
.state_type_ids()
.contains(&std::any::TypeId::of::<NestedState>()));
}
#[test]
#[should_panic(expected = "ROUTE CONFLICT DETECTED")]
fn test_nested_route_conflict_with_existing_route() {
async fn handler1() -> &'static str {
"handler1"
}
async fn handler2() -> &'static str {
"handler2"
}
let parent = Router::new().route("/api/users/{id}", get(handler1));
let nested = Router::new().route("/{user_id}", get(handler2));
let _app = parent.nest("/api/users", nested);
}
#[test]
#[should_panic(expected = "ROUTE CONFLICT DETECTED")]
fn test_nested_route_conflict_same_path_different_param_names() {
async fn handler1() -> &'static str {
"handler1"
}
async fn handler2() -> &'static str {
"handler2"
}
let nested1 = Router::new().route("/{id}", get(handler1));
let nested2 = Router::new().route("/{user_id}", get(handler2));
let _app = Router::new()
.nest("/api/users", nested1)
.nest("/api/users", nested2);
}
#[test]
fn test_nested_route_conflict_error_contains_both_paths() {
use std::panic::{catch_unwind, AssertUnwindSafe};
async fn handler1() -> &'static str {
"handler1"
}
async fn handler2() -> &'static str {
"handler2"
}
let result = catch_unwind(AssertUnwindSafe(|| {
let parent = Router::new().route("/api/users/{id}", get(handler1));
let nested = Router::new().route("/{user_id}", get(handler2));
let _app = parent.nest("/api/users", nested);
}));
assert!(result.is_err(), "Should have panicked due to conflict");
if let Err(panic_info) = result {
if let Some(msg) = panic_info.downcast_ref::<String>() {
assert!(
msg.contains("ROUTE CONFLICT DETECTED"),
"Error should contain 'ROUTE CONFLICT DETECTED'"
);
assert!(
msg.contains("Existing:") && msg.contains("New:"),
"Error should contain both 'Existing:' and 'New:' labels"
);
assert!(
msg.contains("How to resolve:"),
"Error should contain resolution guidance"
);
}
}
}
#[test]
fn test_nested_routes_no_conflict_different_prefixes() {
async fn handler1() -> &'static str {
"handler1"
}
async fn handler2() -> &'static str {
"handler2"
}
let nested1 = Router::new().route("/{id}", get(handler1));
let nested2 = Router::new().route("/{id}", get(handler2));
let app = Router::new()
.nest("/api/users", nested1)
.nest("/api/posts", nested2);
assert_eq!(app.registered_routes().len(), 2);
assert!(app.registered_routes().contains_key("/api/users/:id"));
assert!(app.registered_routes().contains_key("/api/posts/:id"));
}
#[test]
fn test_multiple_router_composition_all_routes_registered() {
async fn users_list() -> &'static str {
"users list"
}
async fn users_get() -> &'static str {
"users get"
}
async fn posts_list() -> &'static str {
"posts list"
}
async fn posts_get() -> &'static str {
"posts get"
}
async fn comments_list() -> &'static str {
"comments list"
}
let users_router = Router::new()
.route("/", get(users_list))
.route("/{id}", get(users_get));
let posts_router = Router::new()
.route("/", get(posts_list))
.route("/{id}", get(posts_get));
let comments_router = Router::new().route("/", get(comments_list));
let app = Router::new()
.nest("/api/users", users_router)
.nest("/api/posts", posts_router)
.nest("/api/comments", comments_router);
let routes = app.registered_routes();
assert_eq!(routes.len(), 5, "Should have 5 routes registered");
assert!(
routes.contains_key("/api/users"),
"Should have /api/users route"
);
assert!(
routes.contains_key("/api/users/:id"),
"Should have /api/users/:id route"
);
assert!(
routes.contains_key("/api/posts"),
"Should have /api/posts route"
);
assert!(
routes.contains_key("/api/posts/:id"),
"Should have /api/posts/:id route"
);
assert!(
routes.contains_key("/api/comments"),
"Should have /api/comments route"
);
}
#[test]
fn test_multiple_router_composition_no_interference() {
async fn users_handler() -> &'static str {
"users"
}
async fn posts_handler() -> &'static str {
"posts"
}
async fn admin_handler() -> &'static str {
"admin"
}
let users_router = Router::new()
.route("/list", get(users_handler))
.route("/{id}", get(users_handler));
let posts_router = Router::new()
.route("/list", get(posts_handler))
.route("/{id}", get(posts_handler));
let admin_router = Router::new()
.route("/list", get(admin_handler))
.route("/{id}", get(admin_handler));
let app = Router::new()
.nest("/api/v1/users", users_router)
.nest("/api/v1/posts", posts_router)
.nest("/admin", admin_router);
let routes = app.registered_routes();
assert_eq!(routes.len(), 6, "Should have 6 routes registered");
assert!(routes.contains_key("/api/v1/users/list"));
assert!(routes.contains_key("/api/v1/users/:id"));
assert!(routes.contains_key("/api/v1/posts/list"));
assert!(routes.contains_key("/api/v1/posts/:id"));
assert!(routes.contains_key("/admin/list"));
assert!(routes.contains_key("/admin/:id"));
match app.match_route("/api/v1/users/list", &Method::GET) {
RouteMatch::Found { params, .. } => {
assert!(params.is_empty(), "Static path should have no params");
}
_ => panic!("Should find /api/v1/users/list"),
}
match app.match_route("/api/v1/posts/123", &Method::GET) {
RouteMatch::Found { params, .. } => {
assert_eq!(params.get("id"), Some(&"123".to_string()));
}
_ => panic!("Should find /api/v1/posts/123"),
}
match app.match_route("/admin/456", &Method::GET) {
RouteMatch::Found { params, .. } => {
assert_eq!(params.get("id"), Some(&"456".to_string()));
}
_ => panic!("Should find /admin/456"),
}
}
#[test]
fn test_multiple_router_composition_with_multiple_methods() {
async fn get_handler() -> &'static str {
"get"
}
async fn post_handler() -> &'static str {
"post"
}
async fn put_handler() -> &'static str {
"put"
}
let get_router = get(get_handler);
let post_router = post(post_handler);
let mut users_root_combined = MethodRouter::new();
for (method, handler) in get_router.handlers {
users_root_combined.handlers.insert(method, handler);
}
for (method, handler) in post_router.handlers {
users_root_combined.handlers.insert(method, handler);
}
let get_router2 = get(get_handler);
let put_router = put(put_handler);
let mut users_id_combined = MethodRouter::new();
for (method, handler) in get_router2.handlers {
users_id_combined.handlers.insert(method, handler);
}
for (method, handler) in put_router.handlers {
users_id_combined.handlers.insert(method, handler);
}
let users_router = Router::new()
.route("/", users_root_combined)
.route("/{id}", users_id_combined);
let get_router3 = get(get_handler);
let post_router2 = post(post_handler);
let mut posts_root_combined = MethodRouter::new();
for (method, handler) in get_router3.handlers {
posts_root_combined.handlers.insert(method, handler);
}
for (method, handler) in post_router2.handlers {
posts_root_combined.handlers.insert(method, handler);
}
let posts_router = Router::new().route("/", posts_root_combined);
let app = Router::new()
.nest("/users", users_router)
.nest("/posts", posts_router);
let routes = app.registered_routes();
assert_eq!(routes.len(), 3, "Should have 3 routes registered");
let users_root = routes.get("/users").unwrap();
assert!(users_root.methods.contains(&Method::GET));
assert!(users_root.methods.contains(&Method::POST));
let users_id = routes.get("/users/:id").unwrap();
assert!(users_id.methods.contains(&Method::GET));
assert!(users_id.methods.contains(&Method::PUT));
let posts_root = routes.get("/posts").unwrap();
assert!(posts_root.methods.contains(&Method::GET));
assert!(posts_root.methods.contains(&Method::POST));
match app.match_route("/users", &Method::GET) {
RouteMatch::Found { .. } => {}
_ => panic!("GET /users should be found"),
}
match app.match_route("/users", &Method::POST) {
RouteMatch::Found { .. } => {}
_ => panic!("POST /users should be found"),
}
match app.match_route("/users/123", &Method::PUT) {
RouteMatch::Found { .. } => {}
_ => panic!("PUT /users/123 should be found"),
}
}
#[test]
fn test_multiple_router_composition_deep_nesting() {
async fn handler() -> &'static str {
"handler"
}
let deep_router = Router::new().route("/action", get(handler));
let mid_router = Router::new().route("/info", get(handler));
let shallow_router = Router::new().route("/status", get(handler));
let app = Router::new()
.nest("/api/v1/resources/items", deep_router)
.nest("/api/v1/resources", mid_router)
.nest("/api", shallow_router);
let routes = app.registered_routes();
assert_eq!(routes.len(), 3, "Should have 3 routes registered");
assert!(routes.contains_key("/api/v1/resources/items/action"));
assert!(routes.contains_key("/api/v1/resources/info"));
assert!(routes.contains_key("/api/status"));
match app.match_route("/api/v1/resources/items/action", &Method::GET) {
RouteMatch::Found { .. } => {}
_ => panic!("Should find deep route"),
}
match app.match_route("/api/v1/resources/info", &Method::GET) {
RouteMatch::Found { .. } => {}
_ => panic!("Should find mid route"),
}
match app.match_route("/api/status", &Method::GET) {
RouteMatch::Found { .. } => {}
_ => panic!("Should find shallow route"),
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_normalized_prefix_starts_with_single_slash(
leading_slashes in prop::collection::vec(Just('/'), 0..5),
segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 0..4),
trailing_slashes in prop::collection::vec(Just('/'), 0..5),
) {
let mut prefix = String::new();
for _ in &leading_slashes {
prefix.push('/');
}
for (i, segment) in segments.iter().enumerate() {
if i > 0 {
prefix.push('/');
}
prefix.push_str(segment);
}
for _ in &trailing_slashes {
prefix.push('/');
}
let normalized = normalize_prefix(&prefix);
prop_assert!(
normalized.starts_with('/'),
"Normalized prefix '{}' should start with '/', input was '{}'",
normalized, prefix
);
prop_assert!(
!normalized.starts_with("//"),
"Normalized prefix '{}' should not start with '//', input was '{}'",
normalized, prefix
);
}
#[test]
fn prop_normalized_prefix_no_trailing_slash(
segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
trailing_slashes in prop::collection::vec(Just('/'), 0..5),
) {
let mut prefix = String::from("/");
for (i, segment) in segments.iter().enumerate() {
if i > 0 {
prefix.push('/');
}
prefix.push_str(segment);
}
for _ in &trailing_slashes {
prefix.push('/');
}
let normalized = normalize_prefix(&prefix);
prop_assert!(
!normalized.ends_with('/'),
"Normalized prefix '{}' should not end with '/', input was '{}'",
normalized, prefix
);
}
#[test]
fn prop_normalized_prefix_no_double_slashes(
segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
extra_slashes in prop::collection::vec(0..4usize, 1..4),
) {
let mut prefix = String::from("/");
for (i, segment) in segments.iter().enumerate() {
if i > 0 {
let num_slashes = extra_slashes.get(i).copied().unwrap_or(1);
for _ in 0..=num_slashes {
prefix.push('/');
}
}
prefix.push_str(segment);
}
let normalized = normalize_prefix(&prefix);
prop_assert!(
!normalized.contains("//"),
"Normalized prefix '{}' should not contain '//', input was '{}'",
normalized, prefix
);
}
#[test]
fn prop_normalized_prefix_preserves_segments(
segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..4),
) {
let prefix = format!("/{}", segments.join("/"));
let normalized = normalize_prefix(&prefix);
let normalized_segments: Vec<&str> = normalized
.split('/')
.filter(|s| !s.is_empty())
.collect();
prop_assert_eq!(
segments.len(),
normalized_segments.len(),
"Segment count should be preserved"
);
for (original, normalized_seg) in segments.iter().zip(normalized_segments.iter()) {
prop_assert_eq!(
original, normalized_seg,
"Segment content should be preserved"
);
}
}
#[test]
fn prop_empty_or_slashes_normalize_to_root(
num_slashes in 0..10usize,
) {
let prefix = "/".repeat(num_slashes);
let normalized = normalize_prefix(&prefix);
prop_assert_eq!(
normalized, "/",
"Empty or slash-only prefix '{}' should normalize to '/'",
prefix
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_method_router_clone_preserves_methods(
use_get in any::<bool>(),
use_post in any::<bool>(),
use_put in any::<bool>(),
use_patch in any::<bool>(),
use_delete in any::<bool>(),
) {
prop_assume!(use_get || use_post || use_put || use_patch || use_delete);
let mut method_router = MethodRouter::new();
let mut expected_methods: Vec<Method> = Vec::new();
async fn handler() -> &'static str { "handler" }
if use_get {
method_router = get(handler);
expected_methods.push(Method::GET);
}
if use_post {
let post_router = post(handler);
for (method, handler) in post_router.handlers {
method_router.handlers.insert(method.clone(), handler);
if !expected_methods.contains(&method) {
expected_methods.push(method);
}
}
}
if use_put {
let put_router = put(handler);
for (method, handler) in put_router.handlers {
method_router.handlers.insert(method.clone(), handler);
if !expected_methods.contains(&method) {
expected_methods.push(method);
}
}
}
if use_patch {
let patch_router = patch(handler);
for (method, handler) in patch_router.handlers {
method_router.handlers.insert(method.clone(), handler);
if !expected_methods.contains(&method) {
expected_methods.push(method);
}
}
}
if use_delete {
let delete_router = delete(handler);
for (method, handler) in delete_router.handlers {
method_router.handlers.insert(method.clone(), handler);
if !expected_methods.contains(&method) {
expected_methods.push(method);
}
}
}
let cloned_router = method_router.clone();
let original_methods = method_router.allowed_methods();
let cloned_methods = cloned_router.allowed_methods();
prop_assert_eq!(
original_methods.len(),
cloned_methods.len(),
"Cloned router should have same number of methods"
);
for method in &expected_methods {
prop_assert!(
cloned_router.get_handler(method).is_some(),
"Cloned router should have handler for method {:?}",
method
);
}
for method in &cloned_methods {
prop_assert!(
cloned_router.get_handler(method).is_some(),
"Handler for {:?} should be accessible after clone",
method
);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_nested_routes_have_prefix(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
has_param in any::<bool>(),
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix_segments.join("/"));
let mut route_path = format!("/{}", route_segments.join("/"));
if has_param {
route_path.push_str("/{id}");
}
let nested_router = Router::new().route(&route_path, get(handler));
let app = Router::new().nest(&prefix, nested_router);
let expected_matchit_path = if has_param {
format!("{}/{}/:id", prefix, route_segments.join("/"))
} else {
format!("{}/{}", prefix, route_segments.join("/"))
};
let routes = app.registered_routes();
prop_assert!(
routes.contains_key(&expected_matchit_path),
"Expected route '{}' not found. Available routes: {:?}",
expected_matchit_path,
routes.keys().collect::<Vec<_>>()
);
let route_info = routes.get(&expected_matchit_path).unwrap();
let expected_display_path = format!("{}{}", prefix, route_path);
prop_assert_eq!(
&route_info.path, &expected_display_path,
"Display path should be prefix + original path"
);
}
#[test]
fn prop_route_count_preserved_after_nesting(
num_routes in 1..4usize,
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix_segments.join("/"));
let mut nested_router = Router::new();
for i in 0..num_routes {
let path = format!("/route{}", i);
nested_router = nested_router.route(&path, get(handler));
}
let app = Router::new().nest(&prefix, nested_router);
prop_assert_eq!(
app.registered_routes().len(),
num_routes,
"Number of routes should be preserved after nesting"
);
}
#[test]
fn prop_nested_routes_are_matchable(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix_segments.join("/"));
let route_path = format!("/{}", route_segments.join("/"));
let nested_router = Router::new().route(&route_path, get(handler));
let app = Router::new().nest(&prefix, nested_router);
let full_path = format!("{}{}", prefix, route_path);
match app.match_route(&full_path, &Method::GET) {
RouteMatch::Found { .. } => {
}
RouteMatch::NotFound => {
prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
}
RouteMatch::MethodNotAllowed { .. } => {
prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
}
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_state_type_ids_merged(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
has_nested_state in any::<bool>(),
) {
#[derive(Clone)]
struct TestState(#[allow(dead_code)] i32);
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix_segments.join("/"));
let mut nested = Router::new().route("/test", get(handler));
if has_nested_state {
nested = nested.state(TestState(42));
}
let parent = Router::new().nest(&prefix, nested);
if has_nested_state {
prop_assert!(
parent.state_type_ids().contains(&std::any::TypeId::of::<TestState>()),
"Parent should track nested state type ID"
);
}
}
#[test]
fn prop_merge_state_adds_nested_state(
state_value in any::<i32>(),
) {
#[derive(Clone, PartialEq, Debug)]
struct UniqueState(i32);
let source = Router::new().state(UniqueState(state_value));
let parent = Router::new().merge_state::<UniqueState>(&source);
prop_assert!(
parent.has_state::<UniqueState>(),
"Parent should have state after merge"
);
let merged_state = parent.state.get::<UniqueState>().unwrap();
prop_assert_eq!(
merged_state.0, state_value,
"Merged state value should match source"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_parent_state_takes_precedence(
parent_value in any::<i32>(),
nested_value in any::<i32>(),
) {
prop_assume!(parent_value != nested_value);
#[derive(Clone, PartialEq, Debug)]
struct SharedState(i32);
let source = Router::new().state(SharedState(nested_value));
let parent = Router::new()
.state(SharedState(parent_value))
.merge_state::<SharedState>(&source);
prop_assert!(
parent.has_state::<SharedState>(),
"Parent should have state"
);
let final_state = parent.state.get::<SharedState>().unwrap();
prop_assert_eq!(
final_state.0, parent_value,
"Parent state value should be preserved, not overwritten by nested"
);
}
#[test]
fn prop_state_precedence_consistent(
parent_value in any::<i32>(),
source1_value in any::<i32>(),
source2_value in any::<i32>(),
) {
#[derive(Clone, PartialEq, Debug)]
struct ConsistentState(i32);
let source1 = Router::new().state(ConsistentState(source1_value));
let source2 = Router::new().state(ConsistentState(source2_value));
let parent = Router::new()
.state(ConsistentState(parent_value))
.merge_state::<ConsistentState>(&source1)
.merge_state::<ConsistentState>(&source2);
let final_state = parent.state.get::<ConsistentState>().unwrap();
prop_assert_eq!(
final_state.0, parent_value,
"Parent state should be preserved after multiple merges"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_same_structure_different_param_names_conflict(
segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
param1 in "[a-z][a-z0-9]{0,5}",
param2 in "[a-z][a-z0-9]{0,5}",
) {
prop_assume!(param1 != param2);
let mut path1 = String::from("/");
let mut path2 = String::from("/");
for segment in &segments {
path1.push_str(segment);
path1.push('/');
path2.push_str(segment);
path2.push('/');
}
path1.push('{');
path1.push_str(¶m1);
path1.push('}');
path2.push('{');
path2.push_str(¶m2);
path2.push('}');
let result = catch_unwind(AssertUnwindSafe(|| {
async fn handler1() -> &'static str { "handler1" }
async fn handler2() -> &'static str { "handler2" }
let _router = Router::new()
.route(&path1, get(handler1))
.route(&path2, get(handler2));
}));
prop_assert!(
result.is_err(),
"Routes '{}' and '{}' should conflict but didn't",
path1, path2
);
}
#[test]
fn prop_different_structures_no_conflict(
segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
has_param1 in any::<bool>(),
has_param2 in any::<bool>(),
) {
let mut path1 = String::from("/");
let mut path2 = String::from("/");
for segment in &segments1 {
path1.push_str(segment);
path1.push('/');
}
path1.pop();
for segment in &segments2 {
path2.push_str(segment);
path2.push('/');
}
path2.pop();
if has_param1 {
path1.push_str("/{id}");
}
if has_param2 {
path2.push_str("/{id}");
}
let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
prop_assume!(norm1 != norm2);
let result = catch_unwind(AssertUnwindSafe(|| {
async fn handler1() -> &'static str { "handler1" }
async fn handler2() -> &'static str { "handler2" }
let router = Router::new()
.route(&path1, get(handler1))
.route(&path2, get(handler2));
router.registered_routes().len()
}));
prop_assert!(
result.is_ok(),
"Routes '{}' and '{}' should not conflict but did",
path1, path2
);
if let Ok(count) = result {
prop_assert_eq!(count, 2, "Should have registered 2 routes");
}
}
#[test]
fn prop_conflict_error_contains_both_paths(
segment in "[a-z][a-z0-9]{1,5}",
param1 in "[a-z][a-z0-9]{1,5}",
param2 in "[a-z][a-z0-9]{1,5}",
) {
prop_assume!(param1 != param2);
let path1 = format!("/{}/{{{}}}", segment, param1);
let path2 = format!("/{}/{{{}}}", segment, param2);
let result = catch_unwind(AssertUnwindSafe(|| {
async fn handler1() -> &'static str { "handler1" }
async fn handler2() -> &'static str { "handler2" }
let _router = Router::new()
.route(&path1, get(handler1))
.route(&path2, get(handler2));
}));
prop_assert!(result.is_err(), "Should have panicked due to conflict");
if let Err(panic_info) = result {
if let Some(msg) = panic_info.downcast_ref::<String>() {
prop_assert!(
msg.contains("ROUTE CONFLICT DETECTED"),
"Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
msg
);
prop_assert!(
msg.contains("Existing:") && msg.contains("New:"),
"Error should contain both 'Existing:' and 'New:' labels, got: {}",
msg
);
prop_assert!(
msg.contains("How to resolve:"),
"Error should contain resolution guidance, got: {}",
msg
);
}
}
}
#[test]
fn prop_exact_duplicate_paths_conflict(
segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
has_param in any::<bool>(),
) {
let mut path = String::from("/");
for segment in &segments {
path.push_str(segment);
path.push('/');
}
path.pop();
if has_param {
path.push_str("/{id}");
}
let result = catch_unwind(AssertUnwindSafe(|| {
async fn handler1() -> &'static str { "handler1" }
async fn handler2() -> &'static str { "handler2" }
let _router = Router::new()
.route(&path, get(handler1))
.route(&path, get(handler2));
}));
prop_assert!(
result.is_err(),
"Registering path '{}' twice should conflict but didn't",
path
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_nested_route_with_params_matches(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
param_value in "[a-z0-9]{1,10}",
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix_segments.join("/"));
let route_path = if route_segments.is_empty() {
"/{id}".to_string()
} else {
format!("/{}/{{id}}", route_segments.join("/"))
};
let nested_router = Router::new().route(&route_path, get(handler));
let app = Router::new().nest(&prefix, nested_router);
let full_path = if route_segments.is_empty() {
format!("{}/{}", prefix, param_value)
} else {
format!("{}/{}/{}", prefix, route_segments.join("/"), param_value)
};
match app.match_route(&full_path, &Method::GET) {
RouteMatch::Found { params, .. } => {
prop_assert!(
params.contains_key("id"),
"Should have 'id' parameter, got: {:?}",
params
);
prop_assert_eq!(
params.get("id").unwrap(),
¶m_value,
"Parameter value should match"
);
}
RouteMatch::NotFound => {
prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
}
RouteMatch::MethodNotAllowed { .. } => {
prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
}
}
}
#[test]
fn prop_nested_route_matches_correct_method(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
use_get in any::<bool>(),
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix_segments.join("/"));
let route_path = format!("/{}", route_segments.join("/"));
let method_router = if use_get { get(handler) } else { post(handler) };
let nested_router = Router::new().route(&route_path, method_router);
let app = Router::new().nest(&prefix, nested_router);
let full_path = format!("{}{}", prefix, route_path);
let registered_method = if use_get { Method::GET } else { Method::POST };
let other_method = if use_get { Method::POST } else { Method::GET };
match app.match_route(&full_path, ®istered_method) {
RouteMatch::Found { .. } => {
}
other => {
prop_assert!(false, "Route should be found for registered method, got: {:?}",
match other {
RouteMatch::NotFound => "NotFound",
RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
_ => "Found",
}
);
}
}
match app.match_route(&full_path, &other_method) {
RouteMatch::MethodNotAllowed { allowed } => {
prop_assert!(
allowed.contains(®istered_method),
"Allowed methods should contain {:?}",
registered_method
);
}
other => {
prop_assert!(false, "Route should return MethodNotAllowed for other method, got: {:?}",
match other {
RouteMatch::NotFound => "NotFound",
RouteMatch::Found { .. } => "Found",
_ => "MethodNotAllowed",
}
);
}
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_single_param_extraction(
prefix in "[a-z][a-z0-9]{1,5}",
param_name in "[a-z][a-z0-9]{1,5}",
param_value in "[a-z0-9]{1,10}",
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix);
let route_path = format!("/{{{}}}", param_name);
let nested_router = Router::new().route(&route_path, get(handler));
let app = Router::new().nest(&prefix, nested_router);
let full_path = format!("{}/{}", prefix, param_value);
match app.match_route(&full_path, &Method::GET) {
RouteMatch::Found { params, .. } => {
prop_assert!(
params.contains_key(¶m_name),
"Should have '{}' parameter, got: {:?}",
param_name, params
);
prop_assert_eq!(
params.get(¶m_name).unwrap(),
¶m_value,
"Parameter '{}' value should be '{}'",
param_name, param_value
);
}
_ => {
prop_assert!(false, "Route should be found");
}
}
}
#[test]
fn prop_multiple_params_extraction(
prefix in "[a-z][a-z0-9]{1,5}",
param1_name in "[a-z]{1,5}",
param1_value in "[a-z0-9]{1,10}",
param2_name in "[a-z]{1,5}",
param2_value in "[a-z0-9]{1,10}",
) {
prop_assume!(param1_name != param2_name);
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix);
let route_path = format!("/{{{}}}/items/{{{}}}", param1_name, param2_name);
let nested_router = Router::new().route(&route_path, get(handler));
let app = Router::new().nest(&prefix, nested_router);
let full_path = format!("{}/{}/items/{}", prefix, param1_value, param2_value);
match app.match_route(&full_path, &Method::GET) {
RouteMatch::Found { params, .. } => {
prop_assert!(
params.contains_key(¶m1_name),
"Should have '{}' parameter, got: {:?}",
param1_name, params
);
prop_assert_eq!(
params.get(¶m1_name).unwrap(),
¶m1_value,
"Parameter '{}' value should be '{}'",
param1_name, param1_value
);
prop_assert!(
params.contains_key(¶m2_name),
"Should have '{}' parameter, got: {:?}",
param2_name, params
);
prop_assert_eq!(
params.get(¶m2_name).unwrap(),
¶m2_value,
"Parameter '{}' value should be '{}'",
param2_name, param2_value
);
}
_ => {
prop_assert!(false, "Route should be found");
}
}
}
#[test]
fn prop_param_value_preservation(
prefix in "[a-z]{1,5}",
param_value in "[a-zA-Z0-9_-]{1,15}",
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix);
let route_path = "/{id}".to_string();
let nested_router = Router::new().route(&route_path, get(handler));
let app = Router::new().nest(&prefix, nested_router);
let full_path = format!("{}/{}", prefix, param_value);
match app.match_route(&full_path, &Method::GET) {
RouteMatch::Found { params, .. } => {
prop_assert_eq!(
params.get("id").unwrap(),
¶m_value,
"Parameter value should be preserved exactly"
);
}
_ => {
prop_assert!(false, "Route should be found");
}
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_unregistered_path_returns_not_found(
prefix in "[a-z][a-z0-9]{1,5}",
route_segment in "[a-z][a-z0-9]{1,5}",
unregistered_segment in "[a-z][a-z0-9]{6,10}",
) {
prop_assume!(route_segment != unregistered_segment);
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix);
let route_path = format!("/{}", route_segment);
let nested_router = Router::new().route(&route_path, get(handler));
let app = Router::new().nest(&prefix, nested_router);
let unregistered_path = format!("{}/{}", prefix, unregistered_segment);
match app.match_route(&unregistered_path, &Method::GET) {
RouteMatch::NotFound => {
}
RouteMatch::Found { .. } => {
prop_assert!(false, "Path '{}' should not be found", unregistered_path);
}
RouteMatch::MethodNotAllowed { .. } => {
prop_assert!(false, "Path '{}' should return NotFound, not MethodNotAllowed", unregistered_path);
}
}
}
#[test]
fn prop_wrong_prefix_returns_not_found(
prefix1 in "[a-z][a-z0-9]{1,5}",
prefix2 in "[a-z][a-z0-9]{6,10}",
route_segment in "[a-z][a-z0-9]{1,5}",
) {
prop_assume!(prefix1 != prefix2);
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix1);
let route_path = format!("/{}", route_segment);
let nested_router = Router::new().route(&route_path, get(handler));
let app = Router::new().nest(&prefix, nested_router);
let wrong_prefix_path = format!("/{}/{}", prefix2, route_segment);
match app.match_route(&wrong_prefix_path, &Method::GET) {
RouteMatch::NotFound => {
}
_ => {
prop_assert!(false, "Path '{}' with wrong prefix should return NotFound", wrong_prefix_path);
}
}
}
#[test]
fn prop_partial_path_returns_not_found(
prefix in "[a-z][a-z0-9]{1,5}",
segment1 in "[a-z][a-z0-9]{1,5}",
segment2 in "[a-z][a-z0-9]{1,5}",
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix);
let route_path = format!("/{}/{}", segment1, segment2);
let nested_router = Router::new().route(&route_path, get(handler));
let app = Router::new().nest(&prefix, nested_router);
let partial_path = format!("{}/{}", prefix, segment1);
match app.match_route(&partial_path, &Method::GET) {
RouteMatch::NotFound => {
}
_ => {
prop_assert!(false, "Partial path '{}' should return NotFound", partial_path);
}
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_unregistered_method_returns_method_not_allowed(
prefix in "[a-z][a-z0-9]{1,5}",
route_segment in "[a-z][a-z0-9]{1,5}",
) {
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix);
let route_path = format!("/{}", route_segment);
let nested_router = Router::new().route(&route_path, get(handler));
let app = Router::new().nest(&prefix, nested_router);
let full_path = format!("{}{}", prefix, route_path);
match app.match_route(&full_path, &Method::POST) {
RouteMatch::MethodNotAllowed { allowed } => {
prop_assert!(
allowed.contains(&Method::GET),
"Allowed methods should contain GET, got: {:?}",
allowed
);
prop_assert!(
!allowed.contains(&Method::POST),
"Allowed methods should not contain POST"
);
}
RouteMatch::Found { .. } => {
prop_assert!(false, "POST should not be found on GET-only route");
}
RouteMatch::NotFound => {
prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
}
}
}
#[test]
fn prop_multiple_methods_in_allowed_list(
prefix in "[a-z][a-z0-9]{1,5}",
route_segment in "[a-z][a-z0-9]{1,5}",
use_get in any::<bool>(),
use_post in any::<bool>(),
use_put in any::<bool>(),
) {
prop_assume!(use_get || use_post || use_put);
async fn handler() -> &'static str { "handler" }
let prefix = format!("/{}", prefix);
let route_path = format!("/{}", route_segment);
let mut method_router = MethodRouter::new();
let mut expected_methods: Vec<Method> = Vec::new();
if use_get {
let get_router = get(handler);
for (method, h) in get_router.handlers {
method_router.handlers.insert(method.clone(), h);
expected_methods.push(method);
}
}
if use_post {
let post_router = post(handler);
for (method, h) in post_router.handlers {
method_router.handlers.insert(method.clone(), h);
expected_methods.push(method);
}
}
if use_put {
let put_router = put(handler);
for (method, h) in put_router.handlers {
method_router.handlers.insert(method.clone(), h);
expected_methods.push(method);
}
}
let nested_router = Router::new().route(&route_path, method_router);
let app = Router::new().nest(&prefix, nested_router);
let full_path = format!("{}{}", prefix, route_path);
match app.match_route(&full_path, &Method::DELETE) {
RouteMatch::MethodNotAllowed { allowed } => {
for method in &expected_methods {
prop_assert!(
allowed.contains(method),
"Allowed methods should contain {:?}, got: {:?}",
method, allowed
);
}
prop_assert!(
!allowed.contains(&Method::DELETE),
"Allowed methods should not contain DELETE"
);
}
RouteMatch::Found { .. } => {
prop_assert!(false, "DELETE should not be found");
}
RouteMatch::NotFound => {
prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
}
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_multiple_routers_all_routes_registered(
prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
num_routes1 in 1..4usize,
num_routes2 in 1..4usize,
) {
let prefix1 = format!("/{}", prefix1_segments.join("/"));
let prefix2 = format!("/{}", prefix2_segments.join("/"));
prop_assume!(prefix1 != prefix2);
async fn handler() -> &'static str { "handler" }
let mut router1 = Router::new();
for i in 0..num_routes1 {
let path = format!("/route1_{}", i);
router1 = router1.route(&path, get(handler));
}
let mut router2 = Router::new();
for i in 0..num_routes2 {
let path = format!("/route2_{}", i);
router2 = router2.route(&path, get(handler));
}
let app = Router::new()
.nest(&prefix1, router1)
.nest(&prefix2, router2);
let routes = app.registered_routes();
let expected_count = num_routes1 + num_routes2;
prop_assert_eq!(
routes.len(),
expected_count,
"Should have {} routes ({}+{}), got {}",
expected_count, num_routes1, num_routes2, routes.len()
);
for i in 0..num_routes1 {
let expected_path = format!("{}/route1_{}", prefix1, i);
let matchit_path = convert_path_params(&expected_path);
prop_assert!(
routes.contains_key(&matchit_path),
"Route '{}' should be registered",
expected_path
);
}
for i in 0..num_routes2 {
let expected_path = format!("{}/route2_{}", prefix2, i);
let matchit_path = convert_path_params(&expected_path);
prop_assert!(
routes.contains_key(&matchit_path),
"Route '{}' should be registered",
expected_path
);
}
}
#[test]
fn prop_multiple_routers_no_interference(
prefix1 in "[a-z][a-z0-9]{1,5}",
prefix2 in "[a-z][a-z0-9]{1,5}",
route_segment in "[a-z][a-z0-9]{1,5}",
param_value1 in "[a-z0-9]{1,10}",
param_value2 in "[a-z0-9]{1,10}",
) {
prop_assume!(prefix1 != prefix2);
let prefix1 = format!("/{}", prefix1);
let prefix2 = format!("/{}", prefix2);
async fn handler() -> &'static str { "handler" }
let router1 = Router::new()
.route(&format!("/{}", route_segment), get(handler))
.route("/{id}", get(handler));
let router2 = Router::new()
.route(&format!("/{}", route_segment), get(handler))
.route("/{id}", get(handler));
let app = Router::new()
.nest(&prefix1, router1)
.nest(&prefix2, router2);
let path1_static = format!("{}/{}", prefix1, route_segment);
match app.match_route(&path1_static, &Method::GET) {
RouteMatch::Found { params, .. } => {
prop_assert!(params.is_empty(), "Static path should have no params");
}
_ => {
prop_assert!(false, "Route '{}' should be found", path1_static);
}
}
let path1_param = format!("{}/{}", prefix1, param_value1);
match app.match_route(&path1_param, &Method::GET) {
RouteMatch::Found { params, .. } => {
prop_assert_eq!(
params.get("id"),
Some(¶m_value1.to_string()),
"Parameter should be extracted correctly"
);
}
_ => {
prop_assert!(false, "Route '{}' should be found", path1_param);
}
}
let path2_static = format!("{}/{}", prefix2, route_segment);
match app.match_route(&path2_static, &Method::GET) {
RouteMatch::Found { params, .. } => {
prop_assert!(params.is_empty(), "Static path should have no params");
}
_ => {
prop_assert!(false, "Route '{}' should be found", path2_static);
}
}
let path2_param = format!("{}/{}", prefix2, param_value2);
match app.match_route(&path2_param, &Method::GET) {
RouteMatch::Found { params, .. } => {
prop_assert_eq!(
params.get("id"),
Some(¶m_value2.to_string()),
"Parameter should be extracted correctly"
);
}
_ => {
prop_assert!(false, "Route '{}' should be found", path2_param);
}
}
}
#[test]
fn prop_multiple_routers_preserve_methods(
prefix1 in "[a-z][a-z0-9]{1,5}",
prefix2 in "[a-z][a-z0-9]{1,5}",
route_segment in "[a-z][a-z0-9]{1,5}",
router1_use_get in any::<bool>(),
router1_use_post in any::<bool>(),
router2_use_get in any::<bool>(),
router2_use_put in any::<bool>(),
) {
prop_assume!(router1_use_get || router1_use_post);
prop_assume!(router2_use_get || router2_use_put);
prop_assume!(prefix1 != prefix2);
let prefix1 = format!("/{}", prefix1);
let prefix2 = format!("/{}", prefix2);
let route_path = format!("/{}", route_segment);
async fn handler() -> &'static str { "handler" }
let mut method_router1 = MethodRouter::new();
let mut expected_methods1: Vec<Method> = Vec::new();
if router1_use_get {
let get_router = get(handler);
for (method, h) in get_router.handlers {
method_router1.handlers.insert(method.clone(), h);
expected_methods1.push(method);
}
}
if router1_use_post {
let post_router = post(handler);
for (method, h) in post_router.handlers {
method_router1.handlers.insert(method.clone(), h);
expected_methods1.push(method);
}
}
let mut method_router2 = MethodRouter::new();
let mut expected_methods2: Vec<Method> = Vec::new();
if router2_use_get {
let get_router = get(handler);
for (method, h) in get_router.handlers {
method_router2.handlers.insert(method.clone(), h);
expected_methods2.push(method);
}
}
if router2_use_put {
let put_router = put(handler);
for (method, h) in put_router.handlers {
method_router2.handlers.insert(method.clone(), h);
expected_methods2.push(method);
}
}
let router1 = Router::new().route(&route_path, method_router1);
let router2 = Router::new().route(&route_path, method_router2);
let app = Router::new()
.nest(&prefix1, router1)
.nest(&prefix2, router2);
let full_path1 = format!("{}{}", prefix1, route_path);
let full_path2 = format!("{}{}", prefix2, route_path);
for method in &expected_methods1 {
match app.match_route(&full_path1, method) {
RouteMatch::Found { .. } => {}
_ => {
prop_assert!(false, "Method {:?} should be found for {}", method, full_path1);
}
}
}
for method in &expected_methods2 {
match app.match_route(&full_path2, method) {
RouteMatch::Found { .. } => {}
_ => {
prop_assert!(false, "Method {:?} should be found for {}", method, full_path2);
}
}
}
if !expected_methods1.contains(&Method::DELETE) {
match app.match_route(&full_path1, &Method::DELETE) {
RouteMatch::MethodNotAllowed { allowed } => {
for method in &expected_methods1 {
prop_assert!(
allowed.contains(method),
"Allowed methods for {} should contain {:?}",
full_path1, method
);
}
}
_ => {
prop_assert!(false, "DELETE should return MethodNotAllowed for {}", full_path1);
}
}
}
}
#[test]
fn prop_three_routers_composition(
prefix1 in "[a-z]{1,3}",
prefix2 in "[a-z]{4,6}",
prefix3 in "[a-z]{7,9}",
num_routes in 1..3usize,
) {
let prefix1 = format!("/{}", prefix1);
let prefix2 = format!("/{}", prefix2);
let prefix3 = format!("/{}", prefix3);
async fn handler() -> &'static str { "handler" }
let mut router1 = Router::new();
let mut router2 = Router::new();
let mut router3 = Router::new();
for i in 0..num_routes {
let path = format!("/item{}", i);
router1 = router1.route(&path, get(handler));
router2 = router2.route(&path, get(handler));
router3 = router3.route(&path, get(handler));
}
let app = Router::new()
.nest(&prefix1, router1)
.nest(&prefix2, router2)
.nest(&prefix3, router3);
let routes = app.registered_routes();
let expected_count = 3 * num_routes;
prop_assert_eq!(
routes.len(),
expected_count,
"Should have {} routes, got {}",
expected_count, routes.len()
);
for i in 0..num_routes {
let path1 = format!("{}/item{}", prefix1, i);
let path2 = format!("{}/item{}", prefix2, i);
let path3 = format!("{}/item{}", prefix3, i);
match app.match_route(&path1, &Method::GET) {
RouteMatch::Found { .. } => {}
_ => prop_assert!(false, "Route '{}' should be found", path1),
}
match app.match_route(&path2, &Method::GET) {
RouteMatch::Found { .. } => {}
_ => prop_assert!(false, "Route '{}' should be found", path2),
}
match app.match_route(&path3, &Method::GET) {
RouteMatch::Found { .. } => {}
_ => prop_assert!(false, "Route '{}' should be found", path3),
}
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_nested_route_conflict_different_param_names(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
param1 in "[a-z][a-z0-9]{1,5}",
param2 in "[a-z][a-z0-9]{1,5}",
) {
prop_assume!(param1 != param2);
async fn handler1() -> &'static str { "handler1" }
async fn handler2() -> &'static str { "handler2" }
let prefix = format!("/{}", prefix_segments.join("/"));
let existing_path = if route_segments.is_empty() {
format!("{}/{{{}}}", prefix, param1)
} else {
format!("{}/{}/{{{}}}", prefix, route_segments.join("/"), param1)
};
let nested_path = if route_segments.is_empty() {
format!("/{{{}}}", param2)
} else {
format!("/{}/{{{}}}", route_segments.join("/"), param2)
};
let result = catch_unwind(AssertUnwindSafe(|| {
let parent = Router::new().route(&existing_path, get(handler1));
let nested = Router::new().route(&nested_path, get(handler2));
let _app = parent.nest(&prefix, nested);
}));
prop_assert!(
result.is_err(),
"Nested route '{}{}' should conflict with existing route '{}' but didn't",
prefix, nested_path, existing_path
);
if let Err(panic_info) = result {
if let Some(msg) = panic_info.downcast_ref::<String>() {
prop_assert!(
msg.contains("ROUTE CONFLICT DETECTED"),
"Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
msg
);
prop_assert!(
msg.contains("Existing:") && msg.contains("New:"),
"Error should contain both 'Existing:' and 'New:' labels, got: {}",
msg
);
}
}
}
#[test]
fn prop_nested_route_conflict_exact_same_path(
prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
) {
async fn handler1() -> &'static str { "handler1" }
async fn handler2() -> &'static str { "handler2" }
let prefix = format!("/{}", prefix_segments.join("/"));
let route_path = format!("/{}", route_segments.join("/"));
let existing_path = format!("{}{}", prefix, route_path);
let result = catch_unwind(AssertUnwindSafe(|| {
let parent = Router::new().route(&existing_path, get(handler1));
let nested = Router::new().route(&route_path, get(handler2));
let _app = parent.nest(&prefix, nested);
}));
prop_assert!(
result.is_err(),
"Nested route '{}{}' should conflict with existing route '{}' but didn't",
prefix, route_path, existing_path
);
}
#[test]
fn prop_nested_routes_different_prefixes_no_conflict(
prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
has_param in any::<bool>(),
) {
let prefix1 = format!("/{}", prefix1_segments.join("/"));
let prefix2 = format!("/{}", prefix2_segments.join("/"));
prop_assume!(prefix1 != prefix2);
async fn handler1() -> &'static str { "handler1" }
async fn handler2() -> &'static str { "handler2" }
let route_path = if has_param {
format!("/{}/{{id}}", route_segments.join("/"))
} else {
format!("/{}", route_segments.join("/"))
};
let result = catch_unwind(AssertUnwindSafe(|| {
let nested1 = Router::new().route(&route_path, get(handler1));
let nested2 = Router::new().route(&route_path, get(handler2));
let app = Router::new()
.nest(&prefix1, nested1)
.nest(&prefix2, nested2);
app.registered_routes().len()
}));
prop_assert!(
result.is_ok(),
"Routes under different prefixes '{}' and '{}' should not conflict",
prefix1, prefix2
);
if let Ok(count) = result {
prop_assert_eq!(count, 2, "Should have registered 2 routes");
}
}
#[test]
fn prop_nested_conflict_error_contains_guidance(
prefix in "[a-z][a-z0-9]{1,5}",
segment in "[a-z][a-z0-9]{1,5}",
param1 in "[a-z][a-z0-9]{1,5}",
param2 in "[a-z][a-z0-9]{1,5}",
) {
prop_assume!(param1 != param2);
async fn handler1() -> &'static str { "handler1" }
async fn handler2() -> &'static str { "handler2" }
let prefix = format!("/{}", prefix);
let existing_path = format!("{}/{}/{{{}}}", prefix, segment, param1);
let nested_path = format!("/{}/{{{}}}", segment, param2);
let result = catch_unwind(AssertUnwindSafe(|| {
let parent = Router::new().route(&existing_path, get(handler1));
let nested = Router::new().route(&nested_path, get(handler2));
let _app = parent.nest(&prefix, nested);
}));
prop_assert!(result.is_err(), "Should have detected conflict");
if let Err(panic_info) = result {
if let Some(msg) = panic_info.downcast_ref::<String>() {
prop_assert!(
msg.contains("How to resolve:"),
"Error should contain 'How to resolve:' guidance, got: {}",
msg
);
prop_assert!(
msg.contains("Use different path patterns") ||
msg.contains("different path patterns"),
"Error should suggest using different path patterns, got: {}",
msg
);
}
}
}
}