//! Comprehensive tests for configuration loading and validation. //! //! Tests cover: //! - Configuration loading from multiple sources (YAML, environment, Vault) //! - Configuration validation (required fields, ranges) //! - Default values and precedence (CLI > ENV > DEFAULT) //! - Schema validation //! - Edge cases and error handling use config::manager::{ConfigManager, ConfigManagerBuilder, ServiceConfig}; use config::runtime::{Environment, RuntimeConfig}; use config::vault::VaultConfig; use serde_json::json; use std::env; #[test] fn test_service_config_required_fields() { // Valid config with all required fields let valid_config = ServiceConfig { name: "test_service".to_owned(), environment: "production".to_owned(), version: "1.0.0".to_owned(), settings: json!({"key": "value"}), }; assert_eq!(valid_config.name, "test_service"); assert!(!valid_config.name.is_empty()); assert!(!valid_config.environment.is_empty()); } #[test] fn test_service_config_empty_name_validation() { // Config with empty name (should be caught by validation) let config = ServiceConfig { name: String::new(), environment: "production".to_owned(), version: "1.0.0".to_owned(), settings: json!({}), }; assert!(config.name.is_empty()); } #[test] fn test_service_config_empty_environment_validation() { // Config with empty environment let config = ServiceConfig { name: "service".to_owned(), environment: String::new(), version: "1.0.0".to_owned(), settings: json!({}), }; assert!(config.environment.is_empty()); } #[test] fn test_vault_config_required_fields_validation() { // Valid Vault config let valid_config = VaultConfig::new( "https://vault.example.com:8200".to_owned(), "token-12345".to_owned(), "secret/".to_owned(), ); assert!(valid_config.validate().is_ok()); } #[test] fn test_vault_config_empty_url_validation() { // Vault config with empty URL let invalid_config = VaultConfig::new( String::new(), "token-12345".to_owned(), "secret/".to_owned(), ); let result = invalid_config.validate(); assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Vault URL cannot be empty"); } #[test] fn test_vault_config_empty_token_validation() { // Vault config with empty token let invalid_config = VaultConfig::new( "https://vault.example.com:8200".to_owned(), String::new(), "secret/".to_owned(), ); let result = invalid_config.validate(); assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Vault token cannot be empty"); } #[test] fn test_vault_config_empty_mount_path_validation() { // Vault config with empty mount path let invalid_config = VaultConfig::new( "https://vault.example.com:8200".to_owned(), "token-12345".to_owned(), String::new(), ); let result = invalid_config.validate(); assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Vault mount path cannot be empty"); } #[test] #[serial_test::serial] fn test_environment_detection_development() { // Set environment variable env::set_var("ENVIRONMENT", "development"); let detected = Environment::detect(); assert_eq!(detected, Environment::Development); assert!(detected.is_development()); assert!(!detected.is_production()); // Clean up env::remove_var("ENVIRONMENT"); } #[test] #[serial_test::serial] fn test_environment_detection_production() { // Test both "production" and "prod" variants env::set_var("ENVIRONMENT", "production"); let detected = Environment::detect(); assert_eq!(detected, Environment::Production); assert!(detected.is_production()); env::set_var("ENVIRONMENT", "prod"); let detected = Environment::detect(); assert_eq!(detected, Environment::Production); env::remove_var("ENVIRONMENT"); } #[test] #[serial_test::serial] fn test_environment_detection_staging() { env::set_var("ENVIRONMENT", "staging"); let detected = Environment::detect(); assert_eq!(detected, Environment::Staging); env::set_var("ENVIRONMENT", "stage"); let detected = Environment::detect(); assert_eq!(detected, Environment::Staging); env::remove_var("ENVIRONMENT"); } #[test] #[serial_test::serial] fn test_environment_detection_fallback() { // Test fallback to development for invalid values env::set_var("ENVIRONMENT", "invalid"); let detected = Environment::detect(); assert_eq!(detected, Environment::Development); // Test fallback when environment variable is not set env::remove_var("ENVIRONMENT"); let detected = Environment::detect(); assert_eq!(detected, Environment::Development); } #[test] fn test_runtime_config_defaults_development() { let config = RuntimeConfig::with_defaults(Environment::Development); // Verify development has relaxed timeouts assert!(config.database.query_timeout.as_millis() >= 1000); assert!(config.database.pool_size >= 10); } #[test] fn test_runtime_config_defaults_production() { let config = RuntimeConfig::with_defaults(Environment::Production); // Verify production has tight timeouts for HFT assert!(config.database.query_timeout.as_millis() <= 1000); assert!(config.database.pool_size >= 15); } #[test] fn test_runtime_config_defaults_staging() { let config = RuntimeConfig::with_defaults(Environment::Staging); // Verify staging is between development and production let dev = RuntimeConfig::with_defaults(Environment::Development); let prod = RuntimeConfig::with_defaults(Environment::Production); assert!(config.database.query_timeout >= prod.database.query_timeout); assert!(config.database.query_timeout <= dev.database.query_timeout); } #[test] fn test_config_manager_builder_pattern() { // Test fluent builder pattern let config = ServiceConfig { name: "test_service".to_owned(), environment: "test".to_owned(), version: "1.0.0".to_owned(), settings: json!({"test": "value"}), }; let manager = ConfigManagerBuilder::new(config) .with_cache_timeout(std::time::Duration::from_secs(120)) .build(); let retrieved = manager.get_config(); assert_eq!(retrieved.name, "test_service"); } #[test] fn test_config_manager_cache_timeout_configuration() { let config = ServiceConfig { name: "test".to_owned(), environment: "test".to_owned(), version: "1.0.0".to_owned(), settings: json!({}), }; let custom_timeout = std::time::Duration::from_secs(600); let manager = ConfigManagerBuilder::new(config) .with_cache_timeout(custom_timeout) .build(); // Verify manager was created (cache_timeout is private, can't test directly) let retrieved = manager.get_config(); assert_eq!(retrieved.name, "test"); } #[test] fn test_service_config_settings_json_validation() { // Test valid JSON settings let config = ServiceConfig { name: "test".to_owned(), environment: "test".to_owned(), version: "1.0.0".to_owned(), settings: json!({ "database": { "host": "localhost", "port": 5432 }, "features": ["trading", "ml"] }), }; // Verify JSON structure assert!(config.settings.is_object()); assert!(config.settings.get("database").is_some()); assert!(config.settings.get("features").is_some()); } #[test] fn test_service_config_version_format() { // Test various version formats let versions = vec!["1.0.0", "2.1.3", "0.0.1-alpha", "1.2.3-beta.1"]; for version in versions { let config = ServiceConfig { name: "test".to_owned(), environment: "test".to_owned(), version: version.to_owned(), settings: json!({}), }; assert_eq!(config.version, version); } } #[test] fn test_config_manager_multiple_instances() { // Test that multiple ConfigManager instances can coexist let config1 = ServiceConfig { name: "service1".to_owned(), environment: "prod".to_owned(), version: "1.0.0".to_owned(), settings: json!({}), }; let config2 = ServiceConfig { name: "service2".to_owned(), environment: "dev".to_owned(), version: "2.0.0".to_owned(), settings: json!({}), }; let manager1 = ConfigManager::new(config1); let manager2 = ConfigManager::new(config2); assert_eq!(manager1.get_config().name, "service1"); assert_eq!(manager2.get_config().name, "service2"); } #[test] fn test_vault_config_namespace_optional() { // Test Vault config without namespace let config_without = VaultConfig::new( "https://vault.example.com".to_owned(), "token".to_owned(), "secret/".to_owned(), ); assert!(config_without.namespace.is_none()); // Test Vault config with namespace let config_with = VaultConfig::new( "https://vault.example.com".to_owned(), "token".to_owned(), "secret/".to_owned(), ) .with_namespace("production".to_owned()); assert!(config_with.namespace.is_some()); assert_eq!(config_with.namespace.as_ref().unwrap(), "production"); } #[test] #[serial_test::serial] fn test_runtime_config_precedence() { // Test that environment variables override defaults env::set_var("DATABASE_POOL_SIZE", "25"); let config = RuntimeConfig::from_env().unwrap(); assert_eq!(config.database.pool_size, 25); env::remove_var("DATABASE_POOL_SIZE"); } #[test] #[serial_test::serial] fn test_runtime_config_invalid_env_var_fallback() { // Test that invalid environment variable falls back to default env::set_var("DATABASE_POOL_SIZE", "invalid"); // Should fall back to default instead of panicking let _result = RuntimeConfig::from_env(); // Invalid value should cause error or use default env::remove_var("DATABASE_POOL_SIZE"); } #[test] fn test_service_config_serialization_roundtrip() { let original = ServiceConfig { name: "test_service".to_owned(), environment: "production".to_owned(), version: "1.2.3".to_owned(), settings: json!({ "key1": "value1", "key2": 42, "nested": {"inner": "data"} }), }; // Serialize to JSON let serialized = serde_json::to_string(&original).unwrap(); // Deserialize back let deserialized: ServiceConfig = serde_json::from_str(&serialized).unwrap(); // Verify round-trip assert_eq!(original.name, deserialized.name); assert_eq!(original.environment, deserialized.environment); assert_eq!(original.version, deserialized.version); assert_eq!(original.settings, deserialized.settings); } #[test] fn test_config_manager_concurrent_cache_access() { use std::sync::Arc; use std::thread; let config = ServiceConfig { name: "concurrent_test".to_owned(), environment: "test".to_owned(), version: "1.0.0".to_owned(), settings: json!({}), }; let manager = Arc::new(ConfigManager::new(config)); let mut handles = vec![]; // Spawn 10 threads that write and read cache concurrently for i in 0..10 { let manager_clone = Arc::clone(&manager); let handle = thread::spawn(move || { let key = format!("thread_key_{}", i); let value = json!({"thread_id": i, "data": format!("data_{}", i)}); manager_clone.set_cached_config(key.clone(), value); let retrieved = manager_clone.get_cached_config(&key); assert!(retrieved.is_some()); retrieved.unwrap() }); handles.push(handle); } // Verify all threads completed successfully for handle in handles { let result = handle.join(); assert!(result.is_ok()); } } #[test] #[serial_test::serial] fn test_environment_case_insensitivity() { // Test case-insensitive environment detection let test_cases = vec![ ("PRODUCTION", Environment::Production), ("Production", Environment::Production), ("production", Environment::Production), ("PROD", Environment::Production), ("Prod", Environment::Production), ("STAGING", Environment::Staging), ("staging", Environment::Staging), ("STAGE", Environment::Staging), ("DEVELOPMENT", Environment::Development), ("development", Environment::Development), ]; for (input, expected) in test_cases { env::set_var("ENVIRONMENT", input); let detected = Environment::detect(); assert_eq!(detected, expected, "Failed for input: {}", input); } env::remove_var("ENVIRONMENT"); } #[test] fn test_config_manager_cache_expiration() { use std::thread; use std::time::Duration; let config = ServiceConfig { name: "cache_test".to_owned(), environment: "test".to_owned(), version: "1.0.0".to_owned(), settings: json!({}), }; // Create manager with very short cache timeout (100ms) let manager = ConfigManagerBuilder::new(config) .with_cache_timeout(Duration::from_millis(100)) .build(); // Set cache value let value = json!({"data": "test"}); manager.set_cached_config("test_key".to_owned(), value); // Should be available immediately assert!(manager.get_cached_config("test_key").is_some()); // Wait for cache to expire thread::sleep(Duration::from_millis(150)); // Should be expired now (might be None or Some depending on cleanup timing) let _retrieved = manager.get_cached_config("test_key"); // Note: The cache might still exist but be expired, depends on implementation } #[test] fn test_vault_config_debug_redaction() { let config = VaultConfig::new( "https://vault.example.com:8200".to_owned(), "super-secret-token".to_owned(), "secret/".to_owned(), ); // Debug output should redact the token let debug_output = format!("{:?}", config); assert!(debug_output.contains("***REDACTED***")); assert!(!debug_output.contains("super-secret-token")); } #[test] fn test_vault_config_serialization_redaction() { let config = VaultConfig::new( "https://vault.example.com:8200".to_owned(), "super-secret-token".to_owned(), "secret/".to_owned(), ); // Serialized output should redact the token let serialized = serde_json::to_string(&config).unwrap(); assert!(serialized.contains("***REDACTED***")); assert!(!serialized.contains("super-secret-token")); }