mockforge_chaos/protocols/
graphql.rs

1//! GraphQL chaos engineering
2
3use 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/// GraphQL-specific fault types
11#[derive(Debug, Clone)]
12pub enum GraphQLFault {
13    /// GraphQL error in response
14    GraphQLError(String),
15    /// Field resolution error
16    FieldError(String),
17    /// Partial data (some fields null)
18    PartialData,
19    /// Slow resolver
20    SlowResolver,
21}
22
23/// GraphQL chaos handler
24#[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    /// Create new GraphQL chaos handler
35    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    /// Apply chaos before GraphQL query execution
58    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        // Check rate limits
72        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        // Check connection limits
78        if !self.traffic_shaper.check_connection_limit() {
79            warn!("GraphQL connection limit exceeded");
80            return Err(ChaosError::ConnectionThrottled);
81        }
82
83        // Inject query latency
84        self.latency_injector.inject().await;
85
86        // Check for fault injection
87        self.fault_injector.inject()?;
88
89        Ok(())
90    }
91
92    /// Apply chaos after GraphQL query execution
93    pub async fn apply_post_query(&self, response_size: usize) -> Result<()> {
94        if !self.config.enabled {
95            return Ok(());
96        }
97
98        // Throttle bandwidth based on response size
99        self.traffic_shaper.throttle_bandwidth(response_size).await;
100
101        // Check for packet loss (simulated)
102        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    /// Apply chaos for resolver execution
111    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        // Inject resolver latency (typically smaller than query latency)
119        if self.latency_injector.is_enabled() {
120            // Apply 10% of normal latency for field resolvers
121            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    /// Check if should inject GraphQL error
134    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    /// Check if should return partial data
141    pub fn should_return_partial_data(&self) -> bool {
142        self.fault_injector.should_truncate_response()
143    }
144
145    /// Get GraphQL error code for fault injection
146    pub fn get_error_code(&self) -> Option<&str> {
147        if let Some(http_code) = self.fault_injector.get_http_error_status() {
148            // Map HTTP codes to GraphQL error codes
149            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    /// Get traffic shaper for connection management
165    pub fn traffic_shaper(&self) -> &Arc<TrafficShaper> {
166        &self.traffic_shaper
167    }
168}
169
170/// GraphQL error codes
171pub 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        // Should map 401 to UNAUTHENTICATED
222        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), // 100ms query latency
232                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        // Should be ~10ms (10% of query latency)
246        assert!(elapsed >= std::time::Duration::from_millis(10));
247        assert!(elapsed < std::time::Duration::from_millis(50));
248    }
249}