1use crate::{config::ErrorPattern, config::FaultInjectionConfig, ChaosError, Result};
4use parking_lot::RwLock;
5use rand::Rng;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8use std::time::{SystemTime, UNIX_EPOCH};
9use tracing::debug;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum FaultType {
14 HttpError(u16),
16 ConnectionError,
18 Timeout,
20 PartialResponse,
22 PayloadCorruption,
24}
25
26#[derive(Debug, Clone)]
28struct PatternState {
29 burst_state: Option<(usize, u64)>,
31 sequential_index: usize,
33}
34
35impl Default for PatternState {
36 fn default() -> Self {
37 Self {
38 burst_state: None,
39 sequential_index: 0,
40 }
41 }
42}
43
44#[derive(Clone)]
46pub struct FaultInjector {
47 config: FaultInjectionConfig,
48 pattern_state: Arc<RwLock<PatternState>>,
50}
51
52impl FaultInjector {
53 pub fn new(config: FaultInjectionConfig) -> Self {
55 Self {
56 config,
57 pattern_state: Arc::new(RwLock::new(PatternState::default())),
58 }
59 }
60
61 pub fn is_enabled(&self) -> bool {
63 self.config.enabled
64 }
65
66 pub fn should_inject_fault(&self) -> Option<FaultType> {
68 if !self.config.enabled {
69 return None;
70 }
71
72 if let Some(ref pattern) = self.config.error_pattern {
74 if let Some(fault) = self.check_pattern(pattern) {
75 return Some(fault);
76 }
77 return None;
79 }
80
81 let mut rng = rand::rng();
83
84 if !self.config.http_errors.is_empty()
86 && rng.random::<f64>() < self.config.http_error_probability
87 {
88 let error_code =
89 self.config.http_errors[rng.random_range(0..self.config.http_errors.len())];
90 debug!("Injecting HTTP error: {}", error_code);
91 return Some(FaultType::HttpError(error_code));
92 }
93
94 if self.config.connection_errors
96 && rng.random::<f64>() < self.config.connection_error_probability
97 {
98 debug!("Injecting connection error");
99 return Some(FaultType::ConnectionError);
100 }
101
102 if self.config.timeout_errors && rng.random::<f64>() < self.config.timeout_probability {
104 debug!("Injecting timeout error");
105 return Some(FaultType::Timeout);
106 }
107
108 if self.config.partial_responses
110 && rng.random::<f64>() < self.config.partial_response_probability
111 {
112 debug!("Injecting partial response");
113 return Some(FaultType::PartialResponse);
114 }
115
116 if self.config.payload_corruption
118 && rng.random::<f64>() < self.config.payload_corruption_probability
119 {
120 debug!("Injecting payload corruption");
121 return Some(FaultType::PayloadCorruption);
122 }
123
124 None
125 }
126
127 pub fn inject(&self) -> Result<()> {
129 if let Some(fault) = self.should_inject_fault() {
130 match fault {
131 FaultType::HttpError(code) => {
132 Err(ChaosError::InjectedFault(format!("HTTP error {}", code)))
133 }
134 FaultType::ConnectionError => {
135 Err(ChaosError::InjectedFault("Connection error".to_string()))
136 }
137 FaultType::Timeout => Err(ChaosError::Timeout(self.config.timeout_ms)),
138 FaultType::PartialResponse => {
139 Err(ChaosError::InjectedFault("Partial response".to_string()))
140 }
141 FaultType::PayloadCorruption => {
142 Err(ChaosError::InjectedFault("Payload corruption".to_string()))
143 }
144 }
145 } else {
146 Ok(())
147 }
148 }
149
150 pub fn get_http_error_status(&self) -> Option<u16> {
152 if let Some(FaultType::HttpError(code)) = self.should_inject_fault() {
153 Some(code)
154 } else {
155 None
156 }
157 }
158
159 pub fn should_truncate_response(&self) -> bool {
161 matches!(self.should_inject_fault(), Some(FaultType::PartialResponse))
162 }
163
164 pub fn should_corrupt_payload(&self) -> bool {
166 if !self.config.enabled || !self.config.payload_corruption {
167 return false;
168 }
169
170 let mut rng = rand::rng();
171 rng.random::<f64>() < self.config.payload_corruption_probability
172 }
173
174 pub fn corruption_type(&self) -> crate::config::CorruptionType {
176 self.config.corruption_type
177 }
178
179 pub fn config(&self) -> &FaultInjectionConfig {
181 &self.config
182 }
183
184 pub fn update_config(&mut self, config: FaultInjectionConfig) {
186 let mut state = self.pattern_state.write();
188 *state = PatternState::default();
189 self.config = config;
190 }
191
192 fn check_pattern(&self, pattern: &ErrorPattern) -> Option<FaultType> {
194 let mut state = self.pattern_state.write();
195 let now_ms =
196 SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64;
197
198 match pattern {
199 ErrorPattern::Burst { count, interval_ms } => {
200 let (errors_in_burst, burst_start) = state.burst_state.unwrap_or((0, now_ms));
202 let elapsed = now_ms.saturating_sub(burst_start);
203
204 if elapsed < *interval_ms {
205 if errors_in_burst < *count {
207 state.burst_state = Some((errors_in_burst + 1, burst_start));
209 let error_code = self.get_next_error_code();
210 debug!(
211 "Burst pattern: injecting error {} ({}/{})",
212 error_code,
213 errors_in_burst + 1,
214 count
215 );
216 return Some(FaultType::HttpError(error_code));
217 } else {
218 return None;
220 }
221 } else {
222 state.burst_state = Some((1, now_ms));
224 let error_code = self.get_next_error_code();
225 debug!("Burst pattern: starting new burst, injecting error {}", error_code);
226 return Some(FaultType::HttpError(error_code));
227 }
228 }
229 ErrorPattern::Random { probability } => {
230 let mut rng = rand::rng();
231 if rng.random::<f64>() < *probability {
232 let error_code = self.get_next_error_code();
233 debug!(
234 "Random pattern: injecting error {} (probability: {})",
235 error_code, probability
236 );
237 return Some(FaultType::HttpError(error_code));
238 }
239 return None;
240 }
241 ErrorPattern::Sequential { sequence } => {
242 if sequence.is_empty() {
243 return None;
244 }
245 let error_code = sequence[state.sequential_index % sequence.len()];
246 state.sequential_index = (state.sequential_index + 1) % sequence.len();
247 debug!(
248 "Sequential pattern: injecting error {} (index: {})",
249 error_code, state.sequential_index
250 );
251 return Some(FaultType::HttpError(error_code));
252 }
253 }
254 }
255
256 fn get_next_error_code(&self) -> u16 {
258 if self.config.http_errors.is_empty() {
259 500 } else {
261 let mut rng = rand::rng();
262 self.config.http_errors[rng.random_range(0..self.config.http_errors.len())]
263 }
264 }
265
266 pub async fn generate_error_message(
270 &self,
271 status_code: u16,
272 mockai: Option<
273 &std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
274 >,
275 request_context: Option<&str>,
276 ) -> String {
277 if let Some(mockai_arc) = mockai {
279 if let Ok(mockai_guard) = mockai_arc.try_read() {
280 let error_context = format!(
282 "Generate a realistic HTTP {} error message{}",
283 status_code,
284 request_context
285 .map(|ctx| format!(" for the following request context: {}", ctx))
286 .unwrap_or_default()
287 );
288
289 match status_code {
293 400 => "Bad Request: Invalid input provided".to_string(),
294 401 => "Unauthorized: Authentication required".to_string(),
295 403 => "Forbidden: Insufficient permissions".to_string(),
296 404 => "Not Found: The requested resource does not exist".to_string(),
297 429 => "Too Many Requests: Rate limit exceeded".to_string(),
298 500 => "Internal Server Error: An unexpected error occurred".to_string(),
299 502 => "Bad Gateway: Upstream server error".to_string(),
300 503 => {
301 "Service Unavailable: The service is temporarily unavailable".to_string()
302 }
303 504 => {
304 "Gateway Timeout: The upstream server did not respond in time".to_string()
305 }
306 _ => format!("HTTP {} Error", status_code),
307 }
308 } else {
309 self.get_default_error_message(status_code)
311 }
312 } else {
313 self.get_default_error_message(status_code)
315 }
316 }
317
318 fn get_default_error_message(&self, status_code: u16) -> String {
320 match status_code {
321 400 => "Bad Request".to_string(),
322 401 => "Unauthorized".to_string(),
323 403 => "Forbidden".to_string(),
324 404 => "Not Found".to_string(),
325 429 => "Too Many Requests".to_string(),
326 500 => "Internal Server Error".to_string(),
327 502 => "Bad Gateway".to_string(),
328 503 => "Service Unavailable".to_string(),
329 504 => "Gateway Timeout".to_string(),
330 _ => format!("HTTP {} Error", status_code),
331 }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_http_error_injection() {
341 let config = FaultInjectionConfig {
342 enabled: true,
343 http_errors: vec![500, 503],
344 http_error_probability: 1.0, ..Default::default()
346 };
347
348 let injector = FaultInjector::new(config);
349
350 let fault = injector.should_inject_fault();
352 assert!(fault.is_some());
353
354 if let Some(FaultType::HttpError(code)) = fault {
355 assert!(code == 500 || code == 503);
356 } else {
357 panic!("Expected HTTP error");
358 }
359 }
360
361 #[test]
362 fn test_no_injection_when_disabled() {
363 let config = FaultInjectionConfig {
364 enabled: false,
365 ..Default::default()
366 };
367
368 let injector = FaultInjector::new(config);
369 let fault = injector.should_inject_fault();
370 assert!(fault.is_none());
371 }
372
373 #[test]
374 fn test_connection_error_injection() {
375 let config = FaultInjectionConfig {
376 enabled: true,
377 connection_errors: true,
378 connection_error_probability: 1.0,
379 http_errors: vec![],
380 ..Default::default()
381 };
382
383 let injector = FaultInjector::new(config);
384 let fault = injector.should_inject_fault();
385 assert!(matches!(fault, Some(FaultType::ConnectionError)));
386 }
387
388 #[test]
389 fn test_timeout_injection() {
390 let config = FaultInjectionConfig {
391 enabled: true,
392 timeout_errors: true,
393 timeout_probability: 1.0,
394 http_errors: vec![],
395 ..Default::default()
396 };
397
398 let injector = FaultInjector::new(config);
399 let fault = injector.should_inject_fault();
400 assert!(matches!(fault, Some(FaultType::Timeout)));
401 }
402
403 #[test]
404 fn test_inject_returns_error() {
405 let config = FaultInjectionConfig {
406 enabled: true,
407 http_errors: vec![500],
408 http_error_probability: 1.0,
409 ..Default::default()
410 };
411
412 let injector = FaultInjector::new(config);
413 let result = injector.inject();
414 assert!(result.is_err());
415 }
416}