mockforge_chaos/
fault.rs

1//! Fault injection for simulating errors and failures
2
3use crate::{config::FaultInjectionConfig, ChaosError, Result};
4use rand::Rng;
5use serde::{Deserialize, Serialize};
6use tracing::debug;
7
8/// Types of faults that can be injected
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub enum FaultType {
11    /// HTTP error with status code
12    HttpError(u16),
13    /// Connection error
14    ConnectionError,
15    /// Timeout error
16    Timeout,
17    /// Partial response (incomplete data)
18    PartialResponse,
19}
20
21/// Fault injector for simulating errors
22#[derive(Clone)]
23pub struct FaultInjector {
24    config: FaultInjectionConfig,
25}
26
27impl FaultInjector {
28    /// Create a new fault injector
29    pub fn new(config: FaultInjectionConfig) -> Self {
30        Self { config }
31    }
32
33    /// Check if fault injection is enabled
34    pub fn is_enabled(&self) -> bool {
35        self.config.enabled
36    }
37
38    /// Check if a fault should be injected
39    pub fn should_inject_fault(&self) -> Option<FaultType> {
40        if !self.config.enabled {
41            return None;
42        }
43
44        let mut rng = rand::rng();
45
46        // Check for HTTP errors
47        if !self.config.http_errors.is_empty()
48            && rng.random::<f64>() < self.config.http_error_probability
49        {
50            let error_code =
51                self.config.http_errors[rng.random_range(0..self.config.http_errors.len())];
52            debug!("Injecting HTTP error: {}", error_code);
53            return Some(FaultType::HttpError(error_code));
54        }
55
56        // Check for connection errors
57        if self.config.connection_errors
58            && rng.random::<f64>() < self.config.connection_error_probability
59        {
60            debug!("Injecting connection error");
61            return Some(FaultType::ConnectionError);
62        }
63
64        // Check for timeout errors
65        if self.config.timeout_errors && rng.random::<f64>() < self.config.timeout_probability {
66            debug!("Injecting timeout error");
67            return Some(FaultType::Timeout);
68        }
69
70        // Check for partial responses
71        if self.config.partial_responses
72            && rng.random::<f64>() < self.config.partial_response_probability
73        {
74            debug!("Injecting partial response");
75            return Some(FaultType::PartialResponse);
76        }
77
78        None
79    }
80
81    /// Inject a fault, returning an error if injection succeeds
82    pub fn inject(&self) -> Result<()> {
83        if let Some(fault) = self.should_inject_fault() {
84            match fault {
85                FaultType::HttpError(code) => {
86                    Err(ChaosError::InjectedFault(format!("HTTP error {}", code)))
87                }
88                FaultType::ConnectionError => {
89                    Err(ChaosError::InjectedFault("Connection error".to_string()))
90                }
91                FaultType::Timeout => Err(ChaosError::Timeout(self.config.timeout_ms)),
92                FaultType::PartialResponse => {
93                    Err(ChaosError::InjectedFault("Partial response".to_string()))
94                }
95            }
96        } else {
97            Ok(())
98        }
99    }
100
101    /// Get HTTP error status code for injection
102    pub fn get_http_error_status(&self) -> Option<u16> {
103        if let Some(FaultType::HttpError(code)) = self.should_inject_fault() {
104            Some(code)
105        } else {
106            None
107        }
108    }
109
110    /// Check if should truncate response (for partial response simulation)
111    pub fn should_truncate_response(&self) -> bool {
112        matches!(self.should_inject_fault(), Some(FaultType::PartialResponse))
113    }
114
115    /// Get configuration
116    pub fn config(&self) -> &FaultInjectionConfig {
117        &self.config
118    }
119
120    /// Update configuration
121    pub fn update_config(&mut self, config: FaultInjectionConfig) {
122        self.config = config;
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_http_error_injection() {
132        let config = FaultInjectionConfig {
133            enabled: true,
134            http_errors: vec![500, 503],
135            http_error_probability: 1.0, // Always inject
136            ..Default::default()
137        };
138
139        let injector = FaultInjector::new(config);
140
141        // Should inject an error
142        let fault = injector.should_inject_fault();
143        assert!(fault.is_some());
144
145        if let Some(FaultType::HttpError(code)) = fault {
146            assert!(code == 500 || code == 503);
147        } else {
148            panic!("Expected HTTP error");
149        }
150    }
151
152    #[test]
153    fn test_no_injection_when_disabled() {
154        let config = FaultInjectionConfig {
155            enabled: false,
156            ..Default::default()
157        };
158
159        let injector = FaultInjector::new(config);
160        let fault = injector.should_inject_fault();
161        assert!(fault.is_none());
162    }
163
164    #[test]
165    fn test_connection_error_injection() {
166        let config = FaultInjectionConfig {
167            enabled: true,
168            connection_errors: true,
169            connection_error_probability: 1.0,
170            http_errors: vec![],
171            ..Default::default()
172        };
173
174        let injector = FaultInjector::new(config);
175        let fault = injector.should_inject_fault();
176        assert!(matches!(fault, Some(FaultType::ConnectionError)));
177    }
178
179    #[test]
180    fn test_timeout_injection() {
181        let config = FaultInjectionConfig {
182            enabled: true,
183            timeout_errors: true,
184            timeout_probability: 1.0,
185            http_errors: vec![],
186            ..Default::default()
187        };
188
189        let injector = FaultInjector::new(config);
190        let fault = injector.should_inject_fault();
191        assert!(matches!(fault, Some(FaultType::Timeout)));
192    }
193
194    #[test]
195    fn test_inject_returns_error() {
196        let config = FaultInjectionConfig {
197            enabled: true,
198            http_errors: vec![500],
199            http_error_probability: 1.0,
200            ..Default::default()
201        };
202
203        let injector = FaultInjector::new(config);
204        let result = injector.inject();
205        assert!(result.is_err());
206    }
207}