#![allow(clippy::unwrap_in_result)]
use std::{env, fs, io::Write, path::PathBuf, sync::Mutex};
use tempfile::{Builder, TempDir};
use zakura_chain::{block::Height, parameters::NetworkUpgrade};
use zakura_network::zakura::{DEFAULT_ZAKURA_LISTEN_ADDR, DEFAULT_ZAKURA_MAX_CONNS_PER_IP};
use zakurad::components::{
tracing::{Config as TracingConfig, InnerConfig as TracingInnerConfig, ProgressConfig},
zcashd_compat::ConfigZcashdBinarySource,
};
use zakurad::config::ZakuradConfig;
const CONFIG_ENV_PREFIXES: [&str; 2] = ["ZEBRA_", "ZAKURA_"];
static TEST_MUTEX: Mutex<()> = Mutex::new(());
struct EnvGuard {
_guard: std::sync::MutexGuard<'static, ()>,
original_vars: Vec<(String, String)>,
}
impl EnvGuard {
fn new() -> Self {
let guard = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let original_vars: Vec<(String, String)> = env::vars()
.filter(|(key, _val)| {
CONFIG_ENV_PREFIXES
.iter()
.any(|prefix| key.starts_with(prefix))
})
.collect();
for (key, _) in &original_vars {
env::remove_var(key);
}
Self {
_guard: guard,
original_vars,
}
}
fn set_var(&self, key: &str, value: &str) {
env::set_var(key, value);
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
let current_vars: Vec<String> = env::vars()
.filter(|(key, _)| {
CONFIG_ENV_PREFIXES
.iter()
.any(|prefix| key.starts_with(prefix))
})
.map(|(key, _)| key)
.collect();
for key in current_vars {
env::remove_var(&key);
}
for (key, value) in &self.original_vars {
env::set_var(key, value);
}
}
}
#[test]
fn config_load_defaults() {
let _env = EnvGuard::new();
let config = ZakuradConfig::load(None).expect("Should load default config");
assert_eq!(config.network.network.to_string(), "Mainnet");
assert_eq!(
config.network.zakura.listen_addr,
Some(DEFAULT_ZAKURA_LISTEN_ADDR)
);
assert_eq!(
config.network.zakura.max_connections_per_ip,
DEFAULT_ZAKURA_MAX_CONNS_PER_IP
);
assert_eq!(config.rpc.listen_addr, None); assert_eq!(config.metrics.endpoint_addr, None); assert!(!config.network.expose_peer_addresses); }
#[test]
fn tracing_progress_bar_uses_zakura_log_file_by_default() {
let tracing_config = TracingConfig::from(TracingInnerConfig {
progress_bar: Some(ProgressConfig::Summary),
..TracingInnerConfig::default()
});
assert_eq!(
tracing_config
.log_file
.as_ref()
.and_then(|path| path.file_name())
.and_then(|name| name.to_str()),
Some("zakura.log")
);
}
#[test]
fn config_load_from_file() {
let _env = EnvGuard::new();
let temp_dir = TempDir::new().expect("create temp dir");
let config_path = temp_dir.path().join("test_config.toml");
let test_config = r#"
[network]
network = "Testnet"
expose_peer_addresses = true
[rpc]
listen_addr = "127.0.0.1:8232"
[metrics]
endpoint_addr = "127.0.0.1:9999"
"#;
fs::write(&config_path, test_config).expect("write test config");
let config = ZakuradConfig::load(Some(config_path)).expect("load config from file");
assert_eq!(config.network.network.to_string(), "Testnet");
assert_eq!(
config.rpc.listen_addr.unwrap().to_string(),
"127.0.0.1:8232"
);
assert_eq!(
config.metrics.endpoint_addr.unwrap().to_string(),
"127.0.0.1:9999"
);
assert!(config.network.expose_peer_addresses);
}
#[test]
fn config_loads_quoted_nu6_3_activation_height() {
let _env = EnvGuard::new();
let temp_dir = TempDir::new().expect("create temp dir");
let config_path = temp_dir.path().join("nu6_3_config.toml");
let test_config = r#"
[network]
network = "Regtest"
[network.testnet_parameters.activation_heights]
"NU6.3" = 23
"#;
fs::write(&config_path, test_config).expect("write test config");
let config = ZakuradConfig::load(Some(config_path)).expect("load NU6.3 config from file");
assert_eq!(
NetworkUpgrade::Nu6_3.activation_height(&config.network.network),
Some(Height(23)),
"the exact dotted TOML key must configure the requested NU6.3 height"
);
}
#[test]
fn config_nonexistent_file_errors() {
let _env = EnvGuard::new();
let nonexistent_path = PathBuf::from("/this/path/does/not/exist.toml");
ZakuradConfig::load(Some(nonexistent_path)).expect_err("Should fail to load nonexistent file");
}
#[test]
fn config_env_override_defaults() {
let env = EnvGuard::new();
env.set_var("ZAKURA_NETWORK__NETWORK", "Testnet");
env.set_var("ZAKURA_RPC__LISTEN_ADDR", "127.0.0.1:8232");
let config = ZakuradConfig::load(None).expect("load config with env vars");
assert_eq!(config.network.network.to_string(), "Testnet");
assert_eq!(
config.rpc.listen_addr.unwrap().to_string(),
"127.0.0.1:8232"
);
}
#[test]
fn config_zakura_env_overrides_legacy_zebra_env() {
let env = EnvGuard::new();
env.set_var("ZEBRA_RPC__LISTEN_ADDR", "127.0.0.1:18231");
env.set_var("ZAKURA_RPC__LISTEN_ADDR", "127.0.0.1:18232");
let config = ZakuradConfig::load(None).expect("load config with both env prefixes");
assert_eq!(
config.rpc.listen_addr.unwrap().to_string(),
"127.0.0.1:18232"
);
}
#[test]
fn config_env_override_file() {
let env = EnvGuard::new();
let temp_dir = TempDir::new().expect("create temp dir");
let config_path = temp_dir.path().join("test_config.toml");
let test_config = r#"
[network]
network = "Mainnet"
[rpc]
listen_addr = "127.0.0.1:8233"
"#;
fs::write(&config_path, test_config).expect("write test config");
env.set_var("ZAKURA_NETWORK__NETWORK", "Testnet");
env.set_var("ZAKURA_RPC__LISTEN_ADDR", "127.0.0.1:8232");
let config = ZakuradConfig::load(Some(config_path)).expect("load config");
assert_eq!(config.network.network.to_string(), "Testnet");
assert_eq!(
config.rpc.listen_addr.unwrap().to_string(),
"127.0.0.1:8232"
);
}
#[test]
fn config_invalid_toml_errors() {
let _env = EnvGuard::new();
let temp_dir = TempDir::new().expect("create temp dir");
let config_path = temp_dir.path().join("invalid_config.toml");
let invalid_config = r#"
[network
network = "Testnet"
"#;
fs::write(&config_path, invalid_config).expect("write invalid config");
ZakuradConfig::load(Some(config_path)).expect_err("Should fail to load invalid TOML");
}
#[test]
fn config_invalid_env_values_error() {
let env = EnvGuard::new();
env.set_var("ZAKURA_RPC__LISTEN_ADDR", "invalid_address");
ZakuradConfig::load(None).expect_err("Should fail with invalid RPC listen address");
}
#[test]
fn config_nested_env_vars() {
let env = EnvGuard::new();
env.set_var("ZAKURA_TRACING__FILTER", "debug");
let config = ZakuradConfig::load(None).expect("load config with nested env vars");
assert_eq!(config.tracing.filter.as_deref(), Some("debug"));
}
#[test]
fn config_zcashd_compat_source_and_path_env() {
let env = EnvGuard::new();
env.set_var("ZAKURA_ZCASHD_COMPAT__ZCASHD_SOURCE", "path");
env.set_var("ZAKURA_ZCASHD_COMPAT__ZCASHD_PATH", "/usr/local/bin/zcashd");
let config = ZakuradConfig::load(None).expect("load config with zcashd compat env vars");
assert_eq!(
config.zcashd_compat.zcashd_source,
ConfigZcashdBinarySource::Path
);
assert_eq!(
config.zcashd_compat.zcashd_path,
Some(PathBuf::from("/usr/local/bin/zcashd"))
);
}
#[test]
fn config_zcashd_compat_embedded_source_env() {
let env = EnvGuard::new();
env.set_var("ZAKURA_ZCASHD_COMPAT__ZCASHD_SOURCE", "embedded");
let config = ZakuradConfig::load(None).expect("load config with embedded source");
assert_eq!(
config.zcashd_compat.zcashd_source,
ConfigZcashdBinarySource::Embedded
);
}
#[test]
fn config_zakura_network_network_env() {
let env = EnvGuard::new();
env.set_var("ZAKURA_NETWORK__NETWORK", "Testnet");
let config = ZakuradConfig::load(None).expect("load config with ZAKURA_NETWORK__NETWORK");
assert_eq!(config.network.network.to_string(), "Testnet");
}
#[test]
fn config_zakura_rpc_listen_addr_env() {
let env = EnvGuard::new();
env.set_var("ZAKURA_RPC__LISTEN_ADDR", "127.0.0.1:18232");
let config = ZakuradConfig::load(None).expect("load config with ZAKURA_RPC__LISTEN_ADDR");
assert_eq!(
config.rpc.listen_addr.unwrap().to_string(),
"127.0.0.1:18232"
);
assert_eq!(config.rpc.cookie_file_name, ".cookie");
}
#[test]
fn config_zakura_state_cache_dir_env() {
let env = EnvGuard::new();
env.set_var("ZAKURA_STATE__CACHE_DIR", "/test/cache");
let config = ZakuradConfig::load(None).expect("load config with ZAKURA_STATE__CACHE_DIR");
assert_eq!(config.state.cache_dir, PathBuf::from("/test/cache"));
}
#[test]
fn config_zakura_metrics_endpoint_addr_env() {
let env = EnvGuard::new();
env.set_var("ZAKURA_METRICS__ENDPOINT_ADDR", "0.0.0.0:9999");
let config = ZakuradConfig::load(None).expect("load config with ZAKURA_METRICS__ENDPOINT_ADDR");
assert_eq!(
config.metrics.endpoint_addr.unwrap().to_string(),
"0.0.0.0:9999"
);
}
#[test]
fn config_zakura_network_expose_peer_addresses_env() {
let env = EnvGuard::new();
env.set_var("ZAKURA_NETWORK__EXPOSE_PEER_ADDRESSES", "true");
let config =
ZakuradConfig::load(None).expect("load config with ZAKURA_NETWORK__EXPOSE_PEER_ADDRESSES");
assert!(config.network.expose_peer_addresses);
}
#[test]
fn config_zakura_tracing_log_file_env() {
let env = EnvGuard::new();
env.set_var("ZAKURA_TRACING__LOG_FILE", "/test/zakura.log");
let config = ZakuradConfig::load(None).expect("load config with ZAKURA_TRACING__LOG_FILE");
assert_eq!(
config.tracing.log_file.as_ref().unwrap(),
&PathBuf::from("/test/zakura.log")
);
}
#[test]
fn config_legacy_zebra_env_is_supported() {
let env = EnvGuard::new();
env.set_var("ZEBRA_RPC__LISTEN_ADDR", "127.0.0.1:18232");
let config = ZakuradConfig::load(None).expect("load config with legacy ZEBRA_ env var");
assert_eq!(
config.rpc.listen_addr.unwrap().to_string(),
"127.0.0.1:18232"
);
}
#[test]
fn config_zakura_target_env_is_supported() {
let env = EnvGuard::new();
env.set_var("ZAKURA_TARGET_STATE__CACHE_DIR", "/target/cache");
let config = ZakuradConfig::load_with_env(None, "ZAKURA_TARGET")
.expect("load config with ZAKURA_TARGET_STATE__CACHE_DIR");
assert_eq!(config.state.cache_dir, PathBuf::from("/target/cache"));
}
#[test]
fn config_legacy_zebra_target_env_is_supported() {
let env = EnvGuard::new();
env.set_var("ZEBRA_TARGET_STATE__CACHE_DIR", "/target/cache");
let config = ZakuradConfig::load_with_env(None, "ZAKURA_TARGET")
.expect("load config with legacy ZEBRA_TARGET_ env var");
assert_eq!(config.state.cache_dir, PathBuf::from("/target/cache"));
}
#[test]
fn config_zakura_target_env_overrides_legacy_zebra_target_env() {
let env = EnvGuard::new();
env.set_var("ZEBRA_TARGET_STATE__CACHE_DIR", "/legacy/cache");
env.set_var("ZAKURA_TARGET_STATE__CACHE_DIR", "/target/cache");
let config = ZakuradConfig::load_with_env(None, "ZAKURA_TARGET")
.expect("load target config with both env prefixes");
assert_eq!(config.state.cache_dir, PathBuf::from("/target/cache"));
}
#[test]
fn config_zakura_mining_miner_address_from_toml() {
let _env = EnvGuard::new();
let miner_address = "u1cymdny2u2vllkx7t5jnelp0kde0dgnwu0jzmggzguxvxj6fe7gpuqehywejndlrjwgk9snr6g69azs8jfet78s9zy60uepx6tltk7ee57jlax49dezkhkgvjy2puuue6dvaevt53nah7t2cc2k4p0h0jxmlu9sx58m2xdm5f9sy2n89jdf8llflvtml2ll43e334avu2fwytuna404a";
let toml_string = format!(
r#"[network]
network = "Testnet"
[mining]
miner_address = "{miner_address}""#,
);
let mut file = Builder::new()
.suffix(".toml")
.tempfile()
.expect("create temp file");
file.write_all(toml_string.as_bytes())
.expect("write temp file");
let config = ZakuradConfig::load(Some(file.path().to_path_buf()))
.expect("load config with miner_address");
assert_eq!(
config.mining.miner_address.as_ref().unwrap().to_string(),
miner_address
);
}
#[test]
fn config_env_unknown_non_sensitive_key_errors() {
let env = EnvGuard::new();
env.set_var("ZAKURA_MINING__FOO", "bar");
ZakuradConfig::load(None)
.expect_err("Unknown non-sensitive env key should error (deny_unknown_fields)");
}
#[test]
fn config_env_unknown_sensitive_key_errors() {
let env = EnvGuard::new();
env.set_var("ZAKURA_MINING__TOKEN", "secret-token");
let result = ZakuradConfig::load(None);
assert!(result.is_err(), "Sensitive env key should cause an error");
let msg = result.unwrap_err().to_string();
assert!(msg.contains("sensitive key"), "error message: {}", msg);
}