mockforge_chaos/
fault.rs

1//! Fault injection for simulating errors and failures
2
3use 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/// Types of faults that can be injected
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum FaultType {
14    /// HTTP error with status code
15    HttpError(u16),
16    /// Connection error
17    ConnectionError,
18    /// Timeout error
19    Timeout,
20    /// Partial response (incomplete data)
21    PartialResponse,
22    /// Payload corruption
23    PayloadCorruption,
24}
25
26/// Pattern execution state
27#[derive(Debug, Clone)]
28struct PatternState {
29    /// Burst pattern state: (errors_in_burst, burst_start_time_ms)
30    burst_state: Option<(usize, u64)>,
31    /// Sequential pattern state: current index in sequence
32    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/// Fault injector for simulating errors
45#[derive(Clone)]
46pub struct FaultInjector {
47    config: FaultInjectionConfig,
48    /// Pattern execution state (shared for thread safety)
49    pattern_state: Arc<RwLock<PatternState>>,
50}
51
52impl FaultInjector {
53    /// Create a new fault injector
54    pub fn new(config: FaultInjectionConfig) -> Self {
55        Self {
56            config,
57            pattern_state: Arc::new(RwLock::new(PatternState::default())),
58        }
59    }
60
61    /// Check if fault injection is enabled
62    pub fn is_enabled(&self) -> bool {
63        self.config.enabled
64    }
65
66    /// Check if a fault should be injected
67    pub fn should_inject_fault(&self) -> Option<FaultType> {
68        if !self.config.enabled {
69            return None;
70        }
71
72        // Check error pattern first if configured
73        if let Some(ref pattern) = self.config.error_pattern {
74            if let Some(fault) = self.check_pattern(pattern) {
75                return Some(fault);
76            }
77            // If pattern says no, don't inject (pattern takes precedence)
78            return None;
79        }
80
81        // Fall back to probability-based injection
82        let mut rng = rand::rng();
83
84        // Check for HTTP errors
85        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        // Check for connection errors
95        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        // Check for timeout errors
103        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        // Check for partial responses
109        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        // Check for payload corruption
117        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    /// Inject a fault, returning an error if injection succeeds
128    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    /// Get HTTP error status code for injection
151    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    /// Check if should truncate response (for partial response simulation)
160    pub fn should_truncate_response(&self) -> bool {
161        matches!(self.should_inject_fault(), Some(FaultType::PartialResponse))
162    }
163
164    /// Check if should corrupt payload
165    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    /// Get corruption type from config
175    pub fn corruption_type(&self) -> crate::config::CorruptionType {
176        self.config.corruption_type
177    }
178
179    /// Get configuration
180    pub fn config(&self) -> &FaultInjectionConfig {
181        &self.config
182    }
183
184    /// Update configuration
185    pub fn update_config(&mut self, config: FaultInjectionConfig) {
186        // Reset pattern state when config changes
187        let mut state = self.pattern_state.write();
188        *state = PatternState::default();
189        self.config = config;
190    }
191
192    /// Check error pattern and return fault if pattern matches
193    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                // Check if we're in a burst window
201                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                    // Still in burst window
206                    if errors_in_burst < *count {
207                        // Inject error and increment counter
208                        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                        // Burst quota reached, don't inject
219                        return None;
220                    }
221                } else {
222                    // Burst window expired, start new burst
223                    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    /// Get next error code from configured HTTP errors
257    fn get_next_error_code(&self) -> u16 {
258        if self.config.http_errors.is_empty() {
259            500 // Default error code
260        } 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    /// Generate dynamic error message using MockAI if available
267    ///
268    /// This generates context-aware error messages based on the request context
269    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 MockAI is enabled and available, use it to generate context-aware error messages
278        if let Some(mockai_arc) = mockai {
279            if let Ok(mockai_guard) = mockai_arc.try_read() {
280                // Generate error message based on status code and context
281                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                // Use MockAI's validation generator to create error messages
290                // This is a simplified approach - in a full implementation, we'd use
291                // MockAI's error generation capabilities
292                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                // Fallback if MockAI is locked
310                self.get_default_error_message(status_code)
311            }
312        } else {
313            // No MockAI available, use default messages
314            self.get_default_error_message(status_code)
315        }
316    }
317
318    /// Get default error message for a status code
319    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, // Always inject
345            ..Default::default()
346        };
347
348        let injector = FaultInjector::new(config);
349
350        // Should inject an error
351        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}