Skip to main content

max / makenotwork

23.5 KB · 911 lines History Blame Raw
1 //! Tests for [`super`].
2
3 use super::*;
4
5 #[test]
6 fn parse_full_config() {
7 let toml = r#"
8 [serve]
9 interval_secs = 120
10 listen = "127.0.0.1:9100"
11 peer_heartbeat_secs = 30
12
13 [instance]
14 name = "hetzner"
15
16 [targets.mnw]
17 label = "MakeNotWork"
18 [targets.mnw.health]
19 url = "https://makenot.work/health"
20 timeout_secs = 5
21 [targets.mnw.tests]
22 ssh = "hetzner"
23 command = "cd /srv/mnw && ./ci.sh"
24
25 [peers.astra]
26 address = "100.0.0.1:9100"
27 on_missing = "alert"
28 grace_count = 5
29 "#;
30
31 let config: Config = toml::from_str(toml).unwrap();
32 assert_eq!(config.serve.interval_secs, 120);
33 assert_eq!(config.serve.listen, "127.0.0.1:9100");
34 assert_eq!(config.serve.peer_heartbeat_secs, 30);
35 assert_eq!(config.instance.name.as_deref(), Some("hetzner"));
36 assert_eq!(config.target_names(), vec!["mnw"]);
37
38 let mnw = config.get_target("mnw").unwrap();
39 assert_eq!(mnw.label, "MakeNotWork");
40 assert_eq!(mnw.health.as_ref().unwrap().timeout_secs, 5);
41 assert_eq!(mnw.tests.as_ref().unwrap().ssh.as_deref(), Some("hetzner"));
42
43 let astra = config.peers.get("astra").unwrap();
44 assert_eq!(astra.address, "100.0.0.1:9100");
45 assert_eq!(astra.on_missing, OnMissing::Alert);
46 assert_eq!(astra.grace_count, Some(5));
47 }
48
49 #[test]
50 fn empty_config_uses_defaults() {
51 let config: Config = toml::from_str("").unwrap();
52 assert_eq!(config.serve.interval_secs, 300);
53 assert_eq!(config.serve.prune_days, 30);
54 assert_eq!(config.serve.listen, "127.0.0.1:9100");
55 assert_eq!(config.serve.peer_heartbeat_secs, 60);
56 assert!(config.targets.is_empty());
57 assert!(config.peers.is_empty());
58 assert!(config.instance.name.is_none());
59 }
60
61 #[test]
62 fn peer_on_missing_defaults_to_log() {
63 let toml = r#"
64 [peers.test]
65 address = "10.0.0.1:9100"
66 "#;
67 let config: Config = toml::from_str(toml).unwrap();
68 let peer = config.peers.get("test").unwrap();
69 assert_eq!(peer.on_missing, OnMissing::Log);
70 assert_eq!(peer.grace_count, None);
71 assert!(peer.token.is_none());
72 }
73
74 #[test]
75 fn peer_with_token() {
76 let toml = r#"
77 [peers.test]
78 address = "10.0.0.1:9100"
79 token = "peer-secret-123"
80 "#;
81 let config: Config = toml::from_str(toml).unwrap();
82 let peer = config.peers.get("test").unwrap();
83 assert_eq!(peer.token.as_deref(), Some("peer-secret-123"));
84 }
85
86 #[test]
87 fn serve_api_token_from_config() {
88 let toml = r#"
89 [serve]
90 api_token = "my-api-secret"
91 "#;
92 let config: Config = toml::from_str(toml).unwrap();
93 assert_eq!(config.serve.api_token.as_deref(), Some("my-api-secret"));
94 }
95
96 #[test]
97 fn serve_api_token_defaults_to_none() {
98 let config: Config = toml::from_str("").unwrap();
99 assert!(config.serve.api_token.is_none());
100 }
101
102 #[test]
103 fn instance_name_falls_back_to_hostname() {
104 let config: Config = toml::from_str("").unwrap();
105 let name = config.instance_name();
106 assert!(!name.is_empty());
107 }
108
109 #[test]
110 fn config_without_alerts_section() {
111 let config: Config = toml::from_str("").unwrap();
112 assert!(config.alerts.is_none());
113 }
114
115 #[test]
116 fn config_with_alerts_section() {
117 let toml = r#"
118 [alerts]
119 postmark_token = "test-token"
120 to = "alerts@example.com"
121 "#;
122 let config: Config = toml::from_str(toml).unwrap();
123 let alerts = config.alerts.unwrap();
124 assert_eq!(alerts.postmark_token.as_deref(), Some("test-token"));
125 assert_eq!(alerts.to, "alerts@example.com");
126 assert_eq!(alerts.from, "PoM Alerts <pom-alerts@makenot.work>");
127 assert_eq!(alerts.cooldown_secs, 300);
128 }
129
130 #[test]
131 fn config_alerts_wam_token() {
132 let toml = r#"
133 [alerts]
134 to = "alerts@example.com"
135 wam_url = "http://wam.tailnet:9000"
136 wam_token = "test-wam-token"
137 "#;
138 let config: Config = toml::from_str(toml).unwrap();
139 let alerts = config.alerts.unwrap();
140 assert_eq!(alerts.wam_url.as_deref(), Some("http://wam.tailnet:9000"));
141 assert_eq!(alerts.wam_token.as_deref(), Some("test-wam-token"));
142 }
143
144 #[test]
145 fn config_alerts_wam_token_defaults_to_none() {
146 let toml = r#"
147 [alerts]
148 to = "alerts@example.com"
149 wam_url = "http://wam.tailnet:9000"
150 "#;
151 let config: Config = toml::from_str(toml).unwrap();
152 assert!(config.alerts.unwrap().wam_token.is_none());
153 }
154
155 #[test]
156 fn config_with_tls() {
157 let toml = r#"
158 [targets.mnw]
159 label = "MakeNotWork"
160 [targets.mnw.tls]
161 host = "makenot.work"
162 port = 8443
163 warn_days = 30
164 "#;
165 let config: Config = toml::from_str(toml).unwrap();
166 let mnw = config.get_target("mnw").unwrap();
167 let tls = mnw.tls.as_ref().unwrap();
168 assert_eq!(tls.host, "makenot.work");
169 assert_eq!(tls.port, 8443);
170 assert_eq!(tls.warn_days, 30);
171 }
172
173 #[test]
174 fn config_tls_defaults() {
175 let toml = r#"
176 [targets.mnw]
177 label = "MakeNotWork"
178 [targets.mnw.tls]
179 host = "makenot.work"
180 "#;
181 let config: Config = toml::from_str(toml).unwrap();
182 let tls = config.get_target("mnw").unwrap().tls.as_ref().unwrap();
183 assert_eq!(tls.port, 443);
184 assert_eq!(tls.warn_days, 14);
185 }
186
187 #[test]
188 fn config_without_tls() {
189 let toml = r#"
190 [targets.mnw]
191 label = "MakeNotWork"
192 "#;
193 let config: Config = toml::from_str(toml).unwrap();
194 assert!(config.get_target("mnw").unwrap().tls.is_none());
195 }
196
197 #[test]
198 fn config_tls_check_interval_default() {
199 let config: Config = toml::from_str("").unwrap();
200 assert_eq!(config.serve.tls_check_interval_secs, 3600);
201 }
202
203 #[test]
204 fn config_tls_check_interval_custom() {
205 let toml = r"
206 [serve]
207 tls_check_interval_secs = 1800
208 ";
209 let config: Config = toml::from_str(toml).unwrap();
210 assert_eq!(config.serve.tls_check_interval_secs, 1800);
211 }
212
213 #[test]
214 fn config_with_health_expect() {
215 let toml = r#"
216 [targets.mnw]
217 label = "MakeNotWork"
218 [targets.mnw.health]
219 url = "https://makenot.work/health"
220 [targets.mnw.health.expect]
221 status_code = 200
222 body_contains = "operational"
223 json_fields = { "status" = "operational", "checks.db" = "ok" }
224 "#;
225 let config: Config = toml::from_str(toml).unwrap();
226 let expect = config
227 .get_target("mnw")
228 .unwrap()
229 .health
230 .as_ref()
231 .unwrap()
232 .expect
233 .as_ref()
234 .unwrap();
235 assert_eq!(expect.status_code, Some(200));
236 assert_eq!(expect.body_contains.as_deref(), Some("operational"));
237 assert_eq!(expect.json_fields.get("status").unwrap(), "operational");
238 assert_eq!(expect.json_fields.get("checks.db").unwrap(), "ok");
239 }
240
241 #[test]
242 fn config_health_without_expect() {
243 let toml = r#"
244 [targets.mnw]
245 label = "MakeNotWork"
246 [targets.mnw.health]
247 url = "https://makenot.work/health"
248 "#;
249 let config: Config = toml::from_str(toml).unwrap();
250 assert!(
251 config
252 .get_target("mnw")
253 .unwrap()
254 .health
255 .as_ref()
256 .unwrap()
257 .expect
258 .is_none()
259 );
260 }
261
262 #[test]
263 fn config_with_trending() {
264 let toml = r#"
265 [targets.mnw]
266 label = "MakeNotWork"
267 [targets.mnw.health]
268 url = "https://makenot.work/health"
269 [targets.mnw.health.trending]
270 baseline_window_hours = 48
271 spike_threshold = 1.5
272 "#;
273 let config: Config = toml::from_str(toml).unwrap();
274 let trending = config
275 .get_target("mnw")
276 .unwrap()
277 .health
278 .as_ref()
279 .unwrap()
280 .trending
281 .as_ref()
282 .unwrap();
283 assert_eq!(trending.baseline_window_hours, 48);
284 assert!((trending.spike_threshold - 1.5).abs() < f64::EPSILON);
285 }
286
287 #[test]
288 fn config_trending_defaults() {
289 let toml = r#"
290 [targets.mnw]
291 label = "MakeNotWork"
292 [targets.mnw.health]
293 url = "https://makenot.work/health"
294 [targets.mnw.health.trending]
295 "#;
296 let config: Config = toml::from_str(toml).unwrap();
297 let trending = config
298 .get_target("mnw")
299 .unwrap()
300 .health
301 .as_ref()
302 .unwrap()
303 .trending
304 .as_ref()
305 .unwrap();
306 assert_eq!(trending.baseline_window_hours, 168);
307 assert!((trending.spike_threshold - 2.0).abs() < f64::EPSILON);
308 }
309
310 #[test]
311 fn config_without_trending() {
312 let toml = r#"
313 [targets.mnw]
314 label = "MakeNotWork"
315 [targets.mnw.health]
316 url = "https://makenot.work/health"
317 "#;
318 let config: Config = toml::from_str(toml).unwrap();
319 assert!(
320 config
321 .get_target("mnw")
322 .unwrap()
323 .health
324 .as_ref()
325 .unwrap()
326 .trending
327 .is_none()
328 );
329 }
330
331 #[test]
332 fn config_health_expect_empty() {
333 let toml = r#"
334 [targets.mnw]
335 label = "MakeNotWork"
336 [targets.mnw.health]
337 url = "https://makenot.work/health"
338 [targets.mnw.health.expect]
339 "#;
340 let config: Config = toml::from_str(toml).unwrap();
341 let expect = config
342 .get_target("mnw")
343 .unwrap()
344 .health
345 .as_ref()
346 .unwrap()
347 .expect
348 .as_ref()
349 .unwrap();
350 assert_eq!(expect.status_code, None);
351 assert!(expect.json_fields.is_empty());
352 assert_eq!(expect.body_contains, None);
353 }
354
355 #[test]
356 fn config_staleness_days_default() {
357 let toml = r#"
358 [targets.mnw]
359 label = "MakeNotWork"
360 [targets.mnw.tests]
361 ssh = "host"
362 command = "./ci.sh"
363 "#;
364 let config: Config = toml::from_str(toml).unwrap();
365 assert_eq!(
366 config
367 .get_target("mnw")
368 .unwrap()
369 .tests
370 .as_ref()
371 .unwrap()
372 .staleness_days,
373 7
374 );
375 }
376
377 #[test]
378 fn config_staleness_days_custom() {
379 let toml = r#"
380 [targets.mnw]
381 label = "MakeNotWork"
382 [targets.mnw.tests]
383 ssh = "host"
384 command = "./ci.sh"
385 staleness_days = 14
386 "#;
387 let config: Config = toml::from_str(toml).unwrap();
388 assert_eq!(
389 config
390 .get_target("mnw")
391 .unwrap()
392 .tests
393 .as_ref()
394 .unwrap()
395 .staleness_days,
396 14
397 );
398 }
399
400 #[test]
401 fn config_with_alerts_custom_defaults() {
402 let toml = r#"
403 [alerts]
404 to = "alerts@example.com"
405 from = "Custom <custom@example.com>"
406 cooldown_secs = 60
407 "#;
408 let config: Config = toml::from_str(toml).unwrap();
409 let alerts = config.alerts.unwrap();
410 assert!(alerts.postmark_token.is_none());
411 assert_eq!(alerts.from, "Custom <custom@example.com>");
412 assert_eq!(alerts.cooldown_secs, 60);
413 }
414
415 #[test]
416 fn config_expected_routes() {
417 let toml = r#"
418 [targets.mnw]
419 label = "MakeNotWork"
420 expected_routes = ["/", "/discover", "/login", "/docs"]
421 [targets.mnw.health]
422 url = "https://makenot.work/api/health"
423 "#;
424 let config: Config = toml::from_str(toml).unwrap();
425 let mnw = config.get_target("mnw").unwrap();
426 assert_eq!(
427 mnw.expected_routes,
428 vec!["/", "/discover", "/login", "/docs"]
429 );
430 }
431
432 #[test]
433 fn config_expected_routes_default_empty() {
434 let toml = r#"
435 [targets.mnw]
436 label = "MakeNotWork"
437 "#;
438 let config: Config = toml::from_str(toml).unwrap();
439 assert!(config.get_target("mnw").unwrap().expected_routes.is_empty());
440 }
441
442 #[test]
443 fn config_route_check_interval_default() {
444 let config: Config = toml::from_str("").unwrap();
445 assert_eq!(config.serve.route_check_interval_secs, 300);
446 }
447
448 #[test]
449 fn config_route_check_interval_custom() {
450 let toml = r"
451 [serve]
452 route_check_interval_secs = 600
453 ";
454 let config: Config = toml::from_str(toml).unwrap();
455 assert_eq!(config.serve.route_check_interval_secs, 600);
456 }
457
458 #[test]
459 fn config_dns_check_interval_default() {
460 let config: Config = toml::from_str("").unwrap();
461 assert_eq!(config.serve.dns_check_interval_secs, 3600);
462 }
463
464 #[test]
465 fn config_dns_check_interval_custom() {
466 let toml = r"
467 [serve]
468 dns_check_interval_secs = 1800
469 ";
470 let config: Config = toml::from_str(toml).unwrap();
471 assert_eq!(config.serve.dns_check_interval_secs, 1800);
472 }
473
474 #[test]
475 fn config_with_dns_records() {
476 let toml = r#"
477 [targets.mnw]
478 label = "MakeNotWork"
479
480 [[targets.mnw.dns]]
481 name = "makenot.work"
482 record_type = "A"
483 expected = ["5.78.144.244"]
484
485 [[targets.mnw.dns]]
486 name = "git.makenot.work"
487 record_type = "A"
488 expected = ["5.78.144.244"]
489 "#;
490 let config: Config = toml::from_str(toml).unwrap();
491 let mnw = config.get_target("mnw").unwrap();
492 assert_eq!(mnw.dns.len(), 2);
493 assert_eq!(mnw.dns[0].name, "makenot.work");
494 assert_eq!(mnw.dns[0].record_type, DnsRecordType::A);
495 assert_eq!(mnw.dns[0].expected, vec!["5.78.144.244"]);
496 assert_eq!(mnw.dns[1].name, "git.makenot.work");
497 }
498
499 #[test]
500 fn config_dns_default_empty() {
501 let toml = r#"
502 [targets.mnw]
503 label = "MakeNotWork"
504 "#;
505 let config: Config = toml::from_str(toml).unwrap();
506 assert!(config.get_target("mnw").unwrap().dns.is_empty());
507 }
508
509 #[test]
510 fn config_with_whois() {
511 let toml = r#"
512 [targets.mnw]
513 label = "MakeNotWork"
514
515 [targets.mnw.whois]
516 domain = "makenot.work"
517 warn_days = 60
518 "#;
519 let config: Config = toml::from_str(toml).unwrap();
520 let whois = config.get_target("mnw").unwrap().whois.as_ref().unwrap();
521 assert_eq!(whois.domain, "makenot.work");
522 assert_eq!(whois.warn_days, 60);
523 }
524
525 #[test]
526 fn config_whois_default_warn_days() {
527 let toml = r#"
528 [targets.mnw]
529 label = "MakeNotWork"
530
531 [targets.mnw.whois]
532 domain = "makenot.work"
533 "#;
534 let config: Config = toml::from_str(toml).unwrap();
535 let whois = config.get_target("mnw").unwrap().whois.as_ref().unwrap();
536 assert_eq!(whois.warn_days, 30);
537 }
538
539 #[test]
540 fn config_without_whois() {
541 let toml = r#"
542 [targets.mnw]
543 label = "MakeNotWork"
544 "#;
545 let config: Config = toml::from_str(toml).unwrap();
546 assert!(config.get_target("mnw").unwrap().whois.is_none());
547 }
548
549 #[test]
550 fn config_with_systemd() {
551 let toml = r#"
552 [targets.fw13]
553 label = "fw13 daemons"
554
555 [targets.fw13.systemd]
556 restart_threshold = 3
557 interval_secs = 30
558 check_failed = false
559
560 [[targets.fw13.systemd.units]]
561 name = "sandod.service"
562
563 [[targets.fw13.systemd.units]]
564 name = "bentod.service"
565 user = true
566 "#;
567 let config: Config = toml::from_str(toml).unwrap();
568 let sd = config.get_target("fw13").unwrap().systemd.as_ref().unwrap();
569 assert_eq!(sd.restart_threshold, 3);
570 assert_eq!(sd.interval_secs, 30);
571 assert!(!sd.check_failed);
572 assert_eq!(sd.units.len(), 2);
573 assert_eq!(sd.units[0].name, "sandod.service");
574 assert!(!sd.units[0].user, "system bus by default");
575 assert_eq!(sd.units[1].name, "bentod.service");
576 assert!(sd.units[1].user, "bentod is on the --user bus");
577 }
578
579 #[test]
580 fn config_systemd_defaults() {
581 let toml = r#"
582 [targets.fw13]
583 label = "fw13 daemons"
584 [targets.fw13.systemd]
585 [[targets.fw13.systemd.units]]
586 name = "pom.service"
587 "#;
588 let config: Config = toml::from_str(toml).unwrap();
589 let sd = config.get_target("fw13").unwrap().systemd.as_ref().unwrap();
590 assert!(sd.check_failed, "failed-unit sweep on by default");
591 assert_eq!(sd.restart_threshold, 5);
592 assert_eq!(sd.interval_secs, 60);
593 }
594
595 #[test]
596 fn config_without_systemd() {
597 let toml = r#"
598 [targets.mnw]
599 label = "MakeNotWork"
600 "#;
601 let config: Config = toml::from_str(toml).unwrap();
602 assert!(config.get_target("mnw").unwrap().systemd.is_none());
603 }
604
605 #[test]
606 fn defaults_systemd() {
607 assert!(default_systemd_check_failed(), "failed sweep on by default");
608 assert_eq!(default_systemd_restart_threshold(), 5);
609 assert_eq!(default_systemd_interval(), 60, "1-minute liveness cadence");
610 }
611
612 #[test]
613 fn config_dashboard_default_false() {
614 let config: Config = toml::from_str("").unwrap();
615 assert!(!config.serve.dashboard);
616 }
617
618 #[test]
619 fn config_dashboard_enabled() {
620 let toml = r"
621 [serve]
622 dashboard = true
623 ";
624 let config: Config = toml::from_str(toml).unwrap();
625 assert!(config.serve.dashboard);
626 }
627
628 #[test]
629 fn config_expected_routes_without_slash_detected() {
630 let toml = r#"
631 [targets.mnw]
632 label = "MakeNotWork"
633 expected_routes = ["discover", "/login"]
634 "#;
635 let config: Config = toml::from_str(toml).unwrap();
636 let bad_routes: Vec<_> = config
637 .get_target("mnw")
638 .unwrap()
639 .expected_routes
640 .iter()
641 .filter(|r| !r.starts_with('/'))
642 .collect();
643 assert_eq!(bad_routes, vec!["discover"]);
644 }
645
646 #[test]
647 fn config_whois_check_interval_default() {
648 let config: Config = toml::from_str("").unwrap();
649 assert_eq!(config.serve.whois_check_interval_secs, 86400);
650 }
651
652 #[test]
653 fn config_whois_check_interval_custom() {
654 let toml = r"
655 [serve]
656 whois_check_interval_secs = 43200
657 ";
658 let config: Config = toml::from_str(toml).unwrap();
659 assert_eq!(config.serve.whois_check_interval_secs, 43200);
660 }
661
662 // Defaults-pin tests, every `default_*` constant function is pinned to
663 // its expected value. Catches `replace fn -> u64 with 0/1` mutations and
664 // accidental drift when defaults are tweaked. These constants encode
665 // operational policy (check cadence, retention, etc.) so changes should
666 // be deliberate.
667
668 #[test]
669 fn defaults_numeric_intervals() {
670 assert_eq!(default_peer_heartbeat(), 60, "peer heartbeat = 1 min");
671 assert_eq!(default_tls_check_interval(), 3600, "tls = 1 hour");
672 assert_eq!(default_route_check_interval(), 300, "routes = 5 min");
673 assert_eq!(default_dns_check_interval(), 3600, "dns = 1 hour");
674 assert_eq!(default_cors_check_interval(), 3600, "cors = 1 hour");
675 assert_eq!(default_whois_check_interval(), 86400, "whois = 24 hours");
676 assert_eq!(default_serve_interval(), 300, "serve = 5 min");
677 assert_eq!(default_prune_days(), 30, "prune = 30 days");
678 }
679
680 #[test]
681 fn defaults_listen_address() {
682 assert_eq!(default_listen(), "127.0.0.1:9100");
683 }
684
685 #[test]
686 fn defaults_warn_thresholds() {
687 assert_eq!(default_whois_warn_days(), 30, "whois 30 days lead time");
688 assert_eq!(default_tls_warn_days(), 14, "tls 14 days lead time");
689 assert_eq!(default_tls_port(), 443);
690 }
691
692 #[test]
693 fn defaults_cors() {
694 assert_eq!(default_cors_method(), "PUT");
695 assert_eq!(default_max_age_hours(), 25, "25h allows cron drift");
696 }
697
698 #[test]
699 fn defaults_backup() {
700 assert_eq!(default_backup_interval(), 3600, "hourly backup check");
701 }
702
703 #[test]
704 fn defaults_ssh_banner() {
705 assert_eq!(default_ssh_banner_port(), 22);
706 assert_eq!(default_ssh_banner_timeout(), 5);
707 }
708
709 #[test]
710 fn defaults_latency_baseline() {
711 assert_eq!(default_baseline_window_hours(), 168, "7 days");
712 // Spike threshold compares as f64; pin with bit-exact match.
713 assert_eq!(default_spike_threshold().to_bits(), 2.0_f64.to_bits());
714 }
715
716 #[test]
717 fn defaults_health_and_test_timeouts() {
718 assert_eq!(default_health_timeout(), 10);
719 assert_eq!(default_test_timeout(), 600, "10-minute CI suite budget");
720 assert_eq!(default_staleness_days(), 7);
721 }
722
723 #[test]
724 fn defaults_alerts() {
725 assert_eq!(default_alert_from(), "PoM Alerts <pom-alerts@makenot.work>");
726 assert_eq!(default_cooldown_secs(), 300, "5-minute alert cooldown");
727 }
728
729 // Config method tests
730
731 #[test]
732 fn instance_name_returns_configured_value() {
733 let toml = r#"
734 [serve]
735 [instance]
736 name = "test-host"
737 [targets.x]
738 label = "X"
739 [targets.x.health]
740 url = "https://example.com"
741 "#;
742 let config: Config = toml::from_str(toml).unwrap();
743 assert_eq!(config.instance_name(), "test-host");
744 }
745
746 #[test]
747 fn instance_name_falls_back_to_non_empty() {
748 // When `name` is None, fall back to hostname or "unknown", must not be
749 // empty regardless. Catches the `instance_name -> String with "xyzzy"`
750 // mutant and the empty-string variant.
751 let toml = r#"
752 [serve]
753 [instance]
754 [targets.x]
755 label = "X"
756 [targets.x.health]
757 url = "https://example.com"
758 "#;
759 let config: Config = toml::from_str(toml).unwrap();
760 let name = config.instance_name();
761 assert!(!name.is_empty(), "fallback must produce a non-empty name");
762 // It also must not be the cargo-mutants sentinel.
763 assert_ne!(name, "xyzzy");
764 }
765
766 #[test]
767 fn default_config_path_ends_in_pom_toml() {
768 // The exact dir varies per OS, but the suffix is stable.
769 // Catches `default_config_path -> Ok(Default::default())` (which would
770 // return an empty PathBuf and fail the ends_with check).
771 let path = default_config_path().unwrap();
772 assert!(
773 path.ends_with("pom/pom.toml") || path.ends_with("pom\\pom.toml"),
774 "expected .../pom/pom.toml, got {path:?}"
775 );
776 }
777
778 #[test]
779 fn xdg_db_path_ends_in_pom_db() {
780 // Same rationale as default_config_path: the exact dir varies per OS,
781 // the suffix is stable.
782 let path = xdg_db_path().unwrap();
783 assert!(
784 path.ends_with("pom/pom.db") || path.ends_with("pom\\pom.db"),
785 "expected .../pom/pom.db, got {path:?}"
786 );
787 }
788
789 #[test]
790 fn configured_db_path_wins_over_the_environment() {
791 // The whole point of storage.db_path: the service unit and a hand-run
792 // CLI must resolve to the same file whatever XDG_DATA_HOME says.
793 let toml = r#"
794 [serve]
795 [storage]
796 db_path = "/var/lib/pom/pom.db"
797 "#;
798 let config: Config = toml::from_str(toml).unwrap();
799 let loc = config.db_path().unwrap();
800 assert_eq!(loc.path, PathBuf::from("/var/lib/pom/pom.db"));
801 assert_eq!(loc.source, DbPathSource::Config);
802 assert_eq!(loc.dir(), Path::new("/var/lib/pom"));
803 }
804
805 #[test]
806 fn unconfigured_db_path_falls_back_to_xdg() {
807 let toml = "[serve]\n";
808 let config: Config = toml::from_str(toml).unwrap();
809 let loc = config.db_path().unwrap();
810 assert_eq!(loc.path, xdg_db_path().unwrap());
811 assert_eq!(loc.source, DbPathSource::XdgDataHome);
812 }
813
814 #[test]
815 fn config_load_rejects_relative_db_path() {
816 let toml = r#"
817 [serve]
818 [storage]
819 db_path = "pom.db"
820 "#;
821 let dir = std::env::temp_dir().join(format!("pom-cfg-{}", std::process::id()));
822 std::fs::create_dir_all(&dir).unwrap();
823 let path = dir.join("relative-db.toml");
824 std::fs::write(&path, toml).unwrap();
825 let err = Config::load(Some(&path)).unwrap_err();
826 assert!(
827 err.to_string().contains("must be absolute"),
828 "expected an absolute-path complaint, got {err}"
829 );
830 std::fs::remove_file(&path).ok();
831 }
832
833 #[test]
834 fn config_load_rejects_route_without_leading_slash() {
835 // Catches `delete ! in Config::load` (L431): without the `!`, the
836 // validator would only reject routes that DO start with '/', wrong.
837 let toml = r#"
838 [serve]
839 [targets.bad]
840 label = "Bad"
841 expected_routes = ["no-leading-slash"]
842 [targets.bad.health]
843 url = "https://example.com"
844 "#;
845 let tmp = std::env::temp_dir().join(format!("pom_test_{}.toml", std::process::id()));
846 std::fs::write(&tmp, toml).unwrap();
847 let result = Config::load(Some(tmp.as_path()));
848 let _ = std::fs::remove_file(&tmp);
849 assert!(
850 matches!(result, Err(PomError::Config(_))),
851 "expected Config error rejecting bad route; got {result:?}"
852 );
853 }
854
855 #[test]
856 fn config_load_rejects_expected_routes_without_health() {
857 // expected_routes needs [health] for base-URL derivation, a target that
858 // lists routes but has no health config would silently never check them.
859 let toml = r#"
860 [serve]
861 [targets.noroutes]
862 label = "NoHealth"
863 expected_routes = ["/status"]
864 "#;
865 let tmp = std::env::temp_dir().join(format!("pom_test_nh_{}.toml", std::process::id()));
866 std::fs::write(&tmp, toml).unwrap();
867 let result = Config::load(Some(tmp.as_path()));
868 let _ = std::fs::remove_file(&tmp);
869 assert!(
870 matches!(result, Err(PomError::Config(_))),
871 "expected_routes without [health] must be rejected; got {result:?}"
872 );
873 }
874
875 #[test]
876 fn debug_redacts_secrets() {
877 let alerts = AlertConfig {
878 postmark_token: Some("super-secret-token".to_string()),
879 to: "a@b.c".to_string(),
880 from: "PoM".to_string(),
881 cooldown_secs: 300,
882 wam_url: None,
883 wam_token: Some("super-secret-wam-token".to_string()),
884 mnw_url: None,
885 alerts_ingest_token: Some("super-secret-ingest-token".to_string()),
886 };
887 let rendered = format!("{alerts:?}");
888 assert!(
889 !rendered.contains("super-secret-token"),
890 "token must be redacted in Debug"
891 );
892 assert!(
893 !rendered.contains("super-secret-wam-token"),
894 "wam token must be redacted in Debug"
895 );
896 assert!(
897 !rendered.contains("super-secret-ingest-token"),
898 "ingest token must be redacted in Debug"
899 );
900 assert!(rendered.contains("***"), "redaction marker expected");
901
902 let serve = ServeConfig {
903 api_token: Some("api-secret-xyz".to_string()),
904 ..ServeConfig::default()
905 };
906 assert!(
907 !format!("{serve:?}").contains("api-secret-xyz"),
908 "api_token must be redacted"
909 );
910 }
911