use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CorruptionType {
None,
RandomBytes,
Truncate,
BitFlip,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum ErrorPattern {
Burst {
count: usize,
interval_ms: u64,
},
Random {
probability: f64,
},
Sequential {
sequence: Vec<u16>,
},
}
impl Default for ErrorPattern {
fn default() -> Self {
ErrorPattern::Random { probability: 0.1 }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChaosConfig {
pub enabled: bool,
pub latency: Option<LatencyConfig>,
pub fault_injection: Option<FaultInjectionConfig>,
pub rate_limit: Option<RateLimitConfig>,
pub traffic_shaping: Option<TrafficShapingConfig>,
pub circuit_breaker: Option<CircuitBreakerConfig>,
pub bulkhead: Option<BulkheadConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LatencyConfig {
pub enabled: bool,
pub fixed_delay_ms: Option<u64>,
pub random_delay_range_ms: Option<(u64, u64)>,
pub jitter_percent: f64,
pub probability: f64,
}
impl Default for LatencyConfig {
fn default() -> Self {
Self {
enabled: false,
fixed_delay_ms: None,
random_delay_range_ms: None,
jitter_percent: 0.0,
probability: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FaultInjectionConfig {
pub enabled: bool,
pub http_errors: Vec<u16>,
pub http_error_probability: f64,
pub connection_errors: bool,
pub connection_error_probability: f64,
pub timeout_errors: bool,
pub timeout_ms: u64,
pub timeout_probability: f64,
pub partial_responses: bool,
pub partial_response_probability: f64,
pub payload_corruption: bool,
pub payload_corruption_probability: f64,
pub corruption_type: CorruptionType,
#[serde(default)]
pub error_pattern: Option<ErrorPattern>,
#[serde(default)]
pub mockai_enabled: bool,
}
impl Default for FaultInjectionConfig {
fn default() -> Self {
Self {
enabled: false,
http_errors: vec![500, 502, 503, 504],
http_error_probability: 0.1,
connection_errors: false,
connection_error_probability: 0.05,
timeout_errors: false,
timeout_ms: 5000,
timeout_probability: 0.05,
partial_responses: false,
partial_response_probability: 0.05,
payload_corruption: false,
payload_corruption_probability: 0.05,
corruption_type: CorruptionType::None,
error_pattern: None,
mockai_enabled: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitConfig {
pub enabled: bool,
pub requests_per_second: u32,
pub burst_size: u32,
pub per_ip: bool,
pub per_endpoint: bool,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
enabled: false,
requests_per_second: 100,
burst_size: 10,
per_ip: false,
per_endpoint: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrafficShapingConfig {
pub enabled: bool,
pub bandwidth_limit_bps: u64,
pub packet_loss_percent: f64,
pub max_connections: u32,
pub connection_timeout_ms: u64,
}
impl Default for TrafficShapingConfig {
fn default() -> Self {
Self {
enabled: false,
bandwidth_limit_bps: 0,
packet_loss_percent: 0.0,
max_connections: 0,
connection_timeout_ms: 30000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CircuitBreakerConfig {
pub enabled: bool,
pub failure_threshold: u64,
pub success_threshold: u64,
pub timeout_ms: u64,
pub half_open_max_requests: u32,
pub failure_rate_threshold: f64,
pub min_requests_for_rate: u64,
pub rolling_window_ms: u64,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
enabled: false,
failure_threshold: 5,
success_threshold: 2,
timeout_ms: 60000,
half_open_max_requests: 3,
failure_rate_threshold: 50.0,
min_requests_for_rate: 10,
rolling_window_ms: 10000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BulkheadConfig {
pub enabled: bool,
pub max_concurrent_requests: u32,
pub max_queue_size: u32,
pub queue_timeout_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkProfile {
pub name: String,
pub description: String,
pub chaos_config: ChaosConfig,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub builtin: bool,
}
impl NetworkProfile {
pub fn new(name: String, description: String, chaos_config: ChaosConfig) -> Self {
Self {
name,
description,
chaos_config,
tags: Vec::new(),
builtin: false,
}
}
pub fn predefined_profiles() -> Vec<Self> {
vec![
Self {
name: "slow_3g".to_string(),
description:
"Simulates slow 3G network: 400ms latency, 1% packet loss, 400KB/s bandwidth"
.to_string(),
chaos_config: ChaosConfig {
enabled: true,
latency: Some(LatencyConfig {
enabled: true,
fixed_delay_ms: Some(400),
random_delay_range_ms: Some((300, 500)),
jitter_percent: 10.0,
probability: 1.0,
}),
fault_injection: None,
rate_limit: None,
traffic_shaping: Some(TrafficShapingConfig {
enabled: true,
bandwidth_limit_bps: 400_000, packet_loss_percent: 1.0,
max_connections: 0,
connection_timeout_ms: 30000,
}),
circuit_breaker: None,
bulkhead: None,
},
tags: vec!["mobile".to_string(), "slow".to_string(), "3g".to_string()],
builtin: true,
},
Self {
name: "fast_3g".to_string(),
description:
"Simulates fast 3G network: 150ms latency, 0.5% packet loss, 1.5MB/s bandwidth"
.to_string(),
chaos_config: ChaosConfig {
enabled: true,
latency: Some(LatencyConfig {
enabled: true,
fixed_delay_ms: Some(150),
random_delay_range_ms: Some((100, 200)),
jitter_percent: 5.0,
probability: 1.0,
}),
fault_injection: None,
rate_limit: None,
traffic_shaping: Some(TrafficShapingConfig {
enabled: true,
bandwidth_limit_bps: 1_500_000, packet_loss_percent: 0.5,
max_connections: 0,
connection_timeout_ms: 30000,
}),
circuit_breaker: None,
bulkhead: None,
},
tags: vec!["mobile".to_string(), "fast".to_string(), "3g".to_string()],
builtin: true,
},
Self {
name: "flaky_wifi".to_string(),
description:
"Simulates flaky Wi-Fi: 50ms latency, 5% packet loss, random connection errors"
.to_string(),
chaos_config: ChaosConfig {
enabled: true,
latency: Some(LatencyConfig {
enabled: true,
fixed_delay_ms: Some(50),
random_delay_range_ms: Some((30, 100)),
jitter_percent: 20.0,
probability: 1.0,
}),
fault_injection: Some(FaultInjectionConfig {
enabled: true,
http_errors: vec![500, 502, 503],
http_error_probability: 0.05, connection_errors: true,
connection_error_probability: 0.03, timeout_errors: false,
timeout_ms: 5000,
timeout_probability: 0.0,
partial_responses: false,
partial_response_probability: 0.0,
payload_corruption: false,
payload_corruption_probability: 0.0,
corruption_type: CorruptionType::None,
error_pattern: None,
mockai_enabled: false,
}),
rate_limit: None,
traffic_shaping: Some(TrafficShapingConfig {
enabled: true,
bandwidth_limit_bps: 0, packet_loss_percent: 5.0,
max_connections: 0,
connection_timeout_ms: 30000,
}),
circuit_breaker: None,
bulkhead: None,
},
tags: vec![
"wifi".to_string(),
"unstable".to_string(),
"wireless".to_string(),
],
builtin: true,
},
Self {
name: "cable".to_string(),
description:
"Simulates cable internet: 20ms latency, no packet loss, 10MB/s bandwidth"
.to_string(),
chaos_config: ChaosConfig {
enabled: true,
latency: Some(LatencyConfig {
enabled: true,
fixed_delay_ms: Some(20),
random_delay_range_ms: Some((10, 30)),
jitter_percent: 2.0,
probability: 1.0,
}),
fault_injection: None,
rate_limit: None,
traffic_shaping: Some(TrafficShapingConfig {
enabled: true,
bandwidth_limit_bps: 10_000_000, packet_loss_percent: 0.0,
max_connections: 0,
connection_timeout_ms: 30000,
}),
circuit_breaker: None,
bulkhead: None,
},
tags: vec![
"broadband".to_string(),
"fast".to_string(),
"stable".to_string(),
],
builtin: true,
},
Self {
name: "dialup".to_string(),
description:
"Simulates dial-up connection: 2000ms latency, 2% packet loss, 50KB/s bandwidth"
.to_string(),
chaos_config: ChaosConfig {
enabled: true,
latency: Some(LatencyConfig {
enabled: true,
fixed_delay_ms: Some(2000),
random_delay_range_ms: Some((1500, 2500)),
jitter_percent: 15.0,
probability: 1.0,
}),
fault_injection: None,
rate_limit: None,
traffic_shaping: Some(TrafficShapingConfig {
enabled: true,
bandwidth_limit_bps: 50_000, packet_loss_percent: 2.0,
max_connections: 0,
connection_timeout_ms: 60000, }),
circuit_breaker: None,
bulkhead: None,
},
tags: vec![
"dialup".to_string(),
"slow".to_string(),
"legacy".to_string(),
],
builtin: true,
},
]
}
}
impl Default for BulkheadConfig {
fn default() -> Self {
Self {
enabled: false,
max_concurrent_requests: 100,
max_queue_size: 10,
queue_timeout_ms: 5000,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_corruption_type_variants() {
let none = CorruptionType::None;
let random_bytes = CorruptionType::RandomBytes;
let truncate = CorruptionType::Truncate;
let bit_flip = CorruptionType::BitFlip;
assert!(matches!(none, CorruptionType::None));
assert!(matches!(random_bytes, CorruptionType::RandomBytes));
assert!(matches!(truncate, CorruptionType::Truncate));
assert!(matches!(bit_flip, CorruptionType::BitFlip));
}
#[test]
fn test_corruption_type_serialize() {
let ct = CorruptionType::RandomBytes;
let json = serde_json::to_string(&ct).unwrap();
assert!(json.contains("random_bytes"));
}
#[test]
fn test_corruption_type_deserialize() {
let json = r#""bit_flip""#;
let ct: CorruptionType = serde_json::from_str(json).unwrap();
assert!(matches!(ct, CorruptionType::BitFlip));
}
#[test]
fn test_error_pattern_default() {
let pattern = ErrorPattern::default();
assert!(
matches!(pattern, ErrorPattern::Random { probability } if (probability - 0.1).abs() < f64::EPSILON)
);
}
#[test]
fn test_error_pattern_burst() {
let pattern = ErrorPattern::Burst {
count: 5,
interval_ms: 1000,
};
if let ErrorPattern::Burst { count, interval_ms } = pattern {
assert_eq!(count, 5);
assert_eq!(interval_ms, 1000);
} else {
panic!("Expected Burst pattern");
}
}
#[test]
fn test_error_pattern_sequential() {
let pattern = ErrorPattern::Sequential {
sequence: vec![500, 502, 503],
};
if let ErrorPattern::Sequential { sequence } = pattern {
assert_eq!(sequence.len(), 3);
assert!(sequence.contains(&500));
} else {
panic!("Expected Sequential pattern");
}
}
#[test]
fn test_error_pattern_serialize() {
let pattern = ErrorPattern::Burst {
count: 3,
interval_ms: 500,
};
let json = serde_json::to_string(&pattern).unwrap();
assert!(json.contains("burst"));
assert!(json.contains("count"));
}
#[test]
fn test_chaos_config_default() {
let config = ChaosConfig::default();
assert!(!config.enabled);
assert!(config.latency.is_none());
assert!(config.fault_injection.is_none());
assert!(config.rate_limit.is_none());
assert!(config.traffic_shaping.is_none());
assert!(config.circuit_breaker.is_none());
assert!(config.bulkhead.is_none());
}
#[test]
fn test_chaos_config_with_latency() {
let config = ChaosConfig {
enabled: true,
latency: Some(LatencyConfig::default()),
..Default::default()
};
assert!(config.enabled);
assert!(config.latency.is_some());
}
#[test]
fn test_chaos_config_serialize() {
let config = ChaosConfig::default();
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("enabled"));
}
#[test]
fn test_latency_config_default() {
let config = LatencyConfig::default();
assert!(!config.enabled);
assert!(config.fixed_delay_ms.is_none());
assert!(config.random_delay_range_ms.is_none());
assert_eq!(config.jitter_percent, 0.0);
assert_eq!(config.probability, 1.0);
}
#[test]
fn test_latency_config_with_fixed_delay() {
let config = LatencyConfig {
enabled: true,
fixed_delay_ms: Some(100),
..Default::default()
};
assert_eq!(config.fixed_delay_ms, Some(100));
}
#[test]
fn test_latency_config_with_random_range() {
let config = LatencyConfig {
enabled: true,
random_delay_range_ms: Some((50, 150)),
..Default::default()
};
let (min, max) = config.random_delay_range_ms.unwrap();
assert_eq!(min, 50);
assert_eq!(max, 150);
}
#[test]
fn test_fault_injection_config_default() {
let config = FaultInjectionConfig::default();
assert!(!config.enabled);
assert_eq!(config.http_errors, vec![500, 502, 503, 504]);
assert_eq!(config.http_error_probability, 0.1);
assert!(!config.connection_errors);
assert_eq!(config.corruption_type, CorruptionType::None);
assert!(!config.mockai_enabled);
}
#[test]
fn test_fault_injection_config_with_errors() {
let config = FaultInjectionConfig {
enabled: true,
http_errors: vec![400, 401, 403, 404],
http_error_probability: 0.5,
..Default::default()
};
assert_eq!(config.http_errors.len(), 4);
assert!(config.http_errors.contains(&401));
}
#[test]
fn test_rate_limit_config_default() {
let config = RateLimitConfig::default();
assert!(!config.enabled);
assert_eq!(config.requests_per_second, 100);
assert_eq!(config.burst_size, 10);
assert!(!config.per_ip);
assert!(!config.per_endpoint);
}
#[test]
fn test_traffic_shaping_config_default() {
let config = TrafficShapingConfig::default();
assert!(!config.enabled);
assert_eq!(config.bandwidth_limit_bps, 0);
assert_eq!(config.packet_loss_percent, 0.0);
assert_eq!(config.max_connections, 0);
assert_eq!(config.connection_timeout_ms, 30000);
}
#[test]
fn test_circuit_breaker_config_default() {
let config = CircuitBreakerConfig::default();
assert!(!config.enabled);
assert_eq!(config.failure_threshold, 5);
assert_eq!(config.success_threshold, 2);
assert_eq!(config.timeout_ms, 60000);
assert_eq!(config.half_open_max_requests, 3);
assert_eq!(config.failure_rate_threshold, 50.0);
}
#[test]
fn test_bulkhead_config_default() {
let config = BulkheadConfig::default();
assert!(!config.enabled);
assert_eq!(config.max_concurrent_requests, 100);
assert_eq!(config.max_queue_size, 10);
assert_eq!(config.queue_timeout_ms, 5000);
}
#[test]
fn test_network_profile_new() {
let profile = NetworkProfile::new(
"test-profile".to_string(),
"A test profile".to_string(),
ChaosConfig::default(),
);
assert_eq!(profile.name, "test-profile");
assert_eq!(profile.description, "A test profile");
assert!(profile.tags.is_empty());
assert!(!profile.builtin);
}
#[test]
fn test_network_profile_predefined() {
let profiles = NetworkProfile::predefined_profiles();
assert!(!profiles.is_empty());
let names: Vec<_> = profiles.iter().map(|p| p.name.as_str()).collect();
assert!(names.contains(&"slow_3g"));
assert!(names.contains(&"fast_3g"));
assert!(names.contains(&"flaky_wifi"));
assert!(names.contains(&"cable"));
assert!(names.contains(&"dialup"));
}
#[test]
fn test_network_profile_predefined_are_builtin() {
let profiles = NetworkProfile::predefined_profiles();
for profile in &profiles {
assert!(profile.builtin, "Profile {} should be builtin", profile.name);
assert!(profile.chaos_config.enabled, "Profile {} should be enabled", profile.name);
}
}
#[test]
fn test_network_profile_slow_3g_has_latency() {
let profiles = NetworkProfile::predefined_profiles();
let slow_3g = profiles.iter().find(|p| p.name == "slow_3g").unwrap();
assert!(slow_3g.chaos_config.latency.is_some());
let latency = slow_3g.chaos_config.latency.as_ref().unwrap();
assert!(latency.enabled);
assert_eq!(latency.fixed_delay_ms, Some(400));
}
#[test]
fn test_network_profile_flaky_wifi_has_fault_injection() {
let profiles = NetworkProfile::predefined_profiles();
let flaky_wifi = profiles.iter().find(|p| p.name == "flaky_wifi").unwrap();
assert!(flaky_wifi.chaos_config.fault_injection.is_some());
let fault = flaky_wifi.chaos_config.fault_injection.as_ref().unwrap();
assert!(fault.enabled);
assert!(fault.connection_errors);
}
#[test]
fn test_network_profile_serialize() {
let profile =
NetworkProfile::new("test".to_string(), "desc".to_string(), ChaosConfig::default());
let json = serde_json::to_string(&profile).unwrap();
assert!(json.contains("test"));
assert!(json.contains("desc"));
}
#[test]
fn test_network_profile_deserialize() {
let json = r#"{"name":"test","description":"desc","chaos_config":{"enabled":false},"tags":[],"builtin":false}"#;
let profile: NetworkProfile = serde_json::from_str(json).unwrap();
assert_eq!(profile.name, "test");
assert!(!profile.builtin);
}
#[test]
fn test_network_profile_clone() {
let profile = NetworkProfile::new(
"clone-test".to_string(),
"Clone test".to_string(),
ChaosConfig::default(),
);
let cloned = profile.clone();
assert_eq!(profile.name, cloned.name);
assert_eq!(profile.description, cloned.description);
}
}