use crate::{
config::ChaosConfig, fault::FaultInjector, latency::LatencyInjector, rate_limit::RateLimiter,
traffic_shaping::TrafficShaper, ChaosError, Result,
};
use std::sync::Arc;
use tracing::{debug, warn};
#[derive(Debug, Clone)]
pub enum GraphQLFault {
GraphQLError(String),
FieldError(String),
PartialData,
SlowResolver,
}
#[derive(Clone)]
pub struct GraphQLChaos {
latency_injector: Arc<LatencyInjector>,
fault_injector: Arc<FaultInjector>,
rate_limiter: Arc<RateLimiter>,
traffic_shaper: Arc<TrafficShaper>,
config: Arc<ChaosConfig>,
}
impl GraphQLChaos {
pub fn new(config: ChaosConfig) -> Self {
let latency_injector =
Arc::new(LatencyInjector::new(config.latency.clone().unwrap_or_default()));
let fault_injector =
Arc::new(FaultInjector::new(config.fault_injection.clone().unwrap_or_default()));
let rate_limiter =
Arc::new(RateLimiter::new(config.rate_limit.clone().unwrap_or_default()));
let traffic_shaper =
Arc::new(TrafficShaper::new(config.traffic_shaping.clone().unwrap_or_default()));
Self {
latency_injector,
fault_injector,
rate_limiter,
traffic_shaper,
config: Arc::new(config),
}
}
pub async fn apply_pre_query(
&self,
operation_type: &str,
operation_name: Option<&str>,
client_ip: Option<&str>,
) -> Result<()> {
if !self.config.enabled {
return Ok(());
}
let endpoint = format!("/graphql/{}", operation_name.unwrap_or("anonymous"));
debug!("Applying GraphQL chaos for: {} {}", operation_type, endpoint);
if let Err(e) = self.rate_limiter.check(client_ip, Some(&endpoint)) {
warn!("GraphQL rate limit exceeded: {}", endpoint);
return Err(e);
}
if !self.traffic_shaper.check_connection_limit() {
warn!("GraphQL connection limit exceeded");
return Err(ChaosError::ConnectionThrottled);
}
self.latency_injector.inject().await;
self.fault_injector.inject()?;
Ok(())
}
pub async fn apply_post_query(&self, response_size: usize) -> Result<()> {
if !self.config.enabled {
return Ok(());
}
self.traffic_shaper.throttle_bandwidth(response_size).await;
if self.traffic_shaper.should_drop_packet() {
warn!("Simulating GraphQL packet loss");
return Err(ChaosError::InjectedFault("Packet loss".to_string()));
}
Ok(())
}
pub async fn apply_resolver(&self, field_name: &str) -> Result<()> {
if !self.config.enabled {
return Ok(());
}
debug!("Applying GraphQL chaos for resolver: {}", field_name);
if self.latency_injector.is_enabled() {
let config = self.latency_injector.config();
if let Some(delay_ms) = config.fixed_delay_ms {
let resolver_delay = delay_ms / 10;
if resolver_delay > 0 {
tokio::time::sleep(std::time::Duration::from_millis(resolver_delay)).await;
}
}
}
Ok(())
}
pub fn should_inject_error(&self) -> Option<String> {
self.fault_injector
.get_http_error_status()
.map(|_http_code| "Internal server error".to_string())
}
pub fn should_return_partial_data(&self) -> bool {
self.fault_injector.should_truncate_response()
}
pub fn get_error_code(&self) -> Option<&str> {
if let Some(http_code) = self.fault_injector.get_http_error_status() {
Some(match http_code {
400 => "BAD_USER_INPUT",
401 => "UNAUTHENTICATED",
403 => "FORBIDDEN",
404 => "NOT_FOUND",
429 => "PERSISTED_QUERY_NOT_SUPPORTED",
500 => "INTERNAL_SERVER_ERROR",
503 => "SERVICE_UNAVAILABLE",
_ => "INTERNAL_SERVER_ERROR",
})
} else {
None
}
}
pub fn traffic_shaper(&self) -> &Arc<TrafficShaper> {
&self.traffic_shaper
}
}
pub mod error_code {
pub const GRAPHQL_PARSE_FAILED: &str = "GRAPHQL_PARSE_FAILED";
pub const GRAPHQL_VALIDATION_FAILED: &str = "GRAPHQL_VALIDATION_FAILED";
pub const BAD_USER_INPUT: &str = "BAD_USER_INPUT";
pub const UNAUTHENTICATED: &str = "UNAUTHENTICATED";
pub const FORBIDDEN: &str = "FORBIDDEN";
pub const NOT_FOUND: &str = "NOT_FOUND";
pub const INTERNAL_SERVER_ERROR: &str = "INTERNAL_SERVER_ERROR";
pub const SERVICE_UNAVAILABLE: &str = "SERVICE_UNAVAILABLE";
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{FaultInjectionConfig, LatencyConfig};
#[tokio::test]
async fn test_graphql_chaos_creation() {
let config = ChaosConfig {
enabled: true,
latency: Some(LatencyConfig {
enabled: true,
fixed_delay_ms: Some(100),
random_delay_range_ms: None,
jitter_percent: 0.0,
probability: 1.0,
}),
..Default::default()
};
let chaos = GraphQLChaos::new(config);
assert!(chaos.config.enabled);
}
#[tokio::test]
async fn test_graphql_error_code_mapping() {
let config = ChaosConfig {
enabled: true,
fault_injection: Some(FaultInjectionConfig {
enabled: true,
http_errors: vec![401],
http_error_probability: 1.0,
..Default::default()
}),
..Default::default()
};
let chaos = GraphQLChaos::new(config);
let error_code = chaos.get_error_code();
assert_eq!(error_code, Some("UNAUTHENTICATED"));
}
#[tokio::test]
async fn test_resolver_latency() {
let config = ChaosConfig {
enabled: true,
latency: Some(LatencyConfig {
enabled: true,
fixed_delay_ms: Some(100), random_delay_range_ms: None,
jitter_percent: 0.0,
probability: 1.0,
}),
..Default::default()
};
let chaos = GraphQLChaos::new(config);
let start = std::time::Instant::now();
chaos.apply_resolver("user").await.unwrap();
let elapsed = start.elapsed();
assert!(elapsed >= std::time::Duration::from_millis(10));
assert!(elapsed < std::time::Duration::from_millis(50));
}
}