rust_serv/config_reloader/
diff.rs1use crate::config::Config;
4use std::collections::HashSet;
5
6#[derive(Debug, Clone, PartialEq)]
8pub struct ConfigDiff {
9 pub changed_fields: Vec<String>,
11 pub requires_restart: bool,
13}
14
15impl ConfigDiff {
16 pub fn new() -> Self {
18 Self {
19 changed_fields: Vec::new(),
20 requires_restart: false,
21 }
22 }
23
24 pub fn compare(old: &Config, new: &Config) -> Self {
26 let mut diff = Self::new();
27
28 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 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 pub fn has_changes(&self) -> bool {
145 !self.changed_fields.is_empty()
146 }
147
148 pub fn change_count(&self) -> usize {
150 self.changed_fields.len()
151 }
152
153 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 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); }
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 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}