1use crate::{config::FaultInjectionConfig, ChaosError, Result};
4use rand::Rng;
5use serde::{Deserialize, Serialize};
6use tracing::debug;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub enum FaultType {
11 HttpError(u16),
13 ConnectionError,
15 Timeout,
17 PartialResponse,
19}
20
21#[derive(Clone)]
23pub struct FaultInjector {
24 config: FaultInjectionConfig,
25}
26
27impl FaultInjector {
28 pub fn new(config: FaultInjectionConfig) -> Self {
30 Self { config }
31 }
32
33 pub fn is_enabled(&self) -> bool {
35 self.config.enabled
36 }
37
38 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 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 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 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 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 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 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 pub fn should_truncate_response(&self) -> bool {
112 matches!(self.should_inject_fault(), Some(FaultType::PartialResponse))
113 }
114
115 pub fn config(&self) -> &FaultInjectionConfig {
117 &self.config
118 }
119
120 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, ..Default::default()
137 };
138
139 let injector = FaultInjector::new(config);
140
141 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}