use super::*;
#[test]
fn parse_duration_str_seconds() {
let d = parse_duration_str("30s").unwrap();
assert_eq!(d, std::time::Duration::from_secs(30));
}
#[test]
fn parse_duration_str_minutes() {
let d = parse_duration_str("5m").unwrap();
assert_eq!(d, std::time::Duration::from_secs(300));
}
#[test]
fn parse_duration_str_hours() {
let d = parse_duration_str("1h").unwrap();
assert_eq!(d, std::time::Duration::from_secs(3600));
}
#[test]
fn parse_duration_str_plain_seconds() {
let d = parse_duration_str("60").unwrap();
assert_eq!(d, std::time::Duration::from_secs(60));
}
#[test]
fn parse_duration_str_whitespace() {
let d = parse_duration_str(" 10 s ").unwrap();
assert_eq!(d, std::time::Duration::from_secs(10));
}
#[test]
fn parse_duration_str_days() {
let d = parse_duration_str("30d").unwrap();
assert_eq!(d, std::time::Duration::from_secs(30 * 86400));
}
#[test]
fn parse_duration_str_invalid() {
assert!(
parse_duration_str("abc")
.unwrap_err()
.contains("invalid timeout"),
"bare letters should fail with a useful message"
);
assert!(
parse_duration_str("")
.unwrap_err()
.contains("invalid timeout"),
"empty string should fail"
);
assert!(
parse_duration_str("xs")
.unwrap_err()
.contains("invalid timeout"),
"non-numeric prefix should fail"
);
}
#[test]
fn parse_duration_str_zero() {
assert_eq!(
parse_duration_str("0s").unwrap(),
std::time::Duration::from_secs(0)
);
assert_eq!(
parse_duration_str("0").unwrap(),
std::time::Duration::from_secs(0)
);
}
#[test]
fn parse_duration_str_negative() {
assert!(
parse_duration_str("-5s").is_err(),
"negative durations should be rejected"
);
}
#[test]
fn parse_loose_version_full_semver() {
assert_eq!(
parse_loose_version("1.28.3"),
Some(semver::Version::new(1, 28, 3))
);
assert_eq!(
parse_loose_version("0.1.0"),
Some(semver::Version::new(0, 1, 0))
);
}
#[test]
fn parse_loose_version_two_part() {
let ver = parse_loose_version("1.28").unwrap();
assert_eq!(ver, semver::Version::new(1, 28, 0));
}
#[test]
fn parse_loose_version_single_part() {
let ver = parse_loose_version("1").unwrap();
assert_eq!(ver, semver::Version::new(1, 0, 0));
}
#[test]
fn parse_loose_version_rejects_garbage() {
assert!(parse_loose_version("abc").is_none());
assert!(parse_loose_version("").is_none());
assert!(parse_loose_version("1.2.3.4").is_none());
assert!(
parse_loose_version("-1").is_none(),
"negative numbers are not valid versions"
);
}
#[test]
fn parse_loose_version_preserves_prerelease() {
let ver = parse_loose_version("1.2.3-beta.1").unwrap();
assert_eq!(ver.major, 1);
assert_eq!(ver.minor, 2);
assert_eq!(ver.patch, 3);
assert!(!ver.pre.is_empty(), "pre-release should be preserved");
}
#[test]
fn parse_loose_version_strips_v_prefix() {
assert_eq!(
parse_loose_version("v1.10.0"),
Some(semver::Version::new(1, 10, 0))
);
assert_eq!(
parse_loose_version("v1.9.0"),
Some(semver::Version::new(1, 9, 0))
);
assert_eq!(
parse_loose_version("V2.0.0"),
Some(semver::Version::new(2, 0, 0))
);
assert_eq!(
parse_loose_version("v1.28"),
Some(semver::Version::new(1, 28, 0))
);
assert_eq!(
parse_loose_version("v1"),
Some(semver::Version::new(1, 0, 0))
);
}
#[test]
fn parse_loose_version_v_prefix_compares_correctly() {
let lo = parse_loose_version("v1.9.0").unwrap();
let hi = parse_loose_version("v1.10.0").unwrap();
assert!(lo < hi, "v1.9.0 must sort before v1.10.0");
}
#[test]
fn parse_loose_version_does_not_strip_other_prefixes() {
assert!(parse_loose_version("release-1.0.0").is_none());
assert!(parse_loose_version("ver1.0.0").is_none());
assert!(parse_loose_version("1v.0.0").is_none());
}
#[test]
fn version_satisfies_accepts_v_prefixed_version() {
assert!(version_satisfies("v1.28.3", ">=1.28"));
assert!(!version_satisfies("v1.27.0", ">=1.28"));
}
#[test]
fn version_satisfies_basic() {
assert!(version_satisfies("1.28.3", ">=1.28"));
assert!(!version_satisfies("1.27.0", ">=1.28"));
assert!(version_satisfies("2.40.1", "~2.40"));
assert!(!version_satisfies("2.39.0", "~2.40"));
}
#[test]
fn version_satisfies_loose() {
assert!(version_satisfies("1.28", ">=1.28"));
assert!(version_satisfies("2", ">=1.28"));
assert!(!version_satisfies("1", ">=1.28"));
}
#[test]
fn version_satisfies_invalid_requirement() {
assert!(!version_satisfies("1.0.0", "not valid"));
}
#[cfg(unix)]
#[test]
fn home_dir_var_uses_home_on_unix() {
let result = home_dir_var();
assert!(result.is_some());
assert_eq!(result.unwrap(), std::env::var("HOME").unwrap());
}
#[test]
fn version_satisfies_invalid_version() {
assert!(!version_satisfies("abc", ">=1.0"));
}
#[test]
fn atomic_write_creates_file_with_content() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("test.txt");
let hash = atomic_write(&target, b"hello world").unwrap();
assert_eq!(std::fs::read_to_string(&target).unwrap(), "hello world");
assert!(!hash.is_empty());
assert_eq!(hash.len(), 64); }
#[test]
fn atomic_write_creates_parent_dirs() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("a/b/c/test.txt");
atomic_write(&target, b"nested").unwrap();
assert_eq!(std::fs::read_to_string(&target).unwrap(), "nested");
}
#[cfg(unix)]
#[test]
fn atomic_write_preserves_permissions() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("perms.txt");
std::fs::write(&target, "old").unwrap();
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o600)).unwrap();
atomic_write(&target, b"new").unwrap();
let mode = std::fs::metadata(&target).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600);
}
#[test]
fn atomic_write_str_works() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("str.txt");
let hash = atomic_write_str(&target, "string content").unwrap();
assert_eq!(std::fs::read_to_string(&target).unwrap(), "string content");
assert_eq!(hash.len(), 64);
}
#[test]
fn capture_file_state_regular_file() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("file.txt");
std::fs::write(&target, "contents").unwrap();
let state = capture_file_state(&target).unwrap().unwrap();
assert_eq!(state.content, b"contents");
assert!(!state.content_hash.is_empty());
assert!(!state.is_symlink);
assert!(state.symlink_target.is_none());
assert!(!state.oversized);
}
#[test]
#[cfg(unix)]
fn capture_file_state_symlink() {
let dir = tempfile::tempdir().unwrap();
let real = dir.path().join("real.txt");
let link = dir.path().join("link.txt");
std::fs::write(&real, "target").unwrap();
std::os::unix::fs::symlink(&real, &link).unwrap();
let state = capture_file_state(&link).unwrap().unwrap();
assert!(state.is_symlink);
assert_eq!(state.symlink_target.unwrap(), real);
assert!(state.content.is_empty());
}
#[test]
fn capture_file_state_missing_returns_none() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("does_not_exist.txt");
let state = capture_file_state(&missing).unwrap();
assert!(state.is_none());
}
#[test]
fn create_symlink_creates_link() {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("source.txt");
std::fs::write(&source, "hello").unwrap();
let link = dir.path().join("link.txt");
create_symlink(&source, &link).unwrap();
assert!(link.symlink_metadata().unwrap().file_type().is_symlink());
assert_eq!(std::fs::read_to_string(&link).unwrap(), "hello");
}
#[cfg(unix)]
#[test]
fn file_permissions_mode_returns_mode() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, "test").unwrap();
let meta = std::fs::metadata(&file).unwrap();
let mode = file_permissions_mode(&meta);
assert!(mode.is_some());
let bits = mode.unwrap();
assert!(bits & 0o777 > 0, "mode bits should be non-zero");
assert!(
bits & 0o400 != 0,
"owner read bit should be set on a newly created file"
);
}
#[cfg(unix)]
#[test]
fn set_file_permissions_changes_mode() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, "test").unwrap();
set_file_permissions(&file, 0o755).unwrap();
let meta = std::fs::metadata(&file).unwrap();
assert_eq!(file_permissions_mode(&meta), Some(0o755));
}
#[cfg(unix)]
#[test]
fn is_executable_checks_mode() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("script.sh");
std::fs::write(&file, "#!/bin/sh").unwrap();
set_file_permissions(&file, 0o644).unwrap();
let meta = std::fs::metadata(&file).unwrap();
assert!(!is_executable(&file, &meta));
set_file_permissions(&file, 0o755).unwrap();
let meta = std::fs::metadata(&file).unwrap();
assert!(is_executable(&file, &meta));
}
#[cfg(unix)]
#[test]
fn is_same_inode_detects_hard_links() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("original.txt");
std::fs::write(&file, "content").unwrap();
let link = dir.path().join("hardlink.txt");
std::fs::hard_link(&file, &link).unwrap();
assert!(is_same_inode(&file, &link));
assert!(!is_same_inode(&file, &dir.path().join("nonexistent")));
}
#[test]
fn validate_path_within_accepts_child() {
let dir = tempfile::tempdir().unwrap();
let child = dir.path().join("sub/file.txt");
std::fs::create_dir_all(dir.path().join("sub")).unwrap();
std::fs::write(&child, "").unwrap();
assert!(validate_path_within(&child, dir.path()).is_ok());
}
#[test]
fn validate_path_within_rejects_escape() {
let root = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let result = validate_path_within(outside.path(), root.path());
let err = result.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
assert!(
err.to_string().contains("escapes root"),
"expected 'escapes root' message, got: {err}"
);
}
#[test]
fn validate_no_traversal_accepts_clean_path() {
assert!(validate_no_traversal(std::path::Path::new("a/b/c")).is_ok());
assert!(validate_no_traversal(std::path::Path::new("/absolute/path")).is_ok());
}
#[test]
fn validate_no_traversal_rejects_dotdot() {
assert!(validate_no_traversal(std::path::Path::new("a/../b")).is_err());
assert!(validate_no_traversal(std::path::Path::new("../../etc")).is_err());
}
#[test]
fn shell_escape_value_simple() {
assert_eq!(shell_escape_value("hello"), "\"hello\"");
}
#[test]
fn shell_escape_value_with_dollar() {
assert_eq!(shell_escape_value("$HOME/bin"), "'$HOME/bin'");
}
#[test]
fn shell_escape_value_with_single_quote() {
assert_eq!(shell_escape_value("it's"), "'it'\\''s'");
}
#[test]
fn xml_escape_special_chars() {
assert_eq!(xml_escape("<tag>&\"'"), "<tag>&"'");
}
#[test]
fn xml_escape_passthrough() {
assert_eq!(xml_escape("normal text"), "normal text");
}
#[test]
#[cfg(unix)] fn acquire_apply_lock_works() {
let dir = tempfile::tempdir().unwrap();
let guard = acquire_apply_lock(dir.path()).unwrap();
let content = std::fs::read_to_string(dir.path().join("apply.lock")).unwrap();
assert_eq!(content, format!("{}", std::process::id()));
drop(guard);
}
#[test]
fn acquire_apply_lock_detects_contention() {
let dir = tempfile::tempdir().unwrap();
let _guard = acquire_apply_lock(dir.path()).unwrap();
let result = acquire_apply_lock(dir.path());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(
err,
errors::CfgdError::State(errors::StateError::ApplyLockHeld { .. })
),
"expected ApplyLockHeld, got: {}",
err
);
}
#[test]
fn merge_aliases_override_by_name() {
let mut base = vec![
config::ShellAlias {
name: "vim".into(),
command: "vi".into(),
},
config::ShellAlias {
name: "ll".into(),
command: "ls -l".into(),
},
];
let updates = vec![config::ShellAlias {
name: "vim".into(),
command: "nvim".into(),
}];
merge_aliases(&mut base, &updates);
assert_eq!(base.len(), 2);
assert_eq!(base[0].command, "nvim");
assert_eq!(base[1].command, "ls -l");
}
#[test]
fn merge_aliases_appends_new() {
let mut base = vec![config::ShellAlias {
name: "vim".into(),
command: "nvim".into(),
}];
let updates = vec![config::ShellAlias {
name: "ll".into(),
command: "ls -la".into(),
}];
merge_aliases(&mut base, &updates);
assert_eq!(base.len(), 2);
}
#[test]
fn split_add_remove_basic() {
let vals: Vec<String> = vec!["foo".into(), "-bar".into(), "baz".into(), "-qux".into()];
let (adds, removes) = split_add_remove(&vals);
assert_eq!(adds, vec!["foo", "baz"]);
assert_eq!(removes, vec!["bar", "qux"]);
}
#[test]
fn split_add_remove_empty() {
let (adds, removes) = split_add_remove(&[]);
assert!(adds.is_empty());
assert!(removes.is_empty());
}
#[test]
fn split_add_remove_all_adds() {
let vals: Vec<String> = vec!["a".into(), "b".into()];
let (adds, removes) = split_add_remove(&vals);
assert_eq!(adds, vec!["a", "b"]);
assert!(removes.is_empty());
}
#[test]
fn split_add_remove_all_removes() {
let vals: Vec<String> = vec!["-x".into(), "-y".into()];
let (adds, removes) = split_add_remove(&vals);
assert!(adds.is_empty());
assert_eq!(removes, vec!["x", "y"]);
}
#[test]
fn parse_alias_valid() {
let alias = parse_alias("vim=nvim").unwrap();
assert_eq!(alias.name, "vim");
assert_eq!(alias.command, "nvim");
}
#[test]
fn parse_alias_with_args() {
let alias = parse_alias("ll=ls -la --color").unwrap();
assert_eq!(alias.name, "ll");
assert_eq!(alias.command, "ls -la --color");
}
#[test]
fn parse_alias_invalid() {
assert!(parse_alias("no-equals-sign").is_err());
}
#[test]
fn deep_merge_yaml_maps() {
let mut base = serde_yaml::from_str::<serde_yaml::Value>("a: 1\nb: 2").unwrap();
let overlay = serde_yaml::from_str::<serde_yaml::Value>("b: 3\nc: 4").unwrap();
deep_merge_yaml(&mut base, &overlay);
assert_eq!(base["a"], serde_yaml::Value::from(1));
assert_eq!(base["b"], serde_yaml::Value::from(3));
assert_eq!(base["c"], serde_yaml::Value::from(4));
}
#[test]
fn deep_merge_yaml_nested() {
let mut base = serde_yaml::from_str::<serde_yaml::Value>("top:\n a: 1\n b: 2").unwrap();
let overlay = serde_yaml::from_str::<serde_yaml::Value>("top:\n b: 9\n c: 3").unwrap();
deep_merge_yaml(&mut base, &overlay);
assert_eq!(base["top"]["a"], serde_yaml::Value::from(1));
assert_eq!(base["top"]["b"], serde_yaml::Value::from(9));
assert_eq!(base["top"]["c"], serde_yaml::Value::from(3));
}
#[test]
fn deep_merge_yaml_overlay_replaces_scalar() {
let mut base = serde_yaml::from_str::<serde_yaml::Value>("x: old").unwrap();
let overlay = serde_yaml::from_str::<serde_yaml::Value>("x: new").unwrap();
deep_merge_yaml(&mut base, &overlay);
assert_eq!(base["x"], serde_yaml::Value::from("new"));
}
#[test]
fn union_extend_deduplicates() {
let mut target = vec!["a".to_string(), "b".to_string()];
union_extend(&mut target, &["b".to_string(), "c".to_string()]);
assert_eq!(target, vec!["a", "b", "c"]);
}
#[test]
fn union_extend_empty_source() {
let mut target = vec!["a".to_string()];
union_extend(&mut target, &[]);
assert_eq!(target, vec!["a"]);
}
#[test]
fn merge_env_overrides_by_name() {
let mut base = vec![
config::EnvVar {
name: "FOO".into(),
value: "old".into(),
},
config::EnvVar {
name: "BAR".into(),
value: "keep".into(),
},
];
let updates = vec![config::EnvVar {
name: "FOO".into(),
value: "new".into(),
}];
merge_env(&mut base, &updates);
assert_eq!(base.len(), 2);
assert_eq!(base.iter().find(|e| e.name == "FOO").unwrap().value, "new");
assert_eq!(base.iter().find(|e| e.name == "BAR").unwrap().value, "keep");
}
#[test]
fn merge_env_adds_new() {
let mut base = vec![];
let updates = vec![config::EnvVar {
name: "NEW".into(),
value: "val".into(),
}];
merge_env(&mut base, &updates);
assert_eq!(base.len(), 1);
assert_eq!(base[0].name, "NEW");
}
#[test]
fn shell_escape_value_metacharacters() {
assert_eq!(shell_escape_value("it's a $test"), "'it'\\''s a $test'");
}
#[test]
fn shell_escape_value_backtick() {
assert_eq!(shell_escape_value("`cmd`"), "'`cmd`'");
}
#[test]
fn shell_escape_value_backslash() {
assert_eq!(shell_escape_value("a\\b"), "'a\\b'");
}
#[test]
fn shell_escape_value_empty() {
assert_eq!(shell_escape_value(""), "\"\"");
}
#[test]
fn xml_escape_all_entities() {
assert_eq!(
xml_escape("a&b<c>d\"e'f"),
"a&b<c>d"e'f"
);
}
#[test]
fn unix_secs_to_iso8601_epoch() {
let result = unix_secs_to_iso8601(0);
assert_eq!(result, "1970-01-01T00:00:00Z");
}
#[test]
fn unix_secs_to_iso8601_known_date() {
let result = unix_secs_to_iso8601(1700000000);
assert!(result.starts_with("2023-11-14"));
}
#[test]
fn copy_dir_recursive_copies_tree() {
let src = tempfile::tempdir().unwrap();
let dst = tempfile::tempdir().unwrap();
let dst_path = dst.path().join("copy");
std::fs::create_dir_all(src.path().join("sub")).unwrap();
std::fs::write(src.path().join("a.txt"), "hello").unwrap();
std::fs::write(src.path().join("sub/b.txt"), "world").unwrap();
copy_dir_recursive(src.path(), &dst_path).unwrap();
assert_eq!(
std::fs::read_to_string(dst_path.join("a.txt")).unwrap(),
"hello"
);
assert_eq!(
std::fs::read_to_string(dst_path.join("sub/b.txt")).unwrap(),
"world"
);
}
#[test]
fn expand_tilde_with_home() {
let result = expand_tilde(std::path::Path::new("~/test"));
let home = home_dir_var().expect("home directory must be available in test");
let expected = std::path::PathBuf::from(home).join("test");
assert_eq!(result, expected);
}
#[test]
fn expand_tilde_absolute_unchanged() {
let result = expand_tilde(std::path::Path::new("/absolute/path"));
assert_eq!(result, std::path::PathBuf::from("/absolute/path"));
}
#[test]
fn with_test_home_scopes_override_and_restores() {
assert!(test_home_override().is_none());
let tmp = tempfile::tempdir().unwrap();
let fake_home = tmp.path().to_path_buf();
let expanded = with_test_home(&fake_home, || {
expand_tilde(std::path::Path::new("~/sub/file"))
});
assert_eq!(expanded, fake_home.join("sub").join("file"));
assert!(test_home_override().is_none());
}
#[test]
fn with_test_home_restores_on_panic() {
let tmp = tempfile::tempdir().unwrap();
let fake_home = tmp.path().to_path_buf();
let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
with_test_home(&fake_home, || {
assert_eq!(test_home_override().as_deref(), Some(fake_home.as_path()));
panic!("simulated failure");
});
}))
.is_err();
assert!(panicked, "closure should have panicked");
assert!(test_home_override().is_none());
}
#[test]
fn with_test_home_nests_correctly() {
let outer_tmp = tempfile::tempdir().unwrap();
let inner_tmp = tempfile::tempdir().unwrap();
let outer = outer_tmp.path().to_path_buf();
let inner = inner_tmp.path().to_path_buf();
with_test_home(&outer, || {
assert_eq!(test_home_override().as_deref(), Some(outer.as_path()));
with_test_home(&inner, || {
assert_eq!(test_home_override().as_deref(), Some(inner.as_path()));
});
assert_eq!(test_home_override().as_deref(), Some(outer.as_path()));
});
assert!(test_home_override().is_none());
}
#[test]
fn default_config_dir_follows_override() {
let tmp = tempfile::tempdir().unwrap();
let fake_home = tmp.path().to_path_buf();
let observed = with_test_home(&fake_home, default_config_dir);
assert_eq!(observed, fake_home.join(".config").join("cfgd"));
}
#[test]
fn acquire_apply_lock_and_release() {
let dir = tempfile::tempdir().unwrap();
let guard = acquire_apply_lock(dir.path()).unwrap();
assert!(dir.path().join("apply.lock").exists());
drop(guard);
}
fn test_reconcile_config(patches: Vec<config::ReconcilePatch>) -> config::ReconcileConfig {
config::ReconcileConfig {
interval: "5m".into(),
on_change: false,
auto_apply: false,
policy: None,
drift_policy: config::DriftPolicy::NotifyOnly,
patches,
}
}
#[test]
fn resolve_reconcile_global_only() {
let cfg = test_reconcile_config(vec![]);
let eff = resolve_effective_reconcile("some-module", &["default"], &cfg);
assert_eq!(eff.interval, "5m");
assert!(!eff.auto_apply);
assert_eq!(eff.drift_policy, config::DriftPolicy::NotifyOnly);
}
#[test]
fn resolve_reconcile_module_patch() {
let cfg = test_reconcile_config(vec![config::ReconcilePatch {
kind: config::ReconcilePatchKind::Module,
name: Some("certs".into()),
interval: Some("1m".into()),
auto_apply: None,
drift_policy: Some(config::DriftPolicy::Auto),
}]);
let eff = resolve_effective_reconcile("certs", &["default"], &cfg);
assert_eq!(eff.interval, "1m");
assert!(!eff.auto_apply); assert_eq!(eff.drift_policy, config::DriftPolicy::Auto);
}
#[test]
fn resolve_reconcile_profile_patch() {
let cfg = test_reconcile_config(vec![config::ReconcilePatch {
kind: config::ReconcilePatchKind::Profile,
name: Some("work".into()),
interval: None,
auto_apply: Some(true),
drift_policy: None,
}]);
let eff = resolve_effective_reconcile("any-mod", &["base", "work"], &cfg);
assert_eq!(eff.interval, "5m"); assert!(eff.auto_apply); }
#[test]
fn resolve_reconcile_module_beats_profile() {
let cfg = test_reconcile_config(vec![
config::ReconcilePatch {
kind: config::ReconcilePatchKind::Profile,
name: Some("work".into()),
interval: None,
auto_apply: Some(false),
drift_policy: None,
},
config::ReconcilePatch {
kind: config::ReconcilePatchKind::Module,
name: Some("certs".into()),
interval: None,
auto_apply: Some(true),
drift_policy: None,
},
]);
let eff = resolve_effective_reconcile("certs", &["work"], &cfg);
assert!(eff.auto_apply); }
#[test]
fn resolve_reconcile_leaf_profile_wins() {
let cfg = test_reconcile_config(vec![
config::ReconcilePatch {
kind: config::ReconcilePatchKind::Profile,
name: Some("base".into()),
interval: Some("10m".into()),
auto_apply: None,
drift_policy: None,
},
config::ReconcilePatch {
kind: config::ReconcilePatchKind::Profile,
name: Some("work".into()),
interval: Some("2m".into()),
auto_apply: None,
drift_policy: None,
},
]);
let eff = resolve_effective_reconcile("any", &["base", "work"], &cfg);
assert_eq!(eff.interval, "2m");
}
#[test]
fn resolve_reconcile_fields_merge_independently() {
let cfg = test_reconcile_config(vec![
config::ReconcilePatch {
kind: config::ReconcilePatchKind::Profile,
name: Some("work".into()),
interval: Some("10m".into()),
auto_apply: None,
drift_policy: None,
},
config::ReconcilePatch {
kind: config::ReconcilePatchKind::Module,
name: Some("certs".into()),
interval: None,
auto_apply: None,
drift_policy: Some(config::DriftPolicy::Auto),
},
]);
let eff = resolve_effective_reconcile("certs", &["work"], &cfg);
assert_eq!(eff.interval, "10m"); assert_eq!(eff.drift_policy, config::DriftPolicy::Auto); assert!(!eff.auto_apply); }
#[test]
fn resolve_reconcile_missing_module_ignored() {
let cfg = test_reconcile_config(vec![config::ReconcilePatch {
kind: config::ReconcilePatchKind::Module,
name: Some("nonexistent".into()),
interval: Some("1s".into()),
auto_apply: None,
drift_policy: None,
}]);
let eff = resolve_effective_reconcile("other", &["default"], &cfg);
assert_eq!(eff.interval, "5m");
}
#[test]
fn resolve_reconcile_duplicate_module_last_wins() {
let cfg = test_reconcile_config(vec![
config::ReconcilePatch {
kind: config::ReconcilePatchKind::Module,
name: Some("certs".into()),
interval: Some("10m".into()),
auto_apply: None,
drift_policy: None,
},
config::ReconcilePatch {
kind: config::ReconcilePatchKind::Module,
name: Some("certs".into()),
interval: Some("1m".into()),
auto_apply: None,
drift_policy: None,
},
]);
let eff = resolve_effective_reconcile("certs", &["default"], &cfg);
assert_eq!(eff.interval, "1m"); }
#[test]
fn resolve_reconcile_kind_wide_module_patch() {
let cfg = test_reconcile_config(vec![config::ReconcilePatch {
kind: config::ReconcilePatchKind::Module,
name: None, interval: Some("30s".into()),
auto_apply: None,
drift_policy: None,
}]);
let eff = resolve_effective_reconcile("any-module", &["default"], &cfg);
assert_eq!(eff.interval, "30s");
}
#[test]
fn resolve_reconcile_named_beats_kind_wide() {
let cfg = test_reconcile_config(vec![
config::ReconcilePatch {
kind: config::ReconcilePatchKind::Module,
name: None, interval: Some("30s".into()),
auto_apply: None,
drift_policy: None,
},
config::ReconcilePatch {
kind: config::ReconcilePatchKind::Module,
name: Some("certs".into()), interval: Some("5s".into()),
auto_apply: None,
drift_policy: None,
},
]);
let eff = resolve_effective_reconcile("certs", &["default"], &cfg);
assert_eq!(eff.interval, "5s");
let eff2 = resolve_effective_reconcile("other", &["default"], &cfg);
assert_eq!(eff2.interval, "30s");
}
#[test]
fn validate_env_var_name_accepts_valid() {
assert!(validate_env_var_name("PATH").is_ok());
assert!(validate_env_var_name("_PRIVATE").is_ok());
assert!(validate_env_var_name("MY_VAR_123").is_ok());
assert!(validate_env_var_name("a").is_ok());
}
#[test]
fn validate_env_var_name_rejects_invalid() {
let err = validate_env_var_name("").unwrap_err();
assert!(err.contains("empty"), "empty should say empty: {err}");
let err = validate_env_var_name("1STARTS_WITH_DIGIT").unwrap_err();
assert!(
err.contains("must start with"),
"digit-prefix should explain: {err}"
);
for bad in ["HAS SPACE", "HAS;SEMI", "HAS$DOLLAR", "HAS-DASH", "a=b"] {
assert!(
validate_env_var_name(bad).is_err(),
"{bad:?} should be rejected"
);
}
}
#[test]
fn validate_env_var_user_name_accepts_normal() {
assert!(validate_env_var_user_name("PATH").is_ok());
assert!(validate_env_var_user_name("MY_APP_KEY").is_ok());
assert!(validate_env_var_user_name("_PRIVATE").is_ok());
assert!(validate_env_var_user_name("APP_FOO").is_ok());
}
#[test]
fn validate_env_var_user_name_rejects_cfgd_prefix() {
let err = validate_env_var_user_name("CFGD_FOO").unwrap_err();
assert!(err.contains("reserved"), "should mention reserved: {err}");
assert!(err.contains("CFGD_"), "should mention the prefix: {err}");
assert!(err.contains("APP_FOO"), "should suggest a rename: {err}");
let err = validate_env_var_user_name("CFGD_").unwrap_err();
assert!(
err.contains("reserved"),
"bare CFGD_ should be rejected: {err}"
);
}
#[test]
fn validate_env_var_user_name_delegates_shell_safety() {
let err = validate_env_var_user_name("").unwrap_err();
assert!(
err.contains("empty"),
"empty should fail shell check: {err}"
);
let err = validate_env_var_user_name("1BAD").unwrap_err();
assert!(
err.contains("must start with"),
"digit-leading should fail shell check: {err}"
);
}
#[test]
fn validate_env_var_user_name_rejects_bash_env() {
let err = validate_env_var_user_name("BASH_ENV").unwrap_err();
assert!(
err.contains("reserved"),
"BASH_ENV should be rejected as reserved: {err}"
);
assert!(
err.contains("alias delivery"),
"error should explain the reservation reason: {err}"
);
}
#[test]
fn validate_env_var_user_name_rejects_zdotdir() {
let err = validate_env_var_user_name("ZDOTDIR").unwrap_err();
assert!(
err.contains("reserved"),
"ZDOTDIR should be rejected as reserved: {err}"
);
assert!(
err.contains("alias delivery"),
"error should explain the reservation reason: {err}"
);
}
#[test]
fn parse_env_var_rejects_cfgd_prefix() {
let err = parse_env_var("CFGD_SECRET=value").unwrap_err();
assert!(
err.contains("reserved"),
"parse_env_var should reject CFGD_*: {err}"
);
}
#[test]
fn validate_alias_name_accepts_valid() {
assert!(validate_alias_name("ls").is_ok());
assert!(validate_alias_name("my-alias").is_ok());
assert!(validate_alias_name("my.alias").is_ok());
assert!(validate_alias_name("my_alias_123").is_ok());
}
#[test]
fn validate_alias_name_rejects_invalid() {
let err = validate_alias_name("").unwrap_err();
assert!(err.contains("empty"), "empty should say empty: {err}");
for bad in ["has space", "has;semi", "has$dollar", "a=b", "has/slash"] {
let err = validate_alias_name(bad).unwrap_err();
assert!(
err.contains("must contain only"),
"{bad:?} rejection should explain allowed chars: {err}"
);
}
}
#[test]
fn parse_env_var_validates_name() {
let ev = parse_env_var("VALID=value").unwrap();
assert_eq!(ev.name, "VALID");
assert_eq!(ev.value, "value");
assert!(
parse_env_var("1BAD=value")
.unwrap_err()
.contains("must start with"),
"digit-leading name should fail"
);
assert!(parse_env_var("BAD;NAME=value").is_err());
}
#[test]
fn parse_env_var_value_with_equals() {
let ev = parse_env_var("PATH=/usr/bin:/bin").unwrap();
assert_eq!(ev.name, "PATH");
assert_eq!(ev.value, "/usr/bin:/bin");
let ev2 = parse_env_var("FOO=a=b=c").unwrap();
assert_eq!(ev2.name, "FOO");
assert_eq!(ev2.value, "a=b=c");
}
#[test]
fn parse_env_var_empty_value() {
let ev = parse_env_var("EMPTY=").unwrap();
assert_eq!(ev.name, "EMPTY");
assert_eq!(ev.value, "");
}
#[test]
fn parse_env_var_no_equals() {
let err = parse_env_var("NOEQUALS").unwrap_err();
assert!(
err.contains("KEY=VALUE"),
"should tell user the expected format, got: {err}"
);
}
#[test]
fn parse_alias_validates_name() {
let a = parse_alias("valid=ls -la").unwrap();
assert_eq!(a.name, "valid");
assert_eq!(a.command, "ls -la");
let a2 = parse_alias("my-alias=git status").unwrap();
assert_eq!(a2.name, "my-alias");
assert_eq!(a2.command, "git status");
assert!(parse_alias("bad;name=cmd").is_err());
}
#[test]
fn parse_alias_command_with_equals() {
let a = parse_alias("env=FOO=bar baz").unwrap();
assert_eq!(a.name, "env");
assert_eq!(a.command, "FOO=bar baz");
}
#[test]
fn to_posix_string_folds_backslashes() {
assert_eq!(to_posix_string("C:\\Users\\foo"), "C:/Users/foo");
assert_eq!(to_posix_string("/home/foo"), "/home/foo");
assert_eq!(to_posix_string("relative/path"), "relative/path");
}
#[test]
fn posixify_text_borrows_when_no_backslash() {
let s = "no backslashes here";
let cow = posixify_text(s);
assert!(matches!(cow, std::borrow::Cow::Borrowed(_)));
assert_eq!(cow, "no backslashes here");
}
#[test]
fn posixify_text_owns_when_backslash() {
let s = "C:\\Users\\foo";
let cow = posixify_text(s);
assert!(matches!(cow, std::borrow::Cow::Owned(_)));
assert_eq!(cow, "C:/Users/foo");
}
#[test]
fn to_file_url_unix_form() {
assert_eq!(to_file_url("/home/foo"), "file:///home/foo");
}
#[test]
fn to_file_url_windows_form_folds_separators() {
assert_eq!(to_file_url("C:\\Users\\foo"), "file:///C:/Users/foo");
assert_eq!(to_file_url("C:/Users/foo"), "file:///C:/Users/foo");
}
#[test]
fn normalize_line_endings_borrows_when_lf_only() {
let s = "line1\nline2\n";
let cow = normalize_line_endings(s);
assert!(matches!(cow, std::borrow::Cow::Borrowed(_)));
assert_eq!(cow, "line1\nline2\n");
}
#[test]
fn normalize_line_endings_owns_when_crlf() {
let cow = normalize_line_endings("line1\r\nline2\r\n");
assert!(matches!(cow, std::borrow::Cow::Owned(_)));
assert_eq!(cow, "line1\nline2\n");
}
#[test]
fn normalize_for_snapshot_handles_crlf_backslash_and_nested_paths() {
let captured = " From file:///C:\\Users\\foo\\bare\\nested\\repo\r\n hi\r\n";
let bare = std::path::PathBuf::from("C:\\Users\\foo\\bare");
let nested = std::path::PathBuf::from("C:\\Users\\foo\\bare\\nested");
let out = normalize_for_snapshot(captured, &[(&bare, "<BARE>"), (&nested, "<NESTED>")]);
assert_eq!(out, " From file:///<NESTED>/repo\n hi\n");
}
#[test]
fn normalize_for_snapshot_skips_empty_path_keys() {
let captured = "no tempdir touched here\n";
let empty = std::path::PathBuf::new();
let out = normalize_for_snapshot(captured, &[(&empty, "<NEVER>")]);
assert_eq!(out, "no tempdir touched here\n");
}
#[test]
fn posixify_os_error_text_collapses_linux_form() {
let s = "file error: io error on /tmp/foo: File exists (os error 17)";
assert_eq!(
posixify_os_error_text(s),
"file error: io error on /tmp/foo: <os error>"
);
}
#[test]
fn posixify_os_error_text_collapses_windows_form() {
let s = "file error: io error on /tmp/foo: Cannot create a file when that file already exists. (os error 183)";
assert_eq!(
posixify_os_error_text(s),
"file error: io error on /tmp/foo: <os error>"
);
}
#[test]
fn posixify_os_error_text_handles_multiple_occurrences() {
let s = "first: foo (os error 1) and second: bar (os error 2)";
assert_eq!(
posixify_os_error_text(s),
"first: <os error> and second: <os error>"
);
}
#[test]
fn posixify_os_error_text_borrows_when_no_marker() {
let s = "no marker here";
let cow = posixify_os_error_text(s);
assert!(matches!(cow, std::borrow::Cow::Borrowed(_)));
}
#[test]
fn posixify_os_error_text_ignores_malformed_marker() {
let s = "weird (os error ) thing";
assert_eq!(posixify_os_error_text(s), s);
}
#[test]
fn posixify_os_error_text_collapses_libgit2_linux_form() {
let s = "failed to resolve path '<BARE>': No such file or directory; class=Os (2)";
assert_eq!(
posixify_os_error_text(s),
"failed to resolve path '<BARE>': <os error>; class=Os (2)"
);
}
#[test]
fn posixify_os_error_text_collapses_libgit2_windows_form() {
let s = "failed to resolve path '<BARE>': The system cannot find the file specified. — ; class=Os (2)";
assert_eq!(
posixify_os_error_text(s),
"failed to resolve path '<BARE>': <os error>; class=Os (2)"
);
}
#[test]
fn posixify_os_error_text_handles_mixed_libgit2_and_std_markers() {
let s = "first: foo; class=Os (2) — also: bar (os error 17)";
assert_eq!(
posixify_os_error_text(s),
"first: <os error>; class=Os (2) — also: <os error>"
);
}
#[test]
fn from_user_input_folds_backslashes() {
assert_eq!(
from_user_input("C:\\Users\\foo"),
std::path::PathBuf::from("C:/Users/foo")
);
}
#[test]
fn from_user_input_passthrough_for_posix_input() {
assert_eq!(
from_user_input("/etc/cfgd.yaml"),
std::path::PathBuf::from("/etc/cfgd.yaml")
);
}
#[test]
fn path_display_ext_display_posix_unix_passthrough() {
let p = std::path::Path::new("/home/foo/bar");
assert_eq!(p.display_posix(), "/home/foo/bar");
}
#[test]
fn path_display_ext_posix_composes_in_format() {
let p = std::path::Path::new("/home/foo");
let formatted = format!("Path is {}", p.posix());
assert_eq!(formatted, "Path is /home/foo");
}
#[cfg(windows)]
#[test]
fn path_display_ext_folds_backslashes_on_windows() {
let p = std::path::Path::new(r"C:\Users\foo");
assert_eq!(p.display_posix(), "C:/Users/foo");
assert_eq!(format!("{}", p.posix()), "C:/Users/foo");
}