use std::fs;
use std::path::PathBuf;
use redisctl_core::config::Config;
use tempfile::TempDir;
#[cfg(unix)]
fn is_root() -> bool {
std::process::Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim() == "0")
.unwrap_or(false)
}
#[test]
fn load_from_nonexistent_path_returns_default_config() {
let path = PathBuf::from("/tmp/redisctl-test-nonexistent/does/not/exist/config.toml");
assert!(!path.exists());
let config = Config::load_from_path(&path).expect("should not panic or error on missing path");
assert!(config.profiles.is_empty());
assert!(config.default_cloud.is_none());
assert!(config.default_enterprise.is_none());
assert!(config.default_database.is_none());
}
#[test]
fn load_empty_config_file_returns_default_config() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.toml");
fs::write(&config_path, "").unwrap();
let config = Config::load_from_path(&config_path).expect("empty file should parse as default");
assert!(config.profiles.is_empty());
assert!(config.default_cloud.is_none());
assert!(config.default_enterprise.is_none());
assert!(config.default_database.is_none());
}
#[test]
fn load_corrupt_toml_returns_parse_error() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.toml");
fs::write(&config_path, "[[[broken").unwrap();
let result = Config::load_from_path(&config_path);
assert!(result.is_err(), "corrupt TOML should produce an error");
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("parse") || msg.contains("Parse"),
"error should mention parsing: {msg}"
);
}
#[test]
fn load_profile_missing_required_fields_returns_error() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.toml");
let content = r#"
[profiles.broken]
deployment_type = "cloud"
"#;
fs::write(&config_path, content).unwrap();
let result = Config::load_from_path(&config_path);
assert!(
result.is_err(),
"incomplete profile should produce an error"
);
}
#[test]
fn load_config_with_unknown_fields_ignores_them() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.toml");
let content = r#"
unknown_top_level_key = "hello"
[profiles.mydb]
deployment_type = "database"
host = "localhost"
port = 6379
totally_unknown_field = true
"#;
fs::write(&config_path, content).unwrap();
let config =
Config::load_from_path(&config_path).expect("unknown fields should be silently ignored");
assert!(config.profiles.contains_key("mydb"));
}
#[cfg(unix)]
#[test]
fn load_unreadable_file_returns_clear_error() {
use std::os::unix::fs::PermissionsExt;
if is_root() {
eprintln!("skipping test: running as root");
return;
}
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.toml");
fs::write(&config_path, "# valid toml").unwrap();
fs::set_permissions(&config_path, fs::Permissions::from_mode(0o000)).unwrap();
let result = Config::load_from_path(&config_path);
assert!(result.is_err(), "unreadable file should produce an error");
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("load") || msg.contains("Load") || msg.contains("Permission"),
"error should reference loading or permissions: {msg}"
);
fs::set_permissions(&config_path, fs::Permissions::from_mode(0o644)).unwrap();
}
#[cfg(unix)]
#[test]
fn save_to_readonly_directory_returns_clear_error() {
use std::os::unix::fs::PermissionsExt;
if is_root() {
eprintln!("skipping test: running as root");
return;
}
let dir = TempDir::new().unwrap();
let readonly_dir = dir.path().join("readonly");
fs::create_dir(&readonly_dir).unwrap();
fs::set_permissions(&readonly_dir, fs::Permissions::from_mode(0o444)).unwrap();
let config_path = readonly_dir.join("config.toml");
let config = Config::default();
let result = config.save_to_path(&config_path);
assert!(
result.is_err(),
"saving to read-only directory should produce an error"
);
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("save") || msg.contains("Save") || msg.contains("Permission"),
"error should reference saving or permissions: {msg}"
);
fs::set_permissions(&readonly_dir, fs::Permissions::from_mode(0o755)).unwrap();
}
#[test]
fn load_profile_without_tags_defaults_to_empty_vec() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.toml");
let content = r#"
[profiles.mycloud]
deployment_type = "cloud"
api_key = "key"
api_secret = "secret"
api_url = "https://api.redislabs.com/v1"
"#;
fs::write(&config_path, content).unwrap();
let config = Config::load_from_path(&config_path).expect("should load without tags field");
let profile = config.profiles.get("mycloud").unwrap();
assert!(profile.tags.is_empty(), "tags should default to empty vec");
}
#[test]
fn tags_round_trip_save_and_reload() {
use redisctl_core::{DeploymentType, Profile, ProfileCredentials};
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.toml");
let mut config = Config::default();
config.set_profile(
"tagged".to_string(),
Profile {
deployment_type: DeploymentType::Cloud,
credentials: ProfileCredentials::Cloud {
api_key: "k".to_string(),
api_secret: "s".to_string(),
api_url: "https://api.redislabs.com/v1".to_string(),
},
files_api_key: None,
resilience: None,
tags: vec!["prod".to_string(), "us-east".to_string()],
},
);
config
.save_to_path(&config_path)
.expect("save should succeed");
let reloaded = Config::load_from_path(&config_path).expect("reload should succeed");
let profile = reloaded.profiles.get("tagged").unwrap();
assert_eq!(profile.tags, vec!["prod", "us-east"]);
}
#[test]
fn empty_tags_not_serialized() {
use redisctl_core::{DeploymentType, Profile, ProfileCredentials};
let mut config = Config::default();
config.set_profile(
"plain".to_string(),
Profile {
deployment_type: DeploymentType::Database,
credentials: ProfileCredentials::Database {
host: "localhost".to_string(),
port: 6379,
password: None,
tls: false,
username: "default".to_string(),
database: 0,
},
files_api_key: None,
resilience: None,
tags: vec![],
},
);
let serialized = toml::to_string(&config).unwrap();
assert!(
!serialized.contains("tags"),
"empty tags should not appear in serialized output: {serialized}"
);
}