#[cfg(feature = "prometheus-exporter")]
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
use crate::clock::ClockSource;
use super::types::{
Config, ConfigError, DEFAULT_PROM_RATE_LIMIT_BURST, DEFAULT_PROM_RATE_LIMIT_PER_SEC,
DEFAULT_SHUTDOWN_GRACE_MS, MIN_SHUTDOWN_GRACE_MS, MIN_THRESHOLD_MS,
};
use super::validate::{parse_exec_cmd, validate_secret_file};
fn args(toks: &[&str]) -> Vec<String> {
toks.iter().map(|s| s.to_string()).collect()
}
#[test]
fn parses_minimal_required_flags() {
let cfg = Config::from_args(args(&["--socket", "/tmp/x.sock", "--threshold-ms", "250"]))
.expect("parse");
assert_eq!(cfg.socket, PathBuf::from("/tmp/x.sock"));
assert_eq!(cfg.threshold, Duration::from_millis(250));
assert_eq!(cfg.recovery_debounce, Duration::from_millis(1000));
assert_eq!(cfg.socket_mode, 0o600);
assert!(cfg.recovery_exec_cmd.is_none());
assert!(cfg.prom_addr.is_none());
}
#[test]
fn recovery_cmd_flag_is_rejected_as_removed() {
match Config::from_args(args(&[
"--socket",
"/tmp/x.sock",
"--threshold-ms",
"100",
"--recovery-cmd",
"foo",
])) {
Err(ConfigError::RemovedFlag { flag, replacement }) => {
assert_eq!(flag, "--recovery-cmd");
assert!(
replacement.contains("--recovery-exec"),
"replacement hint must mention --recovery-exec, got: {replacement}"
);
}
other => panic!("expected RemovedFlag for --recovery-cmd, got {other:?}"),
}
}
#[test]
fn i_accept_shell_risk_flag_is_rejected_as_removed() {
match Config::from_args(args(&[
"--socket",
"/tmp/x.sock",
"--threshold-ms",
"100",
"--i-accept-shell-risk",
])) {
Err(ConfigError::RemovedFlag { .. }) => {}
other => panic!("expected RemovedFlag for --i-accept-shell-risk, got {other:?}"),
}
}
#[cfg(feature = "prometheus-exporter")]
#[test]
fn parses_full_flag_surface() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--recovery-exec",
"echo {pid}",
"--recovery-debounce-ms",
"750",
"--export-file",
"/tmp/e.log",
"--prom-addr",
"127.0.0.1:9090",
"--prom-token-file",
"/tmp/varta-prom.token",
"--shutdown-after-secs",
"3",
]))
.expect("parse");
assert_eq!(cfg.recovery_exec_cmd.as_deref(), Some("echo {pid}"));
assert_eq!(cfg.recovery_debounce, Duration::from_millis(750));
assert_eq!(cfg.file_export, Some(PathBuf::from("/tmp/e.log")));
assert_eq!(
cfg.prom_addr,
Some("127.0.0.1:9090".parse::<SocketAddr>().unwrap())
);
assert_eq!(
cfg.prom_token_file,
Some(PathBuf::from("/tmp/varta-prom.token"))
);
assert_eq!(cfg.shutdown_after, Some(Duration::from_secs(3)));
}
#[test]
fn help_returns_help_requested() {
match Config::from_args(args(&["--help"])) {
Err(ConfigError::HelpRequested) => {}
other => panic!("expected HelpRequested, got {other:?}"),
}
}
#[test]
fn unknown_flag_is_rejected() {
match Config::from_args(args(&["--nope"])) {
Err(ConfigError::UnknownFlag(s)) => assert_eq!(s, "--nope"),
other => panic!("expected UnknownFlag, got {other:?}"),
}
}
#[test]
fn missing_required_socket_is_rejected() {
match Config::from_args(args(&["--threshold-ms", "100"])) {
Err(ConfigError::MissingRequired("--socket")) => {}
other => panic!("expected MissingRequired(--socket), got {other:?}"),
}
}
#[test]
fn catalogue_covers_help_text() {
use super::flag_catalogue::{FlagKind, FLAGS};
const EXEMPT_FROM_HELP: &[&str] = &["--inject-wedge-ms"];
for spec in FLAGS {
if spec.cli.is_empty() {
continue;
}
if EXEMPT_FROM_HELP.contains(&spec.cli) {
continue;
}
let skip = match spec.feature {
"prometheus-exporter" => !cfg!(feature = "prometheus-exporter"),
"unsafe-plaintext-udp" => !cfg!(feature = "unsafe-plaintext-udp"),
"secure-udp" => !cfg!(feature = "secure-udp"),
"test-hooks" => true, _ => false,
};
if skip {
continue;
}
if matches!(spec.kind, FlagKind::Bool) {
let prefix = spec.cli;
assert!(
Config::HELP.contains(prefix),
"Config::HELP missing bool flag {prefix}"
);
} else {
assert!(
Config::HELP.contains(spec.cli),
"Config::HELP missing flag {}",
spec.cli
);
}
}
assert!(
Config::HELP.contains("--help"),
"Config::HELP missing --help"
);
}
#[test]
fn catalogue_has_no_duplicate_cli_names() {
use super::flag_catalogue::FLAGS;
let mut seen: Vec<&'static str> = Vec::new();
for spec in FLAGS {
if spec.cli.is_empty() {
continue;
}
assert!(
!seen.contains(&spec.cli),
"duplicate cli name in FLAGS: {}",
spec.cli
);
seen.push(spec.cli);
}
}
#[test]
fn catalogue_has_no_duplicate_keys() {
use super::flag_catalogue::FLAGS;
let mut seen: Vec<&'static str> = Vec::new();
for spec in FLAGS {
if spec.key.is_empty() {
continue;
}
assert!(
!seen.contains(&spec.key),
"duplicate key in FLAGS: {}",
spec.key
);
seen.push(spec.key);
}
}
#[test]
fn parses_recovery_timeout_ms() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--recovery-timeout-ms",
"2500",
]))
.expect("parse");
assert_eq!(cfg.recovery_timeout, Some(Duration::from_millis(2500)));
}
#[test]
fn recovery_timeout_omitted_is_none() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
assert!(cfg.recovery_timeout.is_none());
}
#[test]
fn parses_socket_mode_octal() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--socket-mode",
"660",
]))
.expect("parse");
assert_eq!(cfg.socket_mode, 0o660);
}
#[test]
fn socket_mode_rejects_non_octal() {
match Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--socket-mode",
"999",
])) {
Err(ConfigError::BadSocketMode(_)) => {}
other => panic!("expected BadSocketMode, got {other:?}"),
}
}
#[test]
fn socket_mode_accepts_0o_prefix() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--socket-mode",
"0o640",
]))
.expect("parse");
assert_eq!(cfg.socket_mode, 0o640);
}
#[test]
fn socket_mode_accepts_uppercase_0o_prefix() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--socket-mode",
"0O640",
]))
.expect("parse");
assert_eq!(cfg.socket_mode, 0o640);
}
#[test]
fn socket_mode_accepts_leading_zero() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--socket-mode",
"0644",
]))
.expect("parse");
assert_eq!(cfg.socket_mode, 0o644);
}
#[test]
fn socket_mode_rejects_empty_after_prefix() {
match Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--socket-mode",
"0o",
])) {
Err(ConfigError::BadSocketMode(raw)) => assert_eq!(raw, "0o"),
other => panic!("expected BadSocketMode, got {other:?}"),
}
}
#[cfg(feature = "prometheus-exporter")]
#[test]
fn prom_addr_without_token_file_is_rejected() {
match Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--prom-addr",
"127.0.0.1:9100",
])) {
Err(ConfigError::PromAddrRequiresToken) => {}
other => panic!("expected PromAddrRequiresToken, got {other:?}"),
}
}
#[cfg(feature = "prometheus-exporter")]
#[test]
fn prom_token_file_without_prom_addr_is_rejected() {
match Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--prom-token-file",
"/dev/null",
])) {
Err(ConfigError::MutuallyExclusive { a, b: _ }) => {
assert_eq!(a, "--prom-token-file");
}
other => panic!("expected MutuallyExclusive(--prom-token-file, ..), got {other:?}"),
}
}
#[test]
fn parses_shutdown_grace_ms() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--shutdown-grace-ms",
"1500",
]))
.expect("parse");
assert_eq!(cfg.shutdown_grace, Duration::from_millis(1500));
}
#[test]
fn shutdown_grace_omitted_is_default() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
assert_eq!(
cfg.shutdown_grace,
Duration::from_millis(DEFAULT_SHUTDOWN_GRACE_MS)
);
}
#[test]
fn shutdown_grace_below_minimum_is_rejected() {
match Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--shutdown-grace-ms",
"50",
])) {
Err(ConfigError::ShutdownGraceTooLow { value, min }) => {
assert_eq!(value, 50);
assert_eq!(min, MIN_SHUTDOWN_GRACE_MS);
}
other => panic!("expected ShutdownGraceTooLow, got {other:?}"),
}
}
#[test]
fn key_env_flag_returns_removed_flag_error() {
match Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--key-env",
"VARTA_KEY",
])) {
Err(ConfigError::RemovedFlag { flag, replacement }) => {
assert_eq!(flag, "--key-env");
assert!(replacement.contains("--key-file"));
}
other => panic!("expected RemovedFlag(--key-env, ..), got {other:?}"),
}
}
#[test]
fn parses_read_timeout_ms() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--read-timeout-ms",
"50",
]))
.expect("parse");
assert_eq!(cfg.read_timeout, Duration::from_millis(50));
}
#[test]
fn read_timeout_omitted_is_default() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
assert_eq!(cfg.read_timeout, Duration::from_millis(100));
}
#[test]
fn read_timeout_rejects_non_numeric() {
match Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--read-timeout-ms",
"abc",
])) {
Err(ConfigError::BadInteger { flag, .. }) => assert_eq!(flag, "--read-timeout-ms"),
other => panic!("expected BadInteger, got {other:?}"),
}
}
#[test]
fn threshold_zero_is_rejected() {
match Config::from_args(args(&["--socket", "/s", "--threshold-ms", "0"])) {
Err(ConfigError::ThresholdTooLow { value, min }) => {
assert_eq!(value, 0);
assert_eq!(min, MIN_THRESHOLD_MS);
}
other => panic!("expected ThresholdTooLow, got {other:?}"),
}
}
#[test]
fn threshold_below_min_is_rejected() {
match Config::from_args(args(&["--socket", "/s", "--threshold-ms", "5"])) {
Err(ConfigError::ThresholdTooLow { value, .. }) => assert_eq!(value, 5),
other => panic!("expected ThresholdTooLow, got {other:?}"),
}
}
#[test]
fn threshold_at_min_is_accepted() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
&MIN_THRESHOLD_MS.to_string(),
]))
.expect("parse");
assert_eq!(cfg.threshold, Duration::from_millis(MIN_THRESHOLD_MS));
}
#[test]
fn parses_recovery_exec_cmd() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--recovery-exec",
"/usr/bin/kill -HUP {pid}",
]))
.expect("parse");
assert!(cfg.recovery_exec_cmd.is_some());
let mode = cfg.resolve_recovery_mode().expect("resolve").expect("some");
#[allow(unreachable_patterns)]
match mode {
crate::recovery::RecoveryMode::Exec { program, args } => {
assert_eq!(program, "/usr/bin/kill");
assert_eq!(args, vec!["-HUP", "{pid}"]);
}
other => panic!("expected Exec mode, got {other:?}"),
}
}
#[test]
fn recovery_cmd_flag_is_removed() {
let err = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--recovery-cmd",
"echo $1",
]))
.expect_err("--recovery-cmd must be rejected");
let msg = err.to_string();
assert!(
msg.contains("--recovery-cmd"),
"error must name the removed flag, got: {msg}"
);
assert!(
msg.contains("--recovery-exec"),
"error must recommend --recovery-exec, got: {msg}"
);
}
#[test]
fn recovery_cmd_file_flag_is_removed() {
let err = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--recovery-cmd-file",
"/nonexistent",
]))
.expect_err("--recovery-cmd-file must be rejected");
let msg = err.to_string();
assert!(
msg.contains("--recovery-cmd-file"),
"error must name the removed flag, got: {msg}"
);
assert!(
msg.contains("--recovery-exec"),
"error must recommend --recovery-exec, got: {msg}"
);
}
#[test]
fn i_accept_shell_risk_flag_is_removed() {
let err = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--i-accept-shell-risk",
]))
.expect_err("--i-accept-shell-risk must be rejected");
let msg = err.to_string();
assert!(
msg.contains("--i-accept-shell-risk"),
"error must name the removed flag, got: {msg}"
);
}
#[test]
fn exec_mode_does_not_require_shell_risk_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--recovery-exec",
"/bin/true",
]))
.expect("parse");
let mode = cfg.resolve_recovery_mode().expect("resolve").expect("some");
#[allow(unreachable_patterns)]
match mode {
crate::recovery::RecoveryMode::Exec { program, .. } => {
assert_eq!(program, "/bin/true");
}
other => panic!("expected Exec mode, got {other:?}"),
}
}
#[test]
fn parses_i_accept_plaintext_udp_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--i-accept-plaintext-udp",
]))
.expect("parse");
assert!(cfg.i_accept_plaintext_udp);
}
#[test]
fn i_accept_plaintext_udp_defaults_to_false() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
assert!(!cfg.i_accept_plaintext_udp);
}
#[test]
fn parses_secure_udp_i_accept_recovery_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--secure-udp-i-accept-recovery-on-unauthenticated-transport",
]))
.expect("parse");
assert!(cfg.i_accept_recovery_on_secure_udp);
assert!(!cfg.i_accept_recovery_on_plaintext_udp);
}
#[test]
fn parses_plaintext_udp_i_accept_recovery_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--plaintext-udp-i-accept-recovery-on-unauthenticated-transport",
]))
.expect("parse");
assert!(!cfg.i_accept_recovery_on_secure_udp);
assert!(cfg.i_accept_recovery_on_plaintext_udp);
}
#[test]
fn recovery_accept_flags_default_to_false() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
assert!(!cfg.i_accept_recovery_on_secure_udp);
assert!(!cfg.i_accept_recovery_on_plaintext_udp);
}
#[test]
fn parses_allow_cross_namespace_agents_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--allow-cross-namespace-agents",
]))
.expect("parse");
assert!(cfg.allow_cross_namespace_agents);
assert!(!cfg.strict_namespace_check);
}
#[test]
fn parses_strict_namespace_check_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--strict-namespace-check",
]))
.expect("parse");
assert!(cfg.strict_namespace_check);
assert!(!cfg.allow_cross_namespace_agents);
}
#[test]
fn namespace_flags_default_to_false() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
assert!(!cfg.allow_cross_namespace_agents);
assert!(!cfg.strict_namespace_check);
}
#[test]
fn recovery_plus_plaintext_udp_without_accept_flag_is_rejected() {
let err = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--udp-port",
"9000",
"--i-accept-plaintext-udp",
"--recovery-exec",
"/bin/true",
]))
.expect_err("must reject");
match err {
ConfigError::RecoveryRequiresAuthenticatedTransport { ref udp_addr } => {
assert!(udp_addr.contains(":9000"), "udp_addr = {udp_addr}");
}
other => panic!("expected RecoveryRequiresAuthenticatedTransport, got {other:?}"),
}
assert!(err
.to_string()
.contains("--plaintext-udp-i-accept-recovery-on-unauthenticated-transport"));
}
#[test]
fn recovery_plus_secure_udp_without_accept_flag_is_rejected() {
let err = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--udp-port",
"9000",
"--key-file",
"/nonexistent-key",
"--recovery-exec",
"/bin/true",
]))
.expect_err("must reject");
match err {
ConfigError::RecoveryRequiresAuthenticatedTransport { ref udp_addr } => {
assert!(udp_addr.contains(":9000"), "udp_addr = {udp_addr}");
}
other => panic!("expected RecoveryRequiresAuthenticatedTransport, got {other:?}"),
}
assert!(err
.to_string()
.contains("--secure-udp-i-accept-recovery-on-unauthenticated-transport"));
}
#[test]
fn recovery_plus_plaintext_udp_with_accept_flag_succeeds() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--udp-port",
"9000",
"--i-accept-plaintext-udp",
"--recovery-exec",
"/bin/true",
"--plaintext-udp-i-accept-recovery-on-unauthenticated-transport",
]))
.expect("parse");
assert!(cfg.i_accept_recovery_on_plaintext_udp);
assert!(!cfg.i_accept_recovery_on_secure_udp);
assert_eq!(cfg.udp_port, Some(9000));
}
#[test]
fn recovery_plus_secure_udp_with_accept_flag_succeeds() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--udp-port",
"9000",
"--key-file",
"/nonexistent-key",
"--recovery-exec",
"/bin/true",
"--secure-udp-i-accept-recovery-on-unauthenticated-transport",
]))
.expect("parse");
assert!(cfg.i_accept_recovery_on_secure_udp);
assert!(!cfg.i_accept_recovery_on_plaintext_udp);
assert_eq!(cfg.udp_port, Some(9000));
}
#[test]
fn recovery_without_udp_port_does_not_require_accept_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--recovery-exec",
"/bin/true",
]))
.expect("parse");
assert!(!cfg.i_accept_recovery_on_secure_udp);
assert!(!cfg.i_accept_recovery_on_plaintext_udp);
assert!(cfg.udp_port.is_none());
}
#[test]
fn parses_i_accept_secure_udp_non_loopback_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--i-accept-secure-udp-non-loopback",
]))
.expect("parse");
assert!(cfg.i_accept_secure_udp_non_loopback);
}
#[test]
fn i_accept_secure_udp_non_loopback_defaults_to_false() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
assert!(!cfg.i_accept_secure_udp_non_loopback);
}
#[test]
fn secure_udp_non_loopback_without_accept_flag_is_rejected() {
let err = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--udp-port",
"9000",
"--udp-bind-addr",
"0.0.0.0",
"--key-file",
"/nonexistent-key",
]))
.expect_err("must reject");
match err {
ConfigError::SecureUdpRequiresLoopbackBind { ref udp_addr } => {
assert!(udp_addr.contains("0.0.0.0:9000"), "udp_addr = {udp_addr}");
}
other => panic!("expected SecureUdpRequiresLoopbackBind, got {other:?}"),
}
let msg = err.to_string();
assert!(
msg.contains("--i-accept-secure-udp-non-loopback"),
"error must name the accept flag, got: {msg}"
);
}
#[test]
fn secure_udp_non_loopback_ipv6_unspecified_is_rejected() {
let err = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--udp-port",
"9000",
"--udp-bind-addr",
"::",
"--key-file",
"/nonexistent-key",
]))
.expect_err("must reject ::");
assert!(matches!(
err,
ConfigError::SecureUdpRequiresLoopbackBind { .. }
));
}
#[test]
fn secure_udp_loopback_bind_is_accepted_without_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--udp-port",
"9000",
"--udp-bind-addr",
"127.0.0.1",
"--key-file",
"/nonexistent-key",
]))
.expect("loopback bind must parse cleanly");
assert_eq!(cfg.udp_port, Some(9000));
assert!(!cfg.i_accept_secure_udp_non_loopback);
}
#[test]
fn secure_udp_ipv6_loopback_is_accepted_without_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--udp-port",
"9000",
"--udp-bind-addr",
"::1",
"--key-file",
"/nonexistent-key",
]))
.expect("::1 bind must parse cleanly");
assert_eq!(cfg.udp_port, Some(9000));
}
#[test]
fn secure_udp_non_loopback_with_accept_flag_succeeds() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--udp-port",
"9000",
"--udp-bind-addr",
"0.0.0.0",
"--key-file",
"/nonexistent-key",
"--i-accept-secure-udp-non-loopback",
]))
.expect("non-loopback with explicit opt-in must parse");
assert!(cfg.i_accept_secure_udp_non_loopback);
assert_eq!(cfg.udp_port, Some(9000));
}
#[test]
fn plaintext_udp_non_loopback_does_not_require_secure_udp_accept_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--udp-port",
"9000",
"--udp-bind-addr",
"0.0.0.0",
"--i-accept-plaintext-udp",
]))
.expect("plaintext UDP non-loopback must parse without secure-UDP flag");
assert!(!cfg.i_accept_secure_udp_non_loopback);
}
#[test]
fn secure_udp_no_bind_addr_parses_cleanly() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--udp-port",
"9000",
"--key-file",
"/nonexistent-key",
]))
.expect("absent bind addr must defer to runtime default");
assert!(cfg.udp_bind_addr.is_none());
}
#[cfg(feature = "prometheus-exporter")]
#[test]
fn parses_prom_rate_limit_flags() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--prom-rate-limit-per-sec",
"20",
"--prom-rate-limit-burst",
"50",
]))
.expect("parse");
assert_eq!(cfg.prom_rate_limit_per_sec, 20);
assert_eq!(cfg.prom_rate_limit_burst, 50);
}
#[test]
fn prom_rate_limit_defaults() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
assert_eq!(cfg.prom_rate_limit_per_sec, DEFAULT_PROM_RATE_LIMIT_PER_SEC);
assert_eq!(cfg.prom_rate_limit_burst, DEFAULT_PROM_RATE_LIMIT_BURST);
}
#[test]
fn no_recovery_flags_yields_none() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
let mode = cfg.resolve_recovery_mode().expect("resolve");
assert!(mode.is_none());
}
#[test]
fn parse_exec_cmd_splits_whitespace() {
let (program, args) = parse_exec_cmd("kill -HUP {pid}").expect("parse");
assert_eq!(program, "kill");
assert_eq!(args, vec!["-HUP", "{pid}"]);
}
#[test]
fn parse_exec_cmd_rejects_empty() {
assert!(parse_exec_cmd("").is_err());
assert!(parse_exec_cmd(" ").is_err());
}
#[test]
fn parses_recovery_env_repeatable() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--recovery-exec",
"/bin/true",
"--recovery-env",
"FOO=bar",
"--recovery-env",
"BAZ=qux",
]))
.expect("parse");
assert_eq!(cfg.recovery_env, vec!["FOO=bar", "BAZ=qux"]);
}
#[test]
fn recovery_env_defaults_to_empty() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
assert!(cfg.recovery_env.is_empty());
}
#[test]
fn parses_recovery_inherit_env_flag() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--recovery-exec",
"/bin/true",
"--recovery-inherit-env",
]))
.expect("parse");
assert!(cfg.recovery_inherit_env, "flag must enable inherit");
}
#[test]
fn recovery_inherit_env_defaults_to_false() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
assert!(
!cfg.recovery_inherit_env,
"recovery_inherit_env must default to false (secure)"
);
}
#[test]
fn parses_heartbeat_file() {
let cfg = Config::from_args(args(&[
"--socket",
"/s",
"--threshold-ms",
"100",
"--heartbeat-file",
"/tmp/varta-hb",
]))
.expect("parse");
assert_eq!(cfg.heartbeat_file, Some(PathBuf::from("/tmp/varta-hb")));
}
#[test]
fn heartbeat_file_omitted_is_none() {
let cfg = Config::from_args(args(&["--socket", "/s", "--threshold-ms", "100"])).expect("parse");
assert!(cfg.heartbeat_file.is_none());
}
fn mk_tmpdir(tag: &str) -> PathBuf {
use std::os::unix::fs::PermissionsExt;
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let pid = std::process::id();
let dir = std::env::temp_dir().join(format!("varta-vsf-{tag}-{pid}-{nanos}"));
std::fs::create_dir(&dir).expect("create tempdir");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).expect("chmod tempdir");
dir
}
fn write_mode(path: &std::path::Path, content: &[u8], mode: u32) {
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(mode)
.open(path)
.expect("open mode");
f.write_all(content).expect("write");
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).expect("set perms");
}
#[test]
fn validate_secret_file_reads_content_after_validation() {
let dir = mk_tmpdir("happy");
let p = dir.join("secret");
write_mode(&p, b"hello-world\n", 0o600);
let out = validate_secret_file(&p).expect("validate");
assert_eq!(out, "hello-world\n");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn validate_secret_file_rejects_symlink() {
let dir = mk_tmpdir("sym");
let target = dir.join("real");
write_mode(&target, b"x", 0o600);
let link = dir.join("link");
std::os::unix::fs::symlink(&target, &link).expect("symlink");
let err = validate_secret_file(&link).expect_err("should reject symlink");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert!(
err.to_string().contains("must not be a symlink"),
"unexpected error: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn validate_secret_file_rejects_bad_mode() {
let dir = mk_tmpdir("mode");
let p = dir.join("perms");
write_mode(&p, b"x", 0o644);
let err = validate_secret_file(&p).expect_err("should reject 0644");
assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
assert!(
err.to_string().contains("insecure permissions"),
"unexpected error: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn validate_secret_file_rejects_non_regular_file() {
let dir = mk_tmpdir("sock");
let p = dir.join("sock");
let _listener = std::os::unix::net::UnixListener::bind(&p).expect("bind sock");
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o600));
let err = validate_secret_file(&p).expect_err("should reject socket");
assert_ne!(err.kind(), std::io::ErrorKind::Other);
let _ = std::fs::remove_file(&p);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[ignore = "probabilistic stress test; run with --ignored"]
fn validate_secret_file_toctou_stress() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
let dir = mk_tmpdir("toctou");
let target = dir.join("file");
let attacker_target = dir.join("attacker-content");
write_mode(&target, b"GOOD\n", 0o600);
write_mode(&attacker_target, b"BAD-DO-NOT-READ\n", 0o600);
let stop = Arc::new(AtomicBool::new(false));
let stop_w = stop.clone();
let target_w = target.clone();
let atk = attacker_target.clone();
let writer = std::thread::spawn(move || {
while !stop_w.load(Ordering::Relaxed) {
let tmp = target_w.with_extension("swap");
let _ = std::fs::remove_file(&tmp);
if std::os::unix::fs::symlink(&atk, &tmp).is_ok() {
let _ = std::fs::rename(&tmp, &target_w);
}
let _ = std::fs::remove_file(&target_w);
write_mode(&target_w, b"GOOD\n", 0o600);
}
});
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
let mut iters = 0u64;
while std::time::Instant::now() < deadline {
if let Ok(s) = validate_secret_file(&target) {
assert!(
!s.contains("BAD"),
"TOCTOU: validate_secret_file returned attacker content after {iters} iters"
);
}
iters += 1;
}
stop.store(true, Ordering::Relaxed);
writer.join().expect("writer thread");
eprintln!("toctou_stress: {iters} validate calls");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn parses_eviction_scan_window() {
let args = [
"--socket",
"/tmp/t.sock",
"--threshold-ms",
"100",
"--eviction-scan-window",
"64",
];
let cfg = Config::from_args(args.iter().map(|s| s.to_string())).unwrap();
assert_eq!(cfg.eviction_scan_window, 64);
}
#[test]
fn rejects_eviction_scan_window_zero() {
let args = [
"--socket",
"/tmp/t.sock",
"--threshold-ms",
"100",
"--eviction-scan-window",
"0",
];
let err = Config::from_args(args.iter().map(|s| s.to_string())).unwrap_err();
assert!(
matches!(
err,
ConfigError::EvictionScanWindowOutOfRange { value: 0, .. }
),
"unexpected error: {err}"
);
}
#[test]
fn rejects_eviction_scan_window_above_max() {
let args = [
"--socket",
"/tmp/t.sock",
"--threshold-ms",
"100",
"--eviction-scan-window",
"9999",
];
let err = Config::from_args(args.iter().map(|s| s.to_string())).unwrap_err();
assert!(
matches!(
err,
ConfigError::EvictionScanWindowOutOfRange { value: 9999, .. }
),
"unexpected error: {err}"
);
}
#[test]
fn clock_source_default_is_monotonic() {
let args = ["--socket", "/tmp/t.sock", "--threshold-ms", "100"];
let cfg = Config::from_args(args.iter().map(|s| s.to_string())).unwrap();
assert_eq!(cfg.clock_source, ClockSource::Monotonic);
}
#[test]
fn clock_source_parses_monotonic() {
let args = [
"--socket",
"/tmp/t.sock",
"--threshold-ms",
"100",
"--clock-source",
"monotonic",
];
let cfg = Config::from_args(args.iter().map(|s| s.to_string())).unwrap();
assert_eq!(cfg.clock_source, ClockSource::Monotonic);
}
#[cfg(target_os = "linux")]
#[test]
fn clock_source_parses_boottime_on_linux() {
let args = [
"--socket",
"/tmp/t.sock",
"--threshold-ms",
"100",
"--clock-source",
"boottime",
];
let cfg = Config::from_args(args.iter().map(|s| s.to_string())).unwrap();
assert_eq!(cfg.clock_source, ClockSource::Boottime);
}
#[cfg(not(target_os = "linux"))]
#[test]
fn clock_source_boottime_rejected_on_unsupported_platform() {
let args = [
"--socket",
"/tmp/t.sock",
"--threshold-ms",
"100",
"--clock-source",
"boottime",
];
let err = Config::from_args(args.iter().map(|s| s.to_string())).unwrap_err();
match err {
ConfigError::ClockSourceUnsupported { source, .. } => {
assert_eq!(source, ClockSource::Boottime);
}
other => panic!("expected ClockSourceUnsupported, got {other}"),
}
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn clock_source_parses_monotonic_raw_on_macos() {
let args = [
"--socket",
"/tmp/t.sock",
"--threshold-ms",
"100",
"--clock-source",
"monotonic-raw",
];
let cfg = Config::from_args(args.iter().map(|s| s.to_string())).unwrap();
assert_eq!(cfg.clock_source, ClockSource::MonotonicRaw);
}
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
#[test]
fn clock_source_monotonic_raw_rejected_off_macos() {
let args = [
"--socket",
"/tmp/t.sock",
"--threshold-ms",
"100",
"--clock-source",
"monotonic-raw",
];
let err = Config::from_args(args.iter().map(|s| s.to_string())).unwrap_err();
match err {
ConfigError::ClockSourceUnsupported { source, .. } => {
assert_eq!(source, ClockSource::MonotonicRaw);
}
other => panic!("expected ClockSourceUnsupported, got {other}"),
}
}
#[test]
fn clock_source_rejects_unknown_value() {
let args = [
"--socket",
"/tmp/t.sock",
"--threshold-ms",
"100",
"--clock-source",
"wallclock",
];
let err = Config::from_args(args.iter().map(|s| s.to_string())).unwrap_err();
match err {
ConfigError::BadValue { flag, raw } => {
assert_eq!(flag, "--clock-source");
assert_eq!(raw, "wallclock");
}
other => panic!("expected BadValue, got {other}"),
}
}