// file: crates/ksp-config-lib/unit_tests/document.rs // version: 3 #[test] fn committed_logging_document_passes_registered_schema_and_semantic_validation() { let workspace = workspace_root(); let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas")); let registry = crate::ConfigFileRegistry::defaults(); let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING); assert!(bootstrap.is_ok(), "workspace Config paths should be valid: {bootstrap:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}"); if let (std::result::Result::Ok(bootstrap), std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (bootstrap, registry, file_id) { let engine = super::ConfigDocumentEngine::new(bootstrap, registry); let document = engine.load_validated_document(&file_id); assert!(document.is_ok(), "committed std.logging.json should validate: {document:?}"); if let std::result::Result::Ok(document) = document { assert_eq!(document.file_id(), &file_id); assert_eq!(document.path(), workspace.join("config/std.logging.json").as_path()); let default_profile = document.value().get("default_profile").and_then(serde_json::Value::as_str); assert!(default_profile.is_some(), "validated Logging document should retain default_profile"); if let std::option::Option::Some(default_profile) = default_profile { let profiles = document.value().get("profiles").and_then(serde_json::Value::as_array); assert!(profiles.is_some(), "validated Logging document should retain profiles"); if let std::option::Option::Some(profiles) = profiles { assert!(profiles.iter().any(|profile| -> bool { return profile.get("profile_id").and_then(serde_json::Value::as_str) == std::option::Option::Some(default_profile); })); } } } } } #[test] fn missing_document_is_reported_with_file_read_error() { let fixture = fixture_roots("missing-document"); let prepared = prepare_fixture(&fixture, std::option::Option::None, valid_minimal_logging_schema()); assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}"); if prepared.is_ok() { let result = load_fixture(&fixture); assert_error_code(result, crate::ERROR_CODE_JSON_FILE_READ_FAILED); } cleanup_fixture(&fixture); } #[test] fn malformed_json_is_reported_before_schema_validation() { let fixture = fixture_roots("malformed-json"); let prepared = prepare_fixture(&fixture, std::option::Option::Some("{ invalid-json"), valid_minimal_logging_schema()); assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}"); if prepared.is_ok() { let result = load_fixture(&fixture); assert_error_code(result, crate::ERROR_CODE_JSON_SYNTAX_INVALID); } cleanup_fixture(&fixture); } #[test] fn invalid_schema_document_is_reported_before_instance_validation() { let fixture = fixture_roots("invalid-schema"); let schema = r#"{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "definitely-not-a-json-schema-type" }"#; let prepared = prepare_fixture(&fixture, std::option::Option::Some("{}"), schema); assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}"); if prepared.is_ok() { let result = load_fixture(&fixture); assert_error_code(result, crate::ERROR_CODE_SCHEMA_INVALID); } cleanup_fixture(&fixture); } #[test] fn schema_violation_is_distinct_from_json_syntax_failure() { let fixture = fixture_roots("schema-violation"); let schema = r#"{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["required_field"], "properties": { "required_field": {"type": "string"} } }"#; let prepared = prepare_fixture(&fixture, std::option::Option::Some("{}"), schema); assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}"); if prepared.is_ok() { let result = load_fixture(&fixture); assert_error_code(result, crate::ERROR_CODE_SCHEMA_VALIDATION_FAILED); } cleanup_fixture(&fixture); } #[test] fn schema_valid_logging_document_can_still_fail_ksp_semantics() { let fixture = fixture_roots("semantic-invalid"); let workspace = workspace_root(); let schema_source = std::fs::read_to_string(workspace.join("config/schemas/std.logging.schema.json")); assert!(schema_source.is_ok(), "committed Logging schema should be readable: {schema_source:?}"); if let std::result::Result::Ok(schema_source) = schema_source { let document = r#"{ "format_version": 1, "logs_directory": "logs", "default_profile": "duplicate-output", "profiles": [ { "profile_id": "duplicate-output", "default_filter": "info", "span_events": "off", "console": { "enabled": false, "output": "stderr", "ansi": false, "format": "human", "filter": {"level": "trace", "targets": ["*"], "domains": ["*"]} }, "files": [ { "output_id": "file.same", "enabled": true, "path": "first.log", "rotation": "daily", "format": "human", "ansi": false, "filter": {"level": "info", "targets": ["*"], "domains": ["*"]} }, { "output_id": "file.same", "enabled": true, "path": "second.log", "rotation": "daily", "format": "human", "ansi": false, "filter": {"level": "info", "targets": ["*"], "domains": ["*"]} } ], "target_filters": [] } ] }"#; let prepared = prepare_fixture(&fixture, std::option::Option::Some(document), schema_source.as_str()); assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}"); if prepared.is_ok() { let result = load_fixture(&fixture); assert_error_code(result, crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID); } } cleanup_fixture(&fixture); } #[test] fn duplicate_profile_ids_are_semantically_invalid() { let fixture = fixture_roots("duplicate-profile-id"); let workspace = workspace_root(); let schema_source = std::fs::read_to_string(workspace.join("config/schemas/std.logging.schema.json")); assert!(schema_source.is_ok(), "committed Logging schema should be readable: {schema_source:?}"); if let std::result::Result::Ok(schema_source) = schema_source { let source = valid_logging_source_with_profiles("first", "first", "first"); let prepared = prepare_fixture(&fixture, std::option::Option::Some(source.as_str()), schema_source.as_str()); assert!(prepared.is_ok(), "duplicate profile fixture should be writable: {prepared:?}"); if prepared.is_ok() { let result = load_fixture(&fixture); assert_error_code(result, crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID); } } cleanup_fixture(&fixture); } #[test] fn missing_default_profile_target_is_semantically_invalid() { let fixture = fixture_roots("missing-default-profile"); let workspace = workspace_root(); let schema_source = std::fs::read_to_string(workspace.join("config/schemas/std.logging.schema.json")); assert!(schema_source.is_ok(), "committed Logging schema should be readable: {schema_source:?}"); if let std::result::Result::Ok(schema_source) = schema_source { let source = valid_logging_source_with_profiles("missing", "first", "second"); let prepared = prepare_fixture(&fixture, std::option::Option::Some(source.as_str()), schema_source.as_str()); assert!(prepared.is_ok(), "missing default profile fixture should be writable: {prepared:?}"); if prepared.is_ok() { let result = load_fixture(&fixture); assert_error_code(result, crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID); } } cleanup_fixture(&fixture); } fn valid_logging_source_with_profiles(default_profile: &str, first_profile: &str, second_profile: &str) -> String { return format!( r#"{{ "format_version": 1, "logs_directory": "logs", "default_profile": "{default_profile}", "profiles": [ {}, {} ] }}"#, valid_logging_profile(first_profile, "file.first"), valid_logging_profile(second_profile, "file.second") ); } fn valid_logging_profile(profile_id: &str, output_id: &str) -> String { return format!( r#"{{ "profile_id": "{profile_id}", "default_filter": "info", "span_events": "off", "console": {{ "enabled": false, "output": "stderr", "ansi": false, "format": "human", "filter": {{"level": "trace", "targets": ["*"], "domains": ["*"]}} }}, "files": [{{ "output_id": "{output_id}", "enabled": true, "path": "output.log", "rotation": "daily", "format": "human", "ansi": false, "filter": {{"level": "info", "targets": ["*"], "domains": ["*"]}} }}], "target_filters": [] }}"# ); } fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result { let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.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 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), }; let engine = super::ConfigDocumentEngine::new(bootstrap, registry); return engine.load_validated_document(&file_id); } fn prepare_fixture(fixture: &FixtureRoots, document: std::option::Option<&str>, schema: &str) -> std::io::Result<()> { cleanup_fixture(fixture); let config = std::fs::create_dir_all(fixture.config.as_path()); if let std::result::Result::Err(error) = config { return std::result::Result::Err(error); } let schemas = std::fs::create_dir_all(fixture.schemas.as_path()); if let std::result::Result::Err(error) = schemas { return std::result::Result::Err(error); } let schema_write = std::fs::write(fixture.schemas.join(crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME), schema); if let std::result::Result::Err(error) = schema_write { return std::result::Result::Err(error); } if let std::option::Option::Some(document) = document { let document_write = std::fs::write(fixture.config.join(crate::DEFAULT_STD_LOGGING_FILENAME), document); if let std::result::Result::Err(error) = document_write { return std::result::Result::Err(error); } } return std::result::Result::Ok(()); } fn valid_minimal_logging_schema() -> &'static str { return r#"{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object" }"#; } fn assert_error_code(result: ksp_core_lib::Result, expected: ksp_core_lib::ErrorCode) { assert!(result.is_err(), "fixture should fail with {expected:?}: {result:?}"); if let std::result::Result::Err(error) = result { assert_eq!(error.code(), expected); } } fn workspace_root() -> std::path::PathBuf { return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); } struct FixtureRoots { root: std::path::PathBuf, config: std::path::PathBuf, schemas: std::path::PathBuf, } fn fixture_roots(name: &str) -> FixtureRoots { let mut root = std::env::temp_dir(); root.push(format!("ksp-config-lib-pre008-{name}-{}", std::process::id())); return FixtureRoots { config: root.join("config"), schemas: root.join("schemas"), root }; } fn cleanup_fixture(fixture: &FixtureRoots) { let result = std::fs::remove_dir_all(fixture.root.as_path()); if let std::result::Result::Err(error) = result { assert_eq!(error.kind(), std::io::ErrorKind::NotFound, "fixture cleanup should only ignore missing directories: {error}"); } }