// file: crates/ksp-config-lib/unit_tests/management.rs // version: 5 static NEXT_FIXTURE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); struct ManagementFixture { root: std::path::PathBuf, config_path: std::path::PathBuf, dotenv_path: std::path::PathBuf, management: crate::ConfigManagement, } #[test] fn raw_management_read_remains_available_for_schema_invalid_source() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let invalid = "{\n \"format_version\": 1\n}\n"; let write = std::fs::write(fixture.config_path.as_path(), invalid.as_bytes()); assert!(write.is_ok(), "schema-invalid management fixture should be written"); 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(_) => { cleanup_fixture(&fixture); return; }, }; let raw = fixture.management.read_source(&file_id); assert!(raw.is_ok(), "raw management read should not require schema validity: {raw:?}"); if let std::result::Result::Ok(raw) = raw { assert_eq!(raw.content(), invalid); assert_eq!(raw.file_id(), &file_id); let debug = format!("{raw:?}"); assert!(!debug.contains("format_version"), "raw source content must not be exposed by Debug"); } let typed = fixture.management.load_logging_document(); assert!(typed.is_err(), "typed management load must still require a valid source document"); cleanup_fixture(&fixture); } #[test] fn valid_raw_source_candidate_is_persisted_exactly_and_reports_reload() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let original = std::fs::read_to_string(fixture.config_path.as_path()); let original = match original { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { cleanup_fixture(&fixture); return; }, }; let candidate = format!("{original} \n"); assert_ne!(candidate, original, "raw source candidate fixture must change the persisted bytes"); let corrupt = std::fs::write(fixture.config_path.as_path(), b"{\n"); assert!(corrupt.is_ok(), "existing managed source should be corruptible for repair test"); 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(_) => { cleanup_fixture(&fixture); return; }, }; let saved = fixture.management.save_source_candidate(&file_id, candidate.as_str()); assert!(saved.is_ok(), "valid raw source candidate should persist: {saved:?}"); if let std::result::Result::Ok(saved) = saved { assert!(saved.source_changed()); assert!(saved.reload_required()); } assert_eq!(std::fs::read_to_string(fixture.config_path.as_path()).ok(), std::option::Option::Some(candidate.clone())); let reloaded = fixture.management.load_logging_document(); assert!(reloaded.is_ok(), "raw source persistence must leave a valid managed document: {reloaded:?}"); let unchanged = fixture.management.save_source_candidate(&file_id, candidate.as_str()); assert!(unchanged.is_ok(), "identical raw source candidate should succeed: {unchanged:?}"); if let std::result::Result::Ok(unchanged) = unchanged { assert!(!unchanged.source_changed()); assert!(!unchanged.reload_required()); } cleanup_fixture(&fixture); } #[test] fn invalid_json_source_candidate_is_rejected_without_modifying_existing_file() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let before = std::fs::read(fixture.config_path.as_path()); let before = match before { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { cleanup_fixture(&fixture); 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(_) => { cleanup_fixture(&fixture); return; }, }; let saved = fixture.management.save_source_candidate(&file_id, "{"); assert!(saved.is_err(), "invalid JSON candidate must be rejected"); if let std::result::Result::Err(error) = saved { assert_eq!(error.code(), crate::ERROR_CODE_JSON_SYNTAX_INVALID); } assert_eq!(std::fs::read(fixture.config_path.as_path()).ok(), std::option::Option::Some(before)); cleanup_fixture(&fixture); } #[test] fn schema_invalid_source_candidate_is_rejected_without_modifying_existing_file() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let before = std::fs::read(fixture.config_path.as_path()); let before = match before { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { cleanup_fixture(&fixture); 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(_) => { cleanup_fixture(&fixture); return; }, }; let saved = fixture.management.save_source_candidate(&file_id, "{\"format_version\":1}"); assert!(saved.is_err(), "schema-invalid candidate must be rejected"); if let std::result::Result::Err(error) = saved { assert_eq!(error.code(), crate::ERROR_CODE_SCHEMA_VALIDATION_FAILED); } assert_eq!(std::fs::read(fixture.config_path.as_path()).ok(), std::option::Option::Some(before)); cleanup_fixture(&fixture); } #[test] fn semantic_invalid_source_candidate_is_rejected_without_modifying_existing_file() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let before = std::fs::read_to_string(fixture.config_path.as_path()); let before = match before { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { cleanup_fixture(&fixture); return; }, }; let candidate_value = serde_json::from_str::(before.as_str()); let mut candidate_value = match candidate_value { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { cleanup_fixture(&fixture); return; }, }; candidate_value["default_profile"] = serde_json::Value::String("missing".to_owned()); let candidate = serde_json::to_string_pretty(&candidate_value); let candidate = match candidate { std::result::Result::Ok(value) => format!("{value}\n"), std::result::Result::Err(_) => { cleanup_fixture(&fixture); return; }, }; assert_ne!(candidate, before, "semantic-invalid fixture must alter the default profile"); 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(_) => { cleanup_fixture(&fixture); return; }, }; let saved = fixture.management.save_source_candidate(&file_id, candidate.as_str()); assert!(saved.is_err(), "semantic-invalid candidate must be rejected"); if let std::result::Result::Err(error) = saved { assert_eq!(error.code(), crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID); } assert_eq!(std::fs::read_to_string(fixture.config_path.as_path()).ok(), std::option::Option::Some(before)); cleanup_fixture(&fixture); } #[test] fn raw_source_candidate_rejects_schema_and_unknown_file_ids_without_filesystem_escape() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let schema_file_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_LOGGING); if let std::result::Result::Ok(schema_file_id) = schema_file_id { let saved = fixture.management.save_source_candidate(&schema_file_id, "{}"); assert!(saved.is_err(), "schema file_id must not be writable through Config source management"); if let std::result::Result::Err(error) = saved { assert_eq!(error.code(), crate::ERROR_CODE_MANAGEMENT_OPERATION_INVALID); } } let unknown_file_id = crate::ConfigFileId::new("cfg.not.registered"); if let std::result::Result::Ok(unknown_file_id) = unknown_file_id { let saved = fixture.management.save_source_candidate(&unknown_file_id, "{}"); assert!(saved.is_err(), "unknown Config file_id must not become an arbitrary filesystem write"); if let std::result::Result::Err(error) = saved { assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN); } } assert!(!fixture.root.join("not.registered").exists()); cleanup_fixture(&fixture); } #[test] fn logging_file_ansi_mutator_completes_typed_management_surface() { let filter = crate::LoggingOutputFilterConfig::new("info", std::vec!["*".to_owned()], std::vec!["*".to_owned()]); let mut file = crate::LoggingFileConfig::new("file.test", true, "test.log", "daily", "human", filter); assert!(!file.ansi()); file.set_ansi(true); assert!(file.ansi()); } #[test] fn typed_logging_document_can_be_mutated_validated_and_persisted_atomically() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let document = fixture.management.load_logging_document(); assert!(document.is_ok(), "committed Logging document should load through management: {document:?}"); let mut document = match document { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { cleanup_fixture(&fixture); return; }, }; assert_eq!(document.format_version(), 1); assert!(!document.default_profile().is_empty()); assert!(!document.profiles().is_empty()); document.set_logs_directory("managed-logs"); if let std::option::Option::Some(profile) = document.profiles_mut().first_mut() { profile.set_default_filter("info"); } let report = fixture.management.save_logging_document(&document); assert!(report.is_ok(), "valid typed Logging mutation should persist: {report:?}"); if let std::result::Result::Ok(report) = report { assert!(report.source_changed()); assert!(report.reload_required()); } let persisted = std::fs::read_to_string(fixture.config_path.as_path()); assert!(persisted.is_ok(), "persisted Logging document should remain readable"); if let std::result::Result::Ok(persisted) = persisted { assert!(persisted.ends_with('\n')); assert!(persisted.contains("\"logs_directory\": \"managed-logs\"")); assert!(persisted.contains("\"default_filter\": \"info\"")); } let reloaded = fixture.management.load_logging_document(); assert!(reloaded.is_ok(), "persisted Logging document should remain valid: {reloaded:?}"); if let std::result::Result::Ok(reloaded) = reloaded { assert_eq!(reloaded.logs_directory(), "managed-logs"); assert_eq!(reloaded.profiles()[0].default_filter(), "info"); } cleanup_fixture(&fixture); } #[test] fn invalid_logging_candidate_does_not_modify_existing_file() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let before = std::fs::read(fixture.config_path.as_path()); let before = match before { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { cleanup_fixture(&fixture); return; }, }; let document = fixture.management.load_logging_document(); let mut document = match document { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { cleanup_fixture(&fixture); return; }, }; document.set_logs_directory(""); let save = fixture.management.save_logging_document(&document); assert!(save.is_err(), "schema-invalid candidate must be rejected before persistence"); let after = std::fs::read(fixture.config_path.as_path()); assert_eq!(after.ok(), std::option::Option::Some(before)); cleanup_fixture(&fixture); } #[test] fn unchanged_logging_candidate_reports_no_reload() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let document = fixture.management.load_logging_document(); let document = match document { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { cleanup_fixture(&fixture); return; }, }; let first = fixture.management.save_logging_document(&document); assert!(first.is_ok(), "normalization save should succeed: {first:?}"); let second = fixture.management.save_logging_document(&document); assert!(second.is_ok(), "second identical save should succeed: {second:?}"); if let std::result::Result::Ok(second) = second { assert!(!second.source_changed()); assert!(!second.reload_required()); } cleanup_fixture(&fixture); } #[test] fn dotenv_create_update_and_remove_round_trip_through_management() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let create = fixture.management.set_dotenv_value("KSP_PRE013_MANAGED_TEST_VALUE", "alpha"); assert!(create.is_ok(), "managed .env create should succeed: {create:?}"); if let std::result::Result::Ok(create) = create { assert!(create.source_changed()); assert!(create.effective_changed()); assert!(!create.shadowed_by_process_environment()); assert!(create.reload_required()); } let update = fixture.management.set_dotenv_value("KSP_PRE013_MANAGED_TEST_VALUE", "hello world # 2"); assert!(update.is_ok(), "managed .env update should support quoting: {update:?}"); let revealed = fixture.management.reveal_dotenv_value("KSP_PRE013_MANAGED_TEST_VALUE"); assert_eq!(revealed.ok(), std::option::Option::Some(std::option::Option::Some("hello world # 2".to_owned()))); #[cfg(unix)] { let metadata = std::fs::metadata(fixture.dotenv_path.as_path()); assert!(metadata.is_ok(), "new managed .env permissions should be inspectable"); if let std::result::Result::Ok(metadata) = metadata { let mode = ::mode(&metadata.permissions()); assert_eq!(mode & 0o777, 0o600, "new .env must be private on Unix"); } } let content = std::fs::read_to_string(fixture.dotenv_path.as_path()); assert!(content.is_ok(), "managed .env should be readable"); if let std::result::Result::Ok(content) = content { assert!(content.contains("KSP_PRE013_MANAGED_TEST_VALUE=\"hello world # 2\"")); } let remove = fixture.management.remove_dotenv_value("KSP_PRE013_MANAGED_TEST_VALUE"); assert!(remove.is_ok(), "managed .env remove should succeed: {remove:?}"); if let std::result::Result::Ok(remove) = remove { assert!(remove.source_changed()); assert!(remove.effective_changed()); assert!(remove.reload_required()); } assert_eq!(fixture.management.reveal_dotenv_value("KSP_PRE013_MANAGED_TEST_VALUE").ok(), std::option::Option::Some(std::option::Option::None)); cleanup_fixture(&fixture); } #[test] fn dotenv_comments_and_unrelated_entries_survive_targeted_mutation() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let source = "# local header\nEXTERNAL_VALUE=keep\n# managed comment\nKSP_PRE013_MANAGED_TEST_VALUE=before\n"; let write = std::fs::write(fixture.dotenv_path.as_path(), source.as_bytes()); assert!(write.is_ok(), "dotenv preservation fixture should be written"); let update = fixture.management.set_dotenv_value("KSP_PRE013_MANAGED_TEST_VALUE", "after"); assert!(update.is_ok(), "targeted .env update should succeed: {update:?}"); let content = std::fs::read_to_string(fixture.dotenv_path.as_path()); assert!(content.is_ok(), "updated .env should remain readable"); if let std::result::Result::Ok(content) = content { assert!(content.contains("# local header")); assert!(content.contains("EXTERNAL_VALUE=keep")); assert!(content.contains("# managed comment")); assert!(content.contains("KSP_PRE013_MANAGED_TEST_VALUE=after")); } cleanup_fixture(&fixture); } #[test] fn invalid_existing_dotenv_is_not_modified_by_management() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let invalid = "KSP_BROKEN\n"; let write = std::fs::write(fixture.dotenv_path.as_path(), invalid.as_bytes()); assert!(write.is_ok(), "invalid .env fixture should be written"); let update = fixture.management.set_dotenv_value("KSP_PRE013_MANAGED_TEST_VALUE", "value"); assert!(update.is_err(), "management must reject mutation when existing .env syntax is invalid"); assert_eq!(std::fs::read_to_string(fixture.dotenv_path.as_path()).ok(), std::option::Option::Some(invalid.to_owned())); cleanup_fixture(&fixture); } #[test] fn unsupported_environment_name_is_rejected_without_creating_dotenv() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let update = fixture.management.set_dotenv_value("OTHER_TOKEN", "value"); assert!(update.is_err(), "non-KSP variable must be rejected"); assert!(!fixture.dotenv_path.exists(), "rejected mutation must not create .env"); cleanup_fixture(&fixture); } #[test] fn management_report_redacts_secret_but_explicit_reveal_returns_real_value() { let fixture = management_fixture(); let fixture = match fixture { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let canary = "SECRET-CANARY-PRE013"; let write = std::fs::write(fixture.dotenv_path.as_path(), format!("KSP_SECRET_PRE013_MANAGED_TEST_TOKEN={canary}\n")); assert!(write.is_ok(), "secret management fixture should be written"); let reports = fixture.management.environment_report(); assert!(reports.is_ok(), "safe environment report should load: {reports:?}"); if let std::result::Result::Ok(reports) = reports { let report = reports.iter().find(|report| -> bool { return report.variable_name() == "KSP_SECRET_PRE013_MANAGED_TEST_TOKEN"; }); assert!(report.is_some(), "secret entry should appear in management report"); if let std::option::Option::Some(report) = report { assert_eq!(report.sensitivity(), crate::ConfigSensitivity::Secret); assert_eq!(report.desired_safe_value(), std::option::Option::Some(crate::REDACTED_CONFIG_VALUE)); assert_eq!(report.effective_safe_value(), std::option::Option::Some(crate::REDACTED_CONFIG_VALUE)); assert!(!report.shadowed_by_process_environment()); assert!(!format!("{report:?}").contains(canary)); } } let reveal = fixture.management.reveal_effective_environment_value("KSP_SECRET_PRE013_MANAGED_TEST_TOKEN"); assert_eq!(reveal.ok(), std::option::Option::Some(std::option::Option::Some(canary.to_owned()))); cleanup_fixture(&fixture); } #[test] fn process_shadowing_report_distinguishes_desired_and_effective_changes() { let mut process_before = std::collections::BTreeMap::::new(); process_before.insert("KSP_MODE".to_owned(), "process".to_owned()); let mut dotenv_before = std::collections::BTreeMap::::new(); dotenv_before.insert("KSP_MODE".to_owned(), "desired-a".to_owned()); let before = crate::ConfigEnvironment::from_maps(process_before.clone(), dotenv_before); let mut dotenv_after = std::collections::BTreeMap::::new(); dotenv_after.insert("KSP_MODE".to_owned(), "desired-b".to_owned()); let after = crate::ConfigEnvironment::from_maps(process_before, dotenv_after); let report = super::environment_change_report("KSP_MODE", &before, &after); assert!(report.source_changed()); assert!(!report.effective_changed()); assert!(report.shadowed_by_process_environment()); assert!(!report.reload_required()); let reports = super::build_environment_reports(&after); assert!(reports.is_ok(), "shadow report fixture should build: {reports:?}"); if let std::result::Result::Ok(reports) = reports { assert_eq!(reports.len(), 1); assert_eq!(reports[0].desired_safe_value(), std::option::Option::Some("desired-b")); assert_eq!(reports[0].effective_safe_value(), std::option::Option::Some("process")); assert_eq!(reports[0].effective_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process)); assert!(reports[0].shadowed_by_process_environment()); } } fn management_fixture() -> ksp_core_lib::Result { let fixture_id = NEXT_FIXTURE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let root = std::env::temp_dir().join(format!("ksp-pre013-management-{}-{fixture_id}", std::process::id())); let cleanup = std::fs::remove_dir_all(root.as_path()); if let std::result::Result::Err(error) = cleanup && error.kind() != std::io::ErrorKind::NotFound { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_PERSISTENCE_WRITE_FAILED, "unable to cleanup previous management fixture").with_source(error), ); } let config_root = root.join("config"); let schema_root = config_root.join("schemas"); let create = std::fs::create_dir_all(schema_root.as_path()); if let std::result::Result::Err(error) = create { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_PERSISTENCE_WRITE_FAILED, "unable to create management fixture").with_source(error), ); } let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); let source_config = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures/std.logging.json"); let source_schema = workspace.join("config/schemas/std.logging.schema.json"); let config_path = config_root.join("std.logging.json"); let schema_path = schema_root.join("std.logging.schema.json"); let copy_config = std::fs::copy(source_config.as_path(), config_path.as_path()); if let std::result::Result::Err(error) = copy_config { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_PERSISTENCE_WRITE_FAILED, "unable to copy management Config fixture").with_source(error), ); } let copy_schema = std::fs::copy(source_schema.as_path(), schema_path.as_path()); if let std::result::Result::Err(error) = copy_schema { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_PERSISTENCE_WRITE_FAILED, "unable to copy management schema fixture").with_source(error), ); } let bootstrap = crate::ConfigBootstrapOptions::from_paths(config_root.as_path(), schema_root.as_path()); 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), }; let engine = crate::ConfigDocumentEngine::new(bootstrap, registry); let dotenv_path = root.join(".env"); let management = crate::ConfigManagement::with_dotenv_path(engine, dotenv_path.clone()); return std::result::Result::Ok(ManagementFixture { root, config_path, dotenv_path, management }); } fn cleanup_fixture(fixture: &ManagementFixture) { let cleanup = std::fs::remove_dir_all(fixture.root.as_path()); if let std::result::Result::Err(error) = cleanup && error.kind() != std::io::ErrorKind::NotFound { eprintln!("unable to cleanup Config management fixture {}: {error}", fixture.root.display()); } }