use std::fs;
use serial_test::serial;
use super::*;
struct XdgGuard {
original: Option<String>
}
impl XdgGuard {
fn set(dir: &std::path::Path) -> Self {
let original = std::env::var("XDG_CONFIG_HOME").ok();
unsafe {
std::env::set_var("XDG_CONFIG_HOME", dir);
}
Self {
original
}
}
}
impl Drop for XdgGuard {
fn drop(&mut self) {
unsafe {
match &self.original {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME")
}
}
}
}
#[test]
fn default_config_has_no_token() {
let cfg = AppConfig::default();
assert!(cfg.token.is_none());
}
#[test]
fn path_returns_valid_toml_path() {
let path = AppConfig::path().expect("config path should resolve");
assert!(path.to_string_lossy().ends_with("config.toml"));
}
#[test]
fn path_contains_twc_rs_dir() {
let path = AppConfig::path().expect("config path should resolve");
let path_str = path.to_string_lossy();
assert!(
path_str.contains("twc-rs"),
"Path should contain twc-rs dir: {path_str}"
);
}
#[test]
#[serial]
fn load_returns_default_when_no_file() {
let dir = tempfile::tempdir().unwrap();
let _guard = XdgGuard::set(dir.path());
let cfg = AppConfig::load().expect("load should succeed");
assert!(cfg.token.is_none());
}
#[test]
#[serial]
fn load_returns_default_when_dir_exists_but_no_file() {
let dir = tempfile::tempdir().unwrap();
let config_dir = dir.path().join("twc-rs");
fs::create_dir_all(&config_dir).unwrap();
let _guard = XdgGuard::set(dir.path());
let cfg = AppConfig::load().expect("load should succeed");
assert!(cfg.token.is_none());
}
#[test]
#[serial]
fn load_parses_valid_toml_with_token() {
let dir = tempfile::tempdir().unwrap();
let config_dir = dir.path().join("twc-rs");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("config.toml"),
"token = \"my-secret-token\"\n"
)
.unwrap();
let _guard = XdgGuard::set(dir.path());
let cfg = AppConfig::load().expect("load should succeed");
assert_eq!(cfg.token.as_deref(), Some("my-secret-token"));
}
#[test]
#[serial]
fn load_returns_config_parse_error_for_invalid_toml() {
let dir = tempfile::tempdir().unwrap();
let config_dir = dir.path().join("twc-rs");
fs::create_dir_all(&config_dir).unwrap();
fs::write(config_dir.join("config.toml"), "this is not [[valid toml").unwrap();
let _guard = XdgGuard::set(dir.path());
let err = AppConfig::load().unwrap_err();
assert!(
err.to_string().contains("failed to parse config"),
"Expected parse error, got: {err}"
);
}
#[test]
#[serial]
fn save_and_load_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let _guard = XdgGuard::set(dir.path());
let cfg = AppConfig {
token: Some("roundtrip-token".to_string()),
..AppConfig::default()
};
cfg.save().expect("save should succeed");
let loaded = AppConfig::load().expect("load should succeed");
assert_eq!(loaded.token.as_deref(), Some("roundtrip-token"));
let path = AppConfig::path().unwrap();
assert!(path.exists());
}
#[test]
#[serial]
fn save_creates_parent_directories() {
let dir = tempfile::tempdir().unwrap();
let _guard = XdgGuard::set(dir.path());
let cfg = AppConfig {
token: Some("test".to_string()),
..AppConfig::default()
};
cfg.save().expect("save should succeed");
let path = AppConfig::path().unwrap();
assert!(path.parent().unwrap().exists());
}
#[test]
#[serial]
fn load_without_token_field() {
let dir = tempfile::tempdir().unwrap();
let config_dir = dir.path().join("twc-rs");
fs::create_dir_all(&config_dir).unwrap();
fs::write(config_dir.join("config.toml"), "").unwrap();
let _guard = XdgGuard::set(dir.path());
let cfg = AppConfig::load().expect("load should succeed");
assert!(cfg.token.is_none());
}
#[test]
#[serial]
fn save_overwrites_existing_config() {
let dir = tempfile::tempdir().unwrap();
let _guard = XdgGuard::set(dir.path());
let cfg1 = AppConfig {
token: Some("first".to_string()),
..AppConfig::default()
};
cfg1.save().unwrap();
let cfg2 = AppConfig {
token: Some("second".to_string()),
..AppConfig::default()
};
cfg2.save().unwrap();
let loaded = AppConfig::load().unwrap();
assert_eq!(loaded.token.as_deref(), Some("second"));
}
#[cfg(unix)]
#[test]
#[serial]
fn save_restricts_file_and_dir_permissions() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let _guard = XdgGuard::set(dir.path());
let cfg = AppConfig {
token: Some("secret".to_string()),
..AppConfig::default()
};
cfg.save().expect("save should succeed");
let path = AppConfig::path().unwrap();
let file_mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(file_mode, 0o600, "config file must be owner-only");
let dir_mode = fs::metadata(path.parent().unwrap())
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(dir_mode, 0o700, "config dir must be owner-only");
}
#[cfg(unix)]
#[test]
#[serial]
fn save_tightens_permissions_on_preexisting_file() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let _guard = XdgGuard::set(dir.path());
let path = AppConfig::path().unwrap();
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(&path, "token = \"stale\"\n").unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
let cfg = AppConfig {
token: Some("fresh".to_string()),
..AppConfig::default()
};
cfg.save().expect("save should succeed");
let file_mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(
file_mode, 0o600,
"existing config file must be re-restricted"
);
}
#[test]
fn serialization_roundtrip_via_string() {
let cfg = AppConfig {
token: Some("my-secret-token".to_string()),
..AppConfig::default()
};
let toml_str = toml::to_string(&cfg).unwrap();
let loaded: AppConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(loaded.token.as_deref(), Some("my-secret-token"));
}
#[test]
fn serialization_roundtrip_empty() {
let cfg = AppConfig::default();
let toml_str = toml::to_string(&cfg).unwrap();
let loaded: AppConfig = toml::from_str(&toml_str).unwrap();
assert!(loaded.token.is_none());
}
#[test]
fn deserialize_without_token_field() {
let cfg: AppConfig = toml::from_str("").unwrap();
assert!(cfg.token.is_none());
}
#[test]
fn deserialize_with_token() {
let cfg: AppConfig = toml::from_str("token = \"abc123\"\n").unwrap();
assert_eq!(cfg.token.as_deref(), Some("abc123"));
}
#[test]
fn deserialize_invalid_toml() {
let result = toml::from_str::<AppConfig>("not [[valid");
assert!(result.is_err());
}
#[test]
fn serialize_produces_valid_toml() {
let cfg = AppConfig {
token: Some("tok".to_string()),
..AppConfig::default()
};
let toml_str = toml::to_string(&cfg).unwrap();
assert!(toml_str.contains("token"));
assert!(toml_str.contains("tok"));
}
#[test]
fn serialize_skips_none_token() {
let cfg = AppConfig::default();
let toml_str = toml::to_string(&cfg).unwrap();
assert!(!toml_str.contains("token"));
}
#[test]
fn save_and_load_roundtrip_filesystem() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("config.toml");
let cfg = AppConfig {
token: Some("roundtrip-token".to_string()),
..AppConfig::default()
};
let content = toml::to_string(&cfg).unwrap();
fs::write(&file_path, content).unwrap();
let read_back = fs::read_to_string(&file_path).unwrap();
let loaded: AppConfig = toml::from_str(&read_back).unwrap();
assert_eq!(loaded.token.as_deref(), Some("roundtrip-token"));
}
#[test]
fn save_creates_parent_directories_filesystem() {
let dir = tempfile::tempdir().unwrap();
let config_dir = dir.path().join("twc-rs");
fs::create_dir_all(&config_dir).unwrap();
let file_path = config_dir.join("config.toml");
let cfg = AppConfig {
token: Some("test".to_string()),
..AppConfig::default()
};
let content = toml::to_string(&cfg).unwrap();
fs::write(&file_path, content).unwrap();
assert!(file_path.exists());
assert!(file_path.parent().unwrap().exists());
}
#[test]
fn load_from_filesystem_invalid_toml() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("config.toml");
fs::write(&file_path, "this is not [[valid toml").unwrap();
let content = fs::read_to_string(&file_path).unwrap();
let result = toml::from_str::<AppConfig>(&content);
assert!(result.is_err());
}
#[test]
fn deserialize_with_extra_fields_ignored() {
let cfg: AppConfig = toml::from_str("token = \"abc\"\nother_field = true\n").unwrap();
assert_eq!(cfg.token.as_deref(), Some("abc"));
}
#[test]
fn clone_config() {
let cfg = AppConfig {
token: Some("cloned".to_string()),
..AppConfig::default()
};
let cloned = cfg;
assert_eq!(cloned.token.as_deref(), Some("cloned"));
}
#[test]
fn debug_format() {
let cfg = AppConfig {
token: Some("debug".to_string()),
..AppConfig::default()
};
let debug_str = format!("{cfg:?}");
assert!(debug_str.contains("AppConfig"));
assert!(debug_str.contains("debug"));
}
#[test]
fn language_locale_en() {
assert_eq!(Language::En.locale(), "en");
}
#[test]
fn language_locale_ru() {
assert_eq!(Language::Ru.locale(), "ru");
}
#[test]
fn language_default_is_en() {
assert_eq!(Language::default(), Language::En);
}
#[test]
fn token_for_none_returns_default_token() {
let cfg = AppConfig {
token: Some("default-tok".to_string()),
..AppConfig::default()
};
let got = cfg.token_for(None).unwrap();
assert_eq!(got.as_deref(), Some("default-tok"));
}
#[test]
fn token_for_none_returns_none_when_unset() {
let cfg = AppConfig::default();
assert!(cfg.token_for(None).unwrap().is_none());
}
#[test]
fn token_for_existing_profile() {
let mut cfg = AppConfig::default();
cfg.profiles
.insert("work".to_string(), "work-tok".to_string());
let got = cfg.token_for(Some("work")).unwrap();
assert_eq!(got.as_deref(), Some("work-tok"));
}
#[test]
fn token_for_missing_profile_errors() {
let cfg = AppConfig::default();
let err = cfg.token_for(Some("ghost")).unwrap_err();
assert!(err.to_string().contains("profile 'ghost' not found"));
}
#[test]
fn path_ends_with_expected_segments() {
let path = AppConfig::path().unwrap();
assert!(path.ends_with("twc-rs/config.toml"));
}
#[test]
fn save_to_fails_when_path_is_a_directory() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("as-dir");
fs::create_dir(&target).unwrap();
let cfg = AppConfig {
token: Some("x".to_string()),
..AppConfig::default()
};
let err = cfg.save_to(&target).unwrap_err();
assert!(err.to_string().contains("failed to write"));
}
#[cfg(unix)]
#[test]
fn restrict_dir_permissions_errors_on_missing_dir() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("does-not-exist");
let err = super::restrict_dir_permissions(&missing).unwrap_err();
assert!(err.to_string().contains("failed to set permissions"));
}
#[test]
fn save_to_fails_when_parent_is_a_file() {
let dir = tempfile::tempdir().unwrap();
let file_as_parent = dir.path().join("afile");
fs::write(&file_as_parent, "x").unwrap();
let target = file_as_parent.join("nested").join("config.toml");
let cfg = AppConfig {
token: Some("x".to_string()),
..AppConfig::default()
};
let err = cfg.save_to(&target).unwrap_err();
assert!(err.to_string().contains("failed to create dir"));
}