use reinhardt_conf::settings::builder::SettingsBuilder;
use reinhardt_conf::settings::sources::{
ConfigSource, DefaultSource, EnvSource, HighPriorityEnvSource, LowPriorityEnvSource,
TomlFileSource,
};
use rstest::rstest;
use serde_json::Value;
use serial_test::serial;
use std::io::Write;
use std::path::PathBuf;
use tempfile::TempDir;
fn write_toml_file(content: &str) -> (TempDir, PathBuf) {
let dir = TempDir::new().unwrap();
let path = dir.path().join("config.toml");
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(content.as_bytes()).unwrap();
(dir, path)
}
unsafe fn set_env_vars(vars: &[(&str, &str)]) {
for (key, value) in vars {
unsafe { std::env::set_var(key, value) };
}
}
unsafe fn remove_env_vars(vars: &[&str]) {
for key in vars {
unsafe { std::env::remove_var(key) };
}
}
#[rstest]
#[serial(env)]
fn env_source_infers_integer_type() {
unsafe { set_env_vars(&[("SRCTEST_PORT", "8080")]) };
let source = EnvSource::new().with_prefix("SRCTEST_");
let config = source.load().unwrap();
assert_eq!(config.get("port").unwrap(), &Value::Number(8080.into()));
unsafe { remove_env_vars(&["SRCTEST_PORT"]) };
}
#[rstest]
#[serial(env)]
fn env_source_infers_bool_type() {
unsafe { set_env_vars(&[("SRCTEST_ENABLED", "true")]) };
let source = EnvSource::new().with_prefix("SRCTEST_");
let config = source.load().unwrap();
assert_eq!(config.get("enabled").unwrap(), &Value::Bool(true));
unsafe { remove_env_vars(&["SRCTEST_ENABLED"]) };
}
#[rstest]
#[serial(env)]
fn env_source_debug_key_smart_parsing_numeric() {
unsafe { set_env_vars(&[("SRCTEST_DEBUG", "1")]) };
let source = EnvSource::new().with_prefix("SRCTEST_");
let config = source.load().unwrap();
assert_eq!(config.get("debug").unwrap(), &Value::Bool(true));
unsafe { remove_env_vars(&["SRCTEST_DEBUG"]) };
}
#[rstest]
#[serial(env)]
fn env_source_debug_key_parses_yes() {
unsafe { set_env_vars(&[("SRCTEST_DEBUG", "yes")]) };
let source = EnvSource::new().with_prefix("SRCTEST_");
let config = source.load().unwrap();
assert_eq!(config.get("debug").unwrap(), &Value::Bool(true));
unsafe { remove_env_vars(&["SRCTEST_DEBUG"]) };
}
#[rstest]
#[serial(env)]
fn env_source_debug_key_parses_off() {
unsafe { set_env_vars(&[("SRCTEST_DEBUG", "off")]) };
let source = EnvSource::new().with_prefix("SRCTEST_");
let config = source.load().unwrap();
assert_eq!(config.get("debug").unwrap(), &Value::Bool(false));
unsafe { remove_env_vars(&["SRCTEST_DEBUG"]) };
}
#[rstest]
#[serial(env)]
fn env_source_allowed_hosts_parsed_as_array() {
unsafe { set_env_vars(&[("SRCTEST_ALLOWED_HOSTS", "a.com,b.com")]) };
let source = EnvSource::new().with_prefix("SRCTEST_");
let config = source.load().unwrap();
let hosts = config.get("allowed_hosts").unwrap();
assert!(hosts.is_array());
let arr = hosts.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0], Value::String("a.com".to_string()));
assert_eq!(arr[1], Value::String("b.com".to_string()));
unsafe { remove_env_vars(&["SRCTEST_ALLOWED_HOSTS"]) };
}
#[rstest]
#[serial(env)]
fn env_source_string_value_remains_string() {
unsafe { set_env_vars(&[("SRCTEST_NAME", "myapp")]) };
let source = EnvSource::new().with_prefix("SRCTEST_");
let config = source.load().unwrap();
assert_eq!(
config.get("name").unwrap(),
&Value::String("myapp".to_string())
);
unsafe { remove_env_vars(&["SRCTEST_NAME"]) };
}
#[rstest]
#[serial(env)]
fn env_source_strips_prefix_and_lowercases() {
unsafe { set_env_vars(&[("SRCTEST_MY_KEY", "val")]) };
let source = EnvSource::new().with_prefix("SRCTEST_");
let config = source.load().unwrap();
assert!(config.contains_key("my_key"));
assert_eq!(
config.get("my_key").unwrap(),
&Value::String("val".to_string())
);
unsafe { remove_env_vars(&["SRCTEST_MY_KEY"]) };
}
#[rstest]
fn low_priority_env_source_priority_is_40() {
assert_eq!(LowPriorityEnvSource::new().priority(), 40);
}
#[rstest]
#[serial(env)]
fn low_priority_env_source_loads_env_vars() {
unsafe { set_env_vars(&[("LPTEST_PORT", "9090")]) };
let source = LowPriorityEnvSource::new().with_prefix("LPTEST_");
let config = source.load().unwrap();
assert_eq!(config.get("port").unwrap(), &Value::Number(9090.into()));
unsafe { remove_env_vars(&["LPTEST_PORT"]) };
}
#[rstest]
#[serial(env)]
fn low_priority_env_source_with_prefix_filters() {
unsafe { set_env_vars(&[("LPTEST_INSIDE", "yes_val"), ("OTHER_OUTSIDE", "no_val")]) };
let source = LowPriorityEnvSource::new().with_prefix("LPTEST_");
let config = source.load().unwrap();
assert!(config.contains_key("inside"));
assert!(!config.contains_key("outside"));
assert!(!config.contains_key("other_outside"));
unsafe { remove_env_vars(&["LPTEST_INSIDE", "OTHER_OUTSIDE"]) };
}
#[rstest]
fn low_priority_env_source_description_contains_low_priority() {
let desc = LowPriorityEnvSource::new().description();
assert!(
desc.to_lowercase().contains("low"),
"description should contain 'low', got: {desc}"
);
}
#[rstest]
#[serial(env)]
fn env_overrides_toml() {
let (_dir, toml_path) = write_toml_file("port = 3000\n");
unsafe { set_env_vars(&[("MPTEST_PORT", "9000")]) };
let settings = SettingsBuilder::new()
.add_source(TomlFileSource::new(&toml_path))
.add_source(EnvSource::new().with_prefix("MPTEST_"))
.build()
.unwrap();
let port: u16 = settings.get("port").unwrap();
assert_eq!(port, 9000);
unsafe { remove_env_vars(&["MPTEST_PORT"]) };
}
#[rstest]
fn toml_overrides_default() {
let (_dir, toml_path) = write_toml_file("debug = false\n");
let default = DefaultSource::new().with_value("debug", Value::Bool(true));
let settings = SettingsBuilder::new()
.add_source(default)
.add_source(TomlFileSource::new(&toml_path))
.build()
.unwrap();
let debug: bool = settings.get("debug").unwrap();
assert!(!debug);
}
#[rstest]
#[serial(env)]
fn toml_overrides_low_priority_env() {
let (_dir, toml_path) = write_toml_file("port = 5000\n");
unsafe { set_env_vars(&[("LPMPTEST_PORT", "4000")]) };
let settings = SettingsBuilder::new()
.add_source(LowPriorityEnvSource::new().with_prefix("LPMPTEST_"))
.add_source(TomlFileSource::new(&toml_path))
.build()
.unwrap();
let port: u16 = settings.get("port").unwrap();
assert_eq!(port, 5000);
unsafe { remove_env_vars(&["LPMPTEST_PORT"]) };
}
#[rstest]
#[serial(env)]
fn low_priority_env_overrides_default() {
let default = DefaultSource::new().with_value("timeout", Value::Number(30.into()));
unsafe { set_env_vars(&[("LPMPTEST2_TIMEOUT", "60")]) };
let settings = SettingsBuilder::new()
.add_source(default)
.add_source(LowPriorityEnvSource::new().with_prefix("LPMPTEST2_"))
.build()
.unwrap();
let timeout: u64 = settings.get("timeout").unwrap();
assert_eq!(timeout, 60);
unsafe { remove_env_vars(&["LPMPTEST2_TIMEOUT"]) };
}
#[rstest]
#[serial(env)]
fn full_chain_same_key_highest_priority_wins() {
let default = DefaultSource::new().with_value("port", Value::Number(1000.into()));
let (_dir, toml_path) = write_toml_file("port = 3000\n");
unsafe { set_env_vars(&[("FCTEST_PORT", "9999")]) };
let low_env = LowPriorityEnvSource::new().with_prefix("FCTEST_");
let high_env = EnvSource::new().with_prefix("FCTEST_");
let settings = SettingsBuilder::new()
.add_source(default) .add_source(low_env) .add_source(TomlFileSource::new(&toml_path)) .add_source(high_env) .build()
.unwrap();
let port: u16 = settings.get("port").unwrap();
assert_eq!(port, 9999);
unsafe { remove_env_vars(&["FCTEST_PORT"]) };
}
#[rstest]
#[serial(env)]
fn full_chain_disjoint_keys_all_present() {
let default = DefaultSource::new().with_value("default_key", Value::String("dv".to_string()));
let (_dir, toml_path) = write_toml_file("toml_key = \"tv\"\n");
unsafe { set_env_vars(&[("FCTEST2_ENV_KEY", "ev")]) };
let settings = SettingsBuilder::new()
.add_source(default)
.add_source(TomlFileSource::new(&toml_path))
.add_source(EnvSource::new().with_prefix("FCTEST2_"))
.build()
.unwrap();
assert!(settings.contains_key("default_key"));
assert!(settings.contains_key("toml_key"));
assert!(settings.contains_key("env_key"));
let dv: String = settings.get("default_key").unwrap();
assert_eq!(dv, "dv");
let tv: String = settings.get("toml_key").unwrap();
assert_eq!(tv, "tv");
let ev: String = settings.get("env_key").unwrap();
assert_eq!(ev, "ev");
unsafe { remove_env_vars(&["FCTEST2_ENV_KEY"]) };
}
#[rstest]
#[serial(env)]
fn full_chain_partial_overlap() {
let default = DefaultSource::new()
.with_value("port", Value::Number(1111.into()))
.with_value("name", Value::String("default_app".to_string()));
let (_dir, toml_path) = write_toml_file("port = 2222\n");
unsafe { set_env_vars(&[("FCTEST3_HOST", "env.local")]) };
let settings = SettingsBuilder::new()
.add_source(default) .add_source(TomlFileSource::new(&toml_path)) .add_source(EnvSource::new().with_prefix("FCTEST3_")) .build()
.unwrap();
let port: u16 = settings.get("port").unwrap();
assert_eq!(port, 2222);
let name: String = settings.get("name").unwrap();
assert_eq!(name, "default_app");
let host: String = settings.get("host").unwrap();
assert_eq!(host, "env.local");
unsafe { remove_env_vars(&["FCTEST3_HOST"]) };
}
#[rstest]
#[serial(env)]
fn high_priority_env_overrides_interpolated_toml() {
unsafe {
set_env_vars(&[
("IT_PG_PORT_PRIO", "8080"), ("PRIO_TEST_PORT", "9999"), ])
};
let (_dir, path) = write_toml_file(r#"port = "${IT_PG_PORT_PRIO:-5432}""#);
let settings = SettingsBuilder::new()
.add_source(TomlFileSource::new(&path).with_interpolation()) .add_source(HighPriorityEnvSource::new().with_prefix("PRIO_TEST_")) .build()
.unwrap();
let port: u16 = settings.get("port").unwrap();
unsafe { remove_env_vars(&["IT_PG_PORT_PRIO", "PRIO_TEST_PORT"]) };
assert_eq!(
port, 9999,
"HighPriorityEnvSource (60) must override interpolated TOML (50)"
);
}