Skip to main content

mockforge_route_chaos/
lib.rs

1//! Per-route fault injection and latency simulation
2//!
3//! Provides route-specific chaos engineering capabilities that allow configuring
4//! fault injection and latency on a per-route basis, with support for multiple
5//! fault types and various latency distributions.
6//!
7//! This crate is isolated from mockforge-core to avoid Send issues. It only uses
8//! `rng()` (which is Send-safe) from rand.
9
10use async_trait::async_trait;
11use axum::http::{Method, Uri};
12use mockforge_core::config::{
13    LatencyDistribution, RouteConfig, RouteFaultType, RouteLatencyConfig,
14};
15use mockforge_core::priority_handler::{
16    RouteChaosInjectorTrait, RouteFaultResponse as CoreRouteFaultResponse,
17};
18use mockforge_core::{Error, Result};
19use rand::rng;
20use rand::Rng;
21use regex::Regex;
22use std::time::Duration;
23use tokio::time::sleep;
24use tracing::debug;
25
26/// Route matcher for matching requests to configured routes
27#[derive(Debug, Clone)]
28pub struct RouteMatcher {
29    /// Compiled route patterns (path -> regex)
30    routes: Vec<CompiledRoute>,
31}
32
33/// Compiled route with pattern matching
34#[derive(Debug, Clone)]
35struct CompiledRoute {
36    /// Original route config
37    config: RouteConfig,
38    /// Compiled regex pattern for path matching
39    path_pattern: Regex,
40    /// HTTP method
41    method: Method,
42}
43
44impl RouteMatcher {
45    /// Create a new route matcher from route configurations
46    ///
47    /// # Errors
48    /// Returns an error if any route has an invalid HTTP method or path pattern.
49    pub fn new(routes: Vec<RouteConfig>) -> Result<Self> {
50        let mut compiled_routes = Vec::new();
51
52        for route in routes {
53            // Convert path pattern to regex (e.g., /users/{id} -> /users/([^/]+))
54            let path_pattern = Self::compile_path_pattern(&route.path)?;
55            let method = route.method.parse::<Method>().map_err(|e| {
56                Error::generic(format!("Invalid HTTP method '{}': {}", route.method, e))
57            })?;
58
59            compiled_routes.push(CompiledRoute {
60                config: route,
61                path_pattern,
62                method,
63            });
64        }
65
66        Ok(Self {
67            routes: compiled_routes,
68        })
69    }
70
71    /// Match a request to a route configuration
72    pub fn match_route(&self, method: &Method, uri: &Uri) -> Option<&RouteConfig> {
73        let path = uri.path();
74
75        for compiled_route in &self.routes {
76            // Check method match
77            if compiled_route.method != method {
78                continue;
79            }
80
81            // Check path match
82            if compiled_route.path_pattern.is_match(path) {
83                return Some(&compiled_route.config);
84            }
85        }
86
87        None
88    }
89
90    /// Compile a path pattern to a regex
91    /// Converts /users/{id} to /users/([^/]+)
92    fn compile_path_pattern(pattern: &str) -> Result<Regex> {
93        // Escape special regex characters except {}
94        let mut regex_pattern = String::new();
95        let mut chars = pattern.chars();
96
97        while let Some(ch) = chars.next() {
98            match ch {
99                '{' => {
100                    // Find the closing brace
101                    let mut param_name = String::new();
102                    loop {
103                        match chars.next() {
104                            Some('}') => {
105                                // Replace with regex group
106                                regex_pattern.push_str("([^/]+)");
107                                break;
108                            }
109                            Some(c) => param_name.push(c),
110                            None => {
111                                // Unclosed brace - escape '{' and treat param name as literal
112                                regex_pattern.push_str("\\{");
113                                regex_pattern.push_str(&param_name);
114                                break;
115                            }
116                        }
117                    }
118                }
119                '*' => {
120                    // Wildcard - match anything
121                    regex_pattern.push_str(".*");
122                }
123                ch if ".+?^$|\\[]()".contains(ch) => {
124                    // Escape regex special characters
125                    regex_pattern.push('\\');
126                    regex_pattern.push(ch);
127                }
128                ch => {
129                    regex_pattern.push(ch);
130                }
131            }
132        }
133
134        // Anchor to start and end
135        let full_pattern = format!("^{regex_pattern}$");
136        Regex::new(&full_pattern)
137            .map_err(|e| Error::generic(format!("Invalid route pattern '{pattern}': {e}")))
138    }
139}
140
141/// Per-route fault and latency injector
142#[derive(Debug, Clone)]
143pub struct RouteChaosInjector {
144    /// Route matcher
145    matcher: RouteMatcher,
146}
147
148#[async_trait]
149impl RouteChaosInjectorTrait for RouteChaosInjector {
150    /// Inject latency for this request
151    async fn inject_latency(&self, method: &Method, uri: &Uri) -> Result<()> {
152        self.inject_latency_impl(method, uri).await
153    }
154
155    /// Get fault injection response for a request
156    fn get_fault_response(&self, method: &Method, uri: &Uri) -> Option<CoreRouteFaultResponse> {
157        self.get_fault_response_impl(method, uri).map(|r| CoreRouteFaultResponse {
158            status_code: r.status_code,
159            error_message: r.error_message,
160            fault_type: r.fault_type,
161        })
162    }
163}
164
165impl RouteChaosInjector {
166    /// Create a new route chaos injector
167    ///
168    /// # Errors
169    /// Returns an error if any route has an invalid HTTP method or path pattern.
170    pub fn new(routes: Vec<RouteConfig>) -> Result<Self> {
171        let matcher = RouteMatcher::new(routes)?;
172        Ok(Self { matcher })
173    }
174
175    /// Check if a fault should be injected for this request
176    pub fn should_inject_fault(
177        &self,
178        method: &Method,
179        uri: &Uri,
180    ) -> Option<RouteFaultInjectionResult> {
181        let route = self.matcher.match_route(method, uri)?;
182        let fault_config = route.fault_injection.as_ref()?;
183
184        if !fault_config.enabled {
185            return None;
186        }
187
188        // Check probability - using rng() which is Send-safe
189        let mut rng = rng();
190        if rng.random::<f64>() > fault_config.probability {
191            return None;
192        }
193
194        // Select a random fault type
195        if fault_config.fault_types.is_empty() {
196            return None;
197        }
198
199        let fault_type =
200            &fault_config.fault_types[rng.random_range(0..fault_config.fault_types.len())];
201
202        Some(RouteFaultInjectionResult {
203            fault_type: fault_type.clone(),
204        })
205    }
206
207    /// Inject latency for this request (internal implementation)
208    async fn inject_latency_impl(&self, method: &Method, uri: &Uri) -> Result<()> {
209        let Some(route) = self.matcher.match_route(method, uri) else {
210            return Ok(()); // No route match, no latency injection
211        };
212
213        let Some(latency_config) = &route.latency else {
214            return Ok(()); // No latency config
215        };
216
217        if !latency_config.enabled {
218            return Ok(());
219        }
220
221        // Calculate delay before any await point to ensure Send safety
222        // All RNG operations must complete before the await
223        let delay_ms = {
224            // Check probability - using rng() which is Send-safe
225            let mut rng = rng();
226            if rng.random::<f64>() > latency_config.probability {
227                return Ok(());
228            }
229
230            // Calculate delay (all RNG operations happen here, before await)
231            Self::calculate_delay(latency_config)
232        };
233
234        // Now we can await safely - all RNG operations are complete
235        if delay_ms > 0 {
236            debug!("Injecting per-route latency: {}ms for {} {}", delay_ms, method, uri.path());
237            sleep(Duration::from_millis(delay_ms)).await;
238        }
239
240        Ok(())
241    }
242
243    /// Calculate delay based on latency configuration
244    #[allow(
245        clippy::cast_possible_truncation,
246        clippy::cast_sign_loss,
247        clippy::cast_precision_loss
248    )]
249    fn calculate_delay(config: &RouteLatencyConfig) -> u64 {
250        // Using rng() which is Send-safe
251        let mut rng = rng();
252
253        let base_delay = match &config.distribution {
254            LatencyDistribution::Fixed => config.fixed_delay_ms.unwrap_or(0),
255            LatencyDistribution::Normal {
256                mean_ms,
257                std_dev_ms,
258            } => {
259                // Use Box-Muller transform for normal distribution
260                let u1: f64 = rng.random();
261                let u2: f64 = rng.random();
262                let z0 = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
263                let value = mean_ms + std_dev_ms * z0;
264                // Clamp to non-negative before casting
265                value.max(0.0) as u64
266            }
267            LatencyDistribution::Exponential { lambda } => {
268                // Inverse transform sampling for exponential distribution
269                let u: f64 = rng.random();
270                let value = -lambda.ln() * (1.0 - u);
271                // Clamp to non-negative before casting
272                value.max(0.0) as u64
273            }
274            LatencyDistribution::Uniform => {
275                if let Some((min, max)) = config.random_delay_range_ms {
276                    rng.random_range(min..=max)
277                } else {
278                    config.fixed_delay_ms.unwrap_or(0)
279                }
280            }
281        };
282
283        // Apply jitter
284        if config.jitter_percent > 0.0 {
285            let jitter = (base_delay as f64 * config.jitter_percent / 100.0) as u64;
286            let jitter_offset = rng.random_range(0..=jitter);
287            if rng.random_bool(0.5) {
288                base_delay + jitter_offset
289            } else {
290                base_delay.saturating_sub(jitter_offset)
291            }
292        } else {
293            base_delay
294        }
295    }
296
297    /// Get fault injection response for a request (internal implementation)
298    fn get_fault_response_impl(&self, method: &Method, uri: &Uri) -> Option<RouteFaultResponse> {
299        let fault_result = self.should_inject_fault(method, uri)?;
300
301        match &fault_result.fault_type {
302            RouteFaultType::HttpError {
303                status_code,
304                message,
305            } => Some(RouteFaultResponse {
306                status_code: *status_code,
307                error_message: message
308                    .clone()
309                    .unwrap_or_else(|| format!("Injected HTTP error {status_code}")),
310                fault_type: "http_error".to_string(),
311            }),
312            RouteFaultType::ConnectionError { message } => Some(RouteFaultResponse {
313                status_code: 503,
314                error_message: message.clone().unwrap_or_else(|| "Connection error".to_string()),
315                fault_type: "connection_error".to_string(),
316            }),
317            RouteFaultType::Timeout {
318                duration_ms,
319                message,
320            } => Some(RouteFaultResponse {
321                status_code: 504,
322                error_message: message
323                    .clone()
324                    .unwrap_or_else(|| format!("Request timeout after {duration_ms}ms")),
325                fault_type: "timeout".to_string(),
326            }),
327            RouteFaultType::PartialResponse { truncate_percent } => Some(RouteFaultResponse {
328                status_code: 200,
329                error_message: format!("Partial response (truncated at {truncate_percent}%)"),
330                fault_type: "partial_response".to_string(),
331            }),
332            RouteFaultType::PayloadCorruption { corruption_type } => Some(RouteFaultResponse {
333                status_code: 200,
334                error_message: format!("Payload corruption ({corruption_type})"),
335                fault_type: "payload_corruption".to_string(),
336            }),
337        }
338    }
339}
340
341/// Result of fault injection check
342#[derive(Debug, Clone)]
343pub struct RouteFaultInjectionResult {
344    /// The fault type to inject
345    pub fault_type: RouteFaultType,
346}
347
348/// Fault injection response
349#[derive(Debug, Clone)]
350pub struct RouteFaultResponse {
351    /// HTTP status code
352    pub status_code: u16,
353    /// Error message
354    pub error_message: String,
355    /// Fault type identifier
356    pub fault_type: String,
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use mockforge_core::config::{RouteConfig, RouteResponseConfig};
363    use std::collections::HashMap;
364
365    fn create_test_route(path: &str, method: &str) -> RouteConfig {
366        RouteConfig {
367            path: path.to_string(),
368            method: method.to_string(),
369            request: None,
370            response: RouteResponseConfig {
371                status: 200,
372                headers: HashMap::new(),
373                body: None,
374            },
375            fault_injection: None,
376            latency: None,
377        }
378    }
379
380    // RouteMatcher tests
381    #[test]
382    fn test_path_pattern_compilation() {
383        let pattern = RouteMatcher::compile_path_pattern("/users/{id}").unwrap();
384        assert!(pattern.is_match("/users/123"));
385        assert!(pattern.is_match("/users/abc"));
386        assert!(!pattern.is_match("/users/123/posts"));
387        assert!(!pattern.is_match("/users"));
388    }
389
390    #[test]
391    fn test_path_pattern_compilation_multiple_params() {
392        let pattern =
393            RouteMatcher::compile_path_pattern("/users/{user_id}/posts/{post_id}").unwrap();
394        assert!(pattern.is_match("/users/123/posts/456"));
395        assert!(pattern.is_match("/users/abc/posts/xyz"));
396        assert!(!pattern.is_match("/users/123/posts"));
397        assert!(!pattern.is_match("/users/123"));
398    }
399
400    #[test]
401    fn test_path_pattern_compilation_wildcard() {
402        let pattern = RouteMatcher::compile_path_pattern("/api/*").unwrap();
403        assert!(pattern.is_match("/api/"));
404        assert!(pattern.is_match("/api/users"));
405        assert!(pattern.is_match("/api/users/123/posts"));
406    }
407
408    #[test]
409    fn test_path_pattern_compilation_special_chars() {
410        let pattern = RouteMatcher::compile_path_pattern("/api/v1.0/users").unwrap();
411        assert!(pattern.is_match("/api/v1.0/users"));
412        assert!(!pattern.is_match("/api/v1X0/users"));
413    }
414
415    #[test]
416    fn test_path_pattern_compilation_empty_path() {
417        let pattern = RouteMatcher::compile_path_pattern("/").unwrap();
418        assert!(pattern.is_match("/"));
419        assert!(!pattern.is_match("/users"));
420    }
421
422    #[test]
423    fn test_route_matching() {
424        let routes = vec![
425            create_test_route("/users/{id}", "GET"),
426            create_test_route("/orders/{order_id}", "POST"),
427            create_test_route("/health", "GET"),
428        ];
429
430        let matcher = RouteMatcher::new(routes).unwrap();
431
432        let get_users = Method::GET;
433        let post_orders = Method::POST;
434        let get_health = Method::GET;
435
436        assert!(matcher.match_route(&get_users, &Uri::from_static("/users/123")).is_some());
437        assert!(matcher.match_route(&post_orders, &Uri::from_static("/orders/456")).is_some());
438        assert!(matcher.match_route(&get_health, &Uri::from_static("/health")).is_some());
439        assert!(matcher.match_route(&get_users, &Uri::from_static("/unknown")).is_none());
440    }
441
442    #[test]
443    fn test_route_matching_method_mismatch() {
444        let routes = vec![create_test_route("/users/{id}", "GET")];
445        let matcher = RouteMatcher::new(routes).unwrap();
446
447        // POST request to GET-only route should not match
448        assert!(matcher.match_route(&Method::POST, &Uri::from_static("/users/123")).is_none());
449    }
450
451    #[test]
452    fn test_route_matching_empty_routes() {
453        let matcher = RouteMatcher::new(vec![]).unwrap();
454        assert!(matcher.match_route(&Method::GET, &Uri::from_static("/anything")).is_none());
455    }
456
457    #[test]
458    fn test_route_matcher_debug() {
459        let routes = vec![create_test_route("/test", "GET")];
460        let matcher = RouteMatcher::new(routes).unwrap();
461        let debug = format!("{matcher:?}");
462        assert!(debug.contains("RouteMatcher"));
463    }
464
465    #[test]
466    fn test_route_matcher_clone() {
467        let routes = vec![create_test_route("/test", "GET")];
468        let matcher = RouteMatcher::new(routes).unwrap();
469        let cloned = matcher.clone();
470        // Use both original and clone to verify Clone trait works correctly
471        assert!(matcher.match_route(&Method::GET, &Uri::from_static("/test")).is_some());
472        assert!(cloned.match_route(&Method::GET, &Uri::from_static("/test")).is_some());
473    }
474
475    // RouteChaosInjector tests
476    #[test]
477    fn test_route_chaos_injector_new() {
478        let routes = vec![create_test_route("/test", "GET")];
479        let injector = RouteChaosInjector::new(routes);
480        assert!(injector.is_ok());
481    }
482
483    #[test]
484    fn test_route_chaos_injector_new_custom_method() {
485        // HTTP allows custom methods, so "CUSTOM" is actually valid
486        let routes = vec![create_test_route("/test", "CUSTOM")];
487        let injector = RouteChaosInjector::new(routes);
488        assert!(injector.is_ok());
489    }
490
491    #[test]
492    fn test_route_chaos_injector_debug() {
493        let routes = vec![create_test_route("/test", "GET")];
494        let injector = RouteChaosInjector::new(routes).unwrap();
495        let debug = format!("{injector:?}");
496        assert!(debug.contains("RouteChaosInjector"));
497    }
498
499    #[test]
500    fn test_route_chaos_injector_clone() {
501        let routes = vec![create_test_route("/test", "GET")];
502        let injector = RouteChaosInjector::new(routes).unwrap();
503        let _cloned = Clone::clone(&injector);
504    }
505
506    #[tokio::test]
507    async fn test_latency_injection() {
508        use mockforge_core::config::RouteLatencyConfig;
509
510        let mut route = create_test_route("/test", "GET");
511        route.latency = Some(RouteLatencyConfig {
512            enabled: true,
513            probability: 1.0,
514            fixed_delay_ms: Some(10),
515            random_delay_range_ms: None,
516            jitter_percent: 0.0,
517            distribution: LatencyDistribution::Fixed,
518        });
519
520        let injector = RouteChaosInjector::new(vec![route]).unwrap();
521        let start = std::time::Instant::now();
522        injector.inject_latency(&Method::GET, &Uri::from_static("/test")).await.unwrap();
523        let elapsed = start.elapsed();
524
525        assert!(elapsed >= Duration::from_millis(10));
526    }
527
528    #[tokio::test]
529    async fn test_latency_injection_disabled() {
530        use mockforge_core::config::RouteLatencyConfig;
531
532        let mut route = create_test_route("/test", "GET");
533        route.latency = Some(RouteLatencyConfig {
534            enabled: false,
535            probability: 1.0,
536            fixed_delay_ms: Some(100),
537            random_delay_range_ms: None,
538            jitter_percent: 0.0,
539            distribution: LatencyDistribution::Fixed,
540        });
541
542        let injector = RouteChaosInjector::new(vec![route]).unwrap();
543        let start = std::time::Instant::now();
544        injector.inject_latency(&Method::GET, &Uri::from_static("/test")).await.unwrap();
545        let elapsed = start.elapsed();
546
547        // Should not delay when disabled
548        assert!(elapsed < Duration::from_millis(50));
549    }
550
551    #[tokio::test]
552    async fn test_latency_injection_no_route_match() {
553        let routes = vec![create_test_route("/test", "GET")];
554        let injector = RouteChaosInjector::new(routes).unwrap();
555        let start = std::time::Instant::now();
556        injector
557            .inject_latency(&Method::GET, &Uri::from_static("/unknown"))
558            .await
559            .unwrap();
560        let elapsed = start.elapsed();
561
562        // Should not delay when no route matches
563        assert!(elapsed < Duration::from_millis(10));
564    }
565
566    #[tokio::test]
567    async fn test_latency_injection_no_latency_config() {
568        let routes = vec![create_test_route("/test", "GET")];
569        let injector = RouteChaosInjector::new(routes).unwrap();
570        let start = std::time::Instant::now();
571        injector.inject_latency(&Method::GET, &Uri::from_static("/test")).await.unwrap();
572        let elapsed = start.elapsed();
573
574        // Should not delay when no latency config
575        assert!(elapsed < Duration::from_millis(10));
576    }
577
578    #[tokio::test]
579    async fn test_latency_injection_uniform_distribution() {
580        use mockforge_core::config::RouteLatencyConfig;
581
582        let mut route = create_test_route("/test", "GET");
583        route.latency = Some(RouteLatencyConfig {
584            enabled: true,
585            probability: 1.0,
586            fixed_delay_ms: None,
587            random_delay_range_ms: Some((5, 15)),
588            jitter_percent: 0.0,
589            distribution: LatencyDistribution::Uniform,
590        });
591
592        let injector = RouteChaosInjector::new(vec![route]).unwrap();
593        let start = std::time::Instant::now();
594        injector.inject_latency(&Method::GET, &Uri::from_static("/test")).await.unwrap();
595        let elapsed = start.elapsed();
596
597        // Should delay somewhere in the range
598        assert!(elapsed >= Duration::from_millis(5));
599        assert!(elapsed < Duration::from_millis(100)); // Allow some buffer
600    }
601
602    #[tokio::test]
603    async fn test_latency_injection_normal_distribution() {
604        use mockforge_core::config::RouteLatencyConfig;
605
606        let mut route = create_test_route("/test", "GET");
607        route.latency = Some(RouteLatencyConfig {
608            enabled: true,
609            probability: 1.0,
610            fixed_delay_ms: None,
611            random_delay_range_ms: None,
612            jitter_percent: 0.0,
613            distribution: LatencyDistribution::Normal {
614                mean_ms: 10.0,
615                std_dev_ms: 1.0,
616            },
617        });
618
619        let injector = RouteChaosInjector::new(vec![route]).unwrap();
620        // Just verify it doesn't panic
621        injector.inject_latency(&Method::GET, &Uri::from_static("/test")).await.unwrap();
622    }
623
624    #[tokio::test]
625    async fn test_latency_injection_exponential_distribution() {
626        use mockforge_core::config::RouteLatencyConfig;
627
628        let mut route = create_test_route("/test", "GET");
629        route.latency = Some(RouteLatencyConfig {
630            enabled: true,
631            probability: 1.0,
632            fixed_delay_ms: None,
633            random_delay_range_ms: None,
634            jitter_percent: 0.0,
635            distribution: LatencyDistribution::Exponential { lambda: 0.1 },
636        });
637
638        let injector = RouteChaosInjector::new(vec![route]).unwrap();
639        // Just verify it doesn't panic
640        injector.inject_latency(&Method::GET, &Uri::from_static("/test")).await.unwrap();
641    }
642
643    #[tokio::test]
644    async fn test_latency_injection_with_jitter() {
645        use mockforge_core::config::RouteLatencyConfig;
646
647        let mut route = create_test_route("/test", "GET");
648        route.latency = Some(RouteLatencyConfig {
649            enabled: true,
650            probability: 1.0,
651            fixed_delay_ms: Some(10),
652            random_delay_range_ms: None,
653            jitter_percent: 50.0,
654            distribution: LatencyDistribution::Fixed,
655        });
656
657        let injector = RouteChaosInjector::new(vec![route]).unwrap();
658        // Just verify it doesn't panic
659        injector.inject_latency(&Method::GET, &Uri::from_static("/test")).await.unwrap();
660    }
661
662    // Fault injection tests
663    #[test]
664    fn test_fault_injection() {
665        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
666
667        let mut route = create_test_route("/test", "GET");
668        route.fault_injection = Some(RouteFaultInjectionConfig {
669            enabled: true,
670            probability: 1.0,
671            fault_types: vec![RouteFaultType::HttpError {
672                status_code: 500,
673                message: Some("Test error".to_string()),
674            }],
675        });
676
677        let injector = RouteChaosInjector::new(vec![route]).unwrap();
678        let response =
679            injector.get_fault_response(&Method::GET, &Uri::from_static("/test")).unwrap();
680
681        assert_eq!(response.status_code, 500);
682        assert_eq!(response.error_message, "Test error");
683    }
684
685    #[test]
686    fn test_fault_injection_http_error_default_message() {
687        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
688
689        let mut route = create_test_route("/test", "GET");
690        route.fault_injection = Some(RouteFaultInjectionConfig {
691            enabled: true,
692            probability: 1.0,
693            fault_types: vec![RouteFaultType::HttpError {
694                status_code: 503,
695                message: None,
696            }],
697        });
698
699        let injector = RouteChaosInjector::new(vec![route]).unwrap();
700        let response =
701            injector.get_fault_response(&Method::GET, &Uri::from_static("/test")).unwrap();
702
703        assert_eq!(response.status_code, 503);
704        assert!(response.error_message.contains("503"));
705        assert_eq!(response.fault_type, "http_error");
706    }
707
708    #[test]
709    fn test_fault_injection_connection_error() {
710        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
711
712        let mut route = create_test_route("/test", "GET");
713        route.fault_injection = Some(RouteFaultInjectionConfig {
714            enabled: true,
715            probability: 1.0,
716            fault_types: vec![RouteFaultType::ConnectionError {
717                message: Some("Network failure".to_string()),
718            }],
719        });
720
721        let injector = RouteChaosInjector::new(vec![route]).unwrap();
722        let response =
723            injector.get_fault_response(&Method::GET, &Uri::from_static("/test")).unwrap();
724
725        assert_eq!(response.status_code, 503);
726        assert_eq!(response.error_message, "Network failure");
727        assert_eq!(response.fault_type, "connection_error");
728    }
729
730    #[test]
731    fn test_fault_injection_connection_error_default_message() {
732        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
733
734        let mut route = create_test_route("/test", "GET");
735        route.fault_injection = Some(RouteFaultInjectionConfig {
736            enabled: true,
737            probability: 1.0,
738            fault_types: vec![RouteFaultType::ConnectionError { message: None }],
739        });
740
741        let injector = RouteChaosInjector::new(vec![route]).unwrap();
742        let response =
743            injector.get_fault_response(&Method::GET, &Uri::from_static("/test")).unwrap();
744
745        assert_eq!(response.status_code, 503);
746        assert_eq!(response.error_message, "Connection error");
747    }
748
749    #[test]
750    fn test_fault_injection_timeout() {
751        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
752
753        let mut route = create_test_route("/test", "GET");
754        route.fault_injection = Some(RouteFaultInjectionConfig {
755            enabled: true,
756            probability: 1.0,
757            fault_types: vec![RouteFaultType::Timeout {
758                duration_ms: 5000,
759                message: Some("Gateway timeout".to_string()),
760            }],
761        });
762
763        let injector = RouteChaosInjector::new(vec![route]).unwrap();
764        let response =
765            injector.get_fault_response(&Method::GET, &Uri::from_static("/test")).unwrap();
766
767        assert_eq!(response.status_code, 504);
768        assert_eq!(response.error_message, "Gateway timeout");
769        assert_eq!(response.fault_type, "timeout");
770    }
771
772    #[test]
773    fn test_fault_injection_timeout_default_message() {
774        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
775
776        let mut route = create_test_route("/test", "GET");
777        route.fault_injection = Some(RouteFaultInjectionConfig {
778            enabled: true,
779            probability: 1.0,
780            fault_types: vec![RouteFaultType::Timeout {
781                duration_ms: 3000,
782                message: None,
783            }],
784        });
785
786        let injector = RouteChaosInjector::new(vec![route]).unwrap();
787        let response =
788            injector.get_fault_response(&Method::GET, &Uri::from_static("/test")).unwrap();
789
790        assert_eq!(response.status_code, 504);
791        assert!(response.error_message.contains("3000"));
792    }
793
794    #[test]
795    fn test_fault_injection_partial_response() {
796        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
797
798        let mut route = create_test_route("/test", "GET");
799        route.fault_injection = Some(RouteFaultInjectionConfig {
800            enabled: true,
801            probability: 1.0,
802            fault_types: vec![RouteFaultType::PartialResponse {
803                truncate_percent: 50.0,
804            }],
805        });
806
807        let injector = RouteChaosInjector::new(vec![route]).unwrap();
808        let response =
809            injector.get_fault_response(&Method::GET, &Uri::from_static("/test")).unwrap();
810
811        assert_eq!(response.status_code, 200);
812        assert!(response.error_message.contains("50%"));
813        assert_eq!(response.fault_type, "partial_response");
814    }
815
816    #[test]
817    fn test_fault_injection_payload_corruption() {
818        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
819
820        let mut route = create_test_route("/test", "GET");
821        route.fault_injection = Some(RouteFaultInjectionConfig {
822            enabled: true,
823            probability: 1.0,
824            fault_types: vec![RouteFaultType::PayloadCorruption {
825                corruption_type: "random_bytes".to_string(),
826            }],
827        });
828
829        let injector = RouteChaosInjector::new(vec![route]).unwrap();
830        let response =
831            injector.get_fault_response(&Method::GET, &Uri::from_static("/test")).unwrap();
832
833        assert_eq!(response.status_code, 200);
834        assert!(response.error_message.contains("random_bytes"));
835        assert_eq!(response.fault_type, "payload_corruption");
836    }
837
838    #[test]
839    fn test_fault_injection_disabled() {
840        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
841
842        let mut route = create_test_route("/test", "GET");
843        route.fault_injection = Some(RouteFaultInjectionConfig {
844            enabled: false,
845            probability: 1.0,
846            fault_types: vec![RouteFaultType::HttpError {
847                status_code: 500,
848                message: None,
849            }],
850        });
851
852        let injector = RouteChaosInjector::new(vec![route]).unwrap();
853        let response = injector.get_fault_response(&Method::GET, &Uri::from_static("/test"));
854
855        assert!(response.is_none());
856    }
857
858    #[test]
859    fn test_fault_injection_no_fault_types() {
860        use mockforge_core::config::RouteFaultInjectionConfig;
861
862        let mut route = create_test_route("/test", "GET");
863        route.fault_injection = Some(RouteFaultInjectionConfig {
864            enabled: true,
865            probability: 1.0,
866            fault_types: vec![],
867        });
868
869        let injector = RouteChaosInjector::new(vec![route]).unwrap();
870        let response = injector.get_fault_response(&Method::GET, &Uri::from_static("/test"));
871
872        assert!(response.is_none());
873    }
874
875    #[test]
876    fn test_fault_injection_no_config() {
877        let route = create_test_route("/test", "GET");
878        let injector = RouteChaosInjector::new(vec![route]).unwrap();
879        let response = injector.get_fault_response(&Method::GET, &Uri::from_static("/test"));
880
881        assert!(response.is_none());
882    }
883
884    #[test]
885    fn test_fault_injection_no_route_match() {
886        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
887
888        let mut route = create_test_route("/test", "GET");
889        route.fault_injection = Some(RouteFaultInjectionConfig {
890            enabled: true,
891            probability: 1.0,
892            fault_types: vec![RouteFaultType::HttpError {
893                status_code: 500,
894                message: None,
895            }],
896        });
897
898        let injector = RouteChaosInjector::new(vec![route]).unwrap();
899        let response = injector.get_fault_response(&Method::GET, &Uri::from_static("/unknown"));
900
901        assert!(response.is_none());
902    }
903
904    // RouteFaultResponse tests
905    #[test]
906    fn test_route_fault_response_debug() {
907        let response = RouteFaultResponse {
908            status_code: 500,
909            error_message: "Error".to_string(),
910            fault_type: "http_error".to_string(),
911        };
912        let debug = format!("{response:?}");
913        assert!(debug.contains("RouteFaultResponse"));
914        assert!(debug.contains("500"));
915    }
916
917    #[test]
918    fn test_route_fault_response_clone() {
919        let response = RouteFaultResponse {
920            status_code: 503,
921            error_message: "Service unavailable".to_string(),
922            fault_type: "connection_error".to_string(),
923        };
924        let cloned = response.clone();
925        assert_eq!(response.status_code, cloned.status_code);
926        assert_eq!(response.error_message, cloned.error_message);
927        assert_eq!(response.fault_type, cloned.fault_type);
928    }
929
930    // RouteFaultInjectionResult tests
931    #[test]
932    fn test_route_fault_injection_result_debug() {
933        use mockforge_core::config::RouteFaultType;
934
935        let result = RouteFaultInjectionResult {
936            fault_type: RouteFaultType::HttpError {
937                status_code: 404,
938                message: None,
939            },
940        };
941        let debug = format!("{result:?}");
942        assert!(debug.contains("RouteFaultInjectionResult"));
943    }
944
945    #[test]
946    fn test_route_fault_injection_result_clone() {
947        use mockforge_core::config::RouteFaultType;
948
949        let result = RouteFaultInjectionResult {
950            fault_type: RouteFaultType::Timeout {
951                duration_ms: 1000,
952                message: None,
953            },
954        };
955        let _cloned = Clone::clone(&result);
956    }
957
958    // RouteChaosInjectorTrait implementation tests
959    #[tokio::test]
960    async fn test_trait_inject_latency() {
961        use mockforge_core::config::RouteLatencyConfig;
962
963        let mut route = create_test_route("/test", "GET");
964        route.latency = Some(RouteLatencyConfig {
965            enabled: true,
966            probability: 1.0,
967            fixed_delay_ms: Some(5),
968            random_delay_range_ms: None,
969            jitter_percent: 0.0,
970            distribution: LatencyDistribution::Fixed,
971        });
972
973        let injector = RouteChaosInjector::new(vec![route]).unwrap();
974        // Use trait method
975        let result = <RouteChaosInjector as RouteChaosInjectorTrait>::inject_latency(
976            &injector,
977            &Method::GET,
978            &Uri::from_static("/test"),
979        )
980        .await;
981        assert!(result.is_ok());
982    }
983
984    #[test]
985    fn test_trait_get_fault_response() {
986        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
987
988        let mut route = create_test_route("/test", "GET");
989        route.fault_injection = Some(RouteFaultInjectionConfig {
990            enabled: true,
991            probability: 1.0,
992            fault_types: vec![RouteFaultType::HttpError {
993                status_code: 502,
994                message: Some("Bad gateway".to_string()),
995            }],
996        });
997
998        let injector = RouteChaosInjector::new(vec![route]).unwrap();
999        // Use trait method
1000        let response = <RouteChaosInjector as RouteChaosInjectorTrait>::get_fault_response(
1001            &injector,
1002            &Method::GET,
1003            &Uri::from_static("/test"),
1004        );
1005
1006        assert!(response.is_some());
1007        let response = response.unwrap();
1008        assert_eq!(response.status_code, 502);
1009        assert_eq!(response.error_message, "Bad gateway");
1010    }
1011
1012    // Edge case tests
1013    #[test]
1014    fn test_route_with_query_params() {
1015        let routes = vec![create_test_route("/users/{id}", "GET")];
1016        let matcher = RouteMatcher::new(routes).unwrap();
1017
1018        // URI with query params - path should still match
1019        let uri = "/users/123?foo=bar".parse::<Uri>().unwrap();
1020        assert!(matcher.match_route(&Method::GET, &uri).is_some());
1021    }
1022
1023    #[test]
1024    fn test_multiple_routes_same_path_different_methods() {
1025        let routes = vec![
1026            create_test_route("/users/{id}", "GET"),
1027            create_test_route("/users/{id}", "DELETE"),
1028            create_test_route("/users/{id}", "PUT"),
1029        ];
1030
1031        let matcher = RouteMatcher::new(routes).unwrap();
1032
1033        assert!(matcher.match_route(&Method::GET, &Uri::from_static("/users/123")).is_some());
1034        assert!(matcher.match_route(&Method::DELETE, &Uri::from_static("/users/123")).is_some());
1035        assert!(matcher.match_route(&Method::PUT, &Uri::from_static("/users/123")).is_some());
1036        assert!(matcher.match_route(&Method::POST, &Uri::from_static("/users/123")).is_none());
1037    }
1038
1039    // Additional edge case tests for path pattern compilation
1040    #[test]
1041    fn test_path_pattern_unclosed_brace() {
1042        let pattern = RouteMatcher::compile_path_pattern("/users/{id").unwrap();
1043        // Should still create a regex, just won't match the intended pattern
1044        assert!(!pattern.is_match("/users/123"));
1045    }
1046
1047    #[test]
1048    fn test_path_pattern_nested_braces() {
1049        let pattern = RouteMatcher::compile_path_pattern("/users/{{id}}").unwrap();
1050        // Nested braces create unusual patterns
1051        let debug = format!("{pattern:?}");
1052        assert!(!debug.is_empty());
1053    }
1054
1055    #[test]
1056    fn test_path_pattern_multiple_wildcards() {
1057        let pattern = RouteMatcher::compile_path_pattern("/*/api/*").unwrap();
1058        assert!(pattern.is_match("/v1/api/users"));
1059        assert!(pattern.is_match("/v2/api/orders"));
1060    }
1061
1062    #[test]
1063    fn test_path_pattern_mixed_params_and_wildcards() {
1064        let pattern = RouteMatcher::compile_path_pattern("/users/{id}/*").unwrap();
1065        assert!(pattern.is_match("/users/123/posts"));
1066        assert!(pattern.is_match("/users/abc/profile/settings"));
1067    }
1068
1069    #[test]
1070    fn test_path_pattern_all_special_chars() {
1071        // Test all special regex chars are properly escaped
1072        let pattern = RouteMatcher::compile_path_pattern("/api/v1.0/data[test]").unwrap();
1073        assert!(pattern.is_match("/api/v1.0/data[test]"));
1074        assert!(!pattern.is_match("/api/v1X0/data[test]"));
1075    }
1076
1077    #[test]
1078    fn test_path_pattern_plus_sign() {
1079        let pattern = RouteMatcher::compile_path_pattern("/api/v1+2").unwrap();
1080        assert!(pattern.is_match("/api/v1+2"));
1081    }
1082
1083    #[test]
1084    fn test_path_pattern_question_mark() {
1085        let pattern = RouteMatcher::compile_path_pattern("/api/test?").unwrap();
1086        assert!(pattern.is_match("/api/test?"));
1087    }
1088
1089    #[test]
1090    fn test_path_pattern_caret_and_dollar() {
1091        let pattern = RouteMatcher::compile_path_pattern("/api/$test^path").unwrap();
1092        assert!(pattern.is_match("/api/$test^path"));
1093    }
1094
1095    #[test]
1096    fn test_path_pattern_pipe() {
1097        let pattern = RouteMatcher::compile_path_pattern("/api/test|path").unwrap();
1098        assert!(pattern.is_match("/api/test|path"));
1099    }
1100
1101    #[test]
1102    fn test_path_pattern_backslash() {
1103        let pattern = RouteMatcher::compile_path_pattern("/api/test\\path").unwrap();
1104        assert!(pattern.is_match("/api/test\\path"));
1105    }
1106
1107    #[test]
1108    fn test_path_pattern_parentheses() {
1109        let pattern = RouteMatcher::compile_path_pattern("/api/test(123)").unwrap();
1110        assert!(pattern.is_match("/api/test(123)"));
1111    }
1112
1113    #[test]
1114    fn test_path_pattern_brackets() {
1115        let pattern = RouteMatcher::compile_path_pattern("/api/test[123]").unwrap();
1116        assert!(pattern.is_match("/api/test[123]"));
1117    }
1118
1119    #[test]
1120    fn test_path_pattern_empty_param_name() {
1121        let pattern = RouteMatcher::compile_path_pattern("/users/{}").unwrap();
1122        assert!(pattern.is_match("/users/123"));
1123        assert!(pattern.is_match("/users/abc"));
1124    }
1125
1126    // calculate_delay tests
1127    #[test]
1128    fn test_calculate_delay_fixed_no_jitter() {
1129        use mockforge_core::config::RouteLatencyConfig;
1130
1131        let config = RouteLatencyConfig {
1132            enabled: true,
1133            probability: 1.0,
1134            fixed_delay_ms: Some(100),
1135            random_delay_range_ms: None,
1136            jitter_percent: 0.0,
1137            distribution: LatencyDistribution::Fixed,
1138        };
1139
1140        let delay = RouteChaosInjector::calculate_delay(&config);
1141        assert_eq!(delay, 100);
1142    }
1143
1144    #[test]
1145    fn test_calculate_delay_fixed_with_jitter() {
1146        use mockforge_core::config::RouteLatencyConfig;
1147
1148        let config = RouteLatencyConfig {
1149            enabled: true,
1150            probability: 1.0,
1151            fixed_delay_ms: Some(100),
1152            random_delay_range_ms: None,
1153            jitter_percent: 20.0,
1154            distribution: LatencyDistribution::Fixed,
1155        };
1156
1157        let delay = RouteChaosInjector::calculate_delay(&config);
1158        // With 20% jitter on 100ms, delay should be between 80 and 120
1159        assert!((80..=120).contains(&delay));
1160    }
1161
1162    #[test]
1163    fn test_calculate_delay_uniform_with_range() {
1164        use mockforge_core::config::RouteLatencyConfig;
1165
1166        let config = RouteLatencyConfig {
1167            enabled: true,
1168            probability: 1.0,
1169            fixed_delay_ms: None,
1170            random_delay_range_ms: Some((50, 150)),
1171            jitter_percent: 0.0,
1172            distribution: LatencyDistribution::Uniform,
1173        };
1174
1175        let delay = RouteChaosInjector::calculate_delay(&config);
1176        assert!((50..=150).contains(&delay));
1177    }
1178
1179    #[test]
1180    fn test_calculate_delay_uniform_without_range() {
1181        use mockforge_core::config::RouteLatencyConfig;
1182
1183        let config = RouteLatencyConfig {
1184            enabled: true,
1185            probability: 1.0,
1186            fixed_delay_ms: Some(75),
1187            random_delay_range_ms: None,
1188            jitter_percent: 0.0,
1189            distribution: LatencyDistribution::Uniform,
1190        };
1191
1192        let delay = RouteChaosInjector::calculate_delay(&config);
1193        // Falls back to fixed_delay_ms when no range is provided
1194        assert_eq!(delay, 75);
1195    }
1196
1197    #[test]
1198    fn test_calculate_delay_uniform_no_fixed_no_range() {
1199        use mockforge_core::config::RouteLatencyConfig;
1200
1201        let config = RouteLatencyConfig {
1202            enabled: true,
1203            probability: 1.0,
1204            fixed_delay_ms: None,
1205            random_delay_range_ms: None,
1206            jitter_percent: 0.0,
1207            distribution: LatencyDistribution::Uniform,
1208        };
1209
1210        let delay = RouteChaosInjector::calculate_delay(&config);
1211        assert_eq!(delay, 0);
1212    }
1213
1214    #[test]
1215    fn test_calculate_delay_normal_distribution() {
1216        use mockforge_core::config::RouteLatencyConfig;
1217
1218        let config = RouteLatencyConfig {
1219            enabled: true,
1220            probability: 1.0,
1221            fixed_delay_ms: None,
1222            random_delay_range_ms: None,
1223            jitter_percent: 0.0,
1224            distribution: LatencyDistribution::Normal {
1225                mean_ms: 100.0,
1226                std_dev_ms: 10.0,
1227            },
1228        };
1229
1230        // Normal distribution should produce values, typically around the mean
1231        // We can't test exact value due to randomness, but delay is u64 so always >= 0
1232        let _ = RouteChaosInjector::calculate_delay(&config);
1233    }
1234
1235    #[test]
1236    fn test_calculate_delay_exponential_distribution() {
1237        use mockforge_core::config::RouteLatencyConfig;
1238
1239        let config = RouteLatencyConfig {
1240            enabled: true,
1241            probability: 1.0,
1242            fixed_delay_ms: None,
1243            random_delay_range_ms: None,
1244            jitter_percent: 0.0,
1245            distribution: LatencyDistribution::Exponential { lambda: 0.01 },
1246        };
1247
1248        // Exponential distribution should produce non-negative values (delay is u64, always >= 0)
1249        let _ = RouteChaosInjector::calculate_delay(&config);
1250    }
1251
1252    #[test]
1253    fn test_calculate_delay_normal_with_jitter() {
1254        use mockforge_core::config::RouteLatencyConfig;
1255
1256        let config = RouteLatencyConfig {
1257            enabled: true,
1258            probability: 1.0,
1259            fixed_delay_ms: None,
1260            random_delay_range_ms: None,
1261            jitter_percent: 10.0,
1262            distribution: LatencyDistribution::Normal {
1263                mean_ms: 100.0,
1264                std_dev_ms: 5.0,
1265            },
1266        };
1267
1268        // Should produce non-negative values with jitter applied (delay is u64, always >= 0)
1269        let _ = RouteChaosInjector::calculate_delay(&config);
1270    }
1271
1272    #[test]
1273    fn test_calculate_delay_fixed_zero() {
1274        use mockforge_core::config::RouteLatencyConfig;
1275
1276        let config = RouteLatencyConfig {
1277            enabled: true,
1278            probability: 1.0,
1279            fixed_delay_ms: Some(0),
1280            random_delay_range_ms: None,
1281            jitter_percent: 0.0,
1282            distribution: LatencyDistribution::Fixed,
1283        };
1284
1285        let delay = RouteChaosInjector::calculate_delay(&config);
1286        assert_eq!(delay, 0);
1287    }
1288
1289    #[test]
1290    fn test_calculate_delay_with_large_jitter() {
1291        use mockforge_core::config::RouteLatencyConfig;
1292
1293        let config = RouteLatencyConfig {
1294            enabled: true,
1295            probability: 1.0,
1296            fixed_delay_ms: Some(100),
1297            random_delay_range_ms: None,
1298            jitter_percent: 100.0,
1299            distribution: LatencyDistribution::Fixed,
1300        };
1301
1302        let delay = RouteChaosInjector::calculate_delay(&config);
1303        // With 100% jitter, delay should be between 0 and 200
1304        assert!(delay <= 200);
1305    }
1306
1307    #[test]
1308    fn test_calculate_delay_jitter_saturating_sub() {
1309        use mockforge_core::config::RouteLatencyConfig;
1310
1311        let config = RouteLatencyConfig {
1312            enabled: true,
1313            probability: 1.0,
1314            fixed_delay_ms: Some(10),
1315            random_delay_range_ms: None,
1316            jitter_percent: 200.0, // 200% jitter could cause subtraction to go negative
1317            distribution: LatencyDistribution::Fixed,
1318        };
1319
1320        let delay = RouteChaosInjector::calculate_delay(&config);
1321        // Should never be negative due to saturating_sub
1322        assert!(delay < u64::MAX);
1323    }
1324
1325    // should_inject_fault tests
1326    #[test]
1327    fn test_should_inject_fault_zero_probability() {
1328        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
1329
1330        let mut route = create_test_route("/test", "GET");
1331        route.fault_injection = Some(RouteFaultInjectionConfig {
1332            enabled: true,
1333            probability: 0.0,
1334            fault_types: vec![RouteFaultType::HttpError {
1335                status_code: 500,
1336                message: None,
1337            }],
1338        });
1339
1340        let injector = RouteChaosInjector::new(vec![route]).unwrap();
1341
1342        // With 0.0 probability, should never inject
1343        let mut found_injection = false;
1344        for _ in 0..100 {
1345            if injector.should_inject_fault(&Method::GET, &Uri::from_static("/test")).is_some() {
1346                found_injection = true;
1347                break;
1348            }
1349        }
1350        assert!(!found_injection);
1351    }
1352
1353    #[test]
1354    fn test_should_inject_fault_multiple_fault_types() {
1355        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
1356
1357        let mut route = create_test_route("/test", "GET");
1358        route.fault_injection = Some(RouteFaultInjectionConfig {
1359            enabled: true,
1360            probability: 1.0,
1361            fault_types: vec![
1362                RouteFaultType::HttpError {
1363                    status_code: 500,
1364                    message: None,
1365                },
1366                RouteFaultType::Timeout {
1367                    duration_ms: 1000,
1368                    message: None,
1369                },
1370                RouteFaultType::ConnectionError { message: None },
1371            ],
1372        });
1373
1374        let injector = RouteChaosInjector::new(vec![route]).unwrap();
1375
1376        // With 1.0 probability and multiple fault types, should inject one of them
1377        let result = injector.should_inject_fault(&Method::GET, &Uri::from_static("/test"));
1378        assert!(result.is_some());
1379    }
1380
1381    #[test]
1382    fn test_should_inject_fault_returns_different_types() {
1383        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
1384        use std::collections::HashSet;
1385
1386        let mut route = create_test_route("/test", "GET");
1387        route.fault_injection = Some(RouteFaultInjectionConfig {
1388            enabled: true,
1389            probability: 1.0,
1390            fault_types: vec![
1391                RouteFaultType::HttpError {
1392                    status_code: 500,
1393                    message: None,
1394                },
1395                RouteFaultType::HttpError {
1396                    status_code: 503,
1397                    message: None,
1398                },
1399                RouteFaultType::Timeout {
1400                    duration_ms: 1000,
1401                    message: None,
1402                },
1403            ],
1404        });
1405
1406        let injector = RouteChaosInjector::new(vec![route]).unwrap();
1407
1408        // Run multiple times and verify we can get different fault types
1409        let mut seen_types = HashSet::new();
1410        for _ in 0..50 {
1411            if let Some(result) =
1412                injector.should_inject_fault(&Method::GET, &Uri::from_static("/test"))
1413            {
1414                seen_types.insert(format!("{:?}", result.fault_type));
1415            }
1416        }
1417        // Should see at least one fault type (possibly more with randomness)
1418        assert!(!seen_types.is_empty());
1419    }
1420
1421    // Latency injection with probability tests
1422    #[tokio::test]
1423    async fn test_latency_injection_zero_probability() {
1424        use mockforge_core::config::RouteLatencyConfig;
1425
1426        let mut route = create_test_route("/test", "GET");
1427        route.latency = Some(RouteLatencyConfig {
1428            enabled: true,
1429            probability: 0.0,
1430            fixed_delay_ms: Some(100),
1431            random_delay_range_ms: None,
1432            jitter_percent: 0.0,
1433            distribution: LatencyDistribution::Fixed,
1434        });
1435
1436        let injector = RouteChaosInjector::new(vec![route]).unwrap();
1437
1438        // With 0.0 probability, should never inject delay
1439        for _ in 0..10 {
1440            let start = std::time::Instant::now();
1441            injector.inject_latency(&Method::GET, &Uri::from_static("/test")).await.unwrap();
1442            let elapsed = start.elapsed();
1443            assert!(elapsed < Duration::from_millis(50));
1444        }
1445    }
1446
1447    #[tokio::test]
1448    async fn test_latency_injection_mid_probability() {
1449        use mockforge_core::config::RouteLatencyConfig;
1450
1451        let mut route = create_test_route("/test", "GET");
1452        route.latency = Some(RouteLatencyConfig {
1453            enabled: true,
1454            probability: 0.5,
1455            fixed_delay_ms: Some(10),
1456            random_delay_range_ms: None,
1457            jitter_percent: 0.0,
1458            distribution: LatencyDistribution::Fixed,
1459        });
1460
1461        let injector = RouteChaosInjector::new(vec![route]).unwrap();
1462
1463        // With 0.5 probability, should sometimes inject, sometimes not
1464        let mut injected_count = 0;
1465        for _ in 0..20 {
1466            let start = std::time::Instant::now();
1467            injector.inject_latency(&Method::GET, &Uri::from_static("/test")).await.unwrap();
1468            let elapsed = start.elapsed();
1469            if elapsed >= Duration::from_millis(10) {
1470                injected_count += 1;
1471            }
1472        }
1473        // Should have injected at least once but not every time
1474        assert!(injected_count > 0 && injected_count < 20);
1475    }
1476
1477    // Route matching edge cases
1478    #[test]
1479    fn test_route_matching_trailing_slash() {
1480        let routes = vec![create_test_route("/users", "GET")];
1481        let matcher = RouteMatcher::new(routes).unwrap();
1482
1483        // Without trailing slash should match
1484        assert!(matcher.match_route(&Method::GET, &Uri::from_static("/users")).is_some());
1485        // With trailing slash should not match (exact match)
1486        assert!(matcher.match_route(&Method::GET, &Uri::from_static("/users/")).is_none());
1487    }
1488
1489    #[test]
1490    fn test_route_matching_case_sensitive() {
1491        let routes = vec![create_test_route("/Users", "GET")];
1492        let matcher = RouteMatcher::new(routes).unwrap();
1493
1494        // Should be case-sensitive
1495        assert!(matcher.match_route(&Method::GET, &Uri::from_static("/Users")).is_some());
1496        assert!(matcher.match_route(&Method::GET, &Uri::from_static("/users")).is_none());
1497    }
1498
1499    #[test]
1500    fn test_first_matching_route_wins() {
1501        let mut route1 = create_test_route("/api/*", "GET");
1502        route1.response.status = 200;
1503
1504        let mut route2 = create_test_route("/api/users", "GET");
1505        route2.response.status = 201;
1506
1507        let matcher = RouteMatcher::new(vec![route1, route2]).unwrap();
1508
1509        // First route with wildcard should match
1510        let result = matcher.match_route(&Method::GET, &Uri::from_static("/api/users")).unwrap();
1511        assert_eq!(result.response.status, 200);
1512    }
1513
1514    #[test]
1515    fn test_route_with_fragment() {
1516        let routes = vec![create_test_route("/users/{id}", "GET")];
1517        let matcher = RouteMatcher::new(routes).unwrap();
1518
1519        // Fragments are not part of the path
1520        let uri = "/users/123#section".parse::<Uri>().unwrap();
1521        assert!(matcher.match_route(&Method::GET, &uri).is_some());
1522    }
1523
1524    // Compiled route tests
1525    #[test]
1526    fn test_compiled_route_debug() {
1527        let route = create_test_route("/test", "GET");
1528        let pattern = RouteMatcher::compile_path_pattern(&route.path).unwrap();
1529        let method = route.method.parse::<Method>().unwrap();
1530
1531        let compiled = CompiledRoute {
1532            config: route,
1533            path_pattern: pattern,
1534            method,
1535        };
1536
1537        let debug = format!("{compiled:?}");
1538        assert!(debug.contains("CompiledRoute"));
1539    }
1540
1541    #[test]
1542    fn test_compiled_route_clone() {
1543        let route = create_test_route("/test", "GET");
1544        let pattern = RouteMatcher::compile_path_pattern(&route.path).unwrap();
1545        let method = route.method.parse::<Method>().unwrap();
1546
1547        let compiled = CompiledRoute {
1548            config: route,
1549            path_pattern: pattern,
1550            method,
1551        };
1552
1553        let _cloned = Clone::clone(&compiled);
1554    }
1555
1556    // Integration-style tests
1557    #[tokio::test]
1558    async fn test_full_chaos_injection_http_error() {
1559        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
1560
1561        let mut route = create_test_route("/api/users", "POST");
1562        route.fault_injection = Some(RouteFaultInjectionConfig {
1563            enabled: true,
1564            probability: 1.0,
1565            fault_types: vec![RouteFaultType::HttpError {
1566                status_code: 429,
1567                message: Some("Rate limit exceeded".to_string()),
1568            }],
1569        });
1570
1571        let injector = RouteChaosInjector::new(vec![route]).unwrap();
1572
1573        // Check fault response
1574        let response = injector.get_fault_response(&Method::POST, &Uri::from_static("/api/users"));
1575        assert!(response.is_some());
1576        let response = response.unwrap();
1577        assert_eq!(response.status_code, 429);
1578        assert_eq!(response.error_message, "Rate limit exceeded");
1579    }
1580
1581    #[tokio::test]
1582    async fn test_full_chaos_injection_with_latency_and_fault() {
1583        use mockforge_core::config::{
1584            RouteFaultInjectionConfig, RouteFaultType, RouteLatencyConfig,
1585        };
1586
1587        let mut route = create_test_route("/api/orders", "GET");
1588        route.latency = Some(RouteLatencyConfig {
1589            enabled: true,
1590            probability: 1.0,
1591            fixed_delay_ms: Some(5),
1592            random_delay_range_ms: None,
1593            jitter_percent: 0.0,
1594            distribution: LatencyDistribution::Fixed,
1595        });
1596        route.fault_injection = Some(RouteFaultInjectionConfig {
1597            enabled: true,
1598            probability: 1.0,
1599            fault_types: vec![RouteFaultType::Timeout {
1600                duration_ms: 5000,
1601                message: None,
1602            }],
1603        });
1604
1605        let injector = RouteChaosInjector::new(vec![route]).unwrap();
1606
1607        // Test latency injection
1608        let start = std::time::Instant::now();
1609        injector
1610            .inject_latency(&Method::GET, &Uri::from_static("/api/orders"))
1611            .await
1612            .unwrap();
1613        let elapsed = start.elapsed();
1614        assert!(elapsed >= Duration::from_millis(5));
1615
1616        // Test fault injection
1617        let response = injector.get_fault_response(&Method::GET, &Uri::from_static("/api/orders"));
1618        assert!(response.is_some());
1619    }
1620
1621    #[test]
1622    fn test_multiple_routes_different_configs() {
1623        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
1624
1625        let mut route1 = create_test_route("/api/v1/users", "GET");
1626        route1.fault_injection = Some(RouteFaultInjectionConfig {
1627            enabled: true,
1628            probability: 1.0,
1629            fault_types: vec![RouteFaultType::HttpError {
1630                status_code: 404,
1631                message: None,
1632            }],
1633        });
1634
1635        let mut route2 = create_test_route("/api/v1/orders", "GET");
1636        route2.fault_injection = Some(RouteFaultInjectionConfig {
1637            enabled: true,
1638            probability: 1.0,
1639            fault_types: vec![RouteFaultType::HttpError {
1640                status_code: 500,
1641                message: None,
1642            }],
1643        });
1644
1645        let injector = RouteChaosInjector::new(vec![route1, route2]).unwrap();
1646
1647        let response1 =
1648            injector.get_fault_response(&Method::GET, &Uri::from_static("/api/v1/users"));
1649        assert_eq!(response1.unwrap().status_code, 404);
1650
1651        let response2 =
1652            injector.get_fault_response(&Method::GET, &Uri::from_static("/api/v1/orders"));
1653        assert_eq!(response2.unwrap().status_code, 500);
1654    }
1655
1656    // Error path tests
1657    #[test]
1658    fn test_invalid_regex_pattern() {
1659        // Pattern with unmatched brackets should still compile
1660        // Regex crate will handle the escaping
1661        let result = RouteMatcher::compile_path_pattern("/api/[invalid");
1662        assert!(result.is_ok());
1663    }
1664
1665    #[test]
1666    fn test_route_matcher_with_invalid_method() {
1667        let mut route = create_test_route("/test", "GET");
1668        // Create a route with an invalid method string directly
1669        route.method = "INVALID METHOD WITH SPACES".to_string();
1670
1671        let result = RouteMatcher::new(vec![route]);
1672        assert!(result.is_err());
1673    }
1674
1675    // Trait coverage tests
1676    #[tokio::test]
1677    async fn test_trait_inject_latency_no_match() {
1678        let routes = vec![create_test_route("/test", "GET")];
1679        let injector = RouteChaosInjector::new(routes).unwrap();
1680
1681        let result = <RouteChaosInjector as RouteChaosInjectorTrait>::inject_latency(
1682            &injector,
1683            &Method::POST,
1684            &Uri::from_static("/nomatch"),
1685        )
1686        .await;
1687
1688        assert!(result.is_ok());
1689    }
1690
1691    #[test]
1692    fn test_trait_get_fault_response_no_match() {
1693        use mockforge_core::config::{RouteFaultInjectionConfig, RouteFaultType};
1694
1695        let mut route = create_test_route("/test", "GET");
1696        route.fault_injection = Some(RouteFaultInjectionConfig {
1697            enabled: true,
1698            probability: 1.0,
1699            fault_types: vec![RouteFaultType::HttpError {
1700                status_code: 500,
1701                message: None,
1702            }],
1703        });
1704
1705        let injector = RouteChaosInjector::new(vec![route]).unwrap();
1706
1707        let response = <RouteChaosInjector as RouteChaosInjectorTrait>::get_fault_response(
1708            &injector,
1709            &Method::POST,
1710            &Uri::from_static("/nomatch"),
1711        );
1712
1713        assert!(response.is_none());
1714    }
1715
1716    // Normal distribution edge case - negative values
1717    #[test]
1718    fn test_calculate_delay_normal_negative_clamp() {
1719        use mockforge_core::config::RouteLatencyConfig;
1720
1721        let config = RouteLatencyConfig {
1722            enabled: true,
1723            probability: 1.0,
1724            fixed_delay_ms: None,
1725            random_delay_range_ms: None,
1726            jitter_percent: 0.0,
1727            distribution: LatencyDistribution::Normal {
1728                mean_ms: 10.0,
1729                std_dev_ms: 100.0, // Large std dev can create negative values
1730            },
1731        };
1732
1733        // Run multiple times to potentially hit negative values that should be clamped
1734        for _ in 0..20 {
1735            let delay = RouteChaosInjector::calculate_delay(&config);
1736            // Should never be negative due to max(0.0) clamp
1737            assert!(delay < u64::MAX);
1738        }
1739    }
1740
1741    // Exponential distribution edge case
1742    #[test]
1743    fn test_calculate_delay_exponential_various_lambdas() {
1744        use mockforge_core::config::RouteLatencyConfig;
1745
1746        // Test with very small lambda
1747        let config = RouteLatencyConfig {
1748            enabled: true,
1749            probability: 1.0,
1750            fixed_delay_ms: None,
1751            random_delay_range_ms: None,
1752            jitter_percent: 0.0,
1753            distribution: LatencyDistribution::Exponential { lambda: 0.001 },
1754        };
1755        // delay is u64, always >= 0
1756        let _ = RouteChaosInjector::calculate_delay(&config);
1757
1758        // Test with large lambda
1759        let config = RouteLatencyConfig {
1760            enabled: true,
1761            probability: 1.0,
1762            fixed_delay_ms: None,
1763            random_delay_range_ms: None,
1764            jitter_percent: 0.0,
1765            distribution: LatencyDistribution::Exponential { lambda: 10.0 },
1766        };
1767        // delay is u64, always >= 0
1768        let _ = RouteChaosInjector::calculate_delay(&config);
1769    }
1770
1771    #[test]
1772    fn test_path_pattern_consecutive_params() {
1773        let pattern = RouteMatcher::compile_path_pattern("/api/{param1}{param2}").unwrap();
1774        // This creates consecutive capture groups
1775        assert!(pattern.is_match("/api/value1value2"));
1776    }
1777
1778    #[test]
1779    fn test_path_matching_numeric_params() {
1780        let routes = vec![create_test_route("/users/{id}", "GET")];
1781        let matcher = RouteMatcher::new(routes).unwrap();
1782
1783        // Should match numeric IDs
1784        assert!(matcher.match_route(&Method::GET, &Uri::from_static("/users/12345")).is_some());
1785        // Should match UUIDs
1786        assert!(matcher
1787            .match_route(
1788                &Method::GET,
1789                &Uri::from_static("/users/550e8400-e29b-41d4-a716-446655440000")
1790            )
1791            .is_some());
1792    }
1793
1794    #[test]
1795    fn test_empty_route_path() {
1796        let pattern = RouteMatcher::compile_path_pattern("").unwrap();
1797        assert!(pattern.is_match(""));
1798    }
1799}