// file: crates/ksp-config-lib/unit_tests/logging.rs // version: 4 #[test] fn fixture_logging_profile_maps_complete_runtime_contract() { let engine = fixture_engine(); let engine = match engine { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); assert!(resolved.is_ok(), "fixture Logging Config should map"); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_LOGGING); assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile); let effective = resolved.effective().value(); let effective_logs_directory = effective.get("logs_directory").and_then(serde_json::Value::as_str); assert!(effective_logs_directory.is_some(), "effective Logging Config should expose logs_directory"); if let std::option::Option::Some(effective_logs_directory) = effective_logs_directory { let source_path = std::path::Path::new(effective_logs_directory); let expected = if source_path.is_absolute() { source_path.to_path_buf() } else { current_directory().join(source_path) }; assert_eq!(resolved.logs_directory(), expected.as_path()); } let settings = resolved.settings(); let default_filter = effective.get("default_filter").and_then(serde_json::Value::as_str); let span_events = effective.get("span_events").and_then(serde_json::Value::as_str); assert_eq!(default_filter.and_then(test_level), std::option::Option::Some(settings.default_filter())); assert_eq!(span_events.and_then(test_span_events), std::option::Option::Some(settings.span_events())); let source_target_filters = effective.get("target_filters").and_then(serde_json::Value::as_array); assert!(source_target_filters.is_some(), "effective Logging Config should expose target_filters"); if let std::option::Option::Some(source_target_filters) = source_target_filters { assert_eq!(settings.target_filters().len(), source_target_filters.len()); for (runtime, source) in settings.target_filters().iter().zip(source_target_filters) { assert_eq!(source.get("target_prefix").and_then(serde_json::Value::as_str), std::option::Option::Some(runtime.target_prefix())); assert_eq!(source.get("level").and_then(serde_json::Value::as_str).and_then(test_level), std::option::Option::Some(runtime.level())); } } let console = settings.console(); let source_console = effective.get("console"); assert!(console.is_some(), "effective Logging Config declares console settings"); assert!(source_console.is_some(), "effective Logging Config should expose console settings"); if let (std::option::Option::Some(console), std::option::Option::Some(source_console)) = (console, source_console) { assert_eq!(source_console.get("enabled").and_then(serde_json::Value::as_bool), std::option::Option::Some(console.enabled())); assert_eq!(source_console.get("ansi").and_then(serde_json::Value::as_bool), std::option::Option::Some(console.ansi())); assert_eq!(source_console.get("output").and_then(serde_json::Value::as_str).and_then(test_console_output), std::option::Option::Some(console.output())); assert_eq!(source_console.get("format").and_then(serde_json::Value::as_str).and_then(test_format), std::option::Option::Some(console.format())); let filter = source_console.get("filter"); assert!(filter.is_some(), "effective console should expose filter"); if let std::option::Option::Some(filter) = filter { assert_eq!(filter.get("level").and_then(serde_json::Value::as_str).and_then(test_level), std::option::Option::Some(console.filter().level())); assert_eq!(json_string_array(filter.get("targets")), console.filter().targets().to_vec()); assert_eq!(json_string_array(filter.get("domains")), console.filter().domains().to_vec()); } } let source_files = effective.get("files").and_then(serde_json::Value::as_array); assert!(source_files.is_some(), "effective Logging Config should expose files"); if let std::option::Option::Some(source_files) = source_files { assert_eq!(settings.files().len(), source_files.len()); for (runtime, source) in settings.files().iter().zip(source_files) { assert_eq!(source.get("output_id").and_then(serde_json::Value::as_str), std::option::Option::Some(runtime.output_id())); assert_eq!(source.get("enabled").and_then(serde_json::Value::as_bool), std::option::Option::Some(runtime.enabled())); assert_eq!(source.get("rotation").and_then(serde_json::Value::as_str).and_then(test_rotation), std::option::Option::Some(runtime.rotation())); assert_eq!(source.get("format").and_then(serde_json::Value::as_str).and_then(test_format), std::option::Option::Some(runtime.format())); assert_eq!(source.get("ansi").and_then(serde_json::Value::as_bool), std::option::Option::Some(runtime.ansi())); let filter = source.get("filter"); assert!(filter.is_some(), "effective file should expose filter"); if let std::option::Option::Some(filter) = filter { assert_eq!(filter.get("level").and_then(serde_json::Value::as_str).and_then(test_level), std::option::Option::Some(runtime.filter().level())); assert_eq!(json_string_array(filter.get("targets")), runtime.filter().targets().to_vec()); assert_eq!(json_string_array(filter.get("domains")), runtime.filter().domains().to_vec()); } } } } fn test_level(value: &str) -> std::option::Option { return match value { "off" => std::option::Option::Some(ksp_logging_lib::LogFilterLevel::Off), "error" => std::option::Option::Some(ksp_logging_lib::LogFilterLevel::Error), "warn" => std::option::Option::Some(ksp_logging_lib::LogFilterLevel::Warn), "info" => std::option::Option::Some(ksp_logging_lib::LogFilterLevel::Info), "debug" => std::option::Option::Some(ksp_logging_lib::LogFilterLevel::Debug), "trace" => std::option::Option::Some(ksp_logging_lib::LogFilterLevel::Trace), _ => std::option::Option::None, }; } fn test_span_events(value: &str) -> std::option::Option { return match value { "off" => std::option::Option::Some(ksp_logging_lib::SpanEvents::Off), "new_and_close" => std::option::Option::Some(ksp_logging_lib::SpanEvents::NewAndClose), "full" => std::option::Option::Some(ksp_logging_lib::SpanEvents::Full), _ => std::option::Option::None, }; } fn test_console_output(value: &str) -> std::option::Option { return match value { "stdout" => std::option::Option::Some(ksp_logging_lib::ConsoleOutput::Stdout), "stderr" => std::option::Option::Some(ksp_logging_lib::ConsoleOutput::Stderr), _ => std::option::Option::None, }; } fn test_format(value: &str) -> std::option::Option { return match value { "human" => std::option::Option::Some(ksp_logging_lib::LogFormat::Human), "compact" => std::option::Option::Some(ksp_logging_lib::LogFormat::Compact), "pretty" => std::option::Option::Some(ksp_logging_lib::LogFormat::Pretty), "json" => std::option::Option::Some(ksp_logging_lib::LogFormat::Json), _ => std::option::Option::None, }; } fn test_rotation(value: &str) -> std::option::Option { return match value { "never" => std::option::Option::Some(ksp_logging_lib::FileRotation::Never), "hourly" => std::option::Option::Some(ksp_logging_lib::FileRotation::Hourly), "daily" => std::option::Option::Some(ksp_logging_lib::FileRotation::Daily), _ => std::option::Option::None, }; } fn json_string_array(value: std::option::Option<&serde_json::Value>) -> std::vec::Vec { let mut result = std::vec::Vec::::new(); if let std::option::Option::Some(values) = value.and_then(serde_json::Value::as_array) { for value in values { if let std::option::Option::Some(value) = value.as_str() { result.push(value.to_owned()); } } } return result; } #[test] fn relative_logs_directory_is_anchored_to_process_current_directory() { let engine = fixture_engine(); let engine = match engine { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let environment = environment_with_logs_directory("relative-ksp-logs"); let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); assert!(resolved.is_ok(), "relative Logging root should map"); if let std::result::Result::Ok(resolved) = resolved { assert_eq!(resolved.logs_directory(), current_directory().join("relative-ksp-logs").as_path()); assert_eq!(resolved.settings().files()[0].directory(), current_directory().join("relative-ksp-logs/debug").as_path()); } } #[test] fn absolute_logs_directory_is_preserved() { let engine = fixture_engine(); let engine = match engine { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let absolute = std::env::temp_dir().join(format!("ksp-pre012-absolute-{}", std::process::id())); let environment = environment_with_logs_directory(absolute.to_string_lossy().as_ref()); let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); assert!(resolved.is_ok(), "absolute Logging root should map even when it does not exist yet"); if let std::result::Result::Ok(resolved) = resolved { assert_eq!(resolved.logs_directory(), absolute.as_path()); } } #[test] fn effective_file_paths_cannot_escape_logging_root() { assert!(super::relative_file_path_is_valid("debug/ksp.log")); assert!(super::relative_file_path_is_valid("ksp.log")); assert!(!super::relative_file_path_is_valid("../ksp.log")); assert!(!super::relative_file_path_is_valid("./ksp.log")); assert!(!super::relative_file_path_is_valid("/var/log/ksp.log")); } #[test] fn explicit_empty_logs_directory_is_invalid_instead_of_using_fallback() { let engine = fixture_engine(); let engine = match engine { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let environment = environment_with_logs_directory(""); let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); let error = match resolved { std::result::Result::Ok(_) => return, std::result::Result::Err(error) => error, }; assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID); assert!(error.context().iter().any(|item| -> bool { return item.key() == "field" && item.value() == "logs_directory"; })); } #[test] fn existing_non_directory_logging_root_is_rejected_without_secret_leak() { let engine = fixture_engine(); let engine = match engine { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let secret = format!("ksp-secret-path-canary-{}", std::process::id()); let path = std::env::temp_dir().join(secret.as_str()); let write = std::fs::write(path.as_path(), b"not a directory"); assert!(write.is_ok(), "secret canary file should be created"); let profile = load_fixture_profile(&engine); let profile = match profile { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { cleanup_file(path.as_path()); return; }, }; let resolved = super::resolve_logs_directory(path.to_string_lossy().as_ref(), crate::REDACTED_CONFIG_VALUE, &profile); cleanup_file(path.as_path()); let error = match resolved { std::result::Result::Ok(_) => return, std::result::Result::Err(error) => error, }; assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID); let debug = format!("{error:?}"); assert!(!debug.contains(secret.as_str()), "effective Config diagnostics must not reveal real secret-derived paths"); assert!(debug.contains(crate::REDACTED_CONFIG_VALUE)); } #[test] fn logging_adapter_rejects_secret_effective_values_without_exposing_canary() { let engine = fixture_engine(); let engine = match engine { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let profile = load_fixture_profile(&engine); let profile = match profile { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let canary = "ksp-pre012-secret-canary-c3e4"; let mut process = std::collections::BTreeMap::::new(); process.insert("KSP_SECRET_LOGGING_CANARY".to_owned(), canary.to_owned()); let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); let effective = environment.resolve_json_detailed(&serde_json::json!({"canary": "${KSP_SECRET_LOGGING_CANARY}"})); assert!(effective.is_ok(), "secret canary fixture should resolve"); let effective = match effective { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let validation = super::validate_logging_sensitivity(&profile, &effective); let error = match validation { std::result::Result::Ok(()) => return, std::result::Result::Err(error) => error, }; assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID); let debug = format!("{error:?}"); assert!(!debug.contains(canary), "Logging adapter diagnostics must not reveal secret canaries"); } #[test] fn mapped_logging_settings_can_initialize_and_reinitialize_runtime() { let engine = fixture_engine(); let engine = match engine { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let root = std::env::temp_dir().join(format!("ksp-pre012-runtime-{}", std::process::id())); cleanup_directory(root.as_path()); let environment = environment_with_logs_directory(root.to_string_lossy().as_ref()); let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); assert!(resolved.is_ok(), "runtime Logging Config should map"); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let guard = ksp_logging_lib::initialize(resolved.settings()); assert!(guard.is_ok(), "mapped Logging settings should initialize the Logging runtime"); let mut guard = match guard { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert!(root.join("debug").is_dir(), "Logging initialization should create the first configured file directory"); assert!(root.join("config").is_dir(), "Logging initialization should create the second configured file directory"); let reload = ksp_logging_lib::reinitialize(&mut guard, resolved.settings()); assert!(reload.is_ok(), "mapped Logging settings should support hot reload"); let disabled = ksp_logging_lib::LoggingSettings::new( ksp_logging_lib::LogFilterLevel::Off, ksp_logging_lib::SpanEvents::Off, std::option::Option::None, std::vec::Vec::new(), ); let disable = ksp_logging_lib::reinitialize(&mut guard, &disabled); assert!(disable.is_ok(), "test Logging runtime should disable outputs before cleanup"); drop(guard); cleanup_directory(root.as_path()); } #[test] fn resolved_logging_debug_uses_safe_effective_view() { let engine = fixture_engine(); let engine = match engine { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let root = std::env::temp_dir().join(format!("ksp-pre012-debug-{}", std::process::id())); let environment = environment_with_logs_directory(root.to_string_lossy().as_ref()); let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); assert!(resolved.is_ok(), "Logging Config should map for Debug contract"); if let std::result::Result::Ok(resolved) = resolved { let debug = format!("{resolved:?}"); assert!(debug.contains("ResolvedLoggingConfig")); assert!(debug.contains("effective")); } } fn fixture_engine() -> ksp_core_lib::Result { let workspace = workspace_root(); let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures"); let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture_root, workspace.join("config/schemas")); let bootstrap = match bootstrap { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let registry = crate::ConfigFileRegistry::defaults(); let registry = match registry { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry)); } fn load_fixture_profile(engine: &crate::ConfigDocumentEngine) -> ksp_core_lib::Result { 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(error) => return std::result::Result::Err(error), }; return engine.load_resolved_profile(&file_id, std::option::Option::None); } fn environment_with_logs_directory(value: &str) -> crate::ConfigEnvironment { let mut process = std::collections::BTreeMap::::new(); process.insert("KSP_LOGS_DIRECTORY".to_owned(), value.to_owned()); return crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); } fn workspace_root() -> std::path::PathBuf { return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); } fn current_directory() -> std::path::PathBuf { let current = std::env::current_dir(); return match current { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => std::path::PathBuf::new(), }; } fn cleanup_file(path: &std::path::Path) { let removal = std::fs::remove_file(path); if let std::result::Result::Err(error) = removal && error.kind() != std::io::ErrorKind::NotFound { eprintln!("unable to cleanup Config Logging adapter file {}: {error}", path.display()); } } fn cleanup_directory(path: &std::path::Path) { let removal = std::fs::remove_dir_all(path); if let std::result::Result::Err(error) = removal && error.kind() != std::io::ErrorKind::NotFound { eprintln!("unable to cleanup Config Logging adapter directory {}: {error}", path.display()); } }