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