use serde::{Deserialize, Serialize};
use serial_test::serial;
use settings_loader::LayerBuilder;
use std::fs;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
struct TestConfig {
#[serde(default)]
app_name: String,
#[serde(default)]
port: u16,
#[serde(default)]
debug: bool,
#[serde(default)]
db: TestDatabaseSettings,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
struct TestDatabaseSettings {
host: String,
user: Option<String>,
password: String,
}
#[test]
fn test_layer_builder_new() {
let builder = LayerBuilder::new();
assert!(builder.is_empty());
}
#[test]
fn test_layer_builder_with_path() {
let builder = LayerBuilder::new().with_path("/path/to/config.yaml");
assert_eq!(builder.layer_count(), 1);
assert!(builder.has_path_layer());
}
#[test]
fn test_layer_builder_with_env_var() {
let builder = LayerBuilder::new().with_env_var("APP_CONFIG_PATH");
assert_eq!(builder.layer_count(), 1);
assert!(builder.has_env_var_layer("APP_CONFIG_PATH"));
}
#[test]
fn test_layer_builder_with_secrets() {
let builder = LayerBuilder::new().with_secrets("/path/to/secrets.yaml");
assert_eq!(builder.layer_count(), 1);
assert!(builder.has_secrets_layer());
}
#[test]
fn test_layer_builder_with_env_vars() {
let builder = LayerBuilder::new().with_env_vars("APP", "__");
assert_eq!(builder.layer_count(), 1);
assert!(builder.has_env_vars_layer("APP", "__"));
}
#[test]
fn test_layer_builder_multiple_layers() {
let builder = LayerBuilder::new()
.with_path("/path/to/base.yaml")
.with_path("/path/to/override.yaml")
.with_env_vars("APP", "__");
assert_eq!(builder.layer_count(), 3);
let layers = builder.layers();
assert!(layers[0].is_path());
assert!(layers[1].is_path());
assert!(layers[2].is_env_vars());
}
#[test]
fn test_layer_precedence_yaml_override() {
let temp_dir = tempfile::tempdir().unwrap();
let base_path = temp_dir.path().join("base.yaml");
fs::write(
&base_path,
"app_name: MyApp\nport: 8000\ndebug: false\ndb:\n host: localhost\n password: base_pass",
)
.unwrap();
let override_path = temp_dir.path().join("override.yaml");
fs::write(&override_path, "port: 9000\ndebug: true").unwrap();
let builder = LayerBuilder::new().with_path(&base_path).with_path(&override_path);
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.port, 9000);
assert!(result.debug);
assert_eq!(result.app_name, "MyApp");
assert_eq!(result.db.host, "localhost");
}
#[test]
fn test_path_layer_missing_file() {
let builder = LayerBuilder::new().with_path("/nonexistent/path/config.yaml");
let result = builder.build();
assert!(result.is_err());
}
#[test]
#[serial]
fn test_env_var_layer_missing_env() {
std::env::remove_var("NONEXISTENT_CONFIG_PATH");
let builder = LayerBuilder::new().with_env_var("NONEXISTENT_CONFIG_PATH");
let result = builder.build();
assert!(result.is_ok());
}
#[test]
#[serial]
fn test_env_var_layer_with_set_env() {
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("config.yaml");
fs::write(
&config_path,
"app_name: EnvVarApp\nport: 3000\ndebug: true\ndb:\n host: host.local\n password: env_pass",
)
.unwrap();
std::env::set_var("TEST_CONFIG_PATH", config_path.to_string_lossy().to_string());
let builder = LayerBuilder::new().with_env_var("TEST_CONFIG_PATH");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.app_name, "EnvVarApp");
assert_eq!(result.port, 3000);
std::env::remove_var("TEST_CONFIG_PATH");
}
#[test]
fn test_secrets_layer_override() {
let temp_dir = tempfile::tempdir().unwrap();
let base_path = temp_dir.path().join("base.yaml");
fs::write(
&base_path,
"app_name: SecretApp\nport: 5000\ndebug: false\ndb:\n host: localhost\n password: base_secret",
)
.unwrap();
let secrets_path = temp_dir.path().join("secrets.yaml");
fs::write(&secrets_path, "db:\n password: actual_secret_password").unwrap();
let builder = LayerBuilder::new().with_path(&base_path).with_secrets(&secrets_path);
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.db.password, "actual_secret_password");
assert_eq!(result.app_name, "SecretApp");
}
#[test]
#[serial]
fn test_env_vars_layer_integration() {
let temp_dir = tempfile::tempdir().unwrap();
let base_path = temp_dir.path().join("base.yaml");
fs::write(
&base_path,
"app_name: EnvVarsApp\nport: 7000\ndebug: false\ndb:\n host: localhost\n password: pass",
)
.unwrap();
std::env::set_var("MYAPP_DB_HOST", "prod.host.com");
std::env::set_var("MYAPP_PORT", "8888");
let builder = LayerBuilder::new().with_path(&base_path).with_env_vars("MYAPP", "_");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.port, 8888);
assert_eq!(result.db.host, "prod.host.com");
assert_eq!(result.app_name, "EnvVarsApp");
std::env::remove_var("MYAPP_PORT");
std::env::remove_var("MYAPP_DB_HOST");
}
#[test]
fn test_multiple_file_formats_yaml() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("config.yaml");
fs::write(
&path,
"app_name: YamlApp\nport: 6000\ndebug: false\ndb:\n host: db.yaml\n password: pass_yaml",
)
.unwrap();
let builder = LayerBuilder::new().with_path(&path);
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.app_name, "YamlApp");
assert_eq!(result.db.host, "db.yaml");
}
#[test]
fn test_multiple_file_formats_json() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("config.json");
fs::write(
&path,
r#"{"app_name":"JsonApp","port":7001,"debug":false,"db":{"host":"db.json","password":"pass_json"}}"#,
)
.unwrap();
let builder = LayerBuilder::new().with_path(&path);
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.app_name, "JsonApp");
assert_eq!(result.db.host, "db.json");
}
#[test]
fn test_multiple_file_formats_toml() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("config.toml");
fs::write(
&path,
r#"app_name = "TomlApp"
port = 7002
debug = false
[db]
host = "db.toml"
password = "pass_toml"
"#,
)
.unwrap();
let builder = LayerBuilder::new().with_path(&path);
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.app_name, "TomlApp");
assert_eq!(result.db.host, "db.toml");
}
#[test]
#[serial]
fn test_turtle_scenario() {
let temp_dir = tempfile::tempdir().unwrap();
let user_path = temp_dir.path().join("user.yaml");
fs::write(
&user_path,
"app_name: Turtle\nport: 7800\ndebug: false\ndb:\n host: user.host\n password: user_pass",
)
.unwrap();
let project_path = temp_dir.path().join("project.yaml");
fs::write(&project_path, "port: 7801\ndb:\n host: project.host").unwrap();
let secrets_path = temp_dir.path().join("secrets.yaml");
fs::write(&secrets_path, "db:\n password: actual_secret").unwrap();
let builder = LayerBuilder::new()
.with_path(&user_path)
.with_path(&project_path)
.with_secrets(&secrets_path)
.with_env_vars("TURTLE", "_");
std::env::set_var("TURTLE_PORT", "7999");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.port, 7999); assert_eq!(result.db.password, "actual_secret"); assert_eq!(result.db.host, "project.host"); assert_eq!(result.app_name, "Turtle");
std::env::remove_var("TURTLE_PORT");
}
#[test]
fn test_empty_builder() {
let builder = LayerBuilder::new();
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
assert!(config.get_string("app_name").is_err());
}
#[test]
fn test_fluent_interface() {
let _builder = LayerBuilder::new()
.with_path("a.yaml")
.with_path("b.yaml")
.with_env_vars("APP", "__");
}
#[test]
fn test_layer_query_methods() {
let builder = LayerBuilder::new()
.with_path("config.yaml")
.with_env_var("CONFIG_VAR")
.with_secrets("secrets.yaml")
.with_env_vars("APP", "_");
assert_eq!(builder.layer_count(), 4);
assert!(!builder.is_empty());
let layers = builder.layers();
assert_eq!(layers.len(), 4);
}
#[test]
fn test_path_layer_extension_detection() {
let temp_dir = tempfile::tempdir().unwrap();
let yml_path = temp_dir.path().join("config.yml");
fs::write(
&yml_path,
"app_name: YmlApp\nport: 5555\ndebug: false\ndb:\n host: localhost\n password: yml_pass",
)
.unwrap();
let builder = LayerBuilder::new().with_path(&yml_path);
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.app_name, "YmlApp");
}
#[test]
fn test_secrets_layer_missing_file() {
let builder = LayerBuilder::new().with_secrets("/nonexistent/secrets.yaml");
let result = builder.build();
assert!(result.is_err());
}
#[test]
#[serial]
fn test_mixed_optional_layers() {
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("config.yaml");
fs::write(
&config_path,
"app_name: MixedApp\nport: 6666\ndebug: true\ndb:\n host: mixed.host\n password: mixed_pass",
)
.unwrap();
std::env::remove_var("MISSING_CONFIG");
let builder = LayerBuilder::new()
.with_path(&config_path)
.with_env_var("MISSING_CONFIG") .with_env_vars("MIXED", "_");
let result = builder.build();
assert!(result.is_ok());
}
#[test]
fn test_layer_builder_traits() {
let builder1 = LayerBuilder::new().with_path("config.yaml").with_env_vars("APP", "_");
let debug_str = format!("{:?}", builder1);
assert!(!debug_str.is_empty());
}
#[test]
fn test_builder_order_matters() {
let temp_dir = tempfile::tempdir().unwrap();
let base_path = temp_dir.path().join("base.yaml");
fs::write(
&base_path,
"port: 1000\napp_name: Base\ndebug: false\ndb:\n host: base\n password: base",
)
.unwrap();
let override_path = temp_dir.path().join("override.yaml");
fs::write(&override_path, "port: 2000").unwrap();
let builder1 = LayerBuilder::new().with_path(&base_path).with_path(&override_path);
let config1 = builder1.build().unwrap().build().unwrap();
let result1: TestConfig = config1.try_deserialize().unwrap();
assert_eq!(result1.port, 2000);
let builder2 = LayerBuilder::new().with_path(&override_path).with_path(&base_path);
let config2 = builder2.build().unwrap().build().unwrap();
let result2: TestConfig = config2.try_deserialize().unwrap();
assert_eq!(result2.port, 1000); }
#[test]
#[serial]
fn test_comprehensive_real_world_scenario() {
let temp_dir = tempfile::tempdir().unwrap();
let app_config = temp_dir.path().join("application.yaml");
fs::write(
&app_config,
"app_name: RealWorldApp\nport: 8000\ndebug: false\ndb:\n host: localhost\n password: default",
)
.unwrap();
let prod_config = temp_dir.path().join("production.yaml");
fs::write(&prod_config, "debug: false\ndb:\n host: prod.db.com").unwrap();
let secrets = temp_dir.path().join("secrets.yaml");
fs::write(&secrets, "db:\n password: prod_secret_password_123").unwrap();
let runtime_config = temp_dir.path().join("runtime.yaml");
fs::write(&runtime_config, "app_name: RuntimeOverride").unwrap();
std::env::set_var("RUNTIME_CONFIG", runtime_config.to_string_lossy().to_string());
let builder = LayerBuilder::new()
.with_path(&app_config) .with_path(&prod_config) .with_env_var("RUNTIME_CONFIG") .with_secrets(&secrets) .with_env_vars("APP", "_");
std::env::set_var("APP_PORT", "9000");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.port, 9000); assert_eq!(result.db.password, "prod_secret_password_123"); assert_eq!(result.db.host, "prod.db.com"); assert_eq!(result.app_name, "RuntimeOverride"); assert!(!result.debug);
std::env::remove_var("RUNTIME_CONFIG");
std::env::remove_var("APP_PORT");
}
#[test]
#[serial]
fn test_debug_env_vars_behavior() {
use std::fs;
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("config.yaml");
fs::write(
&config_path,
"db_host: localhost\nport: 7000\ndb:\n user: default_user",
)
.unwrap();
std::env::set_var("MYAPP_DB_HOST", "prod.host.com");
std::env::set_var("MYAPP_PORT", "8888");
std::env::set_var("MYAPP_DB_USER", "admin");
std::env::set_var("MYAPP_DB__USER", "admin_nested");
let config = config::Config::builder()
.add_source(config::File::from(config_path.as_path()))
.add_source(
config::Environment::default()
.prefix("MYAPP")
.separator("_")
.try_parsing(true),
)
.build()
.unwrap();
println!("\n=== With separator '_' ===");
println!("db_host: {:?}", config.get_string("db_host"));
println!("db.host: {:?}", config.get_string("db.host"));
println!("port: {:?}", config.get_string("port"));
println!("db.user: {:?}", config.get_string("db.user"));
println!("db_user: {:?}", config.get_string("db_user"));
std::env::remove_var("MYAPP_DB_HOST");
std::env::remove_var("MYAPP_PORT");
std::env::remove_var("MYAPP_DB_USER");
std::env::remove_var("MYAPP_DB__USER");
std::env::set_var("MYAPP_DB__HOST", "prod.host.com");
std::env::set_var("MYAPP_PORT", "8888");
let config2 = config::Config::builder()
.add_source(config::File::from(config_path.as_path()))
.add_source(
config::Environment::default()
.prefix("MYAPP")
.separator("__")
.try_parsing(true),
)
.build()
.unwrap();
println!("\n=== With separator '__' ===");
println!("db.host: {:?}", config2.get_string("db.host"));
println!("port: {:?}", config2.get_string("port"));
std::env::remove_var("MYAPP_DB__HOST");
std::env::remove_var("MYAPP_PORT");
}
#[test]
#[serial]
fn test_env_vars_only() {
std::env::set_var("MYAPP_PORT", "8888");
std::env::set_var("MYAPP_DB_HOST", "prod.host.com");
std::env::set_var("MYAPP_DB_PASSWORD", "env_pass");
let builder = LayerBuilder::new().with_env_vars("MYAPP", "_");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.port, 8888);
assert_eq!(result.db.host, "prod.host.com");
assert_eq!(result.db.password, "env_pass");
assert_eq!(result.app_name, "");
std::env::remove_var("MYAPP_PORT");
std::env::remove_var("MYAPP_DB_HOST");
std::env::remove_var("MYAPP_DB_PASSWORD");
}
#[test]
fn test_layer_builder_with_env_search() {
let temp_dir = tempfile::tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
fs::create_dir_all(&config_dir).unwrap();
let prod_config = config_dir.join("production.yaml");
fs::write(&prod_config, "port: 9999\napp_name: ProdApp").unwrap();
let env = settings_loader::Environment::from("production");
let builder = LayerBuilder::new().with_env_search(env, vec![config_dir.clone()]);
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.port, 9999);
assert_eq!(result.app_name, "ProdApp");
}
#[test]
fn test_layer_builder_with_env_search_missing() {
let temp_dir = tempfile::tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
fs::create_dir_all(&config_dir).unwrap();
let env = settings_loader::Environment::from("production");
let builder = LayerBuilder::new().with_env_search(env, vec![config_dir]);
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
assert!(config.get_string("app_name").is_err());
}
#[test]
fn test_with_path_in_dir_discovers_yaml() {
let temp_dir = tempfile::tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
fs::create_dir_all(&config_dir).unwrap();
let yaml_path = config_dir.join("application.yaml");
fs::write(
&yaml_path,
"app_name: YamlDiscovered\nport: 5000\ndebug: true\ndb:\n host: yaml.host\n password: yaml_pass",
)
.unwrap();
let builder = LayerBuilder::new().with_path_in_dir(&config_dir, "application");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.app_name, "YamlDiscovered");
assert_eq!(result.port, 5000);
assert_eq!(result.db.host, "yaml.host");
}
#[test]
fn test_with_path_in_dir_discovers_toml() {
let temp_dir = tempfile::tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
fs::create_dir_all(&config_dir).unwrap();
let toml_path = config_dir.join("application.toml");
fs::write(
&toml_path,
r#"app_name = "TomlDiscovered"
port = 6000
debug = false
[db]
host = "toml.host"
password = "toml_pass"
"#,
)
.unwrap();
let builder = LayerBuilder::new().with_path_in_dir(&config_dir, "application");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.app_name, "TomlDiscovered");
assert_eq!(result.port, 6000);
assert_eq!(result.db.host, "toml.host");
}
#[test]
fn test_with_path_in_dir_discovers_json() {
let temp_dir = tempfile::tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
fs::create_dir_all(&config_dir).unwrap();
let json_path = config_dir.join("application.json");
fs::write(
&json_path,
r#"{"app_name":"JsonDiscovered","port":7000,"debug":true,"db":{"host":"json.host","password":"json_pass"}}"#,
)
.unwrap();
let builder = LayerBuilder::new().with_path_in_dir(&config_dir, "application");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.app_name, "JsonDiscovered");
assert_eq!(result.port, 7000);
assert_eq!(result.db.host, "json.host");
}
#[test]
fn test_with_path_in_dir_format_precedence() {
let temp_dir = tempfile::tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("application.yaml"),
"app_name: FromYaml\nport: 1111\ndebug: false\ndb:\n host: yaml\n password: pass",
)
.unwrap();
fs::write(
config_dir.join("application.toml"),
r#"app_name = "FromToml"
port = 2222
"#,
)
.unwrap();
fs::write(
config_dir.join("application.json"),
r#"{"app_name":"FromJson","port":3333}"#,
)
.unwrap();
let builder = LayerBuilder::new().with_path_in_dir(&config_dir, "application");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.app_name, "FromYaml");
assert_eq!(result.port, 1111);
}
#[test]
fn test_with_path_in_dir_no_file_found() {
let temp_dir = tempfile::tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
fs::create_dir_all(&config_dir).unwrap();
let builder = LayerBuilder::new().with_path_in_dir(&config_dir, "application");
let result = builder.build();
assert!(result.is_err());
}
#[test]
fn test_with_path_in_dir_relative_path() {
let temp_dir = tempfile::tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("settings.yaml"),
"app_name: RelativePath\nport: 4444\ndebug: false\ndb:\n host: relative\n password: pass",
)
.unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&temp_dir).unwrap();
let builder = LayerBuilder::new().with_path_in_dir("config", "settings");
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.app_name, "RelativePath");
assert_eq!(result.port, 4444);
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn test_with_path_in_dir_with_other_layers() {
let temp_dir = tempfile::tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("base.yaml"),
"app_name: BaseApp\nport: 5555\ndebug: false\ndb:\n host: base.host\n password: base_pass",
)
.unwrap();
let override_path = temp_dir.path().join("override.yaml");
fs::write(&override_path, "port: 6666\ndebug: true").unwrap();
let builder = LayerBuilder::new()
.with_path_in_dir(&config_dir, "base")
.with_path(&override_path);
let config_builder = builder.build().unwrap();
let config = config_builder.build().unwrap();
let result: TestConfig = config.try_deserialize().unwrap();
assert_eq!(result.port, 6666);
assert!(result.debug);
assert_eq!(result.app_name, "BaseApp");
assert_eq!(result.db.host, "base.host");
}