1use 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#[derive(Debug, Clone)]
28pub struct RouteMatcher {
29 routes: Vec<CompiledRoute>,
31}
32
33#[derive(Debug, Clone)]
35struct CompiledRoute {
36 config: RouteConfig,
38 path_pattern: Regex,
40 method: Method,
42}
43
44impl RouteMatcher {
45 pub fn new(routes: Vec<RouteConfig>) -> Result<Self> {
50 let mut compiled_routes = Vec::new();
51
52 for route in routes {
53 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 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 if compiled_route.method != method {
78 continue;
79 }
80
81 if compiled_route.path_pattern.is_match(path) {
83 return Some(&compiled_route.config);
84 }
85 }
86
87 None
88 }
89
90 fn compile_path_pattern(pattern: &str) -> Result<Regex> {
93 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 let mut param_name = String::new();
102 loop {
103 match chars.next() {
104 Some('}') => {
105 regex_pattern.push_str("([^/]+)");
107 break;
108 }
109 Some(c) => param_name.push(c),
110 None => {
111 regex_pattern.push_str("\\{");
113 regex_pattern.push_str(¶m_name);
114 break;
115 }
116 }
117 }
118 }
119 '*' => {
120 regex_pattern.push_str(".*");
122 }
123 ch if ".+?^$|\\[]()".contains(ch) => {
124 regex_pattern.push('\\');
126 regex_pattern.push(ch);
127 }
128 ch => {
129 regex_pattern.push(ch);
130 }
131 }
132 }
133
134 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#[derive(Debug, Clone)]
143pub struct RouteChaosInjector {
144 matcher: RouteMatcher,
146}
147
148#[async_trait]
149impl RouteChaosInjectorTrait for RouteChaosInjector {
150 async fn inject_latency(&self, method: &Method, uri: &Uri) -> Result<()> {
152 self.inject_latency_impl(method, uri).await
153 }
154
155 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 pub fn new(routes: Vec<RouteConfig>) -> Result<Self> {
171 let matcher = RouteMatcher::new(routes)?;
172 Ok(Self { matcher })
173 }
174
175 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 let mut rng = rng();
190 if rng.random::<f64>() > fault_config.probability {
191 return None;
192 }
193
194 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 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(()); };
212
213 let Some(latency_config) = &route.latency else {
214 return Ok(()); };
216
217 if !latency_config.enabled {
218 return Ok(());
219 }
220
221 let delay_ms = {
224 let mut rng = rng();
226 if rng.random::<f64>() > latency_config.probability {
227 return Ok(());
228 }
229
230 Self::calculate_delay(latency_config)
232 };
233
234 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 #[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 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 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 value.max(0.0) as u64
266 }
267 LatencyDistribution::Exponential { lambda } => {
268 let u: f64 = rng.random();
270 let value = -lambda.ln() * (1.0 - u);
271 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 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 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#[derive(Debug, Clone)]
343pub struct RouteFaultInjectionResult {
344 pub fault_type: RouteFaultType,
346}
347
348#[derive(Debug, Clone)]
350pub struct RouteFaultResponse {
351 pub status_code: u16,
353 pub error_message: String,
355 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 #[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 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 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 #[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 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 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 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 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 assert!(elapsed >= Duration::from_millis(5));
599 assert!(elapsed < Duration::from_millis(100)); }
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 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 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 injector.inject_latency(&Method::GET, &Uri::from_static("/test")).await.unwrap();
660 }
661
662 #[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 #[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 #[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 #[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 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 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 #[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 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 #[test]
1041 fn test_path_pattern_unclosed_brace() {
1042 let pattern = RouteMatcher::compile_path_pattern("/users/{id").unwrap();
1043 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 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 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 #[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 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 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 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 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 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 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, distribution: LatencyDistribution::Fixed,
1318 };
1319
1320 let delay = RouteChaosInjector::calculate_delay(&config);
1321 assert!(delay < u64::MAX);
1323 }
1324
1325 #[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 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 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 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 assert!(!seen_types.is_empty());
1419 }
1420
1421 #[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 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 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 assert!(injected_count > 0 && injected_count < 20);
1475 }
1476
1477 #[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 assert!(matcher.match_route(&Method::GET, &Uri::from_static("/users")).is_some());
1485 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 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 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 let uri = "/users/123#section".parse::<Uri>().unwrap();
1521 assert!(matcher.match_route(&Method::GET, &uri).is_some());
1522 }
1523
1524 #[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 #[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 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 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 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 #[test]
1658 fn test_invalid_regex_pattern() {
1659 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 route.method = "INVALID METHOD WITH SPACES".to_string();
1670
1671 let result = RouteMatcher::new(vec![route]);
1672 assert!(result.is_err());
1673 }
1674
1675 #[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 #[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, },
1731 };
1732
1733 for _ in 0..20 {
1735 let delay = RouteChaosInjector::calculate_delay(&config);
1736 assert!(delay < u64::MAX);
1738 }
1739 }
1740
1741 #[test]
1743 fn test_calculate_delay_exponential_various_lambdas() {
1744 use mockforge_core::config::RouteLatencyConfig;
1745
1746 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 let _ = RouteChaosInjector::calculate_delay(&config);
1757
1758 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 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 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 assert!(matcher.match_route(&Method::GET, &Uri::from_static("/users/12345")).is_some());
1785 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}