// file: crates/ksp-config-lib/unit_tests/environment.rs // version: 5 #[test] fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() { let mut process = std::collections::BTreeMap::::new(); process.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new()); let mut dotenv = std::collections::BTreeMap::::new(); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned()); let environment = super::ConfigEnvironment::from_maps(process, dotenv); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); assert!(resolved.is_ok(), "process value should resolve"); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(resolved.value(), ""); assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Process); } #[test] fn dotenv_wins_over_fallback_when_process_value_is_absent() { let process = std::collections::BTreeMap::::new(); let mut dotenv = std::collections::BTreeMap::::new(); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned()); let environment = super::ConfigEnvironment::from_maps(process, dotenv); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); assert!(resolved.is_ok(), "dotenv value should resolve"); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(resolved.value(), "dotenv-logs"); assert_eq!(resolved.source(), super::ConfigEnvironmentSource::DotEnv); } #[test] fn empty_dotenv_value_is_defined_and_beats_fallback() { let mut dotenv = std::collections::BTreeMap::::new(); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new()); let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); assert!(resolved.is_ok(), "empty dotenv value should resolve"); if let std::result::Result::Ok(resolved) = resolved { assert_eq!(resolved.value(), ""); assert_eq!(resolved.source(), super::ConfigEnvironmentSource::DotEnv); } } #[test] fn fallback_is_used_only_when_external_sources_are_absent() { let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); assert!(resolved.is_ok(), "fallback should resolve"); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(resolved.value(), "fallback-logs"); assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Fallback); } #[test] fn missing_variable_without_fallback_is_a_distinct_error() { let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None); let error = match resolved { std::result::Result::Ok(_) => return, std::result::Result::Err(error) => error, }; assert_eq!(error.code(), crate::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING); assert!(!error.to_string().contains("fallback-logs")); } #[test] fn ksp_and_kspb_namespaces_are_supported_but_external_names_are_rejected() { let mut process = std::collections::BTreeMap::::new(); process.insert("KSP_LOGS_DIRECTORY".to_owned(), "logs".to_owned()); let bot_variable = ["KSPB_", "TEST_KEY"].concat(); process.insert(bot_variable.clone(), "hidden".to_owned()); let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); assert!(environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None).is_ok()); assert!(environment.resolve_variable(bot_variable.as_str(), std::option::Option::None).is_ok()); let external = environment.resolve_variable("OTHER_NETWORK", std::option::Option::None); let error = match external { std::result::Result::Ok(_) => return, std::result::Result::Err(error) => error, }; assert_eq!(error.code(), crate::ERROR_CODE_ENVIRONMENT_VARIABLE_INVALID); } #[test] fn text_resolver_supports_multiple_placeholders_and_literal_fallbacks() { let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let resolved = environment.resolve_text("logs=${KSP_LOGS_DIRECTORY:-logs};second=${KSP_LOGS_DIRECTORY:-other}"); assert!(resolved.is_ok(), "multiple placeholders should resolve"); if let std::result::Result::Ok(resolved) = resolved { assert_eq!(resolved, "logs=logs;second=other"); } } #[test] fn malformed_or_nested_placeholders_are_rejected() { let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let unclosed = environment.resolve_text("${KSP_LOGS_DIRECTORY"); let unclosed = match unclosed { std::result::Result::Ok(_) => return, std::result::Result::Err(error) => error, }; assert_eq!(unclosed.code(), crate::ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID); let nested = environment.resolve_text("${KSP_LOGS_DIRECTORY:-${KSP_LOGS_DIRECTORY}}}"); let nested = match nested { std::result::Result::Ok(_) => return, std::result::Result::Err(error) => error, }; assert_eq!(nested.code(), crate::ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID); } #[test] fn json_resolver_walks_objects_and_arrays_without_changing_keys() { let mut dotenv = std::collections::BTreeMap::::new(); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "runtime-logs".to_owned()); let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv); let source = serde_json::json!({"path": "${KSP_LOGS_DIRECTORY}", "items": [1, "${KSP_LOGS_DIRECTORY}"], "enabled": true}); let resolved = environment.resolve_json(&source); assert!(resolved.is_ok(), "recursive JSON resolution should succeed"); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(resolved["path"], serde_json::Value::String("runtime-logs".to_owned())); assert_eq!(resolved["items"][1], serde_json::Value::String("runtime-logs".to_owned())); assert_eq!(resolved["enabled"], serde_json::Value::Bool(true)); } #[test] fn dotenv_parser_supports_comments_export_quotes_empty_values_and_ignores_external_keys() { let path = std::path::Path::new("fixture.env"); let content = "# comment\nexport KSP_LOGS_DIRECTORY = 'quoted logs'\nOTHER_TOOL=value\n"; let parsed = super::parse_dotenv_content(path, content); assert!(parsed.is_ok(), "dotenv fixture should parse"); let parsed = match parsed { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(parsed.get("KSP_LOGS_DIRECTORY").map(String::as_str), std::option::Option::Some("quoted logs")); assert!(!parsed.contains_key("OTHER_TOOL")); } #[test] fn dotenv_duplicate_ksp_key_is_rejected() { let parsed = super::parse_dotenv_content(std::path::Path::new("fixture.env"), "KSP_LOGS_DIRECTORY=one\nKSP_LOGS_DIRECTORY=two\n"); let error = match parsed { std::result::Result::Ok(_) => return, std::result::Result::Err(error) => error, }; assert_eq!(error.code(), crate::ERROR_CODE_DOTENV_SYNTAX_INVALID); } #[test] fn fake_process_collection_filters_unrelated_names_without_mutating_real_environment() { let values = vec![ (std::ffi::OsString::from("KSP_LOGS_DIRECTORY"), std::ffi::OsString::from("process-logs")), (std::ffi::OsString::from("OTHER_TOOL"), std::ffi::OsString::from("ignored")), ]; let collected = super::collect_process_environment(values); assert!(collected.is_ok(), "fake process environment should collect"); let collected = match collected { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(collected.len(), 1); assert_eq!(collected.get("KSP_LOGS_DIRECTORY").map(String::as_str), std::option::Option::Some("process-logs")); } #[test] fn logging_fixture_profile_resolves_environment_fallback_without_changing_source_profile() { let bootstrap = logging_fixture_bootstrap(); assert!(bootstrap.is_ok(), "bootstrap should resolve committed roots"); let bootstrap = match bootstrap { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let registry = crate::ConfigFileRegistry::defaults(); assert!(registry.is_ok(), "default registry should build"); let registry = match registry { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let engine = crate::ConfigDocumentEngine::new(bootstrap, registry); let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING); let file_id = match file_id { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let profile = engine.load_resolved_profile(&file_id, std::option::Option::None); assert!(profile.is_ok(), "committed Logging profile should resolve"); let profile = match profile { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let effective = profile.resolve_effective_environment(&environment); assert!(effective.is_ok(), "committed Logging environment fallback should resolve"); let effective = match effective { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(profile.effective().get("logs_directory").and_then(serde_json::Value::as_str), std::option::Option::Some("${KSP_LOGS_DIRECTORY:-logs}")); assert_eq!(effective.get("logs_directory").and_then(serde_json::Value::as_str), std::option::Option::Some("logs")); } #[test] fn env_example_inventory_contains_current_runtime_variable_with_preceding_comment() { let content = std::fs::read_to_string(workspace_root().join(crate::DEFAULT_DOTENV_EXAMPLE_PATH)); assert!(content.is_ok(), ".env.example must be committed at workspace root"); let content = match content { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let lines = content.lines().collect::>(); let mut found = false; for index in 0..lines.len() { if lines[index].starts_with("KSP_LOGS_DIRECTORY=") { found = true; assert!(index > 0, "environment entry must have a preceding comment"); assert!(lines[index - 1].trim_start().starts_with('#'), "environment entry must be immediately preceded by a comment"); } } assert!(found, "current Config environment variable must appear in .env.example"); } fn workspace_root() -> std::path::PathBuf { return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); } fn logging_fixture_bootstrap() -> ksp_core_lib::Result { let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures"); let workspace = workspace_root(); return crate::ConfigBootstrapOptions::from_paths(fixture_root, workspace.join("config/schemas")); } #[test] fn secret_environment_value_keeps_real_value_but_redacts_safe_and_debug_views() { let canary = "KSP_SECRET_CANARY_91b7c6"; let mut process = std::collections::BTreeMap::::new(); process.insert("KSP_SECRET_TEST_TOKEN".to_owned(), canary.to_owned()); let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); let resolved = environment.resolve_variable("KSP_SECRET_TEST_TOKEN", std::option::Option::None); assert!(resolved.is_ok(), "secret process value should resolve"); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(resolved.value(), canary); assert_eq!(resolved.safe_value(), crate::REDACTED_CONFIG_VALUE); assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret); assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Process); let debug = format!("{resolved:?}"); assert!(!debug.contains(canary), "Debug must not reveal the secret canary"); assert!(debug.contains(crate::REDACTED_CONFIG_VALUE)); } #[test] fn detailed_text_redacts_only_secret_segments_and_keeps_ordered_provenance() { let secret = "secret-canary-4a62"; let mut process = std::collections::BTreeMap::::new(); process.insert("KSP_PUBLIC_HOST".to_owned(), "rpc.example.test".to_owned()); process.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned()); let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); let resolved = environment.resolve_text_detailed("https://${KSP_PUBLIC_HOST}/?token=${KSP_SECRET_TOKEN}"); assert!(resolved.is_ok(), "composed secret URL should resolve"); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(resolved.value(), "https://rpc.example.test/?token=secret-canary-4a62"); assert_eq!(resolved.safe_value(), "https://rpc.example.test/?token=********"); assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret); assert_eq!(resolved.provenance().len(), 4); assert_eq!(resolved.provenance()[0], crate::ConfigValueProvenance::DocumentLiteral); assert_eq!(resolved.provenance()[1].variable_name(), std::option::Option::Some("KSP_PUBLIC_HOST")); assert_eq!(resolved.provenance()[2], crate::ConfigValueProvenance::DocumentLiteral); assert_eq!(resolved.provenance()[3].variable_name(), std::option::Option::Some("KSP_SECRET_TOKEN")); let debug = format!("{resolved:?}"); assert!(!debug.contains(secret), "resolved text Debug must not reveal a secret segment"); } #[test] fn secret_fallback_inherits_secret_sensitivity_and_is_redacted() { let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let resolved = environment.resolve_text_detailed("token=${KSP_SECRET_TOKEN:-false-secret}"); assert!(resolved.is_ok(), "secret fallback should resolve"); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(resolved.value(), "token=false-secret"); assert_eq!(resolved.safe_value(), "token=********"); assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret); assert_eq!(resolved.provenance()[1].environment_source(), std::option::Option::Some(super::ConfigEnvironmentSource::Fallback)); } #[test] fn detailed_json_preserves_safe_tree_sensitivity_and_pointer_provenance() { let secret = "nested-secret-canary-2d11"; let mut dotenv = std::collections::BTreeMap::::new(); dotenv.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned()); let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv); let source = serde_json::json!({"transport": {"url": "https://host/?token=${KSP_SECRET_TOKEN}"}, "items": ["plain", 7]}); let resolved = environment.resolve_json_detailed(&source); assert!(resolved.is_ok(), "detailed JSON should resolve"); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(resolved.value()["transport"]["url"], serde_json::Value::String(format!("https://host/?token={secret}"))); assert_eq!(resolved.safe_value()["transport"]["url"], serde_json::Value::String("https://host/?token=********".to_owned())); assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret); let provenance = resolved.provenance_at("/transport/url"); assert!(provenance.is_some(), "JSON pointer provenance should exist"); if let std::option::Option::Some(provenance) = provenance { assert_eq!(provenance.last().and_then(crate::ConfigValueProvenance::variable_name), std::option::Option::Some("KSP_SECRET_TOKEN")); } let debug = format!("{resolved:?}"); assert!(!debug.contains(secret), "resolved JSON Debug must not reveal a secret canary"); } #[test] fn detailed_fixture_profile_environment_keeps_global_origin_and_adds_environment_provenance() { let bootstrap = logging_fixture_bootstrap(); let bootstrap = match bootstrap { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let registry = crate::ConfigFileRegistry::defaults(); let registry = match registry { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING); let file_id = match file_id { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let engine = crate::ConfigDocumentEngine::new(bootstrap, registry); let profile = engine.load_resolved_profile(&file_id, std::option::Option::None); let profile = match profile { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let effective = profile.resolve_effective_environment_detailed(&environment); assert!(effective.is_ok(), "detailed committed Logging profile should resolve"); let effective = match effective { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(profile.origin("logs_directory"), std::option::Option::Some(crate::ConfigValueOrigin::Global)); assert_eq!(effective.value()["logs_directory"], serde_json::Value::String("logs".to_owned())); assert_eq!(effective.safe_value()["logs_directory"], serde_json::Value::String("logs".to_owned())); assert_eq!( effective.provenance_at("/logs_directory").and_then(|items| return items.last()).and_then(crate::ConfigValueProvenance::variable_name), std::option::Option::Some("KSP_LOGS_DIRECTORY"), ); }