mockforge_core/
failure_injection.rs

1//! Enhanced failure injection system with per-tag include/exclude filters
2//! and error rate configuration.
3
4use rand::{rng, Rng};
5use std::collections::HashMap;
6
7/// Failure injection configuration
8#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
9pub struct FailureConfig {
10    /// Global error rate (0.0 to 1.0)
11    pub global_error_rate: f64,
12    /// Default status codes for failures
13    pub default_status_codes: Vec<u16>,
14    /// Per-tag error rates and status overrides
15    pub tag_configs: HashMap<String, TagFailureConfig>,
16    /// Tags to include in failure injection (if empty, all tags are included)
17    pub include_tags: Vec<String>,
18    /// Tags to exclude from failure injection
19    pub exclude_tags: Vec<String>,
20}
21
22/// Per-tag failure configuration
23#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
24pub struct TagFailureConfig {
25    /// Error rate for this tag (0.0 to 1.0)
26    pub error_rate: f64,
27    /// Status codes for this tag (overrides global defaults)
28    pub status_codes: Option<Vec<u16>>,
29    /// Custom error message for this tag
30    pub error_message: Option<String>,
31}
32
33impl Default for FailureConfig {
34    fn default() -> Self {
35        Self {
36            global_error_rate: 0.0,
37            default_status_codes: vec![500, 502, 503, 504],
38            tag_configs: HashMap::new(),
39            include_tags: Vec::new(),
40            exclude_tags: Vec::new(),
41        }
42    }
43}
44
45impl Default for TagFailureConfig {
46    fn default() -> Self {
47        Self {
48            error_rate: 0.0,
49            status_codes: None,
50            error_message: None,
51        }
52    }
53}
54
55/// Enhanced failure injector with tag filtering and error rates
56#[derive(Debug, Clone)]
57pub struct FailureInjector {
58    /// Global failure configuration
59    config: Option<FailureConfig>,
60    /// Whether failure injection is enabled globally
61    enabled: bool,
62}
63
64impl FailureInjector {
65    /// Create a new failure injector
66    pub fn new(config: Option<FailureConfig>, enabled: bool) -> Self {
67        Self { config, enabled }
68    }
69
70    /// Check if failure injection is enabled globally
71    pub fn is_enabled(&self) -> bool {
72        self.enabled && self.config.is_some()
73    }
74
75    /// Determine if a failure should be injected for the given tags
76    pub fn should_inject_failure(&self, tags: &[String]) -> bool {
77        if !self.is_enabled() {
78            return false;
79        }
80
81        let config = match &self.config {
82            Some(cfg) => cfg,
83            None => return false,
84        };
85
86        // Check if any tag is in the exclude list
87        if tags.iter().any(|tag| config.exclude_tags.contains(tag)) {
88            return false;
89        }
90
91        // Check include tags (if specified, only include these tags)
92        if !config.include_tags.is_empty()
93            && !tags.iter().any(|tag| config.include_tags.contains(tag))
94        {
95            return false;
96        }
97
98        // Find the best matching tag configuration
99        let tag_config = self.find_best_tag_config(tags, config);
100
101        // Use tag-specific error rate if available, otherwise global rate
102        let error_rate = tag_config.map(|tc| tc.error_rate).unwrap_or(config.global_error_rate);
103
104        // Check if failure should occur based on error rate
105        if error_rate <= 0.0 {
106            return false;
107        }
108        if error_rate >= 1.0 {
109            return true;
110        }
111
112        let mut rng = rng();
113        rng.random_bool(error_rate)
114    }
115
116    /// Get failure response details for the given tags
117    pub fn get_failure_response(&self, tags: &[String]) -> Option<(u16, String)> {
118        if !self.is_enabled() {
119            return None;
120        }
121
122        let config = match &self.config {
123            Some(cfg) => cfg,
124            None => return None,
125        };
126
127        // Find the best matching tag configuration
128        let tag_config = self.find_best_tag_config(tags, config);
129
130        // Determine status codes to use
131        let status_codes = tag_config
132            .and_then(|tc| tc.status_codes.clone())
133            .unwrap_or_else(|| config.default_status_codes.clone());
134
135        // Determine error message
136        let error_message = tag_config
137            .and_then(|tc| tc.error_message.clone())
138            .unwrap_or_else(|| "Injected failure".to_string());
139
140        // Select a random status code
141        let mut rng = rng();
142        let status_code = if status_codes.is_empty() {
143            500
144        } else {
145            let index = rng.random_range(0..status_codes.len());
146            status_codes[index]
147        };
148
149        Some((status_code, error_message))
150    }
151
152    /// Find the best matching tag configuration for the given tags
153    /// Returns the first matching tag config, or None if no match
154    fn find_best_tag_config<'a>(
155        &self,
156        tags: &[String],
157        config: &'a FailureConfig,
158    ) -> Option<&'a TagFailureConfig> {
159        // Look for the first tag that has a configuration
160        for tag in tags {
161            if let Some(tag_config) = config.tag_configs.get(tag) {
162                return Some(tag_config);
163            }
164        }
165        None
166    }
167
168    /// Process a request with failure injection
169    /// Returns Some((status_code, error_message)) if failure should be injected, None otherwise
170    pub fn process_request(&self, tags: &[String]) -> Option<(u16, String)> {
171        if self.should_inject_failure(tags) {
172            self.get_failure_response(tags)
173        } else {
174            None
175        }
176    }
177
178    /// Update the failure configuration
179    pub fn update_config(&mut self, config: Option<FailureConfig>) {
180        self.config = config;
181    }
182
183    /// Enable or disable failure injection
184    pub fn set_enabled(&mut self, enabled: bool) {
185        self.enabled = enabled;
186    }
187}
188
189impl Default for FailureInjector {
190    fn default() -> Self {
191        Self::new(None, false)
192    }
193}
194
195/// Helper function to create a failure injector from core config
196pub fn create_failure_injector(
197    failures_enabled: bool,
198    failure_config: Option<FailureConfig>,
199) -> FailureInjector {
200    FailureInjector::new(failure_config, failures_enabled)
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use std::collections::HashMap;
207
208    fn create_test_config() -> FailureConfig {
209        let mut tag_configs = HashMap::new();
210        tag_configs.insert(
211            "auth".to_string(),
212            TagFailureConfig {
213                error_rate: 0.1,
214                status_codes: Some(vec![401, 403]),
215                error_message: Some("Authentication failed".to_string()),
216            },
217        );
218        tag_configs.insert(
219            "payments".to_string(),
220            TagFailureConfig {
221                error_rate: 0.05,
222                status_codes: Some(vec![402, 503]),
223                error_message: Some("Payment failed".to_string()),
224            },
225        );
226
227        FailureConfig {
228            global_error_rate: 0.02,
229            default_status_codes: vec![500, 502],
230            tag_configs,
231            include_tags: Vec::new(),
232            exclude_tags: vec!["health".to_string()],
233        }
234    }
235
236    #[test]
237    fn test_failure_injector_disabled() {
238        let injector = FailureInjector::new(Some(create_test_config()), false);
239        assert!(!injector.is_enabled());
240        assert!(!injector.should_inject_failure(&["auth".to_string()]));
241        assert!(injector.get_failure_response(&["auth".to_string()]).is_none());
242    }
243
244    #[test]
245    fn test_failure_injector_no_config() {
246        let injector = FailureInjector::new(None, true);
247        assert!(!injector.is_enabled());
248        assert!(!injector.should_inject_failure(&["auth".to_string()]));
249    }
250
251    #[test]
252    fn test_exclude_tags() {
253        let injector = FailureInjector::new(Some(create_test_config()), true);
254        assert!(!injector.should_inject_failure(&["health".to_string()]));
255        assert!(!injector.should_inject_failure(&["health".to_string(), "auth".to_string()]));
256    }
257
258    #[test]
259    fn test_include_tags() {
260        let mut config = create_test_config();
261        config.include_tags = vec!["auth".to_string()];
262        // Set error rate to 1.0 to ensure failure injection
263        config.tag_configs.get_mut("auth").unwrap().error_rate = 1.0;
264        let injector = FailureInjector::new(Some(config), true);
265
266        assert!(injector.should_inject_failure(&["auth".to_string()]));
267        assert!(!injector.should_inject_failure(&["payments".to_string()]));
268        assert!(!injector.should_inject_failure(&["other".to_string()]));
269    }
270
271    #[test]
272    fn test_tag_config_priority() {
273        let injector = FailureInjector::new(Some(create_test_config()), true);
274
275        // Test with auth tag (should use auth config)
276        let result = injector.get_failure_response(&["auth".to_string()]);
277        assert!(result.is_some());
278        let (status, message) = result.unwrap();
279        assert!(status == 401 || status == 403);
280        assert_eq!(message, "Authentication failed");
281
282        // Test with payments tag (should use payments config)
283        let result = injector.get_failure_response(&["payments".to_string()]);
284        assert!(result.is_some());
285        let (status, message) = result.unwrap();
286        assert!(status == 402 || status == 503);
287        assert_eq!(message, "Payment failed");
288    }
289
290    #[test]
291    fn test_global_config_fallback() {
292        let injector = FailureInjector::new(Some(create_test_config()), true);
293
294        // Test with unknown tag (should use global config)
295        let result = injector.get_failure_response(&["unknown".to_string()]);
296        assert!(result.is_some());
297        let (status, message) = result.unwrap();
298        assert!(status == 500 || status == 502);
299        assert_eq!(message, "Injected failure");
300    }
301}