Skip to main content

rust_serv/config_reloader/
diff.rs

1//! Configuration difference detection
2
3use crate::config::Config;
4use std::collections::HashSet;
5
6/// Configuration change details
7#[derive(Debug, Clone, PartialEq)]
8pub struct ConfigDiff {
9    /// Fields that changed
10    pub changed_fields: Vec<String>,
11    /// Whether the change requires a restart
12    pub requires_restart: bool,
13}
14
15impl ConfigDiff {
16    /// Create a new ConfigDiff
17    pub fn new() -> Self {
18        Self {
19            changed_fields: Vec::new(),
20            requires_restart: false,
21        }
22    }
23
24    /// Compare two configurations and return the differences
25    pub fn compare(old: &Config, new: &Config) -> Self {
26        let mut diff = Self::new();
27        
28        // Check each field
29        if old.port != new.port {
30            diff.changed_fields.push("port".to_string());
31            diff.requires_restart = true;
32        }
33        
34        if old.root != new.root {
35            diff.changed_fields.push("root".to_string());
36        }
37        
38        if old.enable_indexing != new.enable_indexing {
39            diff.changed_fields.push("enable_indexing".to_string());
40        }
41        
42        if old.enable_compression != new.enable_compression {
43            diff.changed_fields.push("enable_compression".to_string());
44        }
45        
46        if old.log_level != new.log_level {
47            diff.changed_fields.push("log_level".to_string());
48        }
49        
50        if old.enable_tls != new.enable_tls {
51            diff.changed_fields.push("enable_tls".to_string());
52            diff.requires_restart = true;
53        }
54        
55        if old.tls_cert != new.tls_cert {
56            diff.changed_fields.push("tls_cert".to_string());
57            diff.requires_restart = true;
58        }
59        
60        if old.tls_key != new.tls_key {
61            diff.changed_fields.push("tls_key".to_string());
62            diff.requires_restart = true;
63        }
64        
65        if old.connection_timeout_secs != new.connection_timeout_secs {
66            diff.changed_fields.push("connection_timeout_secs".to_string());
67        }
68        
69        if old.max_connections != new.max_connections {
70            diff.changed_fields.push("max_connections".to_string());
71        }
72        
73        if old.enable_health_check != new.enable_health_check {
74            diff.changed_fields.push("enable_health_check".to_string());
75        }
76        
77        if old.enable_cors != new.enable_cors {
78            diff.changed_fields.push("enable_cors".to_string());
79        }
80        
81        if !Self::vec_eq(&old.cors_allowed_origins, &new.cors_allowed_origins) {
82            diff.changed_fields.push("cors_allowed_origins".to_string());
83        }
84        
85        if !Self::vec_eq(&old.cors_allowed_methods, &new.cors_allowed_methods) {
86            diff.changed_fields.push("cors_allowed_methods".to_string());
87        }
88        
89        if !Self::vec_eq(&old.cors_allowed_headers, &new.cors_allowed_headers) {
90            diff.changed_fields.push("cors_allowed_headers".to_string());
91        }
92        
93        if old.cors_allow_credentials != new.cors_allow_credentials {
94            diff.changed_fields.push("cors_allow_credentials".to_string());
95        }
96        
97        if !Self::vec_eq(&old.cors_exposed_headers, &new.cors_exposed_headers) {
98            diff.changed_fields.push("cors_exposed_headers".to_string());
99        }
100        
101        if old.cors_max_age != new.cors_max_age {
102            diff.changed_fields.push("cors_max_age".to_string());
103        }
104        
105        if old.enable_security != new.enable_security {
106            diff.changed_fields.push("enable_security".to_string());
107        }
108        
109        if old.rate_limit_max_requests != new.rate_limit_max_requests {
110            diff.changed_fields.push("rate_limit_max_requests".to_string());
111        }
112        
113        if old.rate_limit_window_secs != new.rate_limit_window_secs {
114            diff.changed_fields.push("rate_limit_window_secs".to_string());
115        }
116        
117        if !Self::vec_eq(&old.ip_allowlist, &new.ip_allowlist) {
118            diff.changed_fields.push("ip_allowlist".to_string());
119        }
120        
121        if !Self::vec_eq(&old.ip_blocklist, &new.ip_blocklist) {
122            diff.changed_fields.push("ip_blocklist".to_string());
123        }
124        
125        if old.max_body_size != new.max_body_size {
126            diff.changed_fields.push("max_body_size".to_string());
127        }
128        
129        if old.max_headers != new.max_headers {
130            diff.changed_fields.push("max_headers".to_string());
131        }
132        
133        diff
134    }
135    
136    /// Check if two vectors are equal (order-independent)
137    fn vec_eq(a: &[String], b: &[String]) -> bool {
138        let set_a: HashSet<_> = a.iter().collect();
139        let set_b: HashSet<_> = b.iter().collect();
140        set_a == set_b
141    }
142
143    /// Check if there are any changes
144    pub fn has_changes(&self) -> bool {
145        !self.changed_fields.is_empty()
146    }
147
148    /// Get the number of changed fields
149    pub fn change_count(&self) -> usize {
150        self.changed_fields.len()
151    }
152
153    /// Check if a specific field changed
154    pub fn field_changed(&self, field: &str) -> bool {
155        self.changed_fields.iter().any(|f| f == field)
156    }
157}
158
159impl Default for ConfigDiff {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use std::path::PathBuf;
169
170    #[test]
171    fn test_config_diff_creation() {
172        let diff = ConfigDiff::new();
173        assert!(diff.changed_fields.is_empty());
174        assert!(!diff.requires_restart);
175    }
176
177    #[test]
178    fn test_no_changes() {
179        let config1 = Config::default();
180        let config2 = Config::default();
181        
182        let diff = ConfigDiff::compare(&config1, &config2);
183        
184        assert!(!diff.has_changes());
185        assert_eq!(diff.change_count(), 0);
186    }
187
188    #[test]
189    fn test_port_change_requires_restart() {
190        let mut config1 = Config::default();
191        let mut config2 = Config::default();
192        
193        config1.port = 8080;
194        config2.port = 9090;
195        
196        let diff = ConfigDiff::compare(&config1, &config2);
197        
198        assert!(diff.has_changes());
199        assert!(diff.field_changed("port"));
200        assert!(diff.requires_restart);
201    }
202
203    #[test]
204    fn test_root_change_no_restart() {
205        let mut config1 = Config::default();
206        let mut config2 = Config::default();
207        
208        config1.root = PathBuf::from("/var/www1");
209        config2.root = PathBuf::from("/var/www2");
210        
211        let diff = ConfigDiff::compare(&config1, &config2);
212        
213        assert!(diff.has_changes());
214        assert!(diff.field_changed("root"));
215        assert!(!diff.requires_restart);
216    }
217
218    #[test]
219    fn test_tls_change_requires_restart() {
220        let mut config1 = Config::default();
221        let mut config2 = Config::default();
222        
223        config1.enable_tls = false;
224        config2.enable_tls = true;
225        
226        let diff = ConfigDiff::compare(&config1, &config2);
227        
228        assert!(diff.has_changes());
229        assert!(diff.field_changed("enable_tls"));
230        assert!(diff.requires_restart);
231    }
232
233    #[test]
234    fn test_tls_cert_change_requires_restart() {
235        let mut config1 = Config::default();
236        let mut config2 = Config::default();
237        
238        config1.tls_cert = Some("/old/cert.pem".to_string());
239        config2.tls_cert = Some("/new/cert.pem".to_string());
240        
241        let diff = ConfigDiff::compare(&config1, &config2);
242        
243        assert!(diff.has_changes());
244        assert!(diff.field_changed("tls_cert"));
245        assert!(diff.requires_restart);
246    }
247
248    #[test]
249    fn test_compression_change() {
250        let mut config1 = Config::default();
251        let mut config2 = Config::default();
252        
253        config1.enable_compression = true;
254        config2.enable_compression = false;
255        
256        let diff = ConfigDiff::compare(&config1, &config2);
257        
258        assert!(diff.has_changes());
259        assert!(diff.field_changed("enable_compression"));
260        assert!(!diff.requires_restart);
261    }
262
263    #[test]
264    fn test_log_level_change() {
265        let mut config1 = Config::default();
266        let mut config2 = Config::default();
267        
268        config1.log_level = "info".to_string();
269        config2.log_level = "debug".to_string();
270        
271        let diff = ConfigDiff::compare(&config1, &config2);
272        
273        assert!(diff.has_changes());
274        assert!(diff.field_changed("log_level"));
275    }
276
277    #[test]
278    fn test_max_connections_change() {
279        let mut config1 = Config::default();
280        let mut config2 = Config::default();
281        
282        config1.max_connections = 1000;
283        config2.max_connections = 2000;
284        
285        let diff = ConfigDiff::compare(&config1, &config2);
286        
287        assert!(diff.has_changes());
288        assert!(diff.field_changed("max_connections"));
289    }
290
291    #[test]
292    fn test_cors_origins_change() {
293        let mut config1 = Config::default();
294        let mut config2 = Config::default();
295        
296        config1.cors_allowed_origins = vec!["http://localhost".to_string()];
297        config2.cors_allowed_origins = vec!["http://example.com".to_string()];
298        
299        let diff = ConfigDiff::compare(&config1, &config2);
300        
301        assert!(diff.has_changes());
302        assert!(diff.field_changed("cors_allowed_origins"));
303    }
304
305    #[test]
306    fn test_cors_origins_order_independent() {
307        let mut config1 = Config::default();
308        let mut config2 = Config::default();
309        
310        config1.cors_allowed_origins = vec!["http://a.com".to_string(), "http://b.com".to_string()];
311        config2.cors_allowed_origins = vec!["http://b.com".to_string(), "http://a.com".to_string()];
312        
313        let diff = ConfigDiff::compare(&config1, &config2);
314        
315        // Should not detect changes when order differs
316        assert!(!diff.has_changes());
317    }
318
319    #[test]
320    fn test_rate_limit_change() {
321        let mut config1 = Config::default();
322        let mut config2 = Config::default();
323        
324        config1.rate_limit_max_requests = 100;
325        config2.rate_limit_max_requests = 200;
326        
327        let diff = ConfigDiff::compare(&config1, &config2);
328        
329        assert!(diff.has_changes());
330        assert!(diff.field_changed("rate_limit_max_requests"));
331    }
332
333    #[test]
334    fn test_ip_list_change() {
335        let mut config1 = Config::default();
336        let mut config2 = Config::default();
337        
338        config1.ip_allowlist = vec!["127.0.0.1".to_string()];
339        config2.ip_allowlist = vec!["127.0.0.1".to_string(), "192.168.1.1".to_string()];
340        
341        let diff = ConfigDiff::compare(&config1, &config2);
342        
343        assert!(diff.has_changes());
344        assert!(diff.field_changed("ip_allowlist"));
345    }
346
347    #[test]
348    fn test_multiple_changes() {
349        let mut config1 = Config::default();
350        let mut config2 = Config::default();
351        
352        config1.port = 8080;
353        config1.enable_compression = true;
354        config1.log_level = "info".to_string();
355        
356        config2.port = 9090;
357        config2.enable_compression = false;
358        config2.log_level = "debug".to_string();
359        
360        let diff = ConfigDiff::compare(&config1, &config2);
361        
362        assert_eq!(diff.change_count(), 3);
363        assert!(diff.requires_restart); // port change
364    }
365
366    #[test]
367    fn test_change_count() {
368        let mut config1 = Config::default();
369        let mut config2 = Config::default();
370        
371        config1.port = 8080;
372        config2.port = 9090;
373        config1.max_connections = 1000;
374        config2.max_connections = 2000;
375        
376        let diff = ConfigDiff::compare(&config1, &config2);
377        
378        assert_eq!(diff.change_count(), 2);
379    }
380
381    #[test]
382    fn test_default() {
383        let diff = ConfigDiff::default();
384        assert!(!diff.has_changes());
385    }
386
387    #[test]
388    fn test_connection_timeout_change() {
389        let mut config1 = Config::default();
390        let mut config2 = Config::default();
391        
392        config1.connection_timeout_secs = 30;
393        config2.connection_timeout_secs = 60;
394        
395        let diff = ConfigDiff::compare(&config1, &config2);
396        
397        assert!(diff.has_changes());
398        assert!(diff.field_changed("connection_timeout_secs"));
399    }
400
401    #[test]
402    fn test_health_check_change() {
403        let mut config1 = Config::default();
404        let mut config2 = Config::default();
405        
406        config1.enable_health_check = true;
407        config2.enable_health_check = false;
408        
409        let diff = ConfigDiff::compare(&config1, &config2);
410        
411        assert!(diff.has_changes());
412        assert!(diff.field_changed("enable_health_check"));
413    }
414
415    #[test]
416    fn test_cors_enabled_change() {
417        let mut config1 = Config::default();
418        let mut config2 = Config::default();
419        
420        config1.enable_cors = true;
421        config2.enable_cors = false;
422        
423        let diff = ConfigDiff::compare(&config1, &config2);
424        
425        assert!(diff.has_changes());
426        assert!(diff.field_changed("enable_cors"));
427    }
428
429    #[test]
430    fn test_cors_methods_change() {
431        let mut config1 = Config::default();
432        let mut config2 = Config::default();
433        
434        config1.cors_allowed_methods = vec!["GET".to_string()];
435        config2.cors_allowed_methods = vec!["GET".to_string(), "POST".to_string()];
436        
437        let diff = ConfigDiff::compare(&config1, &config2);
438        
439        assert!(diff.has_changes());
440        assert!(diff.field_changed("cors_allowed_methods"));
441    }
442
443    #[test]
444    fn test_cors_headers_change() {
445        let mut config1 = Config::default();
446        let mut config2 = Config::default();
447        
448        config1.cors_allowed_headers = vec![];
449        config2.cors_allowed_headers = vec!["Content-Type".to_string()];
450        
451        let diff = ConfigDiff::compare(&config1, &config2);
452        
453        assert!(diff.has_changes());
454        assert!(diff.field_changed("cors_allowed_headers"));
455    }
456
457    #[test]
458    fn test_cors_credentials_change() {
459        let mut config1 = Config::default();
460        let mut config2 = Config::default();
461        
462        config1.cors_allow_credentials = false;
463        config2.cors_allow_credentials = true;
464        
465        let diff = ConfigDiff::compare(&config1, &config2);
466        
467        assert!(diff.has_changes());
468        assert!(diff.field_changed("cors_allow_credentials"));
469    }
470
471    #[test]
472    fn test_cors_exposed_headers_change() {
473        let mut config1 = Config::default();
474        let mut config2 = Config::default();
475        
476        config1.cors_exposed_headers = vec![];
477        config2.cors_exposed_headers = vec!["X-Custom".to_string()];
478        
479        let diff = ConfigDiff::compare(&config1, &config2);
480        
481        assert!(diff.has_changes());
482        assert!(diff.field_changed("cors_exposed_headers"));
483    }
484
485    #[test]
486    fn test_cors_max_age_change() {
487        let mut config1 = Config::default();
488        let mut config2 = Config::default();
489        
490        config1.cors_max_age = Some(86400);
491        config2.cors_max_age = Some(3600);
492        
493        let diff = ConfigDiff::compare(&config1, &config2);
494        
495        assert!(diff.has_changes());
496        assert!(diff.field_changed("cors_max_age"));
497    }
498
499    #[test]
500    fn test_enable_security_change() {
501        let mut config1 = Config::default();
502        let mut config2 = Config::default();
503        
504        config1.enable_security = true;
505        config2.enable_security = false;
506        
507        let diff = ConfigDiff::compare(&config1, &config2);
508        
509        assert!(diff.has_changes());
510        assert!(diff.field_changed("enable_security"));
511    }
512
513    #[test]
514    fn test_rate_limit_window_change() {
515        let mut config1 = Config::default();
516        let mut config2 = Config::default();
517        
518        config1.rate_limit_window_secs = 60;
519        config2.rate_limit_window_secs = 120;
520        
521        let diff = ConfigDiff::compare(&config1, &config2);
522        
523        assert!(diff.has_changes());
524        assert!(diff.field_changed("rate_limit_window_secs"));
525    }
526
527    #[test]
528    fn test_ip_blocklist_change() {
529        let mut config1 = Config::default();
530        let mut config2 = Config::default();
531        
532        config1.ip_blocklist = vec![];
533        config2.ip_blocklist = vec!["192.168.1.1".to_string()];
534        
535        let diff = ConfigDiff::compare(&config1, &config2);
536        
537        assert!(diff.has_changes());
538        assert!(diff.field_changed("ip_blocklist"));
539    }
540
541    #[test]
542    fn test_max_body_size_change() {
543        let mut config1 = Config::default();
544        let mut config2 = Config::default();
545        
546        config1.max_body_size = 10485760;
547        config2.max_body_size = 20971520;
548        
549        let diff = ConfigDiff::compare(&config1, &config2);
550        
551        assert!(diff.has_changes());
552        assert!(diff.field_changed("max_body_size"));
553    }
554
555    #[test]
556    fn test_max_headers_change() {
557        let mut config1 = Config::default();
558        let mut config2 = Config::default();
559        
560        config1.max_headers = 100;
561        config2.max_headers = 200;
562        
563        let diff = ConfigDiff::compare(&config1, &config2);
564        
565        assert!(diff.has_changes());
566        assert!(diff.field_changed("max_headers"));
567    }
568
569    #[test]
570    fn test_tls_key_change_requires_restart() {
571        let mut config1 = Config::default();
572        let mut config2 = Config::default();
573        
574        config1.tls_key = Some("/old/key.pem".to_string());
575        config2.tls_key = Some("/new/key.pem".to_string());
576        
577        let diff = ConfigDiff::compare(&config1, &config2);
578        
579        assert!(diff.has_changes());
580        assert!(diff.field_changed("tls_key"));
581        assert!(diff.requires_restart);
582    }
583
584    #[test]
585    fn test_indexing_change() {
586        let mut config1 = Config::default();
587        let mut config2 = Config::default();
588        
589        config1.enable_indexing = true;
590        config2.enable_indexing = false;
591        
592        let diff = ConfigDiff::compare(&config1, &config2);
593        
594        assert!(diff.has_changes());
595        assert!(diff.field_changed("enable_indexing"));
596    }
597
598    #[test]
599    fn test_empty_ip_list_comparison() {
600        let mut config1 = Config::default();
601        let mut config2 = Config::default();
602        
603        // Both empty - should be equal
604        config1.ip_allowlist = vec![];
605        config2.ip_allowlist = vec![];
606        
607        let diff = ConfigDiff::compare(&config1, &config2);
608        assert!(!diff.field_changed("ip_allowlist"));
609    }
610}