use bzod::config::{Config, ConfigError, NX9_BZOD_DATA_DIR_ENV}; use bzod::db::topology::Topology; use bzod::identity::TenantId; use std::env; use std::fs; use std::path::{Path, PathBuf}; use std::sync::Mutex; // Global lock to prevent race conditions during env-var testing static ENV_LOCK: Mutex<()> = Mutex::new(()); struct EnvGuard { key: &'static str, original_val: Option, } impl EnvGuard { fn set(key: &'static str, val: &str) -> Self { let original_val = env::var(key).ok(); env::set_var(key, val); Self { key, original_val } } fn unset(key: &'static str) -> Self { let original_val = env::var(key).ok(); env::remove_var(key); Self { key, original_val } } } impl Drop for EnvGuard { fn drop(&mut self) { if let Some(ref val) = self.original_val { env::set_var(self.key, val); } else { env::remove_var(self.key); } } } #[test] fn test_no_configuration_returns_missing_data_dir_error() { let _lock = ENV_LOCK.lock().unwrap(); let _guard = EnvGuard::unset(NX9_BZOD_DATA_DIR_ENV); // Call load_from_sources with no CLI and load_dotenv=false in an isolated context let res = Config::load_from_sources(None::<&Path>, false); assert!( res.is_err(), "Expected error when no data directory is configured" ); match res.err().unwrap() { ConfigError::MissingDataDir => {} other => panic!("Expected ConfigError::MissingDataDir, got {:?}", other), } } #[test] fn test_environment_variable_resolves_data_dir() { let _lock = ENV_LOCK.lock().unwrap(); let test_path = "/tmp/nx9-env-test-data"; let _guard = EnvGuard::set(NX9_BZOD_DATA_DIR_ENV, test_path); let config = Config::load_from_sources(None::<&Path>, false) .expect("Config should load with NX9_BZOD_DATA_DIR set"); assert_eq!(config.data_dir, PathBuf::from(test_path)); } #[test] fn test_config_file_resolves_data_dir() { let _lock = ENV_LOCK.lock().unwrap(); let _guard = EnvGuard::unset(NX9_BZOD_DATA_DIR_ENV); let temp_dir = std::env::temp_dir().join(format!("bzod_cfg_test_{}", uuid::Uuid::new_v4())); fs::create_dir_all(&temp_dir).unwrap(); let toml_file = temp_dir.join("bzod.toml"); let target_data_dir = "/tmp/config-file-data"; fs::write(&toml_file, format!("data_dir = \"{}\"\n", target_data_dir)).unwrap(); // Change current directory temporarily for config file reading let original_cwd = env::current_dir().unwrap(); env::set_current_dir(&temp_dir).unwrap(); let config_res = Config::load_from_sources(None::<&Path>, false); // Restore cwd env::set_current_dir(original_cwd).unwrap(); let _ = fs::remove_dir_all(&temp_dir); let config = config_res.expect("Config should load from bzod.toml"); assert_eq!(config.data_dir, PathBuf::from(target_data_dir)); } #[test] fn test_environment_overrides_config_file() { let _lock = ENV_LOCK.lock().unwrap(); let env_data_dir = "/tmp/env-override-data"; let _guard = EnvGuard::set(NX9_BZOD_DATA_DIR_ENV, env_data_dir); let temp_dir = std::env::temp_dir().join(format!("bzod_cfg_test_{}", uuid::Uuid::new_v4())); fs::create_dir_all(&temp_dir).unwrap(); let toml_file = temp_dir.join("bzod.toml"); let config_data_dir = "/tmp/config-file-data"; fs::write(&toml_file, format!("data_dir = \"{}\"\n", config_data_dir)).unwrap(); let original_cwd = env::current_dir().unwrap(); env::set_current_dir(&temp_dir).unwrap(); let config_res = Config::load_from_sources(None::<&Path>, false); env::set_current_dir(original_cwd).unwrap(); let _ = fs::remove_dir_all(&temp_dir); let config = config_res.expect("Config should load"); assert_eq!(config.data_dir, PathBuf::from(env_data_dir)); } #[test] fn test_cli_overrides_environment() { let _lock = ENV_LOCK.lock().unwrap(); let env_data_dir = "/tmp/env-data"; let _guard = EnvGuard::set(NX9_BZOD_DATA_DIR_ENV, env_data_dir); let cli_data_dir = "/tmp/cli-override-data"; let config = Config::load_from_sources(Some(cli_data_dir), false) .expect("Config should load with CLI override"); assert_eq!(config.data_dir, PathBuf::from(cli_data_dir)); } #[test] fn test_cli_overrides_config_file() { let _lock = ENV_LOCK.lock().unwrap(); let _guard = EnvGuard::unset(NX9_BZOD_DATA_DIR_ENV); let temp_dir = std::env::temp_dir().join(format!("bzod_cfg_test_{}", uuid::Uuid::new_v4())); fs::create_dir_all(&temp_dir).unwrap(); let toml_file = temp_dir.join("bzod.toml"); let config_data_dir = "/tmp/config-file-data"; fs::write(&toml_file, format!("data_dir = \"{}\"\n", config_data_dir)).unwrap(); let original_cwd = env::current_dir().unwrap(); env::set_current_dir(&temp_dir).unwrap(); let cli_data_dir = "/tmp/cli-data-highest"; let config_res = Config::load_from_sources(Some(cli_data_dir), false); env::set_current_dir(original_cwd).unwrap(); let _ = fs::remove_dir_all(&temp_dir); let config = config_res.expect("Config should load with CLI override"); assert_eq!(config.data_dir, PathBuf::from(cli_data_dir)); } #[test] fn test_relative_path_semantics_preserved() { let _lock = ENV_LOCK.lock().unwrap(); let relative_path = "custom_relative/data"; let config = Config::load_from_sources(Some(relative_path), false) .expect("Relative path in CLI should be accepted as-is without forced canonicalization"); assert_eq!(config.data_dir, PathBuf::from(relative_path)); } #[test] fn test_topology_resolves_all_databases_relative_to_configured_root() { let custom_root = PathBuf::from("/var/lib/bzod/custom_root"); let topology = Topology::new(&custom_root); assert_eq!(topology.root(), &custom_root); assert_eq!(topology.admin_dir(), custom_root.join("admin")); assert_eq!(topology.slugs_dir(), custom_root.join("slugs")); assert_eq!(topology.users_dir(), custom_root.join("users")); // Core databases assert_eq!(topology.admin_db(), custom_root.join("admin/admin.db")); assert_eq!(topology.system_db(), custom_root.join("admin/system.db")); assert_eq!( topology.users_registry_db(), custom_root.join("admin/users.db") ); assert_eq!( topology.global_urls_db(), custom_root.join("slugs/global_urls.db") ); assert_eq!( topology.global_landing_pages_db(), custom_root.join("slugs/global_landing_pages.db") ); assert_eq!( topology.reserved_db(), custom_root.join("slugs/reserved.db") ); // Tenant databases let tenant_id = TenantId::parse("a1b2c3d4e5f6").unwrap(); assert_eq!( topology.tenant_dir(tenant_id), custom_root.join("users/a1b2c3d4e5f6") ); assert_eq!( topology.tenant_content_db(tenant_id), custom_root.join("users/a1b2c3d4e5f6/content.db") ); assert_eq!( topology.tenant_analytics_db(tenant_id), custom_root.join("users/a1b2c3d4e5f6/analytics.db") ); assert_eq!( topology.tenant_profile_db(tenant_id), custom_root.join("users/a1b2c3d4e5f6/profile.db") ); assert_eq!( topology.extension_db("a1b2c3d4e5f6", "cv").unwrap(), custom_root.join("users/a1b2c3d4e5f6/extensions/cv/cv.db") ); }