#![allow(unsafe_code)]
use hyperi_rustlib::config::{Config, ConfigOptions};
use std::fs;
use tempfile::TempDir;
struct EnvGuard {
vars: Vec<String>,
}
impl EnvGuard {
fn new(vars: &[(&str, &str)]) -> Self {
for (k, v) in vars {
unsafe { std::env::set_var(k, v) };
}
Self {
vars: vars.iter().map(|(k, _)| k.to_string()).collect(),
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for var in &self.vars {
unsafe { std::env::remove_var(var) };
}
}
}
fn setup_config_dir() -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().expect("failed to create temp dir");
let path = dir.path().to_path_buf();
fs::write(
path.join("defaults.yaml"),
r#"
log_level: debug
database:
host: default-host
port: 5432
username: default-user
timeout: 30s
cache:
enabled: false
"#,
)
.expect("failed to write defaults.yaml");
fs::write(
path.join("settings.yaml"),
r#"
app_name: test_app
database:
host: settings-host
username: settings-user
feature_flags:
- flag_a
- flag_b
"#,
)
.expect("failed to write settings.yaml");
fs::write(
path.join("settings.development.yaml"),
r#"
debug: true
database:
host: dev-host
password: dev-password
cache:
enabled: true
ttl: 60
"#,
)
.expect("failed to write settings.development.yaml");
fs::write(
path.join("settings.production.yaml"),
r#"
debug: false
database:
host: prod-host
password: prod-password
cache:
enabled: true
ttl: 3600
"#,
)
.expect("failed to write settings.production.yaml");
(dir, path)
}
#[test]
fn test_hardcoded_defaults_loaded() {
let temp_dir = TempDir::new().expect("failed to create temp dir");
let original_dir = std::env::current_dir().expect("failed to get current dir");
std::env::set_current_dir(temp_dir.path()).expect("failed to change to temp dir");
let config = Config::new(ConfigOptions {
load_dotenv: false,
load_home_dotenv: false,
..Default::default()
})
.expect("config should load");
std::env::set_current_dir(&original_dir).expect("failed to restore original dir");
assert_eq!(config.get_string("log_level"), Some("info".to_string()));
assert_eq!(config.get_string("log_format"), Some("auto".to_string()));
}
#[test]
fn test_hardcoded_defaults_are_lowest_priority() {
let (_dir, path) = setup_config_dir();
let config = Config::new(ConfigOptions {
config_paths: vec![path],
..Default::default()
})
.expect("config should load");
assert_eq!(config.get_string("log_level"), Some("debug".to_string()));
}
#[test]
fn test_defaults_yaml_loaded() {
let (_dir, path) = setup_config_dir();
let config = Config::new(ConfigOptions {
config_paths: vec![path],
..Default::default()
})
.expect("config should load");
assert_eq!(
config.get_string("database.host"),
Some("dev-host".to_string()) );
assert_eq!(config.get_int("database.port"), Some(5432)); }
#[test]
fn test_settings_yaml_overrides_defaults() {
let (_dir, path) = setup_config_dir();
let config = Config::new(ConfigOptions {
config_paths: vec![path],
..Default::default()
})
.expect("config should load");
assert_eq!(
config.get_string("database.username"),
Some("settings-user".to_string())
);
assert_eq!(config.get_string("app_name"), Some("test_app".to_string()));
}
#[test]
fn test_env_specific_settings_development() {
let (_dir, path) = setup_config_dir();
let _env = EnvGuard::new(&[("APP_ENV", "development")]);
let config = Config::new(ConfigOptions {
config_paths: vec![path],
app_env: Some("development".to_string()),
..Default::default()
})
.expect("config should load");
assert_eq!(
config.get_string("database.host"),
Some("dev-host".to_string())
);
assert_eq!(config.get_bool("debug"), Some(true));
assert_eq!(config.get_bool("cache.enabled"), Some(true));
assert_eq!(config.get_int("cache.ttl"), Some(60));
}
#[test]
fn test_env_specific_settings_production() {
let (_dir, path) = setup_config_dir();
let config = Config::new(ConfigOptions {
config_paths: vec![path],
app_env: Some("production".to_string()),
..Default::default()
})
.expect("config should load");
assert_eq!(
config.get_string("database.host"),
Some("prod-host".to_string())
);
assert_eq!(config.get_bool("debug"), Some(false));
assert_eq!(config.get_int("cache.ttl"), Some(3600));
}
#[test]
fn test_dotenv_file_loaded() {
let (_dir, path) = setup_config_dir();
let dotenv_path = path.join(".env");
fs::write(&dotenv_path, "DOTENV_TEST_VAR=from_dotenv\n").expect("failed to write .env");
let original_dir = std::env::current_dir().expect("failed to get cwd");
std::env::set_current_dir(&path).expect("failed to change dir");
let config = Config::new(ConfigOptions {
env_prefix: "DOTENV_TEST".to_string(),
config_paths: vec![path.clone()],
load_dotenv: true,
..Default::default()
})
.expect("config should load");
std::env::set_current_dir(original_dir).expect("failed to restore dir");
assert_eq!(config.get_string("var"), Some("from_dotenv".to_string()));
unsafe { std::env::remove_var("DOTENV_TEST_VAR") };
}
#[test]
fn test_env_overrides_file() {
let (_dir, path) = setup_config_dir();
let _env = EnvGuard::new(&[("PARITY_DATABASE__HOST", "env-host")]);
let config = Config::new(ConfigOptions {
env_prefix: "PARITY".to_string(),
config_paths: vec![path],
..Default::default()
})
.expect("config should load");
assert_eq!(
config.get_string("database.host"),
Some("env-host".to_string())
);
}
#[test]
fn test_env_var_flat_key() {
let _env = EnvGuard::new(&[("FLAT_LOG_LEVEL", "warn")]);
let config = Config::new(ConfigOptions {
env_prefix: "FLAT".to_string(),
..Default::default()
})
.expect("config should load");
assert_eq!(config.get_string("log_level"), Some("warn".to_string()));
}
#[test]
fn test_env_var_nested_key() {
let _env = EnvGuard::new(&[("NESTED_DATABASE__HOST", "nested-host")]);
let config = Config::new(ConfigOptions {
env_prefix: "NESTED".to_string(),
..Default::default()
})
.expect("config should load");
assert_eq!(
config.get_string("database.host"),
Some("nested-host".to_string())
);
}
#[test]
fn test_env_var_deeply_nested() {
let _env = EnvGuard::new(&[("DEEP2_CACHE__REDIS__ENABLED", "true")]);
let config = Config::new(ConfigOptions {
env_prefix: "DEEP2".to_string(),
..Default::default()
})
.expect("config should load");
assert_eq!(config.get_bool("cache.redis.enabled"), Some(true));
}
#[test]
fn test_cli_args_highest_priority() {
let (_dir, path) = setup_config_dir();
let _env = EnvGuard::new(&[("CLI_DATABASE__HOST", "env-host")]);
let config = Config::new(ConfigOptions {
env_prefix: "CLI".to_string(),
config_paths: vec![path],
..Default::default()
})
.expect("config should load");
#[derive(serde::Serialize)]
struct CliArgs {
database: Database,
}
#[derive(serde::Serialize)]
struct Database {
host: String,
}
let cli = CliArgs {
database: Database {
host: "cli-host".to_string(),
},
};
let config = config.merge_cli(cli);
assert_eq!(
config.get_string("database.host"),
Some("cli-host".to_string())
);
}
#[test]
fn test_full_cascade_priority() {
let (_dir, path) = setup_config_dir();
let _env = EnvGuard::new(&[("CASCADE_DATABASE__HOST", "env-host")]);
let config = Config::new(ConfigOptions {
env_prefix: "CASCADE".to_string(),
config_paths: vec![path],
app_env: Some("development".to_string()),
..Default::default()
})
.expect("config should load");
assert_eq!(
config.get_string("database.host"),
Some("env-host".to_string())
);
assert_eq!(config.get_bool("debug"), Some(true));
assert_eq!(config.get_string("app_name"), Some("test_app".to_string()));
assert_eq!(config.get_int("database.port"), Some(5432));
assert_eq!(config.get_string("log_format"), Some("auto".to_string()));
}
#[test]
fn test_get_bool_from_string() {
let _env = EnvGuard::new(&[("BOOL_ENABLED", "true")]);
let config = Config::new(ConfigOptions {
env_prefix: "BOOL".to_string(),
..Default::default()
})
.expect("config should load");
assert_eq!(config.get_bool("enabled"), Some(true));
}
#[test]
fn test_get_int_from_string() {
let _env = EnvGuard::new(&[("INT_PORT", "8080")]);
let config = Config::new(ConfigOptions {
env_prefix: "INT".to_string(),
..Default::default()
})
.expect("config should load");
assert_eq!(config.get_int("port"), Some(8080));
}
#[test]
fn test_duration_parsing() {
let (_dir, path) = setup_config_dir();
let config = Config::new(ConfigOptions {
config_paths: vec![path],
..Default::default()
})
.expect("config should load");
let duration = config.get_duration("database.timeout");
assert_eq!(duration, Some(std::time::Duration::from_secs(30)));
}
#[test]
fn test_missing_key_returns_none() {
let config = Config::new(ConfigOptions::default()).expect("config should load");
assert_eq!(config.get_string("nonexistent.key"), None);
assert_eq!(config.get_int("nonexistent.key"), None);
assert_eq!(config.get_bool("nonexistent.key"), None);
}
#[test]
fn test_contains_key() {
let config = Config::new(ConfigOptions::default()).expect("config should load");
assert!(config.contains("log_level"));
assert!(config.contains("log_format"));
assert!(!config.contains("nonexistent.key"));
}
#[test]
fn test_unmarshal_struct() {
let (_dir, path) = setup_config_dir();
let config = Config::new(ConfigOptions {
config_paths: vec![path],
..Default::default()
})
.expect("config should load");
#[derive(Debug, serde::Deserialize, PartialEq)]
struct Database {
host: String,
port: i64,
username: String,
}
let db: Database = config.unmarshal_key("database").expect("should unmarshal");
assert_eq!(db.host, "dev-host"); assert_eq!(db.port, 5432); assert_eq!(db.username, "settings-user"); }
#[test]
fn test_app_name_default_is_none() {
let opts = ConfigOptions::default();
assert!(opts.app_name.is_none());
}
#[test]
fn test_load_home_dotenv_default_is_false() {
let opts = ConfigOptions::default();
assert!(!opts.load_home_dotenv);
}
#[test]
fn test_config_with_app_name_extra_path() {
let dir = TempDir::new().expect("failed to create temp dir");
let app_config = dir.path().to_path_buf();
fs::write(
app_config.join("settings.yaml"),
"user_setting: from_user_dir\n",
)
.expect("write");
let config = Config::new(ConfigOptions {
app_name: Some("testapp".to_string()),
config_paths: vec![app_config],
load_dotenv: false,
..Default::default()
})
.expect("config should load");
assert_eq!(
config.get_string("user_setting"),
Some("from_user_dir".to_string())
);
}
#[test]
fn test_app_name_from_env_does_not_panic() {
let empty_dir = TempDir::new().expect("failed to create temp dir");
let _env = EnvGuard::new(&[("APP_NAME", "nonexistent_app_12345")]);
let config = Config::new(ConfigOptions {
load_dotenv: false,
config_paths: vec![empty_dir.path().to_path_buf()],
..Default::default()
})
.expect("config should load even with non-existent app dir");
assert_eq!(config.get_string("log_level"), Some("info".to_string()));
}