mockforge_chaos/
config.rs

1//! Chaos engineering configuration
2
3use serde::{Deserialize, Serialize};
4
5/// Payload corruption type
6#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "snake_case")]
8pub enum CorruptionType {
9    /// No corruption
10    None,
11    /// Replace random bytes with random values
12    RandomBytes,
13    /// Truncate payload at random position
14    Truncate,
15    /// Flip random bits in the payload
16    BitFlip,
17}
18
19/// Error injection pattern
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21#[serde(rename_all = "snake_case", tag = "type")]
22pub enum ErrorPattern {
23    /// Burst pattern: inject N errors within a time interval
24    Burst {
25        /// Number of errors to inject in the burst
26        count: usize,
27        /// Time interval in milliseconds for the burst
28        interval_ms: u64,
29    },
30    /// Random pattern: inject errors with a probability
31    Random {
32        /// Probability of injecting an error (0.0-1.0)
33        probability: f64,
34    },
35    /// Sequential pattern: inject errors in a specific sequence
36    Sequential {
37        /// Sequence of status codes to inject in order
38        sequence: Vec<u16>,
39    },
40}
41
42impl Default for ErrorPattern {
43    fn default() -> Self {
44        ErrorPattern::Random { probability: 0.1 }
45    }
46}
47
48/// Main chaos engineering configuration
49#[derive(Debug, Clone, Serialize, Deserialize, Default)]
50pub struct ChaosConfig {
51    /// Enable chaos engineering
52    pub enabled: bool,
53    /// Latency injection configuration
54    pub latency: Option<LatencyConfig>,
55    /// Fault injection configuration
56    pub fault_injection: Option<FaultInjectionConfig>,
57    /// Rate limiting configuration
58    pub rate_limit: Option<RateLimitConfig>,
59    /// Traffic shaping configuration
60    pub traffic_shaping: Option<TrafficShapingConfig>,
61    /// Circuit breaker configuration
62    pub circuit_breaker: Option<CircuitBreakerConfig>,
63    /// Bulkhead configuration
64    pub bulkhead: Option<BulkheadConfig>,
65}
66
67/// Latency injection configuration
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct LatencyConfig {
70    /// Enable latency injection
71    pub enabled: bool,
72    /// Fixed delay in milliseconds
73    pub fixed_delay_ms: Option<u64>,
74    /// Random delay range (min, max) in milliseconds
75    pub random_delay_range_ms: Option<(u64, u64)>,
76    /// Jitter percentage (0-100)
77    pub jitter_percent: f64,
78    /// Probability of applying latency (0.0-1.0)
79    pub probability: f64,
80}
81
82impl Default for LatencyConfig {
83    fn default() -> Self {
84        Self {
85            enabled: false,
86            fixed_delay_ms: None,
87            random_delay_range_ms: None,
88            jitter_percent: 0.0,
89            probability: 1.0,
90        }
91    }
92}
93
94/// Fault injection configuration
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct FaultInjectionConfig {
97    /// Enable fault injection
98    pub enabled: bool,
99    /// HTTP error codes to inject
100    pub http_errors: Vec<u16>,
101    /// Probability of HTTP errors (0.0-1.0)
102    pub http_error_probability: f64,
103    /// Inject connection errors
104    pub connection_errors: bool,
105    /// Probability of connection errors (0.0-1.0)
106    pub connection_error_probability: f64,
107    /// Inject timeout errors
108    pub timeout_errors: bool,
109    /// Timeout duration in milliseconds
110    pub timeout_ms: u64,
111    /// Probability of timeout errors (0.0-1.0)
112    pub timeout_probability: f64,
113    /// Inject partial responses (incomplete data)
114    pub partial_responses: bool,
115    /// Probability of partial responses (0.0-1.0)
116    pub partial_response_probability: f64,
117    /// Enable payload corruption
118    pub payload_corruption: bool,
119    /// Probability of payload corruption (0.0-1.0)
120    pub payload_corruption_probability: f64,
121    /// Type of corruption to apply
122    pub corruption_type: CorruptionType,
123    /// Error injection pattern (burst, random, sequential)
124    #[serde(default)]
125    pub error_pattern: Option<ErrorPattern>,
126    /// Enable MockAI for dynamic error message generation
127    #[serde(default)]
128    pub mockai_enabled: bool,
129}
130
131impl Default for FaultInjectionConfig {
132    fn default() -> Self {
133        Self {
134            enabled: false,
135            http_errors: vec![500, 502, 503, 504],
136            http_error_probability: 0.1,
137            connection_errors: false,
138            connection_error_probability: 0.05,
139            timeout_errors: false,
140            timeout_ms: 5000,
141            timeout_probability: 0.05,
142            partial_responses: false,
143            partial_response_probability: 0.05,
144            payload_corruption: false,
145            payload_corruption_probability: 0.05,
146            corruption_type: CorruptionType::None,
147            error_pattern: None,
148            mockai_enabled: false,
149        }
150    }
151}
152
153/// Rate limiting configuration
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct RateLimitConfig {
156    /// Enable rate limiting
157    pub enabled: bool,
158    /// Maximum requests per second
159    pub requests_per_second: u32,
160    /// Burst size (number of requests allowed in burst)
161    pub burst_size: u32,
162    /// Per-IP rate limiting
163    pub per_ip: bool,
164    /// Per-endpoint rate limiting
165    pub per_endpoint: bool,
166}
167
168impl Default for RateLimitConfig {
169    fn default() -> Self {
170        Self {
171            enabled: false,
172            requests_per_second: 100,
173            burst_size: 10,
174            per_ip: false,
175            per_endpoint: false,
176        }
177    }
178}
179
180/// Traffic shaping configuration
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct TrafficShapingConfig {
183    /// Enable traffic shaping
184    pub enabled: bool,
185    /// Bandwidth limit in bytes per second (0 = unlimited)
186    pub bandwidth_limit_bps: u64,
187    /// Packet loss percentage (0-100)
188    pub packet_loss_percent: f64,
189    /// Maximum concurrent connections (0 = unlimited)
190    pub max_connections: u32,
191    /// Connection timeout in milliseconds
192    pub connection_timeout_ms: u64,
193}
194
195impl Default for TrafficShapingConfig {
196    fn default() -> Self {
197        Self {
198            enabled: false,
199            bandwidth_limit_bps: 0,
200            packet_loss_percent: 0.0,
201            max_connections: 0,
202            connection_timeout_ms: 30000,
203        }
204    }
205}
206
207/// Circuit breaker configuration
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct CircuitBreakerConfig {
210    /// Enable circuit breaker
211    pub enabled: bool,
212    /// Failure threshold before opening circuit
213    pub failure_threshold: u64,
214    /// Success threshold before closing circuit from half-open
215    pub success_threshold: u64,
216    /// Timeout before attempting to close circuit (in milliseconds)
217    pub timeout_ms: u64,
218    /// Half-open request limit
219    pub half_open_max_requests: u32,
220    /// Failure rate threshold (percentage, 0-100)
221    pub failure_rate_threshold: f64,
222    /// Minimum number of requests before calculating failure rate
223    pub min_requests_for_rate: u64,
224    /// Rolling window duration for failure rate calculation (in milliseconds)
225    pub rolling_window_ms: u64,
226}
227
228impl Default for CircuitBreakerConfig {
229    fn default() -> Self {
230        Self {
231            enabled: false,
232            failure_threshold: 5,
233            success_threshold: 2,
234            timeout_ms: 60000,
235            half_open_max_requests: 3,
236            failure_rate_threshold: 50.0,
237            min_requests_for_rate: 10,
238            rolling_window_ms: 10000,
239        }
240    }
241}
242
243/// Bulkhead configuration
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct BulkheadConfig {
246    /// Enable bulkhead
247    pub enabled: bool,
248    /// Maximum concurrent requests
249    pub max_concurrent_requests: u32,
250    /// Maximum queue size (0 = no queue)
251    pub max_queue_size: u32,
252    /// Queue timeout in milliseconds
253    pub queue_timeout_ms: u64,
254}
255
256/// Network profile for simulating different network conditions
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct NetworkProfile {
259    /// Profile name
260    pub name: String,
261    /// Profile description
262    pub description: String,
263    /// Chaos configuration for this profile
264    pub chaos_config: ChaosConfig,
265    /// Tags for categorization
266    #[serde(default)]
267    pub tags: Vec<String>,
268    /// Whether this is a built-in profile (not user-created)
269    #[serde(default)]
270    pub builtin: bool,
271}
272
273impl NetworkProfile {
274    /// Create a new network profile
275    pub fn new(name: String, description: String, chaos_config: ChaosConfig) -> Self {
276        Self {
277            name,
278            description,
279            chaos_config,
280            tags: Vec::new(),
281            builtin: false,
282        }
283    }
284
285    /// Create predefined network profiles
286    pub fn predefined_profiles() -> Vec<Self> {
287        vec![
288            // Slow 3G: High latency, packet loss, low bandwidth
289            Self {
290                name: "slow_3g".to_string(),
291                description:
292                    "Simulates slow 3G network: 400ms latency, 1% packet loss, 400KB/s bandwidth"
293                        .to_string(),
294                chaos_config: ChaosConfig {
295                    enabled: true,
296                    latency: Some(LatencyConfig {
297                        enabled: true,
298                        fixed_delay_ms: Some(400),
299                        random_delay_range_ms: Some((300, 500)),
300                        jitter_percent: 10.0,
301                        probability: 1.0,
302                    }),
303                    fault_injection: None,
304                    rate_limit: None,
305                    traffic_shaping: Some(TrafficShapingConfig {
306                        enabled: true,
307                        bandwidth_limit_bps: 400_000, // 400 KB/s
308                        packet_loss_percent: 1.0,
309                        max_connections: 0,
310                        connection_timeout_ms: 30000,
311                    }),
312                    circuit_breaker: None,
313                    bulkhead: None,
314                },
315                tags: vec!["mobile".to_string(), "slow".to_string(), "3g".to_string()],
316                builtin: true,
317            },
318            // Fast 3G: Moderate latency, low packet loss, higher bandwidth
319            Self {
320                name: "fast_3g".to_string(),
321                description:
322                    "Simulates fast 3G network: 150ms latency, 0.5% packet loss, 1.5MB/s bandwidth"
323                        .to_string(),
324                chaos_config: ChaosConfig {
325                    enabled: true,
326                    latency: Some(LatencyConfig {
327                        enabled: true,
328                        fixed_delay_ms: Some(150),
329                        random_delay_range_ms: Some((100, 200)),
330                        jitter_percent: 5.0,
331                        probability: 1.0,
332                    }),
333                    fault_injection: None,
334                    rate_limit: None,
335                    traffic_shaping: Some(TrafficShapingConfig {
336                        enabled: true,
337                        bandwidth_limit_bps: 1_500_000, // 1.5 MB/s
338                        packet_loss_percent: 0.5,
339                        max_connections: 0,
340                        connection_timeout_ms: 30000,
341                    }),
342                    circuit_breaker: None,
343                    bulkhead: None,
344                },
345                tags: vec!["mobile".to_string(), "fast".to_string(), "3g".to_string()],
346                builtin: true,
347            },
348            // Flaky Wi-Fi: Low latency but high packet loss and random disconnects
349            Self {
350                name: "flaky_wifi".to_string(),
351                description:
352                    "Simulates flaky Wi-Fi: 50ms latency, 5% packet loss, random connection errors"
353                        .to_string(),
354                chaos_config: ChaosConfig {
355                    enabled: true,
356                    latency: Some(LatencyConfig {
357                        enabled: true,
358                        fixed_delay_ms: Some(50),
359                        random_delay_range_ms: Some((30, 100)),
360                        jitter_percent: 20.0,
361                        probability: 1.0,
362                    }),
363                    fault_injection: Some(FaultInjectionConfig {
364                        enabled: true,
365                        http_errors: vec![500, 502, 503],
366                        http_error_probability: 0.05, // 5% chance of connection errors
367                        connection_errors: true,
368                        connection_error_probability: 0.03, // 3% chance of disconnects
369                        timeout_errors: false,
370                        timeout_ms: 5000,
371                        timeout_probability: 0.0,
372                        partial_responses: false,
373                        partial_response_probability: 0.0,
374                        payload_corruption: false,
375                        payload_corruption_probability: 0.0,
376                        corruption_type: CorruptionType::None,
377                        error_pattern: None,
378                        mockai_enabled: false,
379                    }),
380                    rate_limit: None,
381                    traffic_shaping: Some(TrafficShapingConfig {
382                        enabled: true,
383                        bandwidth_limit_bps: 0, // No bandwidth limit
384                        packet_loss_percent: 5.0,
385                        max_connections: 0,
386                        connection_timeout_ms: 30000,
387                    }),
388                    circuit_breaker: None,
389                    bulkhead: None,
390                },
391                tags: vec![
392                    "wifi".to_string(),
393                    "unstable".to_string(),
394                    "wireless".to_string(),
395                ],
396                builtin: true,
397            },
398            // Cable: Low latency, no packet loss, high bandwidth
399            Self {
400                name: "cable".to_string(),
401                description:
402                    "Simulates cable internet: 20ms latency, no packet loss, 10MB/s bandwidth"
403                        .to_string(),
404                chaos_config: ChaosConfig {
405                    enabled: true,
406                    latency: Some(LatencyConfig {
407                        enabled: true,
408                        fixed_delay_ms: Some(20),
409                        random_delay_range_ms: Some((10, 30)),
410                        jitter_percent: 2.0,
411                        probability: 1.0,
412                    }),
413                    fault_injection: None,
414                    rate_limit: None,
415                    traffic_shaping: Some(TrafficShapingConfig {
416                        enabled: true,
417                        bandwidth_limit_bps: 10_000_000, // 10 MB/s
418                        packet_loss_percent: 0.0,
419                        max_connections: 0,
420                        connection_timeout_ms: 30000,
421                    }),
422                    circuit_breaker: None,
423                    bulkhead: None,
424                },
425                tags: vec![
426                    "broadband".to_string(),
427                    "fast".to_string(),
428                    "stable".to_string(),
429                ],
430                builtin: true,
431            },
432            // Dial-up: Very high latency, packet loss, very low bandwidth
433            Self {
434                name: "dialup".to_string(),
435                description:
436                    "Simulates dial-up connection: 2000ms latency, 2% packet loss, 50KB/s bandwidth"
437                        .to_string(),
438                chaos_config: ChaosConfig {
439                    enabled: true,
440                    latency: Some(LatencyConfig {
441                        enabled: true,
442                        fixed_delay_ms: Some(2000),
443                        random_delay_range_ms: Some((1500, 2500)),
444                        jitter_percent: 15.0,
445                        probability: 1.0,
446                    }),
447                    fault_injection: None,
448                    rate_limit: None,
449                    traffic_shaping: Some(TrafficShapingConfig {
450                        enabled: true,
451                        bandwidth_limit_bps: 50_000, // 50 KB/s
452                        packet_loss_percent: 2.0,
453                        max_connections: 0,
454                        connection_timeout_ms: 60000, // Longer timeout for dial-up
455                    }),
456                    circuit_breaker: None,
457                    bulkhead: None,
458                },
459                tags: vec![
460                    "dialup".to_string(),
461                    "slow".to_string(),
462                    "legacy".to_string(),
463                ],
464                builtin: true,
465            },
466        ]
467    }
468}
469
470impl Default for BulkheadConfig {
471    fn default() -> Self {
472        Self {
473            enabled: false,
474            max_concurrent_requests: 100,
475            max_queue_size: 10,
476            queue_timeout_ms: 5000,
477        }
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    // CorruptionType tests
486    #[test]
487    fn test_corruption_type_variants() {
488        let none = CorruptionType::None;
489        let random_bytes = CorruptionType::RandomBytes;
490        let truncate = CorruptionType::Truncate;
491        let bit_flip = CorruptionType::BitFlip;
492
493        assert!(matches!(none, CorruptionType::None));
494        assert!(matches!(random_bytes, CorruptionType::RandomBytes));
495        assert!(matches!(truncate, CorruptionType::Truncate));
496        assert!(matches!(bit_flip, CorruptionType::BitFlip));
497    }
498
499    #[test]
500    fn test_corruption_type_serialize() {
501        let ct = CorruptionType::RandomBytes;
502        let json = serde_json::to_string(&ct).unwrap();
503        assert!(json.contains("random_bytes"));
504    }
505
506    #[test]
507    fn test_corruption_type_deserialize() {
508        let json = r#""bit_flip""#;
509        let ct: CorruptionType = serde_json::from_str(json).unwrap();
510        assert!(matches!(ct, CorruptionType::BitFlip));
511    }
512
513    // ErrorPattern tests
514    #[test]
515    fn test_error_pattern_default() {
516        let pattern = ErrorPattern::default();
517        assert!(
518            matches!(pattern, ErrorPattern::Random { probability } if (probability - 0.1).abs() < f64::EPSILON)
519        );
520    }
521
522    #[test]
523    fn test_error_pattern_burst() {
524        let pattern = ErrorPattern::Burst {
525            count: 5,
526            interval_ms: 1000,
527        };
528        if let ErrorPattern::Burst { count, interval_ms } = pattern {
529            assert_eq!(count, 5);
530            assert_eq!(interval_ms, 1000);
531        } else {
532            panic!("Expected Burst pattern");
533        }
534    }
535
536    #[test]
537    fn test_error_pattern_sequential() {
538        let pattern = ErrorPattern::Sequential {
539            sequence: vec![500, 502, 503],
540        };
541        if let ErrorPattern::Sequential { sequence } = pattern {
542            assert_eq!(sequence.len(), 3);
543            assert!(sequence.contains(&500));
544        } else {
545            panic!("Expected Sequential pattern");
546        }
547    }
548
549    #[test]
550    fn test_error_pattern_serialize() {
551        let pattern = ErrorPattern::Burst {
552            count: 3,
553            interval_ms: 500,
554        };
555        let json = serde_json::to_string(&pattern).unwrap();
556        assert!(json.contains("burst"));
557        assert!(json.contains("count"));
558    }
559
560    // ChaosConfig tests
561    #[test]
562    fn test_chaos_config_default() {
563        let config = ChaosConfig::default();
564        assert!(!config.enabled);
565        assert!(config.latency.is_none());
566        assert!(config.fault_injection.is_none());
567        assert!(config.rate_limit.is_none());
568        assert!(config.traffic_shaping.is_none());
569        assert!(config.circuit_breaker.is_none());
570        assert!(config.bulkhead.is_none());
571    }
572
573    #[test]
574    fn test_chaos_config_with_latency() {
575        let config = ChaosConfig {
576            enabled: true,
577            latency: Some(LatencyConfig::default()),
578            ..Default::default()
579        };
580        assert!(config.enabled);
581        assert!(config.latency.is_some());
582    }
583
584    #[test]
585    fn test_chaos_config_serialize() {
586        let config = ChaosConfig::default();
587        let json = serde_json::to_string(&config).unwrap();
588        assert!(json.contains("enabled"));
589    }
590
591    // LatencyConfig tests
592    #[test]
593    fn test_latency_config_default() {
594        let config = LatencyConfig::default();
595        assert!(!config.enabled);
596        assert!(config.fixed_delay_ms.is_none());
597        assert!(config.random_delay_range_ms.is_none());
598        assert_eq!(config.jitter_percent, 0.0);
599        assert_eq!(config.probability, 1.0);
600    }
601
602    #[test]
603    fn test_latency_config_with_fixed_delay() {
604        let config = LatencyConfig {
605            enabled: true,
606            fixed_delay_ms: Some(100),
607            ..Default::default()
608        };
609        assert_eq!(config.fixed_delay_ms, Some(100));
610    }
611
612    #[test]
613    fn test_latency_config_with_random_range() {
614        let config = LatencyConfig {
615            enabled: true,
616            random_delay_range_ms: Some((50, 150)),
617            ..Default::default()
618        };
619        let (min, max) = config.random_delay_range_ms.unwrap();
620        assert_eq!(min, 50);
621        assert_eq!(max, 150);
622    }
623
624    // FaultInjectionConfig tests
625    #[test]
626    fn test_fault_injection_config_default() {
627        let config = FaultInjectionConfig::default();
628        assert!(!config.enabled);
629        assert_eq!(config.http_errors, vec![500, 502, 503, 504]);
630        assert_eq!(config.http_error_probability, 0.1);
631        assert!(!config.connection_errors);
632        assert_eq!(config.corruption_type, CorruptionType::None);
633        assert!(!config.mockai_enabled);
634    }
635
636    #[test]
637    fn test_fault_injection_config_with_errors() {
638        let config = FaultInjectionConfig {
639            enabled: true,
640            http_errors: vec![400, 401, 403, 404],
641            http_error_probability: 0.5,
642            ..Default::default()
643        };
644        assert_eq!(config.http_errors.len(), 4);
645        assert!(config.http_errors.contains(&401));
646    }
647
648    // RateLimitConfig tests
649    #[test]
650    fn test_rate_limit_config_default() {
651        let config = RateLimitConfig::default();
652        assert!(!config.enabled);
653        assert_eq!(config.requests_per_second, 100);
654        assert_eq!(config.burst_size, 10);
655        assert!(!config.per_ip);
656        assert!(!config.per_endpoint);
657    }
658
659    // TrafficShapingConfig tests
660    #[test]
661    fn test_traffic_shaping_config_default() {
662        let config = TrafficShapingConfig::default();
663        assert!(!config.enabled);
664        assert_eq!(config.bandwidth_limit_bps, 0);
665        assert_eq!(config.packet_loss_percent, 0.0);
666        assert_eq!(config.max_connections, 0);
667        assert_eq!(config.connection_timeout_ms, 30000);
668    }
669
670    // CircuitBreakerConfig tests
671    #[test]
672    fn test_circuit_breaker_config_default() {
673        let config = CircuitBreakerConfig::default();
674        assert!(!config.enabled);
675        assert_eq!(config.failure_threshold, 5);
676        assert_eq!(config.success_threshold, 2);
677        assert_eq!(config.timeout_ms, 60000);
678        assert_eq!(config.half_open_max_requests, 3);
679        assert_eq!(config.failure_rate_threshold, 50.0);
680    }
681
682    // BulkheadConfig tests
683    #[test]
684    fn test_bulkhead_config_default() {
685        let config = BulkheadConfig::default();
686        assert!(!config.enabled);
687        assert_eq!(config.max_concurrent_requests, 100);
688        assert_eq!(config.max_queue_size, 10);
689        assert_eq!(config.queue_timeout_ms, 5000);
690    }
691
692    // NetworkProfile tests
693    #[test]
694    fn test_network_profile_new() {
695        let profile = NetworkProfile::new(
696            "test-profile".to_string(),
697            "A test profile".to_string(),
698            ChaosConfig::default(),
699        );
700        assert_eq!(profile.name, "test-profile");
701        assert_eq!(profile.description, "A test profile");
702        assert!(profile.tags.is_empty());
703        assert!(!profile.builtin);
704    }
705
706    #[test]
707    fn test_network_profile_predefined() {
708        let profiles = NetworkProfile::predefined_profiles();
709        assert!(!profiles.is_empty());
710
711        // Check that we have common profiles
712        let names: Vec<_> = profiles.iter().map(|p| p.name.as_str()).collect();
713        assert!(names.contains(&"slow_3g"));
714        assert!(names.contains(&"fast_3g"));
715        assert!(names.contains(&"flaky_wifi"));
716        assert!(names.contains(&"cable"));
717        assert!(names.contains(&"dialup"));
718    }
719
720    #[test]
721    fn test_network_profile_predefined_are_builtin() {
722        let profiles = NetworkProfile::predefined_profiles();
723        for profile in &profiles {
724            assert!(profile.builtin, "Profile {} should be builtin", profile.name);
725            assert!(profile.chaos_config.enabled, "Profile {} should be enabled", profile.name);
726        }
727    }
728
729    #[test]
730    fn test_network_profile_slow_3g_has_latency() {
731        let profiles = NetworkProfile::predefined_profiles();
732        let slow_3g = profiles.iter().find(|p| p.name == "slow_3g").unwrap();
733
734        assert!(slow_3g.chaos_config.latency.is_some());
735        let latency = slow_3g.chaos_config.latency.as_ref().unwrap();
736        assert!(latency.enabled);
737        assert_eq!(latency.fixed_delay_ms, Some(400));
738    }
739
740    #[test]
741    fn test_network_profile_flaky_wifi_has_fault_injection() {
742        let profiles = NetworkProfile::predefined_profiles();
743        let flaky_wifi = profiles.iter().find(|p| p.name == "flaky_wifi").unwrap();
744
745        assert!(flaky_wifi.chaos_config.fault_injection.is_some());
746        let fault = flaky_wifi.chaos_config.fault_injection.as_ref().unwrap();
747        assert!(fault.enabled);
748        assert!(fault.connection_errors);
749    }
750
751    #[test]
752    fn test_network_profile_serialize() {
753        let profile =
754            NetworkProfile::new("test".to_string(), "desc".to_string(), ChaosConfig::default());
755        let json = serde_json::to_string(&profile).unwrap();
756        assert!(json.contains("test"));
757        assert!(json.contains("desc"));
758    }
759
760    #[test]
761    fn test_network_profile_deserialize() {
762        let json = r#"{"name":"test","description":"desc","chaos_config":{"enabled":false},"tags":[],"builtin":false}"#;
763        let profile: NetworkProfile = serde_json::from_str(json).unwrap();
764        assert_eq!(profile.name, "test");
765        assert!(!profile.builtin);
766    }
767
768    #[test]
769    fn test_network_profile_clone() {
770        let profile = NetworkProfile::new(
771            "clone-test".to_string(),
772            "Clone test".to_string(),
773            ChaosConfig::default(),
774        );
775        let cloned = profile.clone();
776        assert_eq!(profile.name, cloned.name);
777        assert_eq!(profile.description, cloned.description);
778    }
779}