mockforge_core/
failure_injection.rs1use rand::{rng, Rng};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
9pub struct FailureConfig {
10 pub global_error_rate: f64,
12 pub default_status_codes: Vec<u16>,
14 pub tag_configs: HashMap<String, TagFailureConfig>,
16 pub include_tags: Vec<String>,
18 pub exclude_tags: Vec<String>,
20}
21
22#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
24pub struct TagFailureConfig {
25 pub error_rate: f64,
27 pub status_codes: Option<Vec<u16>>,
29 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#[derive(Debug, Clone)]
57pub struct FailureInjector {
58 config: Option<FailureConfig>,
60 enabled: bool,
62}
63
64impl FailureInjector {
65 pub fn new(config: Option<FailureConfig>, enabled: bool) -> Self {
67 Self { config, enabled }
68 }
69
70 pub fn is_enabled(&self) -> bool {
72 self.enabled && self.config.is_some()
73 }
74
75 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 if tags.iter().any(|tag| config.exclude_tags.contains(tag)) {
88 return false;
89 }
90
91 if !config.include_tags.is_empty()
93 && !tags.iter().any(|tag| config.include_tags.contains(tag))
94 {
95 return false;
96 }
97
98 let tag_config = self.find_best_tag_config(tags, config);
100
101 let error_rate = tag_config.map(|tc| tc.error_rate).unwrap_or(config.global_error_rate);
103
104 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 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 let tag_config = self.find_best_tag_config(tags, config);
129
130 let status_codes = tag_config
132 .and_then(|tc| tc.status_codes.clone())
133 .unwrap_or_else(|| config.default_status_codes.clone());
134
135 let error_message = tag_config
137 .and_then(|tc| tc.error_message.clone())
138 .unwrap_or_else(|| "Injected failure".to_string());
139
140 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 fn find_best_tag_config<'a>(
155 &self,
156 tags: &[String],
157 config: &'a FailureConfig,
158 ) -> Option<&'a TagFailureConfig> {
159 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 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 pub fn update_config(&mut self, config: Option<FailureConfig>) {
180 self.config = config;
181 }
182
183 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
195pub 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 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 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 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 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}