//! Tests for [`super`]. use super::*; #[test] fn parse_full_config() { let toml = r#" [serve] interval_secs = 120 listen = "127.0.0.1:9100" peer_heartbeat_secs = 30 [instance] name = "hetzner" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" timeout_secs = 5 [targets.mnw.tests] ssh = "hetzner" command = "cd /srv/mnw && ./ci.sh" [peers.astra] address = "100.0.0.1:9100" on_missing = "alert" grace_count = 5 "#; let config: Config = toml::from_str(toml).unwrap(); assert_eq!(config.serve.interval_secs, 120); assert_eq!(config.serve.listen, "127.0.0.1:9100"); assert_eq!(config.serve.peer_heartbeat_secs, 30); assert_eq!(config.instance.name.as_deref(), Some("hetzner")); assert_eq!(config.target_names(), vec!["mnw"]); let mnw = config.get_target("mnw").unwrap(); assert_eq!(mnw.label, "MakeNotWork"); assert_eq!(mnw.health.as_ref().unwrap().timeout_secs, 5); assert_eq!(mnw.tests.as_ref().unwrap().ssh.as_deref(), Some("hetzner")); let astra = config.peers.get("astra").unwrap(); assert_eq!(astra.address, "100.0.0.1:9100"); assert_eq!(astra.on_missing, OnMissing::Alert); assert_eq!(astra.grace_count, Some(5)); } #[test] fn empty_config_uses_defaults() { let config: Config = toml::from_str("").unwrap(); assert_eq!(config.serve.interval_secs, 300); assert_eq!(config.serve.prune_days, 30); assert_eq!(config.serve.listen, "127.0.0.1:9100"); assert_eq!(config.serve.peer_heartbeat_secs, 60); assert!(config.targets.is_empty()); assert!(config.peers.is_empty()); assert!(config.instance.name.is_none()); } #[test] fn peer_on_missing_defaults_to_log() { let toml = r#" [peers.test] address = "10.0.0.1:9100" "#; let config: Config = toml::from_str(toml).unwrap(); let peer = config.peers.get("test").unwrap(); assert_eq!(peer.on_missing, OnMissing::Log); assert_eq!(peer.grace_count, None); assert!(peer.token.is_none()); } #[test] fn peer_with_token() { let toml = r#" [peers.test] address = "10.0.0.1:9100" token = "peer-secret-123" "#; let config: Config = toml::from_str(toml).unwrap(); let peer = config.peers.get("test").unwrap(); assert_eq!(peer.token.as_deref(), Some("peer-secret-123")); } #[test] fn serve_api_token_from_config() { let toml = r#" [serve] api_token = "my-api-secret" "#; let config: Config = toml::from_str(toml).unwrap(); assert_eq!(config.serve.api_token.as_deref(), Some("my-api-secret")); } #[test] fn serve_api_token_defaults_to_none() { let config: Config = toml::from_str("").unwrap(); assert!(config.serve.api_token.is_none()); } #[test] fn instance_name_falls_back_to_hostname() { let config: Config = toml::from_str("").unwrap(); let name = config.instance_name(); assert!(!name.is_empty()); } #[test] fn config_without_alerts_section() { let config: Config = toml::from_str("").unwrap(); assert!(config.alerts.is_none()); } #[test] fn config_with_alerts_section() { let toml = r#" [alerts] postmark_token = "test-token" to = "alerts@example.com" "#; let config: Config = toml::from_str(toml).unwrap(); let alerts = config.alerts.unwrap(); assert_eq!(alerts.postmark_token.as_deref(), Some("test-token")); assert_eq!(alerts.to, "alerts@example.com"); assert_eq!(alerts.from, "PoM Alerts "); assert_eq!(alerts.cooldown_secs, 300); } #[test] fn config_alerts_wam_token() { let toml = r#" [alerts] to = "alerts@example.com" wam_url = "http://wam.tailnet:9000" wam_token = "test-wam-token" "#; let config: Config = toml::from_str(toml).unwrap(); let alerts = config.alerts.unwrap(); assert_eq!(alerts.wam_url.as_deref(), Some("http://wam.tailnet:9000")); assert_eq!(alerts.wam_token.as_deref(), Some("test-wam-token")); } #[test] fn config_alerts_wam_token_defaults_to_none() { let toml = r#" [alerts] to = "alerts@example.com" wam_url = "http://wam.tailnet:9000" "#; let config: Config = toml::from_str(toml).unwrap(); assert!(config.alerts.unwrap().wam_token.is_none()); } #[test] fn config_with_tls() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.tls] host = "makenot.work" port = 8443 warn_days = 30 "#; let config: Config = toml::from_str(toml).unwrap(); let mnw = config.get_target("mnw").unwrap(); let tls = mnw.tls.as_ref().unwrap(); assert_eq!(tls.host, "makenot.work"); assert_eq!(tls.port, 8443); assert_eq!(tls.warn_days, 30); } #[test] fn config_tls_defaults() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.tls] host = "makenot.work" "#; let config: Config = toml::from_str(toml).unwrap(); let tls = config.get_target("mnw").unwrap().tls.as_ref().unwrap(); assert_eq!(tls.port, 443); assert_eq!(tls.warn_days, 14); } #[test] fn config_without_tls() { let toml = r#" [targets.mnw] label = "MakeNotWork" "#; let config: Config = toml::from_str(toml).unwrap(); assert!(config.get_target("mnw").unwrap().tls.is_none()); } #[test] fn config_tls_check_interval_default() { let config: Config = toml::from_str("").unwrap(); assert_eq!(config.serve.tls_check_interval_secs, 3600); } #[test] fn config_tls_check_interval_custom() { let toml = r" [serve] tls_check_interval_secs = 1800 "; let config: Config = toml::from_str(toml).unwrap(); assert_eq!(config.serve.tls_check_interval_secs, 1800); } #[test] fn config_with_health_expect() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" [targets.mnw.health.expect] status_code = 200 body_contains = "operational" json_fields = { "status" = "operational", "checks.db" = "ok" } "#; let config: Config = toml::from_str(toml).unwrap(); let expect = config .get_target("mnw") .unwrap() .health .as_ref() .unwrap() .expect .as_ref() .unwrap(); assert_eq!(expect.status_code, Some(200)); assert_eq!(expect.body_contains.as_deref(), Some("operational")); assert_eq!(expect.json_fields.get("status").unwrap(), "operational"); assert_eq!(expect.json_fields.get("checks.db").unwrap(), "ok"); } #[test] fn config_health_without_expect() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" "#; let config: Config = toml::from_str(toml).unwrap(); assert!( config .get_target("mnw") .unwrap() .health .as_ref() .unwrap() .expect .is_none() ); } #[test] fn config_with_trending() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" [targets.mnw.health.trending] baseline_window_hours = 48 spike_threshold = 1.5 "#; let config: Config = toml::from_str(toml).unwrap(); let trending = config .get_target("mnw") .unwrap() .health .as_ref() .unwrap() .trending .as_ref() .unwrap(); assert_eq!(trending.baseline_window_hours, 48); assert!((trending.spike_threshold - 1.5).abs() < f64::EPSILON); } #[test] fn config_trending_defaults() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" [targets.mnw.health.trending] "#; let config: Config = toml::from_str(toml).unwrap(); let trending = config .get_target("mnw") .unwrap() .health .as_ref() .unwrap() .trending .as_ref() .unwrap(); assert_eq!(trending.baseline_window_hours, 168); assert!((trending.spike_threshold - 2.0).abs() < f64::EPSILON); } #[test] fn config_without_trending() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" "#; let config: Config = toml::from_str(toml).unwrap(); assert!( config .get_target("mnw") .unwrap() .health .as_ref() .unwrap() .trending .is_none() ); } #[test] fn config_health_expect_empty() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" [targets.mnw.health.expect] "#; let config: Config = toml::from_str(toml).unwrap(); let expect = config .get_target("mnw") .unwrap() .health .as_ref() .unwrap() .expect .as_ref() .unwrap(); assert_eq!(expect.status_code, None); assert!(expect.json_fields.is_empty()); assert_eq!(expect.body_contains, None); } #[test] fn config_staleness_days_default() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.tests] ssh = "host" command = "./ci.sh" "#; let config: Config = toml::from_str(toml).unwrap(); assert_eq!( config .get_target("mnw") .unwrap() .tests .as_ref() .unwrap() .staleness_days, 7 ); } #[test] fn config_staleness_days_custom() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.tests] ssh = "host" command = "./ci.sh" staleness_days = 14 "#; let config: Config = toml::from_str(toml).unwrap(); assert_eq!( config .get_target("mnw") .unwrap() .tests .as_ref() .unwrap() .staleness_days, 14 ); } #[test] fn config_with_alerts_custom_defaults() { let toml = r#" [alerts] to = "alerts@example.com" from = "Custom " cooldown_secs = 60 "#; let config: Config = toml::from_str(toml).unwrap(); let alerts = config.alerts.unwrap(); assert!(alerts.postmark_token.is_none()); assert_eq!(alerts.from, "Custom "); assert_eq!(alerts.cooldown_secs, 60); } #[test] fn config_expected_routes() { let toml = r#" [targets.mnw] label = "MakeNotWork" expected_routes = ["/", "/discover", "/login", "/docs"] [targets.mnw.health] url = "https://makenot.work/api/health" "#; let config: Config = toml::from_str(toml).unwrap(); let mnw = config.get_target("mnw").unwrap(); assert_eq!( mnw.expected_routes, vec!["/", "/discover", "/login", "/docs"] ); } #[test] fn config_expected_routes_default_empty() { let toml = r#" [targets.mnw] label = "MakeNotWork" "#; let config: Config = toml::from_str(toml).unwrap(); assert!(config.get_target("mnw").unwrap().expected_routes.is_empty()); } #[test] fn config_route_check_interval_default() { let config: Config = toml::from_str("").unwrap(); assert_eq!(config.serve.route_check_interval_secs, 300); } #[test] fn config_route_check_interval_custom() { let toml = r" [serve] route_check_interval_secs = 600 "; let config: Config = toml::from_str(toml).unwrap(); assert_eq!(config.serve.route_check_interval_secs, 600); } #[test] fn config_dns_check_interval_default() { let config: Config = toml::from_str("").unwrap(); assert_eq!(config.serve.dns_check_interval_secs, 3600); } #[test] fn config_dns_check_interval_custom() { let toml = r" [serve] dns_check_interval_secs = 1800 "; let config: Config = toml::from_str(toml).unwrap(); assert_eq!(config.serve.dns_check_interval_secs, 1800); } #[test] fn config_with_dns_records() { let toml = r#" [targets.mnw] label = "MakeNotWork" [[targets.mnw.dns]] name = "makenot.work" record_type = "A" expected = ["5.78.144.244"] [[targets.mnw.dns]] name = "git.makenot.work" record_type = "A" expected = ["5.78.144.244"] "#; let config: Config = toml::from_str(toml).unwrap(); let mnw = config.get_target("mnw").unwrap(); assert_eq!(mnw.dns.len(), 2); assert_eq!(mnw.dns[0].name, "makenot.work"); assert_eq!(mnw.dns[0].record_type, DnsRecordType::A); assert_eq!(mnw.dns[0].expected, vec!["5.78.144.244"]); assert_eq!(mnw.dns[1].name, "git.makenot.work"); } #[test] fn config_dns_default_empty() { let toml = r#" [targets.mnw] label = "MakeNotWork" "#; let config: Config = toml::from_str(toml).unwrap(); assert!(config.get_target("mnw").unwrap().dns.is_empty()); } #[test] fn config_with_whois() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.whois] domain = "makenot.work" warn_days = 60 "#; let config: Config = toml::from_str(toml).unwrap(); let whois = config.get_target("mnw").unwrap().whois.as_ref().unwrap(); assert_eq!(whois.domain, "makenot.work"); assert_eq!(whois.warn_days, 60); } #[test] fn config_whois_default_warn_days() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.whois] domain = "makenot.work" "#; let config: Config = toml::from_str(toml).unwrap(); let whois = config.get_target("mnw").unwrap().whois.as_ref().unwrap(); assert_eq!(whois.warn_days, 30); } #[test] fn config_without_whois() { let toml = r#" [targets.mnw] label = "MakeNotWork" "#; let config: Config = toml::from_str(toml).unwrap(); assert!(config.get_target("mnw").unwrap().whois.is_none()); } #[test] fn config_with_systemd() { let toml = r#" [targets.fw13] label = "fw13 daemons" [targets.fw13.systemd] restart_threshold = 3 interval_secs = 30 check_failed = false [[targets.fw13.systemd.units]] name = "sandod.service" [[targets.fw13.systemd.units]] name = "bentod.service" user = true "#; let config: Config = toml::from_str(toml).unwrap(); let sd = config.get_target("fw13").unwrap().systemd.as_ref().unwrap(); assert_eq!(sd.restart_threshold, 3); assert_eq!(sd.interval_secs, 30); assert!(!sd.check_failed); assert_eq!(sd.units.len(), 2); assert_eq!(sd.units[0].name, "sandod.service"); assert!(!sd.units[0].user, "system bus by default"); assert_eq!(sd.units[1].name, "bentod.service"); assert!(sd.units[1].user, "bentod is on the --user bus"); } #[test] fn config_systemd_defaults() { let toml = r#" [targets.fw13] label = "fw13 daemons" [targets.fw13.systemd] [[targets.fw13.systemd.units]] name = "pom.service" "#; let config: Config = toml::from_str(toml).unwrap(); let sd = config.get_target("fw13").unwrap().systemd.as_ref().unwrap(); assert!(sd.check_failed, "failed-unit sweep on by default"); assert_eq!(sd.restart_threshold, 5); assert_eq!(sd.interval_secs, 60); } #[test] fn config_without_systemd() { let toml = r#" [targets.mnw] label = "MakeNotWork" "#; let config: Config = toml::from_str(toml).unwrap(); assert!(config.get_target("mnw").unwrap().systemd.is_none()); } #[test] fn defaults_systemd() { assert!(default_systemd_check_failed(), "failed sweep on by default"); assert_eq!(default_systemd_restart_threshold(), 5); assert_eq!(default_systemd_interval(), 60, "1-minute liveness cadence"); } #[test] fn config_dashboard_default_false() { let config: Config = toml::from_str("").unwrap(); assert!(!config.serve.dashboard); } #[test] fn config_dashboard_enabled() { let toml = r" [serve] dashboard = true "; let config: Config = toml::from_str(toml).unwrap(); assert!(config.serve.dashboard); } #[test] fn config_expected_routes_without_slash_detected() { let toml = r#" [targets.mnw] label = "MakeNotWork" expected_routes = ["discover", "/login"] "#; let config: Config = toml::from_str(toml).unwrap(); let bad_routes: Vec<_> = config .get_target("mnw") .unwrap() .expected_routes .iter() .filter(|r| !r.starts_with('/')) .collect(); assert_eq!(bad_routes, vec!["discover"]); } #[test] fn config_whois_check_interval_default() { let config: Config = toml::from_str("").unwrap(); assert_eq!(config.serve.whois_check_interval_secs, 86400); } #[test] fn config_whois_check_interval_custom() { let toml = r" [serve] whois_check_interval_secs = 43200 "; let config: Config = toml::from_str(toml).unwrap(); assert_eq!(config.serve.whois_check_interval_secs, 43200); } // Defaults-pin tests, every `default_*` constant function is pinned to // its expected value. Catches `replace fn -> u64 with 0/1` mutations and // accidental drift when defaults are tweaked. These constants encode // operational policy (check cadence, retention, etc.) so changes should // be deliberate. #[test] fn defaults_numeric_intervals() { assert_eq!(default_peer_heartbeat(), 60, "peer heartbeat = 1 min"); assert_eq!(default_tls_check_interval(), 3600, "tls = 1 hour"); assert_eq!(default_route_check_interval(), 300, "routes = 5 min"); assert_eq!(default_dns_check_interval(), 3600, "dns = 1 hour"); assert_eq!(default_cors_check_interval(), 3600, "cors = 1 hour"); assert_eq!(default_whois_check_interval(), 86400, "whois = 24 hours"); assert_eq!(default_serve_interval(), 300, "serve = 5 min"); assert_eq!(default_prune_days(), 30, "prune = 30 days"); } #[test] fn defaults_listen_address() { assert_eq!(default_listen(), "127.0.0.1:9100"); } #[test] fn defaults_warn_thresholds() { assert_eq!(default_whois_warn_days(), 30, "whois 30 days lead time"); assert_eq!(default_tls_warn_days(), 14, "tls 14 days lead time"); assert_eq!(default_tls_port(), 443); } #[test] fn defaults_cors() { assert_eq!(default_cors_method(), "PUT"); assert_eq!(default_max_age_hours(), 25, "25h allows cron drift"); } #[test] fn defaults_backup() { assert_eq!(default_backup_interval(), 3600, "hourly backup check"); } #[test] fn defaults_ssh_banner() { assert_eq!(default_ssh_banner_port(), 22); assert_eq!(default_ssh_banner_timeout(), 5); } #[test] fn defaults_latency_baseline() { assert_eq!(default_baseline_window_hours(), 168, "7 days"); // Spike threshold compares as f64; pin with bit-exact match. assert_eq!(default_spike_threshold().to_bits(), 2.0_f64.to_bits()); } #[test] fn defaults_health_and_test_timeouts() { assert_eq!(default_health_timeout(), 10); assert_eq!(default_test_timeout(), 600, "10-minute CI suite budget"); assert_eq!(default_staleness_days(), 7); } #[test] fn defaults_alerts() { assert_eq!(default_alert_from(), "PoM Alerts "); assert_eq!(default_cooldown_secs(), 300, "5-minute alert cooldown"); } // Config method tests #[test] fn instance_name_returns_configured_value() { let toml = r#" [serve] [instance] name = "test-host" [targets.x] label = "X" [targets.x.health] url = "https://example.com" "#; let config: Config = toml::from_str(toml).unwrap(); assert_eq!(config.instance_name(), "test-host"); } #[test] fn instance_name_falls_back_to_non_empty() { // When `name` is None, fall back to hostname or "unknown", must not be // empty regardless. Catches the `instance_name -> String with "xyzzy"` // mutant and the empty-string variant. let toml = r#" [serve] [instance] [targets.x] label = "X" [targets.x.health] url = "https://example.com" "#; let config: Config = toml::from_str(toml).unwrap(); let name = config.instance_name(); assert!(!name.is_empty(), "fallback must produce a non-empty name"); // It also must not be the cargo-mutants sentinel. assert_ne!(name, "xyzzy"); } #[test] fn default_config_path_ends_in_pom_toml() { // The exact dir varies per OS, but the suffix is stable. // Catches `default_config_path -> Ok(Default::default())` (which would // return an empty PathBuf and fail the ends_with check). let path = default_config_path().unwrap(); assert!( path.ends_with("pom/pom.toml") || path.ends_with("pom\\pom.toml"), "expected .../pom/pom.toml, got {path:?}" ); } #[test] fn xdg_db_path_ends_in_pom_db() { // Same rationale as default_config_path: the exact dir varies per OS, // the suffix is stable. let path = xdg_db_path().unwrap(); assert!( path.ends_with("pom/pom.db") || path.ends_with("pom\\pom.db"), "expected .../pom/pom.db, got {path:?}" ); } #[test] fn configured_db_path_wins_over_the_environment() { // The whole point of storage.db_path: the service unit and a hand-run // CLI must resolve to the same file whatever XDG_DATA_HOME says. let toml = r#" [serve] [storage] db_path = "/var/lib/pom/pom.db" "#; let config: Config = toml::from_str(toml).unwrap(); let loc = config.db_path().unwrap(); assert_eq!(loc.path, PathBuf::from("/var/lib/pom/pom.db")); assert_eq!(loc.source, DbPathSource::Config); assert_eq!(loc.dir(), Path::new("/var/lib/pom")); } #[test] fn unconfigured_db_path_falls_back_to_xdg() { let toml = "[serve]\n"; let config: Config = toml::from_str(toml).unwrap(); let loc = config.db_path().unwrap(); assert_eq!(loc.path, xdg_db_path().unwrap()); assert_eq!(loc.source, DbPathSource::XdgDataHome); } #[test] fn config_load_rejects_relative_db_path() { let toml = r#" [serve] [storage] db_path = "pom.db" "#; let dir = std::env::temp_dir().join(format!("pom-cfg-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("relative-db.toml"); std::fs::write(&path, toml).unwrap(); let err = Config::load(Some(&path)).unwrap_err(); assert!( err.to_string().contains("must be absolute"), "expected an absolute-path complaint, got {err}" ); std::fs::remove_file(&path).ok(); } #[test] fn config_load_rejects_route_without_leading_slash() { // Catches `delete ! in Config::load` (L431): without the `!`, the // validator would only reject routes that DO start with '/', wrong. let toml = r#" [serve] [targets.bad] label = "Bad" expected_routes = ["no-leading-slash"] [targets.bad.health] url = "https://example.com" "#; let tmp = std::env::temp_dir().join(format!("pom_test_{}.toml", std::process::id())); std::fs::write(&tmp, toml).unwrap(); let result = Config::load(Some(tmp.as_path())); let _ = std::fs::remove_file(&tmp); assert!( matches!(result, Err(PomError::Config(_))), "expected Config error rejecting bad route; got {result:?}" ); } #[test] fn config_load_rejects_expected_routes_without_health() { // expected_routes needs [health] for base-URL derivation, a target that // lists routes but has no health config would silently never check them. let toml = r#" [serve] [targets.noroutes] label = "NoHealth" expected_routes = ["/status"] "#; let tmp = std::env::temp_dir().join(format!("pom_test_nh_{}.toml", std::process::id())); std::fs::write(&tmp, toml).unwrap(); let result = Config::load(Some(tmp.as_path())); let _ = std::fs::remove_file(&tmp); assert!( matches!(result, Err(PomError::Config(_))), "expected_routes without [health] must be rejected; got {result:?}" ); } #[test] fn debug_redacts_secrets() { let alerts = AlertConfig { postmark_token: Some("super-secret-token".to_string()), to: "a@b.c".to_string(), from: "PoM".to_string(), cooldown_secs: 300, wam_url: None, wam_token: Some("super-secret-wam-token".to_string()), mnw_url: None, alerts_ingest_token: Some("super-secret-ingest-token".to_string()), }; let rendered = format!("{alerts:?}"); assert!( !rendered.contains("super-secret-token"), "token must be redacted in Debug" ); assert!( !rendered.contains("super-secret-wam-token"), "wam token must be redacted in Debug" ); assert!( !rendered.contains("super-secret-ingest-token"), "ingest token must be redacted in Debug" ); assert!(rendered.contains("***"), "redaction marker expected"); let serve = ServeConfig { api_token: Some("api-secret-xyz".to_string()), ..ServeConfig::default() }; assert!( !format!("{serve:?}").contains("api-secret-xyz"), "api_token must be redacted" ); }