379 lines
19 KiB
Rust
379 lines
19 KiB
Rust
// 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<ksp_logging_lib::LogFilterLevel> {
|
|
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<ksp_logging_lib::SpanEvents> {
|
|
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<ksp_logging_lib::ConsoleOutput> {
|
|
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<ksp_logging_lib::LogFormat> {
|
|
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<ksp_logging_lib::FileRotation> {
|
|
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<String> {
|
|
let mut result = std::vec::Vec::<String>::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::<String, String>::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<crate::ConfigDocumentEngine> {
|
|
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<crate::ResolvedConfigProfile> {
|
|
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::<String, String>::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());
|
|
}
|
|
}
|