1use rand::{rng, Rng};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
9#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
10pub struct FailureConfig {
11 pub global_error_rate: f64,
13 pub default_status_codes: Vec<u16>,
15 pub tag_configs: HashMap<String, TagFailureConfig>,
17 pub include_tags: Vec<String>,
19 pub exclude_tags: Vec<String>,
21}
22
23#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
25#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
26pub struct TagFailureConfig {
27 pub error_rate: f64,
29 pub status_codes: Option<Vec<u16>>,
31 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#[derive(Debug, Clone)]
59pub struct FailureInjector {
60 config: Option<FailureConfig>,
62 enabled: bool,
64}
65
66impl FailureInjector {
67 pub fn new(config: Option<FailureConfig>, enabled: bool) -> Self {
69 Self { config, enabled }
70 }
71
72 pub fn is_enabled(&self) -> bool {
74 self.enabled && self.config.is_some()
75 }
76
77 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 if tags.iter().any(|tag| config.exclude_tags.contains(tag)) {
90 return false;
91 }
92
93 if !config.include_tags.is_empty()
95 && !tags.iter().any(|tag| config.include_tags.contains(tag))
96 {
97 return false;
98 }
99
100 let tag_config = self.find_best_tag_config(tags, config);
102
103 let error_rate = tag_config.map(|tc| tc.error_rate).unwrap_or(config.global_error_rate);
105
106 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 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 let tag_config = self.find_best_tag_config(tags, config);
131
132 let status_codes = tag_config
134 .and_then(|tc| tc.status_codes.clone())
135 .unwrap_or_else(|| config.default_status_codes.clone());
136
137 let error_message = tag_config
139 .and_then(|tc| tc.error_message.clone())
140 .unwrap_or_else(|| "Injected failure".to_string());
141
142 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 fn find_best_tag_config<'a>(
157 &self,
158 tags: &[String],
159 config: &'a FailureConfig,
160 ) -> Option<&'a TagFailureConfig> {
161 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 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 pub fn update_config(&mut self, config: Option<FailureConfig>) {
182 self.config = config;
183 }
184
185 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
197pub 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 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 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 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 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}