use super::{
expand_path, get_config_lock_path, get_config_path, is_first_run, load_config, save_config, AliasExpansion,
CaseMatching, Config, ConfigLock, GeneralConfig,
};
use crate::test_support::TempHome;
use std::fs;
use std::path::PathBuf;
#[test]
fn test_config_default_theme() {
let config = Config::default();
assert_eq!(config.ui.theme, "default");
}
#[test]
fn test_config_default_keybind_mode() {
let config = Config::default();
assert_eq!(config.ui.keybind_mode, "vim");
}
#[test]
fn test_config_default_case_matching_is_smart() {
let config = Config::default();
assert!(matches!(config.search.case_matching, CaseMatching::Smart));
}
#[test]
fn test_config_default_normalize_is_true() {
let config = Config::default();
assert!(config.search.normalize);
}
#[test]
fn test_config_default_enable_regex_is_true() {
let config = Config::default();
assert!(config.search.enable_regex);
}
#[test]
fn test_config_default_substring_matching_is_true() {
let config = Config::default();
assert!(config.search.substring_matching);
}
#[test]
fn test_config_default_show_type_badges() {
let config = Config::default();
assert!(config.display.show_type_badges);
}
#[test]
fn test_config_default_syntax_highlighting() {
let config = Config::default();
assert!(config.display.syntax_highlighting);
}
#[test]
fn test_config_default_parse_comments() {
let config = Config::default();
assert!(config.display.parse_comments);
}
#[test]
fn test_config_default_shell_files_empty() {
let config = Config::default();
assert!(config.general.shell_files.is_empty());
}
#[test]
fn test_config_default_toml_format() {
let toml_str = toml::to_string_pretty(&Config::default()).expect("Should serialize");
insta::assert_snapshot!(toml_str);
}
#[test]
fn test_config_parse_valid_toml_with_custom_values() {
let toml_content = r#"
[general]
shell_files = ["~/.bashrc", "~/.zshrc"]
[search]
case_matching = "respect"
normalize = false
enable_regex = false
substring_matching = false
[ui]
theme = "dracula"
keybind_mode = "vim"
[display]
show_type_badges = false
syntax_highlighting = false
parse_comments = false
"#;
let config: Config = toml::from_str(toml_content).expect("Should parse valid TOML");
assert_eq!(config.ui.theme, "dracula");
assert_eq!(config.general.shell_files, vec!["~/.bashrc", "~/.zshrc"]);
assert!(!config.display.show_type_badges);
assert!(!config.display.syntax_highlighting);
assert!(!config.search.enable_regex);
assert!(!config.search.normalize);
assert!(matches!(config.search.case_matching, CaseMatching::Respect));
}
#[test]
fn test_config_parse_invalid_toml_returns_error() {
let invalid_toml = "this is not valid [[[ toml !!!";
let result: Result<Config, _> = toml::from_str(invalid_toml);
assert!(result.is_err(), "Invalid TOML should fail to parse");
}
#[test]
fn test_config_default_alias_expansion_is_name() {
let config = Config::default();
assert!(matches!(config.general.alias_expansion, AliasExpansion::Name));
}
#[test]
fn test_config_script_expansion_toml_format() {
let config = Config {
general: GeneralConfig {
alias_expansion: AliasExpansion::Script,
..Default::default()
},
..Config::default()
};
let toml_str = toml::to_string_pretty(&config).expect("Should serialize");
insta::assert_snapshot!(toml_str);
}
#[test]
fn test_config_parse_toml_with_alias_expansion_script() {
let toml_content = r#"
[general]
alias_expansion = "script"
[search]
case_matching = "smart"
normalize = true
enable_regex = true
substring_matching = true
[ui]
theme = "default"
keybind_mode = "vim"
[display]
show_type_badges = true
syntax_highlighting = true
parse_comments = true
"#;
let config: Config = toml::from_str(toml_content).expect("Should parse");
assert!(matches!(config.general.alias_expansion, AliasExpansion::Script));
}
#[test]
fn test_get_config_path_contains_alf_segment() {
let path = get_config_path().expect("Should succeed when HOME is set");
let path_str = path.to_str().unwrap();
assert!(path_str.contains("alf"), "Path should contain 'alf': {}", path_str);
}
#[test]
fn test_get_config_path_ends_with_config_toml() {
let path = get_config_path().expect("Should succeed when HOME is set");
let path_str = path.to_str().unwrap();
assert!(path_str.ends_with("config.toml"), "Path should end with config.toml: {}", path_str);
}
#[test]
fn test_get_config_path_falls_back_to_userprofile_when_home_is_empty() {
let mut home = TempHome::new();
let profile = home.path().join("profile");
home.set_env("USERPROFILE", &profile);
home.set_env("HOME", "");
let path = get_config_path().expect("Should resolve a home from USERPROFILE");
assert!(path.is_absolute(), "An empty HOME should not produce a relative config path, got {}", path.display());
assert_eq!(path, profile.join(".config").join("alf").join("config.toml"));
assert_eq!(expand_path("~"), profile, "Both resolvers should agree on the fallback home");
}
#[test]
fn test_get_config_path_falls_back_when_home_is_unset() {
let mut home = TempHome::new();
let profile = home.path().join("profile");
home.set_env("USERPROFILE", &profile);
home.unset_env("HOME");
assert_eq!(
get_config_path().expect("Should resolve a home"),
profile.join(".config").join("alf").join("config.toml")
);
}
#[cfg(unix)]
#[test]
fn test_home_resolution_accepts_non_unicode_values() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let mut home = TempHome::new();
let mut bytes = home.path().into_os_string().into_vec();
bytes.extend_from_slice(b"/home\xff");
let non_unicode = PathBuf::from(OsString::from_vec(bytes));
home.set_env("HOME", &non_unicode);
assert_eq!(
get_config_path().expect("A non-Unicode HOME should still resolve"),
non_unicode.join(".config").join("alf").join("config.toml")
);
assert_eq!(expand_path("~"), non_unicode, "Both resolvers should accept a non-Unicode home");
}
#[cfg(unix)]
#[test]
fn test_get_config_path_errors_when_no_home_can_be_resolved() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let mut home = TempHome::new();
home.set_env("HOME", "");
home.set_env("USERPROFILE", OsString::from_vec(Vec::new()));
let resolved = get_config_path();
match dirs::home_dir() {
Some(fallback) => {
assert_eq!(
resolved.expect("Should use the platform fallback"),
fallback.join(".config").join("alf").join("config.toml")
);
},
None => {
assert!(resolved.is_err(), "With no home anywhere, the config path should be an error");
},
}
}
#[test]
fn test_expand_path_resolves_home_prefixes() {
let home = TempHome::new();
for (raw_path, expected) in [
("~/foo/bar", home.path().join("foo/bar")),
("$HOME/foo/bar", home.path().join("foo/bar")),
("~", home.path()),
("$HOME", home.path()),
] {
assert_eq!(expand_path(raw_path), expected, "Unexpected expansion for {raw_path}");
}
}
#[test]
fn test_expand_path_uses_the_same_home_as_the_config_path() {
let home = TempHome::new();
let config_path = get_config_path().expect("Should build config path");
assert!(
config_path.starts_with(expand_path("~")),
"`expand_path` and `get_config_path` should agree on home, got {} vs {}",
expand_path("~").display(),
config_path.display()
);
assert_eq!(expand_path("~"), home.path(), "Home should come from the environment, not the platform default");
}
#[test]
fn test_expand_path_passes_through_paths_without_a_home_prefix() {
let _home = TempHome::new();
for raw_path in ["/etc/shells", "relative/path"] {
assert_eq!(expand_path(raw_path), PathBuf::from(raw_path), "Unexpected expansion for {raw_path}");
}
}
#[test]
fn test_save_and_load_config_roundtrip() {
let _home = TempHome::new();
let mut config = Config::default();
config.ui.theme = "gruvbox".to_string();
config.display.syntax_highlighting = false;
save_config(&config).expect("Should save config");
let loaded = load_config().expect("Should load saved config");
assert_eq!(loaded.ui.theme, "gruvbox");
assert!(!loaded.display.syntax_highlighting);
}
#[test]
fn test_load_config_fails_when_missing() {
let _home = TempHome::new();
let result = load_config();
assert!(result.is_err(), "Should fail when config file does not exist");
}
#[test]
fn test_is_first_run_returns_true_when_no_config() {
let _home = TempHome::new();
let result = is_first_run().expect("Should succeed");
assert!(result, "Should be first run when no config file exists");
}
#[test]
fn test_is_first_run_returns_false_after_save() {
let _home = TempHome::new();
save_config(&Config::default()).expect("Should save config");
let result = is_first_run().expect("Should succeed");
assert!(!result, "Should not be first run after config is saved");
}
#[test]
fn test_save_config_leaves_no_temporary_files_behind() {
let _home = TempHome::new();
save_config(&Config::default()).expect("Should save config");
let config_dir =
get_config_path().expect("Should build config path").parent().expect("Should have a parent").to_path_buf();
let leftovers: Vec<String> = fs::read_dir(&config_dir)
.expect("Should read config dir")
.filter_map(|entry| entry.ok())
.map(|entry| entry.file_name().to_string_lossy().to_string())
.filter(|name| name.ends_with(".tmp"))
.collect();
assert!(leftovers.is_empty(), "A completed save should leave no temp files, found {leftovers:?}");
}
#[test]
fn test_config_lock_path_sits_beside_the_config_file() {
let _home = TempHome::new();
let config_path = get_config_path().expect("Should build config path");
let lock_path = get_config_lock_path().expect("Should build lock path");
assert_eq!(lock_path.parent(), config_path.parent(), "The lock should live in the config directory");
assert_eq!(lock_path.file_name().expect("Should have a file name"), "config.toml.lock");
}
#[test]
fn test_config_lock_is_created_before_any_config_exists() {
let _home = TempHome::new();
let lock = ConfigLock::acquire().expect("Should acquire the lock without a config file");
assert!(get_config_lock_path().expect("Should build lock path").exists(), "Acquiring should create the lock file");
drop(lock);
assert!(is_first_run().expect("Should succeed"), "The lock file alone should not count as a config");
}
#[test]
fn test_config_lock_can_be_reacquired_after_each_release() {
let _home = TempHome::new();
for attempt in 1..=3 {
let lock = ConfigLock::acquire().unwrap_or_else(|_| panic!("Should acquire the lock on attempt {attempt}"));
drop(lock);
}
}