Skip to main content

ic_asset_router/
router.rs

1use ic_http_certification::{HttpRequest, HttpResponse, Method};
2use std::collections::HashMap;
3
4use crate::middleware::MiddlewareFn;
5use crate::route_config::RouteConfig;
6
7/// Dynamic route parameters extracted from the URL path.
8///
9/// Maps parameter names to their captured values. For example, a route
10/// registered as `/:postId/edit` matched against `/42/edit` produces
11/// `{"postId": "42"}`. Wildcard routes store the remaining path under
12/// the key `"*"`.
13pub type RouteParams = HashMap<String, String>;
14
15/// A synchronous route handler function.
16///
17/// Receives the full [`HttpRequest`] and the extracted [`RouteParams`],
18/// and returns an [`HttpResponse`]. This is the standard handler signature
19/// used by the router's middleware chain and certification pipeline.
20pub type HandlerFn = fn(HttpRequest, RouteParams) -> HttpResponse<'static>;
21
22/// Result of matching a path against the route tree (without method dispatch).
23///
24/// Contains references to the handler maps, the extracted route parameters,
25/// and the matched route pattern (e.g. `"/:id/edit"`).
26type MatchResult<'a> = (
27    &'a HashMap<Method, HandlerFn>,
28    &'a HashMap<Method, HandlerResultFn>,
29    RouteParams,
30    String,
31);
32
33/// A route handler that returns [`HandlerResult`] instead of a bare response.
34///
35/// This variant supports conditional regeneration: the handler can return
36/// [`HandlerResult::NotModified`] to signal that the existing cached version
37/// is still valid, avoiding a full recertification cycle.
38///
39/// A standard [`HandlerFn`] must also be registered at the same path/method
40/// as a fallback for the query path and middleware chain. The
41/// `HandlerResultFn` is only called during `http_request_update`.
42///
43/// # Note on signature
44///
45/// This type intentionally uses the internal `(HttpRequest, RouteParams)`
46/// signature rather than the public `RouteContext`-based signature used by
47/// generated route handlers. The build script currently does not generate
48/// wrappers for `insert_result` calls — result handlers are registered
49/// manually via [`RouteNode::insert_result`].
50///
51/// If `insert_result` is ever wired into `__route_tree.rs` code generation,
52/// the same wrapper pattern used for `HandlerFn` (bridging `RouteParams` to
53/// `RouteContext<Params, SearchParams>`) should be applied here as well.
54pub type HandlerResultFn = fn(HttpRequest, RouteParams) -> HandlerResult;
55
56/// Result type for route handlers that supports conditional regeneration.
57///
58/// Handlers can return `Response(...)` with a new response to certify, or
59/// `NotModified` to indicate that the existing cached version is still valid.
60/// When `NotModified` is returned, the library skips recertification entirely
61/// and resets the TTL timer if TTL-based caching is active.
62///
63/// # Examples
64///
65/// ```rust,ignore
66/// use ic_asset_router::{HandlerResult, HttpRequest, HttpResponse, RouteParams};
67///
68/// fn my_result_handler(req: HttpRequest, params: RouteParams) -> HandlerResult {
69///     if content_unchanged() {
70///         HandlerResult::NotModified
71///     } else {
72///         HandlerResult::Response(build_new_response())
73///     }
74/// }
75/// ```
76pub enum HandlerResult {
77    /// New content — certify and cache this response.
78    Response(HttpResponse<'static>),
79
80    /// Content hasn't changed — keep the existing certified version
81    /// and reset the TTL timer (if TTL-based caching is enabled).
82    NotModified,
83}
84
85impl From<HttpResponse<'static>> for HandlerResult {
86    fn from(resp: HttpResponse<'static>) -> Self {
87        HandlerResult::Response(resp)
88    }
89}
90
91/// The type of a node in the route trie.
92///
93/// Each segment of a route path corresponds to a [`RouteNode`] with one of
94/// these types. During path resolution the trie tries `Static` first, then
95/// `Param`, then `Wildcard` — giving static segments the highest priority.
96#[derive(Debug, PartialEq, Eq)]
97pub enum NodeType {
98    /// A literal path segment (e.g. `"users"` in `/users`).
99    Static(String),
100    /// A dynamic parameter segment (e.g. `:id` in `/users/:id`).
101    /// The contained string is the parameter name without the leading colon.
102    Param(String),
103    /// A catch-all wildcard (`*`). Matches one or more remaining segments
104    /// and stores the captured tail in [`RouteParams`] under the key `"*"`.
105    Wildcard,
106}
107
108/// Result of resolving a path and HTTP method against the route tree.
109///
110/// Returned by [`RouteNode::resolve`]. Callers should match on the three
111/// variants to determine how to handle the request.
112pub enum RouteResult {
113    /// A handler was found for the given path and method.
114    ///
115    /// Fields: `(handler, params, result_handler, route_pattern)`.
116    /// The optional `HandlerResultFn` is present when the route supports
117    /// conditional regeneration via `HandlerResult::NotModified`.
118    /// The `route_pattern` is the pattern string as registered (e.g.
119    /// `"/:id/edit"`), used to look up per-route configuration.
120    Found(HandlerFn, RouteParams, Option<HandlerResultFn>, String),
121    /// The path exists but the requested method is not registered.
122    /// Contains the list of methods that *are* registered for this path.
123    MethodNotAllowed(Vec<Method>),
124    /// No route matches the given path.
125    NotFound,
126}
127
128/// A node in the radix-trie-style route tree.
129///
130/// The root node is always `NodeType::Static("")`. Routes are inserted by
131/// splitting the path into segments and descending through (or creating)
132/// child nodes. Resolution follows the same path, preferring static matches
133/// over parameter matches over wildcard matches.
134///
135/// Middleware, the custom not-found handler, and `HandlerResultFn` overrides
136/// are stored on the root node and consulted at dispatch time.
137pub struct RouteNode {
138    /// The type of this node (static segment, parameter, or wildcard).
139    pub node_type: NodeType,
140    /// Static child nodes, keyed by segment name.
141    /// Lookup is O(1) via [`HashMap::get`].
142    pub static_children: HashMap<String, RouteNode>,
143    /// Optional single dynamic-parameter child (`:name` segments).
144    /// At most one param child is allowed per node — this is enforced
145    /// structurally by using `Option` instead of a collection.
146    pub param_child: Option<Box<RouteNode>>,
147    /// Optional single wildcard child (`*` segments).
148    /// At most one wildcard child is allowed per node.
149    pub wildcard_child: Option<Box<RouteNode>>,
150    /// Method → handler map for this node. A handler is present only for
151    /// methods that have been explicitly registered via [`insert`](Self::insert).
152    pub handlers: HashMap<Method, HandlerFn>,
153    /// Optional `HandlerResultFn` overrides for routes that support conditional
154    /// regeneration. When present for a given method, `http_request_update` calls
155    /// this handler first to check for `NotModified` before falling back to the
156    /// standard `HandlerFn` + middleware pipeline.
157    result_handlers: HashMap<Method, HandlerResultFn>,
158    /// Middleware registry stored at the root node.
159    /// Each entry is a `(prefix, middleware_fn)` pair, sorted by prefix segment
160    /// count (shortest/outermost first). Only the root node's list is used at
161    /// dispatch time; child nodes ignore this field.
162    middlewares: Vec<(String, MiddlewareFn)>,
163    /// Optional custom not-found handler. When set, this handler is called
164    /// instead of the default 404 response when no route matches the request
165    /// path. Only the root node's value is used at dispatch time.
166    not_found_handler: Option<HandlerFn>,
167    /// Per-route certification configuration, stored at the root node.
168    /// Keys are route path patterns (e.g. `"/api/users"`, `"/:id"`).
169    /// Only the root node's map is used at dispatch time; child nodes ignore
170    /// this field.
171    route_configs: HashMap<String, RouteConfig>,
172}
173
174impl RouteNode {
175    /// Create a new route node with the given type and no children or handlers.
176    pub fn new(node_type: NodeType) -> Self {
177        Self {
178            node_type,
179            static_children: HashMap::new(),
180            param_child: None,
181            wildcard_child: None,
182            handlers: HashMap::new(),
183            result_handlers: HashMap::new(),
184            middlewares: Vec::new(),
185            not_found_handler: None,
186            route_configs: HashMap::new(),
187        }
188    }
189
190    /// Register a middleware at the given prefix.
191    ///
192    /// One middleware per prefix — calling this again with the same prefix
193    /// replaces the previous middleware. The list is kept sorted by prefix
194    /// segment count (shortest/outermost first) so that the middleware chain
195    /// executes in root → outer → inner order.
196    ///
197    /// Use `"/"` for root-level middleware that runs on every request.
198    pub fn set_middleware(&mut self, prefix: &str, mw: MiddlewareFn) {
199        let normalized = normalize_prefix(prefix);
200        if let Some(entry) = self.middlewares.iter_mut().find(|(p, _)| *p == normalized) {
201            entry.1 = mw;
202        } else {
203            self.middlewares.push((normalized, mw));
204        }
205        // Sort by segment count (shortest first) for correct outer → inner ordering.
206        self.middlewares.sort_by_key(|(p, _)| segment_count(p));
207    }
208
209    /// Register a custom not-found handler.
210    ///
211    /// When no route matches a request path, this handler is called instead of
212    /// returning the default plain-text 404 response. The handler receives the
213    /// full `HttpRequest` and empty `RouteParams`.
214    ///
215    /// Only one not-found handler can be registered; calling this again replaces
216    /// the previous handler.
217    pub fn set_not_found(&mut self, handler: HandlerFn) {
218        self.not_found_handler = Some(handler);
219    }
220
221    /// Returns the custom not-found handler, if one has been registered.
222    pub fn not_found_handler(&self) -> Option<HandlerFn> {
223        self.not_found_handler
224    }
225
226    /// Register a [`RouteConfig`] for the given route path.
227    ///
228    /// This stores per-route certification configuration at the root node.
229    /// The config is keyed by route path pattern (e.g. `"/api/users"`).
230    /// Multiple methods on the same path share the same config.
231    ///
232    /// If a config already exists for the path, it is replaced.
233    pub fn set_route_config(&mut self, path: &str, config: RouteConfig) {
234        self.route_configs.insert(path.to_string(), config);
235    }
236
237    /// Look up the [`RouteConfig`] for a given route path.
238    ///
239    /// Returns `None` if no config has been registered for the path.
240    /// Only the root node's map is consulted.
241    pub fn get_route_config(&self, path: &str) -> Option<&RouteConfig> {
242        self.route_configs.get(path)
243    }
244
245    /// Return all route path patterns configured with
246    /// [`CertificationMode::Skip`](crate::CertificationMode::Skip).
247    ///
248    /// Used during `init`/`post_upgrade` to pre-register skip certification
249    /// tree entries so that skip-mode routes never need to upgrade to an
250    /// update call.
251    pub fn skip_certified_paths(&self) -> Vec<String> {
252        self.route_configs
253            .iter()
254            .filter(|(_, config)| {
255                matches!(
256                    config.certification,
257                    crate::certification::CertificationMode::Skip
258                )
259            })
260            .map(|(path, _)| path.clone())
261            .collect()
262    }
263
264    /// Register a handler for the given path and HTTP method.
265    ///
266    /// Path segments starting with `:` are treated as dynamic parameters;
267    /// a lone `*` segment is a catch-all wildcard. If a handler already
268    /// exists for the same path and method it is silently replaced.
269    pub fn insert(&mut self, path: &str, method: Method, handler: HandlerFn) {
270        let node = self.get_or_create_node(path);
271        node.handlers.insert(method, handler);
272    }
273
274    /// Register a `HandlerResultFn` for the given path and method.
275    ///
276    /// A `HandlerResultFn` returns `HandlerResult` instead of `HttpResponse`,
277    /// enabling conditional regeneration via `HandlerResult::NotModified`.
278    ///
279    /// A standard `HandlerFn` must also be registered at the same path/method
280    /// (via [`insert`](Self::insert)) — it serves as the fallback for the query
281    /// path and middleware chain. The `HandlerResultFn` is only checked in
282    /// `http_request_update`.
283    pub fn insert_result(&mut self, path: &str, method: Method, handler: HandlerResultFn) {
284        let node = self.get_or_create_node(path);
285        node.result_handlers.insert(method, handler);
286    }
287
288    /// Walk (or create) the trie path for the given segments, returning
289    /// a mutable reference to the terminal node.
290    ///
291    /// Each segment is parsed into a [`NodeType`]: `"*"` becomes
292    /// [`Wildcard`](NodeType::Wildcard), a leading `:` becomes
293    /// [`Param`](NodeType::Param), and anything else becomes
294    /// [`Static`](NodeType::Static). Intermediate nodes are created on
295    /// demand. Calling this twice with the same path returns the same
296    /// node (idempotent).
297    fn get_or_create_node(&mut self, path: &str) -> &mut RouteNode {
298        let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
299        let mut current = self;
300        for seg in segments {
301            match seg {
302                "*" => {
303                    if current.wildcard_child.is_none() {
304                        current.wildcard_child = Some(Box::new(RouteNode::new(NodeType::Wildcard)));
305                    }
306                    current = current.wildcard_child.as_mut().unwrap();
307                }
308                s if s.starts_with(':') => {
309                    let name = s[1..].to_string();
310                    if current.param_child.is_none() {
311                        current.param_child = Some(Box::new(RouteNode::new(NodeType::Param(name))));
312                    }
313                    current = current.param_child.as_mut().unwrap();
314                }
315                s => {
316                    current = current
317                        .static_children
318                        .entry(s.to_string())
319                        .or_insert_with(|| RouteNode::new(NodeType::Static(s.to_string())));
320                }
321            }
322        }
323        current
324    }
325
326    /// Execute the middleware chain for a resolved route.
327    ///
328    /// Collects all middleware whose prefix matches `path` (sorted outermost
329    /// first), wraps `handler` as the innermost `next`, and executes the chain.
330    /// Each middleware's `next` calls the next middleware inward, with the
331    /// handler at the center.
332    pub fn execute_with_middleware(
333        &self,
334        path: &str,
335        handler: HandlerFn,
336        req: HttpRequest,
337        params: RouteParams,
338    ) -> HttpResponse<'static> {
339        let matching: Vec<MiddlewareFn> = self
340            .middlewares
341            .iter()
342            .filter(|(prefix, _)| path_matches_prefix(path, prefix))
343            .map(|(_, mw)| *mw)
344            .collect();
345
346        if matching.is_empty() {
347            return handler(req, params);
348        }
349
350        // Build the chain from innermost to outermost.
351        // Start with the handler as the innermost function.
352        // Then wrap each middleware around it, from the last (innermost) to the
353        // first (outermost).
354        build_chain(&matching, handler, req, &params)
355    }
356
357    /// Execute the middleware chain for a not-found request.
358    ///
359    /// This is used when a custom not-found handler is registered: the
360    /// middleware chain still runs (root/global middleware should execute
361    /// before the 404 handler), with the not-found handler at the center
362    /// instead of a route handler.
363    pub fn execute_not_found_with_middleware(
364        &self,
365        path: &str,
366        req: HttpRequest,
367    ) -> Option<HttpResponse<'static>> {
368        let handler = self.not_found_handler?;
369        let params = RouteParams::new();
370        Some(self.execute_with_middleware(path, handler, req, params))
371    }
372
373    /// Resolve a path and method to a `RouteResult`.
374    ///
375    /// 1. Finds the trie node matching `path`.
376    /// 2. If found, looks up `method` in the node's `handlers` map.
377    /// 3. Returns `Found` / `MethodNotAllowed` / `NotFound` accordingly.
378    pub fn resolve(&self, path: &str, method: &Method) -> RouteResult {
379        let segments: Vec<_> = path.split('/').filter(|s| !s.is_empty()).collect();
380        match self._match(&segments) {
381            Some((handlers, result_handlers, params, pattern)) => {
382                if let Some(&handler) = handlers.get(method) {
383                    let result_handler = result_handlers.get(method).copied();
384                    RouteResult::Found(handler, params, result_handler, pattern)
385                } else {
386                    let allowed: Vec<Method> = handlers.keys().cloned().collect();
387                    RouteResult::MethodNotAllowed(allowed)
388                }
389            }
390            None => RouteResult::NotFound,
391        }
392    }
393
394    /// Match a path and return the handlers map and params for the matched node.
395    ///
396    /// This performs path-only matching without method dispatch.
397    /// For method-aware routing, use [`resolve()`](Self::resolve) instead.
398    pub fn match_path(&self, path: &str) -> Option<MatchResult<'_>> {
399        let segments: Vec<_> = path.split('/').filter(|s| !s.is_empty()).collect();
400        self._match(&segments)
401    }
402
403    fn _match(&self, segments: &[&str]) -> Option<MatchResult<'_>> {
404        if segments.is_empty() {
405            if !self.handlers.is_empty() {
406                return Some((
407                    &self.handlers,
408                    &self.result_handlers,
409                    HashMap::new(),
410                    "/".to_string(),
411                ));
412            }
413            // No handlers on this node — check for a wildcard child (empty wildcard match)
414            if let Some(ref wc) = self.wildcard_child {
415                if !wc.handlers.is_empty() {
416                    let mut params = HashMap::new();
417                    params.insert("*".to_string(), String::new());
418                    return Some((&wc.handlers, &wc.result_handlers, params, "/*".to_string()));
419                }
420            }
421            return None;
422        }
423
424        let head = segments[0];
425        let tail = &segments[1..];
426
427        debug_log!("head: {:?}", head);
428
429        // Static match — O(1) via HashMap lookup
430        if let Some(child) = self.static_children.get(head) {
431            if let Some((h, rh, p, pattern)) = child._match(tail) {
432                debug_log!("Static match: {:?}", segments);
433                let full_pattern = if pattern == "/" {
434                    format!("/{head}")
435                } else {
436                    format!("/{head}{pattern}")
437                };
438                return Some((h, rh, p, full_pattern));
439            }
440        }
441
442        // Param match — O(1) via Option
443        if let Some(ref child) = self.param_child {
444            if let NodeType::Param(ref name) = child.node_type {
445                if let Some((h, rh, mut p, pattern)) = child._match(tail) {
446                    p.insert(name.clone(), head.to_string());
447                    debug_log!("Param match: {:?}", segments);
448                    let full_pattern = if pattern == "/" {
449                        format!("/:{name}")
450                    } else {
451                        format!("/:{name}{pattern}")
452                    };
453                    return Some((h, rh, p, full_pattern));
454                }
455            }
456        }
457
458        // Wildcard match — O(1) via Option
459        if let Some(ref child) = self.wildcard_child {
460            if !segments.is_empty() && !child.handlers.is_empty() {
461                debug_log!("Wildcard match: {:?}", segments);
462                let remaining = segments.join("/");
463                let mut params = HashMap::new();
464                params.insert("*".to_string(), remaining);
465                return Some((
466                    &child.handlers,
467                    &child.result_handlers,
468                    params,
469                    "/*".to_string(),
470                ));
471            }
472        }
473
474        None
475    }
476}
477
478/// Check whether a request path matches a middleware prefix.
479///
480/// `"/"` matches all paths. Otherwise, the path must start with the prefix
481/// followed by either end-of-string or a `"/"` separator.
482fn path_matches_prefix(path: &str, prefix: &str) -> bool {
483    if prefix == "/" {
484        return true;
485    }
486    path == prefix || path.starts_with(&format!("{prefix}/"))
487}
488
489/// Build and execute a nested middleware chain.
490///
491/// `middlewares` is sorted outermost-first. The handler is the innermost
492/// function. We recurse: middleware[0] wraps a `next` that calls
493/// `build_chain(middlewares[1..], handler, ...)`.
494fn build_chain(
495    middlewares: &[MiddlewareFn],
496    handler: HandlerFn,
497    req: HttpRequest,
498    params: &RouteParams,
499) -> HttpResponse<'static> {
500    match middlewares.split_first() {
501        None => handler(req, params.clone()),
502        Some((&mw, rest)) => {
503            let next =
504                |inner_req: HttpRequest, inner_params: &RouteParams| -> HttpResponse<'static> {
505                    build_chain(rest, handler, inner_req, inner_params)
506                };
507            mw(req, params, &next)
508        }
509    }
510}
511
512/// Normalize a middleware prefix to a canonical form: `"/"` for root, otherwise
513/// `"/segment1/segment2"` with no trailing slash.
514fn normalize_prefix(prefix: &str) -> String {
515    let trimmed = prefix.trim_matches('/');
516    if trimmed.is_empty() {
517        "/".to_string()
518    } else {
519        format!("/{trimmed}")
520    }
521}
522
523/// Count the number of non-empty path segments in a normalized prefix.
524/// `"/"` has 0 segments; `"/api"` has 1; `"/api/v2"` has 2.
525fn segment_count(prefix: &str) -> usize {
526    prefix.split('/').filter(|s| !s.is_empty()).count()
527}
528
529// Test coverage audit (Session 7, Spec 5.5):
530//
531// Covered:
532//   - Basic path matching: root, static, dynamic, wildcard, nested params, mixed params+wildcard
533//   - Trailing slash normalization, double slash normalization
534//   - Method dispatch: GET/POST differentiation, MethodNotAllowed with allowed list, all 7 methods
535//   - Middleware: root scope, scoped prefix, chain order (root→outer→inner), short-circuit,
536//     response modification, replacement on same prefix, query+update paths
537//   - Custom 404: custom response, default fallback, request pass-through, JSON content-type,
538//     middleware runs before 404
539//   - From<HttpResponse> for HandlerResult conversion
540//
541// Gaps filled in this session:
542//   - Empty segments in paths
543//   - URL-encoded characters in path segments
544//   - Very long paths (100 segments)
545//   - Routes with many (4+) parameters
546//   - Middleware modifying request before handler (header injection)
547//   - Multiple middleware in hierarchy applied to not-found handler
548#[cfg(test)]
549mod tests {
550    use super::*;
551    use ic_http_certification::{Method, StatusCode};
552    use std::{borrow::Cow, str};
553
554    fn test_request(path: &str) -> HttpRequest<'_> {
555        HttpRequest::builder()
556            .with_method(Method::GET)
557            .with_url(path)
558            .build()
559    }
560
561    fn response_with_text(text: &str) -> HttpResponse<'static> {
562        HttpResponse::builder()
563            .with_body(Cow::Owned(text.as_bytes().to_vec()))
564            .with_status_code(StatusCode::OK)
565            .build()
566    }
567
568    /// Resolve a path as GET and unwrap the Found variant, returning (handler, params).
569    fn resolve_get(root: &RouteNode, path: &str) -> (HandlerFn, RouteParams) {
570        match root.resolve(path, &Method::GET) {
571            RouteResult::Found(h, p, _, _) => (h, p),
572            other => panic!(
573                "expected Found for GET {path}, got {}",
574                route_result_name(&other)
575            ),
576        }
577    }
578
579    fn route_result_name(r: &RouteResult) -> &'static str {
580        match r {
581            RouteResult::Found(_, _, _, _) => "Found",
582            RouteResult::MethodNotAllowed(_) => "MethodNotAllowed",
583            RouteResult::NotFound => "NotFound",
584        }
585    }
586
587    fn matched_root(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
588        response_with_text("root")
589    }
590
591    fn matched_404(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
592        response_with_text("404")
593    }
594
595    fn matched_index2(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
596        response_with_text("index2")
597    }
598
599    fn matched_about(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
600        response_with_text("about")
601    }
602
603    fn matched_deep(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
604        response_with_text(&format!("deep: {params:?}"))
605    }
606
607    fn matched_folder(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
608        response_with_text("folder")
609    }
610
611    fn setup_router() -> RouteNode {
612        let mut root = RouteNode::new(NodeType::Static("".into()));
613        root.insert("/", Method::GET, matched_root);
614        root.insert("/*", Method::GET, matched_404);
615        root.insert("/index2", Method::GET, matched_index2);
616        root.insert("/about", Method::GET, matched_about);
617        root.insert("/deep/:pageId", Method::GET, matched_deep);
618        root.insert("/deep/:pageId/:subpageId", Method::GET, matched_deep);
619        root.insert("/alsodeep/:pageId/edit", Method::GET, matched_deep);
620        root.insert("/folder/*", Method::GET, matched_folder);
621        root
622    }
623
624    fn body_str(resp: HttpResponse<'static>) -> String {
625        str::from_utf8(resp.body())
626            .unwrap_or("<invalid utf-8>")
627            .to_string()
628    }
629
630    // ---- Existing path-matching tests (updated for method-aware API) ----
631
632    #[test]
633    fn test_root_match() {
634        let root = setup_router();
635        let (handler, params) = resolve_get(&root, "/");
636        assert_eq!(body_str(handler(test_request("/"), params)), "root");
637    }
638
639    #[test]
640    fn test_404_match() {
641        let root = setup_router();
642        let (handler, _) = resolve_get(&root, "/nonexistent");
643        assert_eq!(
644            body_str(handler(test_request("/nonexistent"), HashMap::new())),
645            "404"
646        );
647    }
648
649    #[test]
650    fn test_exact_match() {
651        let root = setup_router();
652        let (handler, params) = resolve_get(&root, "/index2");
653        assert_eq!(body_str(handler(test_request("/index2"), params)), "index2");
654    }
655
656    #[test]
657    fn test_pathless_layout_route_a() {
658        let mut root = RouteNode::new(NodeType::Static("".into()));
659        root.insert("/about", Method::GET, matched_about);
660        let (handler, params) = resolve_get(&root, "/about");
661        assert_eq!(body_str(handler(test_request("/about"), params)), "about");
662    }
663
664    #[test]
665    fn test_dynamic_match() {
666        let root = setup_router();
667        let (handler, params) = resolve_get(&root, "/deep/page1");
668        let body = body_str(handler(test_request("/deep/page1"), params));
669        assert!(body.contains("page1"));
670    }
671
672    #[test]
673    fn test_posts_postid_edit() {
674        let root = setup_router();
675        let (handler, params) = resolve_get(&root, "/alsodeep/page1/edit");
676        let body = body_str(handler(test_request("/alsodeep/page1/edit"), params));
677        assert!(body.contains("page1"));
678    }
679
680    #[test]
681    fn test_nested_dynamic_match() {
682        let root = setup_router();
683        let (handler, params) = resolve_get(&root, "/deep/page2/subpage1");
684        let body = body_str(handler(test_request("/deep/page2/subpage1"), params));
685        assert!(body.contains("page2"));
686        assert!(body.contains("subpage1"));
687    }
688
689    #[test]
690    fn test_wildcard_match() {
691        let root = setup_router();
692        let (handler, _) = resolve_get(&root, "/folder/anything");
693        assert_eq!(
694            body_str(handler(test_request("/folder/anything"), HashMap::new())),
695            "folder"
696        );
697    }
698
699    #[test]
700    fn test_folder_root_wildcard_match() {
701        let root = setup_router();
702        let (handler, _) = resolve_get(&root, "/folder/any");
703        assert_eq!(
704            body_str(handler(test_request("/folder/any"), HashMap::new())),
705            "folder"
706        );
707    }
708
709    #[test]
710    fn test_deep_wildcard_multi_segments() {
711        let root = setup_router();
712        let (handler, _) = resolve_get(&root, "/folder/a/b/c/d");
713        assert_eq!(
714            body_str(handler(test_request("/folder/a/b/c/d"), HashMap::new())),
715            "folder"
716        );
717    }
718
719    #[test]
720    fn test_trailing_slash_static_match() {
721        let root = setup_router();
722        let (handler, _) = resolve_get(&root, "/index2/");
723        assert_eq!(
724            body_str(handler(test_request("/index2/"), HashMap::new())),
725            "index2"
726        );
727    }
728
729    #[test]
730    fn test_double_slash_matches_normalized() {
731        let root = setup_router();
732        let (handler, _) = resolve_get(&root, "//index2");
733        assert_eq!(
734            body_str(handler(test_request("//index2"), HashMap::new())),
735            "index2"
736        );
737    }
738
739    #[test]
740    fn test_root_wildcard_captures_full_path() {
741        let root = setup_router();
742        let (_, params) = resolve_get(&root, "/a/b/c");
743        assert_eq!(params.get("*").unwrap(), "a/b/c");
744    }
745
746    #[test]
747    fn test_folder_wildcard_captures_tail() {
748        let root = setup_router();
749        let (handler, params) = resolve_get(&root, "/folder/docs/report.pdf");
750        assert_eq!(params.get("*").unwrap(), "docs/report.pdf");
751        assert_eq!(
752            body_str(handler(
753                test_request("/folder/docs/report.pdf"),
754                params.clone()
755            )),
756            "folder"
757        );
758    }
759
760    fn matched_user_files(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
761        response_with_text(&format!("user_files: {params:?}"))
762    }
763
764    #[test]
765    fn test_mixed_params_and_wildcard() {
766        let mut root = RouteNode::new(NodeType::Static("".into()));
767        root.insert("/users/:id/files/*", Method::GET, matched_user_files);
768        let (_, params) = resolve_get(&root, "/users/42/files/docs/report.pdf");
769        assert_eq!(params.get("id").unwrap(), "42");
770        assert_eq!(params.get("*").unwrap(), "docs/report.pdf");
771    }
772
773    #[test]
774    fn test_empty_wildcard_match() {
775        let mut root = RouteNode::new(NodeType::Static("".into()));
776        root.insert("/files/*", Method::GET, matched_folder);
777        let (handler, params) = resolve_get(&root, "/files/");
778        assert_eq!(params.get("*").unwrap(), "");
779        assert_eq!(
780            body_str(handler(test_request("/files/"), params.clone())),
781            "folder"
782        );
783    }
784
785    // ---- 2.1 Method dispatch tests ----
786
787    fn matched_post_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
788        response_with_text("post_handler")
789    }
790
791    fn matched_get_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
792        response_with_text("get_handler")
793    }
794
795    /// 2.1.7a: GET /path routes to get handler, POST /path routes to post handler
796    #[test]
797    fn test_method_dispatch_get_and_post() {
798        let mut root = RouteNode::new(NodeType::Static("".into()));
799        root.insert("/api/users", Method::GET, matched_get_handler);
800        root.insert("/api/users", Method::POST, matched_post_handler);
801
802        // GET resolves to get handler
803        match root.resolve("/api/users", &Method::GET) {
804            RouteResult::Found(handler, params, _, _) => {
805                assert_eq!(
806                    body_str(handler(test_request("/api/users"), params)),
807                    "get_handler"
808                );
809            }
810            other => panic!("expected Found, got {}", route_result_name(&other)),
811        }
812
813        // POST resolves to post handler
814        match root.resolve("/api/users", &Method::POST) {
815            RouteResult::Found(handler, params, _, _) => {
816                let req = HttpRequest::builder()
817                    .with_method(Method::POST)
818                    .with_url("/api/users")
819                    .build();
820                assert_eq!(body_str(handler(req, params)), "post_handler");
821            }
822            other => panic!("expected Found, got {}", route_result_name(&other)),
823        }
824    }
825
826    /// 2.1.7b: PUT /path returns 405 with allowed methods when only GET and POST registered
827    #[test]
828    fn test_method_not_allowed() {
829        let mut root = RouteNode::new(NodeType::Static("".into()));
830        root.insert("/api/users", Method::GET, matched_get_handler);
831        root.insert("/api/users", Method::POST, matched_post_handler);
832
833        match root.resolve("/api/users", &Method::PUT) {
834            RouteResult::MethodNotAllowed(allowed) => {
835                let mut names: Vec<&str> = allowed.iter().map(|m| m.as_str()).collect();
836                names.sort();
837                assert_eq!(names, vec!["GET", "POST"]);
838            }
839            other => panic!(
840                "expected MethodNotAllowed, got {}",
841                route_result_name(&other)
842            ),
843        }
844    }
845
846    /// 2.1.7c: Unknown path returns NotFound
847    #[test]
848    fn test_unknown_path_returns_not_found() {
849        let mut root = RouteNode::new(NodeType::Static("".into()));
850        root.insert("/api/users", Method::GET, matched_get_handler);
851
852        assert!(matches!(
853            root.resolve("/api/nonexistent", &Method::GET),
854            RouteResult::NotFound
855        ));
856    }
857
858    /// 2.1.7d: All 7 HTTP method types can be registered and resolved
859    #[test]
860    fn test_all_seven_methods() {
861        let methods = [
862            Method::GET,
863            Method::POST,
864            Method::PUT,
865            Method::PATCH,
866            Method::DELETE,
867            Method::HEAD,
868            Method::OPTIONS,
869        ];
870
871        let mut root = RouteNode::new(NodeType::Static("".into()));
872        for method in &methods {
873            root.insert("/test", method.clone(), matched_get_handler);
874        }
875
876        // All 7 methods should resolve to Found
877        for method in &methods {
878            match root.resolve("/test", method) {
879                RouteResult::Found(_, _, _, _) => {}
880                other => panic!(
881                    "expected Found for method {}, got {}",
882                    method.as_str(),
883                    route_result_name(&other)
884                ),
885            }
886        }
887    }
888
889    // ---- 2.2 Middleware tests ----
890
891    use std::cell::RefCell;
892
893    thread_local! {
894        static LOG: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
895    }
896
897    fn clear_log() {
898        LOG.with(|l| l.borrow_mut().clear());
899    }
900
901    fn get_log() -> Vec<String> {
902        LOG.with(|l| l.borrow().clone())
903    }
904
905    fn log_entry(msg: &str) {
906        LOG.with(|l| l.borrow_mut().push(msg.to_string()));
907    }
908
909    fn logging_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
910        log_entry("handler");
911        response_with_text("handler_response")
912    }
913
914    fn root_middleware(
915        req: HttpRequest,
916        params: &RouteParams,
917        next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
918    ) -> HttpResponse<'static> {
919        log_entry("root_mw_before");
920        let resp = next(req, params);
921        log_entry("root_mw_after");
922        resp
923    }
924
925    fn api_middleware(
926        req: HttpRequest,
927        params: &RouteParams,
928        next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
929    ) -> HttpResponse<'static> {
930        log_entry("api_mw_before");
931        let resp = next(req, params);
932        log_entry("api_mw_after");
933        resp
934    }
935
936    fn api_v2_middleware(
937        req: HttpRequest,
938        params: &RouteParams,
939        next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
940    ) -> HttpResponse<'static> {
941        log_entry("api_v2_mw_before");
942        let resp = next(req, params);
943        log_entry("api_v2_mw_after");
944        resp
945    }
946
947    /// 2.2.6a: Root middleware runs on all requests
948    #[test]
949    fn test_root_middleware_runs_on_all_requests() {
950        clear_log();
951        let mut root = RouteNode::new(NodeType::Static("".into()));
952        root.insert("/", Method::GET, logging_handler);
953        root.insert("/about", Method::GET, logging_handler);
954        root.insert("/api/users", Method::GET, logging_handler);
955        root.set_middleware("/", root_middleware);
956
957        // Root path
958        let (handler, params) = resolve_get(&root, "/");
959        root.execute_with_middleware("/", handler, test_request("/"), params);
960        assert!(get_log().contains(&"root_mw_before".to_string()));
961        assert!(get_log().contains(&"handler".to_string()));
962        assert!(get_log().contains(&"root_mw_after".to_string()));
963
964        // /about
965        clear_log();
966        let (handler, params) = resolve_get(&root, "/about");
967        root.execute_with_middleware("/about", handler, test_request("/about"), params);
968        assert!(get_log().contains(&"root_mw_before".to_string()));
969        assert!(get_log().contains(&"handler".to_string()));
970
971        // /api/users
972        clear_log();
973        let (handler, params) = resolve_get(&root, "/api/users");
974        root.execute_with_middleware("/api/users", handler, test_request("/api/users"), params);
975        assert!(get_log().contains(&"root_mw_before".to_string()));
976        assert!(get_log().contains(&"handler".to_string()));
977    }
978
979    /// 2.2.6b: Scoped middleware runs only on matching prefix
980    #[test]
981    fn test_scoped_middleware_only_matching_prefix() {
982        clear_log();
983        let mut root = RouteNode::new(NodeType::Static("".into()));
984        root.insert("/api/users", Method::GET, logging_handler);
985        root.insert("/pages/home", Method::GET, logging_handler);
986        root.set_middleware("/api", api_middleware);
987
988        // /api/users — api_middleware should run
989        let (handler, params) = resolve_get(&root, "/api/users");
990        root.execute_with_middleware("/api/users", handler, test_request("/api/users"), params);
991        assert!(get_log().contains(&"api_mw_before".to_string()));
992        assert!(get_log().contains(&"handler".to_string()));
993
994        // /pages/home — api_middleware should NOT run
995        clear_log();
996        let (handler, params) = resolve_get(&root, "/pages/home");
997        root.execute_with_middleware("/pages/home", handler, test_request("/pages/home"), params);
998        assert!(!get_log().contains(&"api_mw_before".to_string()));
999        assert!(get_log().contains(&"handler".to_string()));
1000    }
1001
1002    /// 2.2.6c: Chain order is root → outer → inner → handler → inner → outer → root
1003    #[test]
1004    fn test_middleware_chain_order() {
1005        clear_log();
1006        let mut root = RouteNode::new(NodeType::Static("".into()));
1007        root.insert("/api/v2/data", Method::GET, logging_handler);
1008        root.set_middleware("/", root_middleware);
1009        root.set_middleware("/api", api_middleware);
1010        root.set_middleware("/api/v2", api_v2_middleware);
1011
1012        let (handler, params) = resolve_get(&root, "/api/v2/data");
1013        root.execute_with_middleware(
1014            "/api/v2/data",
1015            handler,
1016            test_request("/api/v2/data"),
1017            params,
1018        );
1019
1020        let log = get_log();
1021        assert_eq!(
1022            log,
1023            vec![
1024                "root_mw_before",
1025                "api_mw_before",
1026                "api_v2_mw_before",
1027                "handler",
1028                "api_v2_mw_after",
1029                "api_mw_after",
1030                "root_mw_after",
1031            ]
1032        );
1033    }
1034
1035    /// 2.2.6d: Middleware can short-circuit (return without calling next)
1036    #[test]
1037    fn test_middleware_short_circuit() {
1038        fn auth_middleware(
1039            _req: HttpRequest,
1040            _params: &RouteParams,
1041            _next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
1042        ) -> HttpResponse<'static> {
1043            log_entry("auth_reject");
1044            HttpResponse::builder()
1045                .with_status_code(StatusCode::UNAUTHORIZED)
1046                .with_body(Cow::Owned(b"Unauthorized".to_vec()))
1047                .build()
1048        }
1049
1050        clear_log();
1051        let mut root = RouteNode::new(NodeType::Static("".into()));
1052        root.insert("/secret", Method::GET, logging_handler);
1053        root.set_middleware("/", auth_middleware);
1054
1055        let (handler, params) = resolve_get(&root, "/secret");
1056        let resp =
1057            root.execute_with_middleware("/secret", handler, test_request("/secret"), params);
1058
1059        assert_eq!(resp.status_code(), StatusCode::UNAUTHORIZED);
1060        let log = get_log();
1061        assert!(log.contains(&"auth_reject".to_string()));
1062        assert!(!log.contains(&"handler".to_string()));
1063    }
1064
1065    /// 2.2.6e: Middleware can modify the response from next
1066    #[test]
1067    fn test_middleware_modifies_response() {
1068        fn header_middleware(
1069            req: HttpRequest,
1070            params: &RouteParams,
1071            next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
1072        ) -> HttpResponse<'static> {
1073            let resp = next(req, params);
1074            // Build a new response with an added header.
1075            let mut headers = resp.headers().to_vec();
1076            headers.push(("x-custom".to_string(), "injected".to_string()));
1077            HttpResponse::builder()
1078                .with_status_code(resp.status_code())
1079                .with_headers(headers)
1080                .with_body(resp.body().to_vec())
1081                .build()
1082        }
1083
1084        let mut root = RouteNode::new(NodeType::Static("".into()));
1085        root.insert("/test", Method::GET, logging_handler);
1086        root.set_middleware("/", header_middleware);
1087
1088        let (handler, params) = resolve_get(&root, "/test");
1089        let resp = root.execute_with_middleware("/test", handler, test_request("/test"), params);
1090
1091        let custom_header = resp
1092            .headers()
1093            .iter()
1094            .find(|(k, _)| k == "x-custom")
1095            .map(|(_, v)| v.clone());
1096        assert_eq!(custom_header, Some("injected".to_string()));
1097        assert_eq!(body_str(resp), "handler_response");
1098    }
1099
1100    /// 2.2.6f: set_middleware on same prefix replaces previous middleware
1101    #[test]
1102    fn test_set_middleware_replaces_previous() {
1103        fn mw_a(
1104            req: HttpRequest,
1105            params: &RouteParams,
1106            next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
1107        ) -> HttpResponse<'static> {
1108            log_entry("mw_a");
1109            next(req, params)
1110        }
1111        fn mw_b(
1112            req: HttpRequest,
1113            params: &RouteParams,
1114            next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
1115        ) -> HttpResponse<'static> {
1116            log_entry("mw_b");
1117            next(req, params)
1118        }
1119
1120        clear_log();
1121        let mut root = RouteNode::new(NodeType::Static("".into()));
1122        root.insert("/test", Method::GET, logging_handler);
1123        root.set_middleware("/", mw_a);
1124        root.set_middleware("/", mw_b); // should replace mw_a
1125
1126        let (handler, params) = resolve_get(&root, "/test");
1127        root.execute_with_middleware("/test", handler, test_request("/test"), params);
1128
1129        let log = get_log();
1130        assert!(!log.contains(&"mw_a".to_string()));
1131        assert!(log.contains(&"mw_b".to_string()));
1132    }
1133
1134    /// 2.2.6g: Middleware works in both query and update paths.
1135    /// This tests that execute_with_middleware works correctly (same function
1136    /// is used by both http_request and http_request_update).
1137    #[test]
1138    fn test_middleware_works_in_both_paths() {
1139        clear_log();
1140        let mut root = RouteNode::new(NodeType::Static("".into()));
1141
1142        fn post_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1143            log_entry("post_handler");
1144            response_with_text("posted")
1145        }
1146
1147        root.insert("/api/data", Method::GET, logging_handler);
1148        root.insert("/api/data", Method::POST, post_handler);
1149        root.set_middleware("/api", api_middleware);
1150
1151        // Simulate query path (GET)
1152        let (handler, params) = resolve_get(&root, "/api/data");
1153        let resp =
1154            root.execute_with_middleware("/api/data", handler, test_request("/api/data"), params);
1155        assert_eq!(body_str(resp), "handler_response");
1156        assert!(get_log().contains(&"api_mw_before".to_string()));
1157
1158        // Simulate update path (POST)
1159        clear_log();
1160        match root.resolve("/api/data", &Method::POST) {
1161            RouteResult::Found(handler, params, _, _) => {
1162                let req = HttpRequest::builder()
1163                    .with_method(Method::POST)
1164                    .with_url("/api/data")
1165                    .build();
1166                let resp = root.execute_with_middleware("/api/data", handler, req, params);
1167                assert_eq!(body_str(resp), "posted");
1168                assert!(get_log().contains(&"api_mw_before".to_string()));
1169                assert!(get_log().contains(&"post_handler".to_string()));
1170            }
1171            other => panic!("expected Found, got {}", route_result_name(&other)),
1172        }
1173    }
1174
1175    // ---- 2.3 Custom 404 tests ----
1176
1177    fn custom_404_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1178        HttpResponse::builder()
1179            .with_status_code(StatusCode::NOT_FOUND)
1180            .with_headers(vec![("content-type".to_string(), "text/html".to_string())])
1181            .with_body(Cow::Owned(b"<h1>Custom Not Found</h1>".to_vec()))
1182            .build()
1183    }
1184
1185    /// 2.3.4a: With custom 404, unmatched route returns custom response
1186    #[test]
1187    fn test_custom_404_returns_custom_response() {
1188        let mut root = RouteNode::new(NodeType::Static("".into()));
1189        root.insert("/exists", Method::GET, matched_get_handler);
1190        root.set_not_found(custom_404_handler);
1191
1192        // Unmatched path should invoke the custom 404 handler
1193        let resp = root
1194            .execute_not_found_with_middleware("/nonexistent", test_request("/nonexistent"))
1195            .expect("expected custom 404 response");
1196        assert_eq!(resp.status_code(), StatusCode::NOT_FOUND);
1197        assert_eq!(body_str(resp), "<h1>Custom Not Found</h1>");
1198    }
1199
1200    /// 2.3.4b: Without custom 404, unmatched route returns default "Not Found"
1201    #[test]
1202    fn test_default_404_without_custom_handler() {
1203        let mut root = RouteNode::new(NodeType::Static("".into()));
1204        root.insert("/exists", Method::GET, matched_get_handler);
1205        // No set_not_found call
1206
1207        // execute_not_found_with_middleware should return None
1208        let resp =
1209            root.execute_not_found_with_middleware("/nonexistent", test_request("/nonexistent"));
1210        assert!(resp.is_none(), "expected None when no custom 404 is set");
1211    }
1212
1213    /// 2.3.4c: Custom 404 handler receives the full HttpRequest
1214    #[test]
1215    fn test_custom_404_receives_full_request() {
1216        fn inspecting_404(req: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1217            // Echo back the URL from the request to prove it was passed through
1218            let url = req.url().to_string();
1219            response_with_text(&format!("404 for: {url}"))
1220        }
1221
1222        let mut root = RouteNode::new(NodeType::Static("".into()));
1223        root.set_not_found(inspecting_404);
1224
1225        let req = HttpRequest::builder()
1226            .with_method(Method::GET)
1227            .with_url("/some/missing/path")
1228            .build();
1229        let resp = root
1230            .execute_not_found_with_middleware("/some/missing/path", req)
1231            .expect("expected custom 404 response");
1232        let body = body_str(resp);
1233        assert!(
1234            body.contains("/some/missing/path"),
1235            "expected URL in response body, got: {body}"
1236        );
1237    }
1238
1239    /// 2.3.4d: Custom 404 can return JSON content-type
1240    #[test]
1241    fn test_custom_404_json_content_type() {
1242        fn json_404(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1243            HttpResponse::builder()
1244                .with_status_code(StatusCode::NOT_FOUND)
1245                .with_headers(vec![(
1246                    "content-type".to_string(),
1247                    "application/json".to_string(),
1248                )])
1249                .with_body(Cow::Owned(br#"{"error":"not found"}"#.to_vec()))
1250                .build()
1251        }
1252
1253        let mut root = RouteNode::new(NodeType::Static("".into()));
1254        root.set_not_found(json_404);
1255
1256        let resp = root
1257            .execute_not_found_with_middleware("/api/missing", test_request("/api/missing"))
1258            .expect("expected custom 404 response");
1259        assert_eq!(resp.status_code(), StatusCode::NOT_FOUND);
1260        let ct = resp
1261            .headers()
1262            .iter()
1263            .find(|(k, _)| k == "content-type")
1264            .map(|(_, v)| v.clone());
1265        assert_eq!(ct, Some("application/json".to_string()));
1266        assert_eq!(body_str(resp), r#"{"error":"not found"}"#);
1267    }
1268
1269    /// 2.3.4e: Root middleware executes before custom 404 handler
1270    #[test]
1271    fn test_root_middleware_runs_before_custom_404() {
1272        fn logging_404(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1273            log_entry("custom_404");
1274            response_with_text("custom 404")
1275        }
1276
1277        clear_log();
1278        let mut root = RouteNode::new(NodeType::Static("".into()));
1279        root.insert("/exists", Method::GET, logging_handler);
1280        root.set_middleware("/", root_middleware);
1281        root.set_not_found(logging_404);
1282
1283        let resp = root
1284            .execute_not_found_with_middleware("/nonexistent", test_request("/nonexistent"))
1285            .expect("expected custom 404 response");
1286
1287        let log = get_log();
1288        assert_eq!(
1289            log,
1290            vec!["root_mw_before", "custom_404", "root_mw_after"],
1291            "middleware should wrap the custom 404 handler"
1292        );
1293        assert_eq!(body_str(resp), "custom 404");
1294    }
1295
1296    // ---- 4.3.11: From<HttpResponse> for HandlerResult conversion ----
1297
1298    #[test]
1299    fn from_http_response_for_handler_result() {
1300        let response = HttpResponse::builder()
1301            .with_status_code(StatusCode::OK)
1302            .with_body(Cow::Owned(b"hello".to_vec()))
1303            .build();
1304
1305        let result: HandlerResult = response.into();
1306
1307        match result {
1308            HandlerResult::Response(resp) => {
1309                assert_eq!(resp.status_code(), StatusCode::OK);
1310                assert_eq!(resp.body(), b"hello");
1311            }
1312            HandlerResult::NotModified => panic!("expected Response, got NotModified"),
1313        }
1314    }
1315
1316    // ---- 5.5.2: Router edge case tests ----
1317
1318    /// Empty segments in paths are ignored by the trie (split + filter removes them).
1319    #[test]
1320    fn test_empty_segments_ignored() {
1321        let root = setup_router();
1322        // Triple slash between segments should still resolve
1323        let (handler, _) = resolve_get(&root, "/about///");
1324        assert_eq!(
1325            body_str(handler(test_request("/about"), HashMap::new())),
1326            "about"
1327        );
1328    }
1329
1330    /// URL-encoded characters are passed as-is to the trie (the trie does not decode).
1331    /// The handler receives the raw URL-encoded segment.
1332    #[test]
1333    fn test_url_encoded_characters_in_static_path() {
1334        let mut root = RouteNode::new(NodeType::Static("".into()));
1335        // Register a route with a literal percent-encoded segment.
1336        root.insert("/hello%20world", Method::GET, matched_about);
1337        let (handler, params) = resolve_get(&root, "/hello%20world");
1338        assert_eq!(
1339            body_str(handler(test_request("/hello%20world"), params)),
1340            "about"
1341        );
1342    }
1343
1344    /// URL-encoded characters captured by a param route are preserved as-is.
1345    #[test]
1346    fn test_url_encoded_characters_in_param() {
1347        let mut root = RouteNode::new(NodeType::Static("".into()));
1348        root.insert("/posts/:id", Method::GET, matched_deep);
1349        let (_, params) = resolve_get(&root, "/posts/hello%20world");
1350        assert_eq!(params.get("id").unwrap(), "hello%20world");
1351    }
1352
1353    /// Very long paths (100 segments) are handled without stack overflow or panic.
1354    #[test]
1355    fn test_very_long_path() {
1356        let mut root = RouteNode::new(NodeType::Static("".into()));
1357        // Build a path with 100 static segments
1358        let segments: Vec<String> = (0..100).map(|i| format!("s{i}")).collect();
1359        let path = format!("/{}", segments.join("/"));
1360        root.insert(&path, Method::GET, matched_about);
1361
1362        let (handler, params) = resolve_get(&root, &path);
1363        assert_eq!(body_str(handler(test_request(&path), params)), "about");
1364    }
1365
1366    /// Very long path that does not match any route returns NotFound.
1367    #[test]
1368    fn test_very_long_path_not_found() {
1369        let root = RouteNode::new(NodeType::Static("".into()));
1370        let segments: Vec<String> = (0..100).map(|i| format!("s{i}")).collect();
1371        let path = format!("/{}", segments.join("/"));
1372        assert!(matches!(
1373            root.resolve(&path, &Method::GET),
1374            RouteResult::NotFound
1375        ));
1376    }
1377
1378    /// Routes with many (4) dynamic parameters all capture correctly.
1379    #[test]
1380    fn test_many_parameters() {
1381        fn many_param_handler(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
1382            response_with_text(&format!(
1383                "{}/{}/{}/{}",
1384                params.get("a").unwrap(),
1385                params.get("b").unwrap(),
1386                params.get("c").unwrap(),
1387                params.get("d").unwrap(),
1388            ))
1389        }
1390
1391        let mut root = RouteNode::new(NodeType::Static("".into()));
1392        root.insert("/:a/:b/:c/:d", Method::GET, many_param_handler);
1393
1394        let (handler, params) = resolve_get(&root, "/w/x/y/z");
1395        assert_eq!(params.get("a").unwrap(), "w");
1396        assert_eq!(params.get("b").unwrap(), "x");
1397        assert_eq!(params.get("c").unwrap(), "y");
1398        assert_eq!(params.get("d").unwrap(), "z");
1399        assert_eq!(
1400            body_str(handler(test_request("/w/x/y/z"), params)),
1401            "w/x/y/z"
1402        );
1403    }
1404
1405    /// Static route takes precedence over param route for the same segment.
1406    #[test]
1407    fn test_static_precedence_over_param() {
1408        fn static_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1409            response_with_text("static")
1410        }
1411        fn param_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1412            response_with_text("param")
1413        }
1414
1415        let mut root = RouteNode::new(NodeType::Static("".into()));
1416        root.insert("/items/special", Method::GET, static_handler);
1417        root.insert("/items/:id", Method::GET, param_handler);
1418
1419        // "/items/special" should match the static route
1420        let (handler, _) = resolve_get(&root, "/items/special");
1421        assert_eq!(
1422            body_str(handler(test_request("/items/special"), HashMap::new())),
1423            "static"
1424        );
1425
1426        // "/items/other" should match the param route
1427        let (handler, params) = resolve_get(&root, "/items/other");
1428        assert_eq!(
1429            body_str(handler(test_request("/items/other"), params)),
1430            "param"
1431        );
1432    }
1433
1434    /// Param route takes precedence over wildcard route.
1435    #[test]
1436    fn test_param_precedence_over_wildcard() {
1437        fn param_handler(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
1438            response_with_text(&format!("param:{}", params.get("id").unwrap()))
1439        }
1440        fn wildcard_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1441            response_with_text("wildcard")
1442        }
1443
1444        let mut root = RouteNode::new(NodeType::Static("".into()));
1445        root.insert("/items/:id", Method::GET, param_handler);
1446        root.insert("/items/*", Method::GET, wildcard_handler);
1447
1448        // Single segment after /items/ should match param route
1449        let (handler, params) = resolve_get(&root, "/items/42");
1450        assert_eq!(
1451            body_str(handler(test_request("/items/42"), params.clone())),
1452            "param:42"
1453        );
1454
1455        // Multiple segments should match wildcard
1456        let (handler, params) = resolve_get(&root, "/items/42/extra");
1457        assert_eq!(params.get("*").unwrap(), "42/extra");
1458        assert_eq!(
1459            body_str(handler(test_request("/items/42/extra"), params)),
1460            "wildcard"
1461        );
1462    }
1463
1464    /// Root path "/" should not match when only nested routes exist.
1465    #[test]
1466    fn test_root_not_found_when_only_nested() {
1467        let mut root = RouteNode::new(NodeType::Static("".into()));
1468        root.insert("/api/data", Method::GET, matched_about);
1469        assert!(matches!(
1470            root.resolve("/", &Method::GET),
1471            RouteResult::NotFound
1472        ));
1473    }
1474
1475    /// insert_result and resolve return the result handler.
1476    #[test]
1477    fn test_insert_result_and_resolve() {
1478        fn handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1479            response_with_text("ok")
1480        }
1481        fn result_handler(_: HttpRequest, _: RouteParams) -> HandlerResult {
1482            HandlerResult::NotModified
1483        }
1484
1485        let mut root = RouteNode::new(NodeType::Static("".into()));
1486        root.insert("/test", Method::GET, handler);
1487        root.insert_result("/test", Method::GET, result_handler);
1488
1489        match root.resolve("/test", &Method::GET) {
1490            RouteResult::Found(_, _, Some(rh), _) => {
1491                // Verify the result handler returns NotModified
1492                let result = rh(test_request("/test"), HashMap::new());
1493                assert!(matches!(result, HandlerResult::NotModified));
1494            }
1495            RouteResult::Found(_, _, None, _) => panic!("expected result handler to be present"),
1496            other => panic!("expected Found, got {}", route_result_name(&other)),
1497        }
1498    }
1499
1500    /// match_path returns handlers and params without method dispatch.
1501    #[test]
1502    fn test_match_path_returns_handlers() {
1503        let mut root = RouteNode::new(NodeType::Static("".into()));
1504        root.insert("/items/:id", Method::GET, matched_get_handler);
1505        root.insert("/items/:id", Method::POST, matched_post_handler);
1506
1507        let (handlers, _, params, _) = root.match_path("/items/42").expect("should match");
1508        assert_eq!(params.get("id").unwrap(), "42");
1509        assert!(handlers.contains_key(&Method::GET));
1510        assert!(handlers.contains_key(&Method::POST));
1511        assert_eq!(handlers.len(), 2);
1512    }
1513
1514    /// match_path returns None for non-existent paths.
1515    #[test]
1516    fn test_match_path_returns_none() {
1517        let root = RouteNode::new(NodeType::Static("".into()));
1518        assert!(root.match_path("/nonexistent").is_none());
1519    }
1520
1521    // ---- 5.5.3: Additional middleware chain tests ----
1522
1523    /// Middleware can modify the request before passing it to the handler.
1524    /// The handler sees the modified request (e.g. added headers).
1525    #[test]
1526    fn test_middleware_modifies_request_before_handler() {
1527        fn inject_header_mw(
1528            req: HttpRequest,
1529            params: &RouteParams,
1530            next: &dyn Fn(HttpRequest, &RouteParams) -> HttpResponse<'static>,
1531        ) -> HttpResponse<'static> {
1532            // Build a new request with an added header.
1533            let mut headers = req.headers().to_vec();
1534            headers.push(("x-injected".to_string(), "mw-value".to_string()));
1535            let modified = HttpRequest::builder()
1536                .with_method(req.method().clone())
1537                .with_url(req.url())
1538                .with_headers(headers)
1539                .with_body(req.body().to_vec())
1540                .build();
1541            next(modified, params)
1542        }
1543
1544        fn header_checking_handler(req: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1545            let has_header = req
1546                .headers()
1547                .iter()
1548                .any(|(k, v)| k == "x-injected" && v == "mw-value");
1549            if has_header {
1550                response_with_text("header_present")
1551            } else {
1552                response_with_text("header_missing")
1553            }
1554        }
1555
1556        let mut root = RouteNode::new(NodeType::Static("".into()));
1557        root.insert("/check", Method::GET, header_checking_handler);
1558        root.set_middleware("/", inject_header_mw);
1559
1560        let (handler, params) = resolve_get(&root, "/check");
1561        let resp = root.execute_with_middleware("/check", handler, test_request("/check"), params);
1562        assert_eq!(body_str(resp), "header_present");
1563    }
1564
1565    /// Multiple middleware at different hierarchy levels all apply to not-found handler.
1566    #[test]
1567    fn test_multiple_middleware_on_not_found() {
1568        fn nf_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1569            log_entry("not_found_handler");
1570            response_with_text("not found")
1571        }
1572
1573        clear_log();
1574        let mut root = RouteNode::new(NodeType::Static("".into()));
1575        root.insert("/api/data", Method::GET, logging_handler);
1576        root.set_middleware("/", root_middleware);
1577        root.set_middleware("/api", api_middleware);
1578        root.set_not_found(nf_handler);
1579
1580        // Request to /api/missing — both root and /api middleware should fire
1581        let resp = root
1582            .execute_not_found_with_middleware("/api/missing", test_request("/api/missing"))
1583            .expect("expected not-found response");
1584
1585        let log = get_log();
1586        assert_eq!(
1587            log,
1588            vec![
1589                "root_mw_before",
1590                "api_mw_before",
1591                "not_found_handler",
1592                "api_mw_after",
1593                "root_mw_after",
1594            ],
1595            "both root and /api middleware should wrap the not-found handler"
1596        );
1597        assert_eq!(body_str(resp), "not found");
1598    }
1599
1600    /// Only root middleware applies to not-found for paths outside /api.
1601    #[test]
1602    fn test_not_found_only_root_middleware_for_non_api() {
1603        fn nf_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1604            log_entry("not_found_handler");
1605            response_with_text("not found")
1606        }
1607
1608        clear_log();
1609        let mut root = RouteNode::new(NodeType::Static("".into()));
1610        root.insert("/api/data", Method::GET, logging_handler);
1611        root.set_middleware("/", root_middleware);
1612        root.set_middleware("/api", api_middleware);
1613        root.set_not_found(nf_handler);
1614
1615        // Request to /other/missing — only root middleware, NOT /api middleware
1616        let resp = root
1617            .execute_not_found_with_middleware("/other/missing", test_request("/other/missing"))
1618            .expect("expected not-found response");
1619
1620        let log = get_log();
1621        assert_eq!(
1622            log,
1623            vec!["root_mw_before", "not_found_handler", "root_mw_after"],
1624            "/api middleware should NOT fire for /other/missing"
1625        );
1626        assert_eq!(body_str(resp), "not found");
1627    }
1628
1629    /// Middleware executes in correct order regardless of the registration order.
1630    /// (Ordering is by prefix segment count, not insertion order.)
1631    #[test]
1632    fn test_middleware_ordering_independent_of_registration_order() {
1633        clear_log();
1634        let mut root = RouteNode::new(NodeType::Static("".into()));
1635        root.insert("/api/v2/data", Method::GET, logging_handler);
1636
1637        // Register in reverse order: inner → outer → root
1638        root.set_middleware("/api/v2", api_v2_middleware);
1639        root.set_middleware("/api", api_middleware);
1640        root.set_middleware("/", root_middleware);
1641
1642        let (handler, params) = resolve_get(&root, "/api/v2/data");
1643        root.execute_with_middleware(
1644            "/api/v2/data",
1645            handler,
1646            test_request("/api/v2/data"),
1647            params,
1648        );
1649
1650        let log = get_log();
1651        assert_eq!(
1652            log,
1653            vec![
1654                "root_mw_before",
1655                "api_mw_before",
1656                "api_v2_mw_before",
1657                "handler",
1658                "api_v2_mw_after",
1659                "api_mw_after",
1660                "root_mw_after",
1661            ],
1662            "order should be root→api→api_v2 regardless of registration order"
1663        );
1664    }
1665
1666    /// No middleware registered — handler runs directly without wrapping.
1667    #[test]
1668    fn test_no_middleware_handler_runs_directly() {
1669        clear_log();
1670        let mut root = RouteNode::new(NodeType::Static("".into()));
1671        root.insert("/test", Method::GET, logging_handler);
1672        // No set_middleware calls
1673
1674        let (handler, params) = resolve_get(&root, "/test");
1675        let resp = root.execute_with_middleware("/test", handler, test_request("/test"), params);
1676
1677        let log = get_log();
1678        assert_eq!(log, vec!["handler"]);
1679        assert_eq!(body_str(resp), "handler_response");
1680    }
1681
1682    /// normalize_prefix normalizes various formats to canonical form.
1683    #[test]
1684    fn test_normalize_prefix_canonical() {
1685        assert_eq!(normalize_prefix("/"), "/");
1686        assert_eq!(normalize_prefix(""), "/");
1687        assert_eq!(normalize_prefix("/api"), "/api");
1688        assert_eq!(normalize_prefix("/api/"), "/api");
1689        assert_eq!(normalize_prefix("api"), "/api");
1690        assert_eq!(normalize_prefix("api/v2/"), "/api/v2");
1691    }
1692
1693    /// segment_count returns correct counts.
1694    #[test]
1695    fn test_segment_count() {
1696        assert_eq!(segment_count("/"), 0);
1697        assert_eq!(segment_count("/api"), 1);
1698        assert_eq!(segment_count("/api/v2"), 2);
1699        assert_eq!(segment_count("/api/v2/data"), 3);
1700    }
1701
1702    /// path_matches_prefix works for various combinations.
1703    #[test]
1704    fn test_path_matches_prefix() {
1705        // Root prefix matches everything
1706        assert!(path_matches_prefix("/api/data", "/"));
1707        assert!(path_matches_prefix("/", "/"));
1708
1709        // Exact match
1710        assert!(path_matches_prefix("/api", "/api"));
1711
1712        // Prefix match with separator
1713        assert!(path_matches_prefix("/api/data", "/api"));
1714        assert!(path_matches_prefix("/api/v2/data", "/api"));
1715
1716        // Does not match partial segment
1717        assert!(!path_matches_prefix("/api-v2", "/api"));
1718        assert!(!path_matches_prefix("/apidata", "/api"));
1719
1720        // No match
1721        assert!(!path_matches_prefix("/other", "/api"));
1722    }
1723
1724    // ---- 5.5.7: Property-based tests (proptest) ----
1725
1726    mod proptests {
1727        use super::*;
1728        use proptest::prelude::*;
1729
1730        fn dummy_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
1731            response_with_text("dummy")
1732        }
1733
1734        proptest! {
1735            /// Inserted routes are always found: any valid path that is inserted
1736            /// should resolve to Found for the same method.
1737            #[test]
1738            fn inserted_routes_are_always_found(path in "/[a-z]{1,5}(/[a-z]{1,5}){0,4}") {
1739                let mut root = RouteNode::new(NodeType::Static("".into()));
1740                root.insert(&path, Method::GET, dummy_handler);
1741                match root.resolve(&path, &Method::GET) {
1742                    RouteResult::Found(_, _, _, _) => {},
1743                    _ => panic!("expected Found for inserted path: {path}"),
1744                }
1745            }
1746
1747            /// Non-inserted routes are not found: a route that was never inserted
1748            /// should resolve to NotFound (assuming no wildcard or param overlap).
1749            #[test]
1750            fn non_inserted_routes_are_not_found(
1751                inserted in "/[a-z]{1,10}",
1752                queried in "/[a-z]{1,10}"
1753            ) {
1754                prop_assume!(inserted != queried);
1755                let mut root = RouteNode::new(NodeType::Static("".into()));
1756                root.insert(&inserted, Method::GET, dummy_handler);
1757                match root.resolve(&queried, &Method::GET) {
1758                    RouteResult::NotFound => {},
1759                    _ => panic!("expected NotFound for non-inserted route: {queried} (inserted: {inserted})"),
1760                }
1761            }
1762
1763            /// Param routes capture any single segment value.
1764            #[test]
1765            fn param_routes_capture_any_segment(
1766                prefix in "/[a-z]{1,5}",
1767                value in "[a-z0-9]{1,20}"
1768            ) {
1769                let route = format!("{prefix}/:id");
1770                let path = format!("{prefix}/{value}");
1771                let mut root = RouteNode::new(NodeType::Static("".into()));
1772                root.insert(&route, Method::GET, dummy_handler);
1773                match root.resolve(&path, &Method::GET) {
1774                    RouteResult::Found(_, params, _, _) => {
1775                        prop_assert_eq!(params.get("id").map(|s| s.as_str()), Some(value.as_str()));
1776                    },
1777                    other => panic!("expected Found, got {}", route_result_name(&other)),
1778                }
1779            }
1780
1781            /// Wildcard routes capture the remaining path (one or more segments).
1782            #[test]
1783            fn wildcard_routes_capture_remaining_path(
1784                prefix in "/[a-z]{1,5}",
1785                tail in "[a-z0-9]{1,5}(/[a-z0-9]{1,5}){0,3}"
1786            ) {
1787                let route = format!("{prefix}/*");
1788                let path = format!("{prefix}/{tail}");
1789                let mut root = RouteNode::new(NodeType::Static("".into()));
1790                root.insert(&route, Method::GET, dummy_handler);
1791                match root.resolve(&path, &Method::GET) {
1792                    RouteResult::Found(_, params, _, _) => {
1793                        prop_assert_eq!(params.get("*").map(|s| s.as_str()), Some(tail.as_str()));
1794                    },
1795                    other => panic!("expected Found, got {}", route_result_name(&other)),
1796                }
1797            }
1798
1799            /// Inserting a route does not affect resolution of a different method
1800            /// on the same path — it should return MethodNotAllowed, not Found.
1801            #[test]
1802            fn wrong_method_returns_method_not_allowed(path in "/[a-z]{1,5}(/[a-z]{1,5}){0,3}") {
1803                let mut root = RouteNode::new(NodeType::Static("".into()));
1804                root.insert(&path, Method::GET, dummy_handler);
1805                match root.resolve(&path, &Method::POST) {
1806                    RouteResult::MethodNotAllowed(allowed) => {
1807                        prop_assert!(allowed.contains(&Method::GET));
1808                    },
1809                    other => panic!("expected MethodNotAllowed, got {}", route_result_name(&other)),
1810                }
1811            }
1812
1813            /// Multiple param routes with different names capture correctly.
1814            #[test]
1815            fn multi_param_routes_capture_all(
1816                a in "[a-z0-9]{1,10}",
1817                b in "[a-z0-9]{1,10}"
1818            ) {
1819                let mut root = RouteNode::new(NodeType::Static("".into()));
1820                root.insert("/x/:first/:second", Method::GET, dummy_handler);
1821                let path = format!("/x/{a}/{b}");
1822                match root.resolve(&path, &Method::GET) {
1823                    RouteResult::Found(_, params, _, _) => {
1824                        prop_assert_eq!(params.get("first").map(|s| s.as_str()), Some(a.as_str()));
1825                        prop_assert_eq!(params.get("second").map(|s| s.as_str()), Some(b.as_str()));
1826                    },
1827                    other => panic!("expected Found, got {}", route_result_name(&other)),
1828                }
1829            }
1830        }
1831    }
1832
1833    // ---- 7.4 Route config and pattern tests ----
1834
1835    #[test]
1836    fn set_and_get_route_config() {
1837        let mut root = RouteNode::new(NodeType::Static("".into()));
1838        root.insert("/api/users", Method::GET, matched_get_handler);
1839
1840        let config = RouteConfig {
1841            certification: crate::certification::CertificationMode::skip(),
1842            ttl: Some(std::time::Duration::from_secs(60)),
1843            headers: vec![],
1844        };
1845        root.set_route_config("/api/users", config);
1846
1847        let rc = root
1848            .get_route_config("/api/users")
1849            .expect("should find config");
1850        assert!(matches!(
1851            rc.certification,
1852            crate::certification::CertificationMode::Skip
1853        ));
1854        assert_eq!(rc.ttl, Some(std::time::Duration::from_secs(60)));
1855    }
1856
1857    #[test]
1858    fn get_route_config_returns_none_for_unknown() {
1859        let root = RouteNode::new(NodeType::Static("".into()));
1860        assert!(root.get_route_config("/nonexistent").is_none());
1861    }
1862
1863    #[test]
1864    fn set_route_config_replaces_existing() {
1865        let mut root = RouteNode::new(NodeType::Static("".into()));
1866        root.insert("/test", Method::GET, matched_get_handler);
1867
1868        let config1 = RouteConfig {
1869            certification: crate::certification::CertificationMode::skip(),
1870            ttl: None,
1871            headers: vec![],
1872        };
1873        root.set_route_config("/test", config1);
1874
1875        let config2 = RouteConfig {
1876            certification: crate::certification::CertificationMode::authenticated(),
1877            ttl: Some(std::time::Duration::from_secs(300)),
1878            headers: vec![],
1879        };
1880        root.set_route_config("/test", config2);
1881
1882        let rc = root.get_route_config("/test").expect("should find config");
1883        assert!(matches!(
1884            rc.certification,
1885            crate::certification::CertificationMode::Full(_)
1886        ));
1887        assert_eq!(rc.ttl, Some(std::time::Duration::from_secs(300)));
1888    }
1889
1890    #[test]
1891    fn routes_without_config_default_to_response_only() {
1892        let mut root = RouteNode::new(NodeType::Static("".into()));
1893        root.insert("/page", Method::GET, matched_get_handler);
1894
1895        // No set_route_config call — get_route_config returns None,
1896        // and the caller should default to response_only.
1897        assert!(root.get_route_config("/page").is_none());
1898    }
1899
1900    #[test]
1901    fn resolve_returns_correct_pattern_for_static_route() {
1902        let mut root = RouteNode::new(NodeType::Static("".into()));
1903        root.insert("/api/users", Method::GET, matched_get_handler);
1904
1905        match root.resolve("/api/users", &Method::GET) {
1906            RouteResult::Found(_, _, _, pattern) => {
1907                assert_eq!(pattern, "/api/users");
1908            }
1909            other => panic!("expected Found, got {}", route_result_name(&other)),
1910        }
1911    }
1912
1913    #[test]
1914    fn resolve_returns_correct_pattern_for_param_route() {
1915        let mut root = RouteNode::new(NodeType::Static("".into()));
1916        root.insert("/users/:id", Method::GET, matched_get_handler);
1917
1918        match root.resolve("/users/42", &Method::GET) {
1919            RouteResult::Found(_, params, _, pattern) => {
1920                assert_eq!(pattern, "/users/:id");
1921                assert_eq!(params.get("id").unwrap(), "42");
1922            }
1923            other => panic!("expected Found, got {}", route_result_name(&other)),
1924        }
1925    }
1926
1927    #[test]
1928    fn resolve_returns_correct_pattern_for_wildcard_route() {
1929        let mut root = RouteNode::new(NodeType::Static("".into()));
1930        root.insert("/files/*", Method::GET, matched_get_handler);
1931
1932        match root.resolve("/files/a/b/c", &Method::GET) {
1933            RouteResult::Found(_, params, _, pattern) => {
1934                assert_eq!(pattern, "/files/*");
1935                assert_eq!(params.get("*").unwrap(), "a/b/c");
1936            }
1937            other => panic!("expected Found, got {}", route_result_name(&other)),
1938        }
1939    }
1940
1941    #[test]
1942    fn resolve_returns_correct_pattern_for_root_route() {
1943        let mut root = RouteNode::new(NodeType::Static("".into()));
1944        root.insert("/", Method::GET, matched_get_handler);
1945
1946        match root.resolve("/", &Method::GET) {
1947            RouteResult::Found(_, _, _, pattern) => {
1948                assert_eq!(pattern, "/");
1949            }
1950            other => panic!("expected Found, got {}", route_result_name(&other)),
1951        }
1952    }
1953
1954    #[test]
1955    fn resolve_returns_correct_pattern_for_nested_param() {
1956        let mut root = RouteNode::new(NodeType::Static("".into()));
1957        root.insert(
1958            "/posts/:postId/comments/:commentId",
1959            Method::GET,
1960            matched_get_handler,
1961        );
1962
1963        match root.resolve("/posts/10/comments/20", &Method::GET) {
1964            RouteResult::Found(_, params, _, pattern) => {
1965                assert_eq!(pattern, "/posts/:postId/comments/:commentId");
1966                assert_eq!(params.get("postId").unwrap(), "10");
1967                assert_eq!(params.get("commentId").unwrap(), "20");
1968            }
1969            other => panic!("expected Found, got {}", route_result_name(&other)),
1970        }
1971    }
1972
1973    #[test]
1974    fn route_config_lookup_via_pattern() {
1975        let mut root = RouteNode::new(NodeType::Static("".into()));
1976        root.insert("/users/:id", Method::GET, matched_get_handler);
1977
1978        let config = RouteConfig {
1979            certification: crate::certification::CertificationMode::skip(),
1980            ttl: None,
1981            headers: vec![],
1982        };
1983        root.set_route_config("/users/:id", config);
1984
1985        // Resolve actual path, get pattern, then look up config.
1986        match root.resolve("/users/42", &Method::GET) {
1987            RouteResult::Found(_, _, _, pattern) => {
1988                let rc = root.get_route_config(&pattern).expect("should find config");
1989                assert!(matches!(
1990                    rc.certification,
1991                    crate::certification::CertificationMode::Skip
1992                ));
1993            }
1994            other => panic!("expected Found, got {}", route_result_name(&other)),
1995        }
1996    }
1997
1998    // ---- 8.3.3: get_or_create_node tests ----
1999
2000    /// get_or_create_node creates intermediate nodes on first call.
2001    #[test]
2002    fn test_get_or_create_node_creates_intermediate_nodes() {
2003        let mut root = RouteNode::new(NodeType::Static("".into()));
2004        let _node = root.get_or_create_node("/api/v2/data");
2005
2006        // Root should have one static child: "api"
2007        assert_eq!(root.static_children.len(), 1);
2008        let api = root.static_children.get("api").expect("api child");
2009        assert_eq!(api.node_type, NodeType::Static("api".into()));
2010
2011        // "api" should have one static child: "v2"
2012        assert_eq!(api.static_children.len(), 1);
2013        let v2 = api.static_children.get("v2").expect("v2 child");
2014        assert_eq!(v2.node_type, NodeType::Static("v2".into()));
2015
2016        // "v2" should have one static child: "data"
2017        assert_eq!(v2.static_children.len(), 1);
2018        let data = v2.static_children.get("data").expect("data child");
2019        assert_eq!(data.node_type, NodeType::Static("data".into()));
2020    }
2021
2022    /// get_or_create_node is idempotent: second call returns the same node
2023    /// without creating duplicates.
2024    #[test]
2025    fn test_get_or_create_node_idempotent() {
2026        let mut root = RouteNode::new(NodeType::Static("".into()));
2027
2028        // First call creates nodes.
2029        let node = root.get_or_create_node("/api/users");
2030        node.handlers.insert(Method::GET, matched_get_handler);
2031
2032        // Second call should find the existing node.
2033        let node2 = root.get_or_create_node("/api/users");
2034        assert!(
2035            node2.handlers.contains_key(&Method::GET),
2036            "second call should return the same node with the handler intact"
2037        );
2038
2039        // Only one child chain should exist (no duplicates).
2040        assert_eq!(root.static_children.len(), 1);
2041        let api = root.static_children.get("api").expect("api child");
2042        assert_eq!(api.static_children.len(), 1);
2043    }
2044
2045    /// get_or_create_node with root path "/" returns self.
2046    #[test]
2047    fn test_get_or_create_node_root_path() {
2048        let mut root = RouteNode::new(NodeType::Static("".into()));
2049
2050        let node = root.get_or_create_node("/");
2051        node.handlers.insert(Method::GET, matched_get_handler);
2052
2053        // The handler should be on the root node itself.
2054        assert!(root.handlers.contains_key(&Method::GET));
2055        // No children created for root path.
2056        assert!(root.static_children.is_empty());
2057        assert!(root.param_child.is_none());
2058        assert!(root.wildcard_child.is_none());
2059    }
2060
2061    /// get_or_create_node handles param and wildcard segments.
2062    #[test]
2063    fn test_get_or_create_node_param_and_wildcard() {
2064        let mut root = RouteNode::new(NodeType::Static("".into()));
2065
2066        let _node = root.get_or_create_node("/users/:id/files/*");
2067
2068        assert_eq!(root.static_children.len(), 1);
2069        let users = root.static_children.get("users").expect("users child");
2070        assert_eq!(users.node_type, NodeType::Static("users".into()));
2071
2072        let param = users.param_child.as_ref().expect("param child");
2073        assert_eq!(param.node_type, NodeType::Param("id".into()));
2074
2075        assert_eq!(param.static_children.len(), 1);
2076        let files = param.static_children.get("files").expect("files child");
2077        assert_eq!(files.node_type, NodeType::Static("files".into()));
2078
2079        let wc = files.wildcard_child.as_ref().expect("wildcard child");
2080        assert_eq!(wc.node_type, NodeType::Wildcard);
2081    }
2082
2083    // ---- 8.5.6: Route trie preserves raw-encoded param values ----
2084
2085    /// Param route with `%20` in the URL resolves correctly and the raw param
2086    /// value contains `%20` — decoding is the responsibility of generated wrapper
2087    /// code, not the trie itself.
2088    #[test]
2089    fn test_param_with_percent_encoded_space_resolves_raw() {
2090        let mut root = RouteNode::new(NodeType::Static("".into()));
2091        root.insert("/posts/:id", Method::GET, matched_deep);
2092        let (_, params) = resolve_get(&root, "/posts/hello%20world");
2093        assert_eq!(
2094            params.get("id").unwrap(),
2095            "hello%20world",
2096            "trie should store the raw percent-encoded value; decoding happens in generated code"
2097        );
2098    }
2099
2100    /// Wildcard route with `%20` in the URL preserves the raw encoded value.
2101    #[test]
2102    fn test_wildcard_with_percent_encoded_space_resolves_raw() {
2103        let mut root = RouteNode::new(NodeType::Static("".into()));
2104        root.insert("/files/*", Method::GET, matched_folder);
2105        let (_, params) = resolve_get(&root, "/files/hello%20world/doc.pdf");
2106        assert_eq!(
2107            params.get("*").unwrap(),
2108            "hello%20world/doc.pdf",
2109            "trie should store the raw percent-encoded wildcard value"
2110        );
2111    }
2112
2113    // ---- 8.6.2: Router trie edge case tests ----
2114
2115    /// Inserting two param routes with different names at the same level reuses
2116    /// the first param child. The second insert's param name is silently ignored
2117    /// because `get_or_create_node` only creates a param child if none exists.
2118    #[test]
2119    fn multiple_param_children_first_wins() {
2120        fn handler_a(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
2121            response_with_text(&format!("a:{}", params.get("a").unwrap_or(&String::new())))
2122        }
2123        fn handler_b(_: HttpRequest, params: RouteParams) -> HttpResponse<'static> {
2124            response_with_text(&format!("b:{}", params.get("b").unwrap_or(&String::new())))
2125        }
2126
2127        let mut root = RouteNode::new(NodeType::Static("".into()));
2128        root.insert("/items/:a", Method::GET, handler_a);
2129        // Second insert reuses the existing param child (named "a"), so the
2130        // handler is added to that same node — but the param name stays "a".
2131        root.insert("/items/:b", Method::POST, handler_b);
2132
2133        // GET resolves and the param is captured under name "a" (first wins).
2134        let (handler, params) = resolve_get(&root, "/items/42");
2135        assert_eq!(params.get("a"), Some(&"42".to_string()));
2136        assert_eq!(params.get("b"), None);
2137        assert_eq!(body_str(handler(test_request("/items/42"), params)), "a:42");
2138
2139        // POST also uses param name "a" — the `:b` name from the second insert
2140        // was silently discarded.
2141        match root.resolve("/items/99", &Method::POST) {
2142            RouteResult::Found(handler, params, _, _) => {
2143                assert_eq!(params.get("a"), Some(&"99".to_string()));
2144                assert_eq!(params.get("b"), None);
2145                // handler_b tries to read "b" which is None, so prints "b:"
2146                assert_eq!(body_str(handler(test_request("/items/99"), params)), "b:");
2147            }
2148            other => panic!("expected Found, got {}", route_result_name(&other)),
2149        }
2150    }
2151
2152    /// Wildcard route `/files/*` consumes all remaining segments.
2153    #[test]
2154    fn wildcard_consumes_remaining_segments() {
2155        let mut root = RouteNode::new(NodeType::Static("".into()));
2156        root.insert("/files/*", Method::GET, matched_folder);
2157
2158        // Single segment after /files/
2159        let (_, params) = resolve_get(&root, "/files/a");
2160        assert_eq!(params.get("*").unwrap(), "a");
2161
2162        // Multiple segments after /files/
2163        let (_, params) = resolve_get(&root, "/files/a/b/c");
2164        assert_eq!(params.get("*").unwrap(), "a/b/c");
2165
2166        // Deep nesting
2167        let (_, params) = resolve_get(&root, "/files/a/b/c/d/e");
2168        assert_eq!(params.get("*").unwrap(), "a/b/c/d/e");
2169    }
2170
2171    /// Routes registered after a wildcard (e.g., `/files/*/edit`) are
2172    /// unreachable because `_match` consumes all remaining segments at the
2173    /// wildcard node without recursing further.
2174    #[test]
2175    fn post_wildcard_segments_unreachable() {
2176        fn edit_handler(_: HttpRequest, _: RouteParams) -> HttpResponse<'static> {
2177            response_with_text("edit")
2178        }
2179
2180        let mut root = RouteNode::new(NodeType::Static("".into()));
2181        root.insert("/files/*", Method::GET, matched_folder);
2182        // This creates a child under the wildcard node, but _match never
2183        // recurses into it — the wildcard greedily matches all segments.
2184        root.insert("/files/*/edit", Method::GET, edit_handler);
2185
2186        // "/files/something/edit" is consumed by the wildcard, not the edit route.
2187        let (handler, params) = resolve_get(&root, "/files/something/edit");
2188        assert_eq!(params.get("*").unwrap(), "something/edit");
2189        assert_eq!(
2190            body_str(handler(test_request("/files/something/edit"), params)),
2191            "folder"
2192        );
2193    }
2194
2195    /// Empty path "/" resolves to the root node's handler.
2196    #[test]
2197    fn empty_path_resolves_to_root() {
2198        let mut root = RouteNode::new(NodeType::Static("".into()));
2199        root.insert("/", Method::GET, matched_root);
2200
2201        let (handler, params) = resolve_get(&root, "/");
2202        assert!(params.is_empty());
2203        assert_eq!(body_str(handler(test_request("/"), params)), "root");
2204    }
2205
2206    /// Trailing slash is normalized: "/about/" matches "/about".
2207    #[test]
2208    fn trailing_slash_normalization() {
2209        let mut root = RouteNode::new(NodeType::Static("".into()));
2210        root.insert("/about", Method::GET, matched_about);
2211
2212        // Without trailing slash
2213        let (handler, _) = resolve_get(&root, "/about");
2214        assert_eq!(
2215            body_str(handler(test_request("/about"), HashMap::new())),
2216            "about"
2217        );
2218
2219        // With trailing slash — should match the same route
2220        let (handler, _) = resolve_get(&root, "/about/");
2221        assert_eq!(
2222            body_str(handler(test_request("/about/"), HashMap::new())),
2223            "about"
2224        );
2225    }
2226}