mockforge_chaos/protocols/
graphql.rs1use crate::{
4 config::ChaosConfig, fault::FaultInjector, latency::LatencyInjector, rate_limit::RateLimiter,
5 traffic_shaping::TrafficShaper, ChaosError, Result,
6};
7use std::sync::Arc;
8use tracing::{debug, warn};
9
10#[derive(Debug, Clone)]
12pub enum GraphQLFault {
13 GraphQLError(String),
15 FieldError(String),
17 PartialData,
19 SlowResolver,
21}
22
23#[derive(Clone)]
25pub struct GraphQLChaos {
26 latency_injector: Arc<LatencyInjector>,
27 fault_injector: Arc<FaultInjector>,
28 rate_limiter: Arc<RateLimiter>,
29 traffic_shaper: Arc<TrafficShaper>,
30 config: Arc<ChaosConfig>,
31}
32
33impl GraphQLChaos {
34 pub fn new(config: ChaosConfig) -> Self {
36 let latency_injector =
37 Arc::new(LatencyInjector::new(config.latency.clone().unwrap_or_default()));
38
39 let fault_injector =
40 Arc::new(FaultInjector::new(config.fault_injection.clone().unwrap_or_default()));
41
42 let rate_limiter =
43 Arc::new(RateLimiter::new(config.rate_limit.clone().unwrap_or_default()));
44
45 let traffic_shaper =
46 Arc::new(TrafficShaper::new(config.traffic_shaping.clone().unwrap_or_default()));
47
48 Self {
49 latency_injector,
50 fault_injector,
51 rate_limiter,
52 traffic_shaper,
53 config: Arc::new(config),
54 }
55 }
56
57 pub async fn apply_pre_query(
59 &self,
60 operation_type: &str,
61 operation_name: Option<&str>,
62 client_ip: Option<&str>,
63 ) -> Result<()> {
64 if !self.config.enabled {
65 return Ok(());
66 }
67
68 let endpoint = format!("/graphql/{}", operation_name.unwrap_or("anonymous"));
69 debug!("Applying GraphQL chaos for: {} {}", operation_type, endpoint);
70
71 if let Err(e) = self.rate_limiter.check(client_ip, Some(&endpoint)) {
73 warn!("GraphQL rate limit exceeded: {}", endpoint);
74 return Err(e);
75 }
76
77 if !self.traffic_shaper.check_connection_limit() {
79 warn!("GraphQL connection limit exceeded");
80 return Err(ChaosError::ConnectionThrottled);
81 }
82
83 self.latency_injector.inject().await;
85
86 self.fault_injector.inject()?;
88
89 Ok(())
90 }
91
92 pub async fn apply_post_query(&self, response_size: usize) -> Result<()> {
94 if !self.config.enabled {
95 return Ok(());
96 }
97
98 self.traffic_shaper.throttle_bandwidth(response_size).await;
100
101 if self.traffic_shaper.should_drop_packet() {
103 warn!("Simulating GraphQL packet loss");
104 return Err(ChaosError::InjectedFault("Packet loss".to_string()));
105 }
106
107 Ok(())
108 }
109
110 pub async fn apply_resolver(&self, field_name: &str) -> Result<()> {
112 if !self.config.enabled {
113 return Ok(());
114 }
115
116 debug!("Applying GraphQL chaos for resolver: {}", field_name);
117
118 if self.latency_injector.is_enabled() {
120 let config = self.latency_injector.config();
122 if let Some(delay_ms) = config.fixed_delay_ms {
123 let resolver_delay = delay_ms / 10;
124 if resolver_delay > 0 {
125 tokio::time::sleep(std::time::Duration::from_millis(resolver_delay)).await;
126 }
127 }
128 }
129
130 Ok(())
131 }
132
133 pub fn should_inject_error(&self) -> Option<String> {
135 self.fault_injector
136 .get_http_error_status()
137 .map(|_http_code| "Internal server error".to_string())
138 }
139
140 pub fn should_return_partial_data(&self) -> bool {
142 self.fault_injector.should_truncate_response()
143 }
144
145 pub fn get_error_code(&self) -> Option<&str> {
147 if let Some(http_code) = self.fault_injector.get_http_error_status() {
148 Some(match http_code {
150 400 => "BAD_USER_INPUT",
151 401 => "UNAUTHENTICATED",
152 403 => "FORBIDDEN",
153 404 => "NOT_FOUND",
154 429 => "PERSISTED_QUERY_NOT_SUPPORTED",
155 500 => "INTERNAL_SERVER_ERROR",
156 503 => "SERVICE_UNAVAILABLE",
157 _ => "INTERNAL_SERVER_ERROR",
158 })
159 } else {
160 None
161 }
162 }
163
164 pub fn traffic_shaper(&self) -> &Arc<TrafficShaper> {
166 &self.traffic_shaper
167 }
168}
169
170pub mod error_code {
172 pub const GRAPHQL_PARSE_FAILED: &str = "GRAPHQL_PARSE_FAILED";
173 pub const GRAPHQL_VALIDATION_FAILED: &str = "GRAPHQL_VALIDATION_FAILED";
174 pub const BAD_USER_INPUT: &str = "BAD_USER_INPUT";
175 pub const UNAUTHENTICATED: &str = "UNAUTHENTICATED";
176 pub const FORBIDDEN: &str = "FORBIDDEN";
177 pub const NOT_FOUND: &str = "NOT_FOUND";
178 pub const INTERNAL_SERVER_ERROR: &str = "INTERNAL_SERVER_ERROR";
179 pub const SERVICE_UNAVAILABLE: &str = "SERVICE_UNAVAILABLE";
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::config::{FaultInjectionConfig, LatencyConfig};
186
187 #[tokio::test]
188 async fn test_graphql_chaos_creation() {
189 let config = ChaosConfig {
190 enabled: true,
191 latency: Some(LatencyConfig {
192 enabled: true,
193 fixed_delay_ms: Some(100),
194 random_delay_range_ms: None,
195 jitter_percent: 0.0,
196 probability: 1.0,
197 }),
198 ..Default::default()
199 };
200
201 let chaos = GraphQLChaos::new(config);
202 assert!(chaos.config.enabled);
203 }
204
205 #[tokio::test]
206 async fn test_graphql_error_code_mapping() {
207 let config = ChaosConfig {
208 enabled: true,
209 fault_injection: Some(FaultInjectionConfig {
210 enabled: true,
211 http_errors: vec![401],
212 http_error_probability: 1.0,
213 ..Default::default()
214 }),
215 ..Default::default()
216 };
217
218 let chaos = GraphQLChaos::new(config);
219 let error_code = chaos.get_error_code();
220
221 assert_eq!(error_code, Some("UNAUTHENTICATED"));
223 }
224
225 #[tokio::test]
226 async fn test_resolver_latency() {
227 let config = ChaosConfig {
228 enabled: true,
229 latency: Some(LatencyConfig {
230 enabled: true,
231 fixed_delay_ms: Some(100), random_delay_range_ms: None,
233 jitter_percent: 0.0,
234 probability: 1.0,
235 }),
236 ..Default::default()
237 };
238
239 let chaos = GraphQLChaos::new(config);
240 let start = std::time::Instant::now();
241
242 chaos.apply_resolver("user").await.unwrap();
243
244 let elapsed = start.elapsed();
245 assert!(elapsed >= std::time::Duration::from_millis(10));
247 assert!(elapsed < std::time::Duration::from_millis(50));
248 }
249}