#![cfg(feature = "metadata")]
#[cfg(test)]
#[cfg(feature = "metadata")]
mod validation_tests {
use proptest::prelude::*;
use serde_json::json;
use settings_loader::metadata::{ConfigSchema, Constraint, SettingGroup, SettingMetadata, SettingType, Visibility};
use std::time::Duration;
#[test]
fn test_pattern_constraint_valid_regex_match() {
let constraint = Constraint::Pattern("[a-z]+".to_string());
let value = json!("hello");
assert!(constraint.validate("test_key", &value).is_ok());
}
#[test]
fn test_pattern_constraint_invalid_regex_mismatch() {
let constraint = Constraint::Pattern("[0-9]+".to_string());
let value = json!("hello");
assert!(constraint.validate("test_key", &value).is_err());
}
#[test]
fn test_range_constraint_within_bounds() {
let constraint = Constraint::Range { min: 1.0, max: 100.0 };
let value = json!(50);
assert!(constraint.validate("test_key", &value).is_ok());
}
#[test]
fn test_range_constraint_below_minimum() {
let constraint = Constraint::Range { min: 1.0, max: 100.0 };
let value = json!(0);
assert!(constraint.validate("test_key", &value).is_err());
}
#[test]
fn test_range_constraint_above_maximum() {
let constraint = Constraint::Range { min: 1.0, max: 100.0 };
let value = json!(101);
assert!(constraint.validate("test_key", &value).is_err());
}
#[test]
fn test_length_constraint_valid_string_length() {
let constraint = Constraint::Length { min: 1, max: 10 };
let value = json!("hello");
assert!(constraint.validate("test_key", &value).is_ok());
}
#[test]
fn test_length_constraint_string_too_short() {
let constraint = Constraint::Length { min: 5, max: 10 };
let value = json!("hi");
assert!(constraint.validate("test_key", &value).is_err());
}
#[test]
fn test_length_constraint_string_too_long() {
let constraint = Constraint::Length { min: 1, max: 5 };
let value = json!("toolongstring");
assert!(constraint.validate("test_key", &value).is_err());
}
#[test]
fn test_required_constraint_with_some_value() {
let constraint = Constraint::Required;
let value = json!("something");
assert!(constraint.validate("test_key", &value).is_ok());
}
#[test]
fn test_required_constraint_with_null_value() {
let constraint = Constraint::Required;
let value = json!(null);
assert!(constraint.validate("test_key", &value).is_err());
}
#[test]
fn test_oneof_constraint_value_in_set() {
let constraint = Constraint::OneOf(vec!["red".to_string(), "green".to_string(), "blue".to_string()]);
let value = json!("red");
assert!(constraint.validate("test_key", &value).is_ok());
}
#[test]
fn test_oneof_constraint_value_not_in_set() {
let constraint = Constraint::OneOf(vec!["red".to_string(), "green".to_string(), "blue".to_string()]);
let value = json!("yellow");
assert!(constraint.validate("test_key", &value).is_err());
}
#[test]
fn test_custom_constraint_placeholder() {
let constraint = Constraint::Custom("my_validator".to_string());
let value = json!("somevalue");
let result = constraint.validate("test_key", &value);
assert!(result.is_ok() || result.is_err()); }
#[test]
fn test_multiple_constraints_all_pass() {
let constraints = vec![
Constraint::Required,
Constraint::Pattern("[a-z]+".to_string()),
Constraint::Length { min: 1, max: 10 },
];
let value = json!("hello");
for constraint in constraints {
assert!(constraint.validate("test_key", &value).is_ok());
}
}
#[test]
fn test_string_type_validation_with_pattern() {
let setting_type = SettingType::String {
pattern: Some("[0-9]+".to_string()),
min_length: Some(1),
max_length: Some(10),
};
let value = json!("12345");
assert!(setting_type.validate("test_key", &value).is_ok());
}
#[test]
fn test_string_type_validation_invalid_pattern() {
let setting_type = SettingType::String {
pattern: Some("[0-9]+".to_string()),
min_length: None,
max_length: None,
};
let value = json!("abc");
assert!(setting_type.validate("test_key", &value).is_err());
}
#[test]
fn test_integer_type_validation_with_range() {
let setting_type = SettingType::Integer { min: Some(10), max: Some(100) };
let value = json!(50);
assert!(setting_type.validate("test_key", &value).is_ok());
}
#[test]
fn test_integer_type_validation_out_of_range() {
let setting_type = SettingType::Integer { min: Some(10), max: Some(100) };
let value = json!(150);
assert!(setting_type.validate("test_key", &value).is_err());
}
#[test]
fn test_float_type_validation_with_range() {
let setting_type = SettingType::Float { min: Some(0.0), max: Some(1.0) };
let value = json!(0.5);
assert!(setting_type.validate("test_key", &value).is_ok());
}
#[test]
fn test_path_type_validation_format() {
let setting_type = SettingType::Path { must_exist: false, is_directory: false };
let value = json!("/etc/config.toml");
assert!(setting_type.validate("test_key", &value).is_ok());
}
#[test]
fn test_url_type_validation_with_scheme() {
let setting_type = SettingType::Url { schemes: vec!["https".to_string()] };
let value = json!("https://example.com");
assert!(setting_type.validate("test_key", &value).is_ok());
}
#[test]
fn test_url_type_validation_invalid_scheme() {
let setting_type = SettingType::Url { schemes: vec!["https".to_string()] };
let value = json!("http://example.com");
assert!(setting_type.validate("test_key", &value).is_err());
}
#[test]
fn test_setting_metadata_validation_integration() {
let metadata = SettingMetadata {
key: "port".to_string(),
label: "Port".to_string(),
description: "Server port".to_string(),
setting_type: SettingType::Integer { min: Some(1024), max: Some(65535) },
default: Some(json!(8080)),
constraints: vec![Constraint::Required],
visibility: Visibility::Public,
group: Some("server".to_string()),
};
assert!(metadata.validate(&json!(8080)).is_valid());
assert!(!metadata.validate(&json!(65536)).is_valid()); }
#[test]
fn test_validate_config_multiple_settings() {
let schema = ConfigSchema {
name: "test-app".to_string(),
version: "1.0.0".to_string(),
settings: vec![
SettingMetadata {
key: "host".to_string(),
label: "Host".to_string(),
description: "Server host".to_string(),
setting_type: SettingType::String {
pattern: None,
min_length: Some(1),
max_length: None,
},
default: Some(json!("localhost")),
constraints: vec![Constraint::Required],
visibility: Visibility::Public,
group: None,
},
SettingMetadata {
key: "port".to_string(),
label: "Port".to_string(),
description: "Server port".to_string(),
setting_type: SettingType::Integer { min: Some(1024), max: Some(65535) },
default: Some(json!(8080)),
constraints: vec![],
visibility: Visibility::Public,
group: None,
},
],
groups: vec![],
};
assert!(schema.settings[0].validate(&json!("localhost")).is_valid());
assert!(schema.settings[1].validate(&json!(8080)).is_valid());
}
#[test]
fn test_validation_error_accumulation() {
let metadata = SettingMetadata {
key: "timeout".to_string(),
label: "Timeout".to_string(),
description: "Request timeout".to_string(),
setting_type: SettingType::Integer { min: Some(1), max: Some(300) },
default: None,
constraints: vec![Constraint::Required, Constraint::Range { min: 1.0, max: 300.0 }],
visibility: Visibility::Public,
group: None,
};
let invalid_value = json!(-10);
let result = metadata.validate(&invalid_value);
assert!(!result.is_valid());
assert!(result.error_count() >= 1);
}
#[test]
fn test_validation_with_trait_object() {
let metadata = SettingMetadata {
key: "api_key".to_string(),
label: "API Key".to_string(),
description: "Authentication key".to_string(),
setting_type: SettingType::String {
pattern: Some("[a-zA-Z0-9]{32}".to_string()),
min_length: Some(32),
max_length: Some(32),
},
default: None,
constraints: vec![Constraint::Required],
visibility: Visibility::Secret,
group: None,
};
let valid_key = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6";
assert!(metadata.validate(&json!(valid_key)).is_valid());
}
#[test]
fn test_validation_warning_generation() {
let metadata = SettingMetadata {
key: "max_connections".to_string(),
label: "Max Connections".to_string(),
description: "Maximum concurrent connections".to_string(),
setting_type: SettingType::Integer { min: Some(1), max: Some(1000) },
default: Some(json!(100)),
constraints: vec![],
visibility: Visibility::Advanced,
group: None,
};
let value = json!(1000);
let result = metadata.validate(&value);
assert!(result.is_valid()); }
#[test]
fn test_turtle_configuration_validation() {
let metadata = SettingMetadata {
key: "cluster_name".to_string(),
label: "Cluster Name".to_string(),
description: "FoundationDB cluster identifier".to_string(),
setting_type: SettingType::String {
pattern: Some("^[a-z0-9_-]+$".to_string()), min_length: Some(1),
max_length: Some(64),
},
default: None,
constraints: vec![Constraint::Required],
visibility: Visibility::Public,
group: Some("cluster".to_string()),
};
let config = json!("my-cluster");
assert!(metadata.validate(&config).is_valid());
let invalid_config = json!("invalid cluster!");
assert!(!metadata.validate(&invalid_config).is_valid());
}
#[test]
fn test_complex_nested_config_validation() {
let schema = ConfigSchema {
name: "complex-app".to_string(),
version: "1.0.0".to_string(),
settings: vec![
SettingMetadata {
key: "server.http.host".to_string(),
label: "HTTP Host".to_string(),
description: "HTTP server bind address".to_string(),
setting_type: SettingType::String { pattern: None, min_length: None, max_length: None },
default: Some(json!("0.0.0.0")),
constraints: vec![Constraint::Required],
visibility: Visibility::Public,
group: Some("server".to_string()),
},
SettingMetadata {
key: "server.http.port".to_string(),
label: "HTTP Port".to_string(),
description: "HTTP server listen port".to_string(),
setting_type: SettingType::Integer { min: Some(1024), max: Some(65535) },
default: Some(json!(8080)),
constraints: vec![Constraint::Required],
visibility: Visibility::Public,
group: Some("server".to_string()),
},
SettingMetadata {
key: "database.connection_pool.min_size".to_string(),
label: "Min Pool Size".to_string(),
description: "Minimum database connection pool size".to_string(),
setting_type: SettingType::Integer { min: Some(1), max: Some(100) },
default: Some(json!(5)),
constraints: vec![],
visibility: Visibility::Advanced,
group: Some("database".to_string()),
},
SettingMetadata {
key: "logging.level".to_string(),
label: "Log Level".to_string(),
description: "Application logging level".to_string(),
setting_type: SettingType::Enum {
variants: vec![
"debug".to_string(),
"info".to_string(),
"warn".to_string(),
"error".to_string(),
],
},
default: Some(json!("info")),
constraints: vec![],
visibility: Visibility::Public,
group: Some("logging".to_string()),
},
],
groups: vec![
SettingGroup {
name: "server".to_string(),
label: "Server Configuration".to_string(),
description: "HTTP server settings".to_string(),
settings: vec!["server.http.host".to_string(), "server.http.port".to_string()],
},
SettingGroup {
name: "database".to_string(),
label: "Database Configuration".to_string(),
description: "Database connection settings".to_string(),
settings: vec!["database.connection_pool.min_size".to_string()],
},
SettingGroup {
name: "logging".to_string(),
label: "Logging Configuration".to_string(),
description: "Application logging settings".to_string(),
settings: vec!["logging.level".to_string()],
},
],
};
assert!(schema.settings[0].validate(&json!("0.0.0.0")).is_valid()); assert!(schema.settings[1].validate(&json!(8080)).is_valid()); assert!(schema.settings[2].validate(&json!(10)).is_valid()); assert!(schema.settings[3].validate(&json!("info")).is_valid()); }
#[test]
fn test_enum_type_validation() {
let setting_type = SettingType::Enum {
variants: vec!["small".to_string(), "medium".to_string(), "large".to_string()],
};
assert!(setting_type.validate("size", &json!("medium")).is_ok());
assert!(setting_type.validate("size", &json!("xlarge")).is_err());
}
#[test]
fn test_boolean_type_validation() {
let setting_type = SettingType::Boolean;
assert!(setting_type.validate("flag", &json!(true)).is_ok());
assert!(setting_type.validate("flag", &json!(false)).is_ok());
assert!(setting_type.validate("flag", &json!("yes")).is_err());
}
#[test]
fn test_duration_type_validation() {
let setting_type = SettingType::Duration {
min: Some(Duration::from_secs(1)),
max: Some(Duration::from_secs(3600)),
};
let valid_duration = json!(30000u64); assert!(setting_type.validate("timeout", &valid_duration).is_ok());
let min_duration = json!(1000u64); assert!(setting_type.validate("timeout", &min_duration).is_ok());
let another_duration = json!(500u64);
assert!(setting_type.validate("timeout", &another_duration).is_ok());
let invalid_type = json!("not a number");
assert!(setting_type.validate("timeout", &invalid_type).is_err());
}
#[test]
fn test_array_type_validation() {
let setting_type = SettingType::Array {
element_type: Box::new(SettingType::String { pattern: None, min_length: None, max_length: None }),
min_items: Some(1),
max_items: Some(5),
};
assert!(setting_type.validate("items", &json!(["a", "b"])).is_ok());
assert!(setting_type.validate("items", &json!([])).is_err()); }
#[test]
fn test_secret_type_validation() {
let setting_type = SettingType::Secret;
assert!(setting_type.validate("password", &json!("secret123")).is_ok());
}
#[test]
fn test_any_type_validation() {
let setting_type = SettingType::Any;
assert!(setting_type.validate("anything", &json!("string")).is_ok());
assert!(setting_type.validate("anything", &json!(123)).is_ok());
assert!(setting_type.validate("anything", &json!(null)).is_ok());
assert!(setting_type.validate("anything", &json!({})).is_ok());
}
#[test]
fn test_nested_object_validation() {
let inner_metadata = vec![SettingMetadata {
key: "name".to_string(),
label: "Name".to_string(),
description: "Item name".to_string(),
setting_type: SettingType::String {
pattern: None,
min_length: Some(1),
max_length: None,
},
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
}];
let setting_type = SettingType::Object { fields: inner_metadata };
let value = json!({"name": "test"});
assert!(setting_type.validate("obj", &value).is_ok());
}
#[test]
fn test_array_of_objects_validation() {
let object_fields = vec![SettingMetadata {
key: "id".to_string(),
label: "ID".to_string(),
description: "Identifier".to_string(),
setting_type: SettingType::Integer { min: None, max: None },
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
}];
let setting_type = SettingType::Array {
element_type: Box::new(SettingType::Object { fields: object_fields }),
min_items: Some(1),
max_items: None,
};
let value = json!([{"id": 1}, {"id": 2}]);
assert!(setting_type.validate("items", &value).is_ok());
}
#[test]
fn test_validation_error_message_format() {
let metadata = SettingMetadata {
key: "email".to_string(),
label: "Email".to_string(),
description: "User email address".to_string(),
setting_type: SettingType::String {
pattern: Some("[a-z0-9]+@[a-z]+\\.[a-z]{2,}".to_string()),
min_length: None,
max_length: None,
},
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
};
let result = metadata.validate(&json!("invalid-email"));
assert!(!result.is_valid());
assert!(!result.errors().is_empty());
}
#[test]
fn test_multiple_constraint_violation_messages() {
let metadata = SettingMetadata {
key: "code".to_string(),
label: "Code".to_string(),
description: "Alphanumeric code".to_string(),
setting_type: SettingType::String {
pattern: Some("[A-Z0-9]+".to_string()),
min_length: Some(3),
max_length: Some(10),
},
default: None,
constraints: vec![Constraint::Required, Constraint::Pattern("[A-Z0-9]+".to_string())],
visibility: Visibility::Public,
group: None,
};
let result = metadata.validate(&json!("ab"));
assert!(!result.is_valid());
}
#[test]
fn test_deeply_nested_validation_with_groups() {
let schema = ConfigSchema {
name: "nested-app".to_string(),
version: "1.0.0".to_string(),
settings: vec![SettingMetadata {
key: "app.database.host".to_string(),
label: "Database Host".to_string(),
description: "Database hostname".to_string(),
setting_type: SettingType::String { pattern: None, min_length: None, max_length: None },
default: Some(json!("localhost")),
constraints: vec![],
visibility: Visibility::Public,
group: Some("database".to_string()),
}],
groups: vec![SettingGroup {
name: "database".to_string(),
label: "Database".to_string(),
description: "Database configuration".to_string(),
settings: vec!["app.database.host".to_string()],
}],
};
assert!(schema.settings[0].validate(&json!("db.example.com")).is_valid());
}
#[test]
fn test_validation_error_contains_key_name() {
let metadata = SettingMetadata {
key: "port".to_string(),
label: "Port".to_string(),
description: "Port number".to_string(),
setting_type: SettingType::Integer { min: Some(1), max: Some(100) },
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
};
let result = metadata.validate(&json!(200));
assert!(!result.is_valid());
if let Some(error) = result.errors().first() {
let msg = error.to_string();
assert!(msg.contains("port"));
}
}
#[test]
fn test_type_mismatch_error() {
let metadata = SettingMetadata {
key: "count".to_string(),
label: "Count".to_string(),
description: "Number of items".to_string(),
setting_type: SettingType::Integer { min: Some(0), max: None },
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
};
let wrong_type = json!("not a number");
let result = metadata.validate(&wrong_type);
assert!(!result.is_valid()); }
#[test]
fn test_validation_edge_case_empty_string() {
let metadata = SettingMetadata {
key: "optional_name".to_string(),
label: "Name".to_string(),
description: "Optional name field".to_string(),
setting_type: SettingType::String {
pattern: None,
min_length: Some(1), max_length: None,
},
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
};
assert!(!metadata.validate(&json!("")).is_valid());
assert!(metadata.validate(&json!("a")).is_valid());
}
#[test]
fn test_validation_edge_case_zero_value() {
let metadata = SettingMetadata {
key: "offset".to_string(),
label: "Offset".to_string(),
description: "Numeric offset".to_string(),
setting_type: SettingType::Integer { min: Some(0), max: Some(100) },
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
};
assert!(metadata.validate(&json!(0)).is_valid());
}
#[test]
fn test_validation_floating_point_precision() {
let metadata = SettingMetadata {
key: "threshold".to_string(),
label: "Threshold".to_string(),
description: "Floating point threshold".to_string(),
setting_type: SettingType::Float { min: Some(0.0), max: Some(1.0) },
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
};
assert!(metadata.validate(&json!(0.9999999999)).is_valid());
assert!(!metadata.validate(&json!(1.0000000001)).is_valid());
}
#[test]
fn test_validation_constraint_interaction() {
let metadata = SettingMetadata {
key: "user_id".to_string(),
label: "User ID".to_string(),
description: "Unique user identifier".to_string(),
setting_type: SettingType::String {
pattern: Some("[0-9]{6}".to_string()),
min_length: Some(6),
max_length: Some(6),
},
default: None,
constraints: vec![Constraint::Required, Constraint::Pattern("[0-9]{6}".to_string())],
visibility: Visibility::Public,
group: None,
};
assert!(metadata.validate(&json!("123456")).is_valid());
assert!(!metadata.validate(&json!("12345")).is_valid());
assert!(!metadata.validate(&json!("abc123")).is_valid());
}
#[test]
fn test_secret_value_redaction_in_pattern_error() {
let metadata = SettingMetadata {
key: "api_key".to_string(),
label: "API Key".to_string(),
description: "Secret API key".to_string(),
setting_type: SettingType::String { pattern: None, min_length: None, max_length: None },
default: None,
constraints: vec![Constraint::Pattern("^[a-zA-Z0-9]{32}$".to_string())],
visibility: Visibility::Secret,
group: None,
};
let result = metadata.validate(&json!("not-long-enough-key-value"));
assert!(!result.is_valid());
let error_msg = result.errors()[0].to_string();
assert!(error_msg.contains("[REDACTED:api_key]"));
assert!(!error_msg.contains("not-long-enough-key-value"));
}
#[test]
fn test_hidden_value_redaction_in_range_error() {
let metadata = SettingMetadata {
key: "password_length".to_string(),
label: "Password Length".to_string(),
description: "Password setting".to_string(),
setting_type: SettingType::Integer { min: Some(8), max: Some(64) },
default: None,
constraints: vec![],
visibility: Visibility::Hidden,
group: None,
};
let result = metadata.validate(&json!(3));
let error_msg = result.errors()[0].to_string();
assert!(error_msg.contains("[REDACTED:password_length]"));
assert!(error_msg.contains("outside allowed range [8, 64]"));
}
#[test]
fn test_public_values_not_redacted() {
let metadata = SettingMetadata {
key: "timeout".to_string(),
label: "Timeout".to_string(),
description: "Timeout in seconds".to_string(),
setting_type: SettingType::Integer { min: Some(1), max: Some(300) },
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
};
let result = metadata.validate(&json!(500));
let error_msg = result.errors()[0].to_string();
assert!(error_msg.contains("500"));
assert!(!error_msg.contains("[REDACTED"));
}
#[test]
fn test_secret_value_redaction_in_oneof_error() {
let metadata = SettingMetadata {
key: "db_password".to_string(),
label: "Database Password".to_string(),
description: "Secret password".to_string(),
setting_type: SettingType::String { pattern: None, min_length: None, max_length: None },
default: None,
constraints: vec![Constraint::OneOf(vec![
"prod-secret-1".to_string(),
"prod-secret-2".to_string(),
])],
visibility: Visibility::Secret,
group: None,
};
let result = metadata.validate(&json!("wrong-password-xyz"));
let error_msg = result.errors()[0].to_string();
assert!(error_msg.contains("[REDACTED:db_password]"));
assert!(!error_msg.contains("wrong-password-xyz"));
}
#[test]
fn test_multiple_errors_all_redacted_for_secret() {
let metadata = SettingMetadata {
key: "token".to_string(),
label: "API Token".to_string(),
description: "Secret token".to_string(),
setting_type: SettingType::String {
pattern: Some("[a-zA-Z0-9]{32}".to_string()),
min_length: Some(32),
max_length: Some(32),
},
default: None,
constraints: vec![Constraint::Required, Constraint::Pattern("[a-zA-Z0-9]{32}".to_string())],
visibility: Visibility::Secret,
group: None,
};
let result = metadata.validate(&json!("short"));
for error in result.errors() {
let error_msg = error.to_string();
assert!(!error_msg.contains("short"));
if error_msg.contains("value") {
assert!(error_msg.contains("[REDACTED:token]"));
}
}
}
#[test]
fn test_deeply_nested_object_with_multiple_constraints() {
let schema = ConfigSchema {
name: "nested-app".to_string(),
version: "1.0.0".to_string(),
settings: vec![
SettingMetadata {
key: "server.tls.cert_path".to_string(),
label: "Certificate Path".to_string(),
description: "Path to TLS certificate".to_string(),
setting_type: SettingType::Path { must_exist: false, is_directory: false },
default: None,
constraints: vec![Constraint::Required],
visibility: Visibility::Public,
group: Some("server.tls".to_string()),
},
SettingMetadata {
key: "server.tls.min_version".to_string(),
label: "TLS Min Version".to_string(),
description: "Minimum TLS version".to_string(),
setting_type: SettingType::Enum {
variants: vec!["1.0".to_string(), "1.2".to_string(), "1.3".to_string()],
},
default: Some(json!("1.2")),
constraints: vec![],
visibility: Visibility::Public,
group: Some("server.tls".to_string()),
},
SettingMetadata {
key: "server.tls.cert_password".to_string(),
label: "Certificate Password".to_string(),
description: "Password for certificate (secret)".to_string(),
setting_type: SettingType::String {
pattern: None,
min_length: Some(8),
max_length: Some(128),
},
default: None,
constraints: vec![Constraint::Required],
visibility: Visibility::Secret,
group: Some("server.tls".to_string()),
},
],
groups: vec![],
};
assert_eq!(schema.settings.len(), 3);
let cert_path_metadata = &schema.settings[0];
assert!(cert_path_metadata.validate(&json!("/etc/ssl/certs/server.crt")).is_valid());
assert!(!cert_path_metadata.validate(&json!(null)).is_valid());
let tls_version_metadata = &schema.settings[1];
assert!(tls_version_metadata.validate(&json!("1.2")).is_valid());
assert!(tls_version_metadata.validate(&json!("1.3")).is_valid());
assert!(!tls_version_metadata.validate(&json!("1.1")).is_valid());
let cert_password_metadata = &schema.settings[2];
let valid_password = json!("SecurePassword123!");
let result = cert_password_metadata.validate(&valid_password);
assert!(result.is_valid());
let short_password = json!("short");
let short_result = cert_password_metadata.validate(&short_password);
assert!(!short_result.is_valid());
if let Some(error) = short_result.errors().first() {
let msg = error.to_string();
assert!(msg.contains("REDACTED") || msg.contains("cert_password"));
}
let null_password = json!(null);
let null_result = cert_password_metadata.validate(&null_password);
assert!(!null_result.is_valid());
}
#[test]
fn test_message_broker_configuration_validation() {
#[allow(clippy::useless_vec)]
let broker_settings = vec![
SettingMetadata {
key: "broker.host".to_string(),
label: "Broker Host".to_string(),
description: "Message broker hostname or IP".to_string(),
setting_type: SettingType::String {
pattern: Some("^([a-z0-9.-]+|localhost|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$".to_string()),
min_length: Some(1),
max_length: Some(255),
},
default: Some(json!("localhost")),
constraints: vec![],
visibility: Visibility::Public,
group: Some("broker".to_string()),
},
SettingMetadata {
key: "broker.port".to_string(),
label: "Broker Port".to_string(),
description: "Message broker port".to_string(),
setting_type: SettingType::Integer { min: Some(1), max: Some(65535) },
default: Some(json!(5672)), constraints: vec![],
visibility: Visibility::Public,
group: Some("broker".to_string()),
},
SettingMetadata {
key: "broker.max_retries".to_string(),
label: "Max Retries".to_string(),
description: "Max connection retries".to_string(),
setting_type: SettingType::Integer { min: Some(0), max: Some(100) },
default: Some(json!(3)),
constraints: vec![],
visibility: Visibility::Advanced,
group: Some("broker".to_string()),
},
];
assert!(broker_settings[0].validate(&json!("rabbitmq.example.com")).is_valid());
assert!(broker_settings[0].validate(&json!("192.168.1.100")).is_valid());
assert!(broker_settings[1].validate(&json!(5672)).is_valid());
assert!(broker_settings[2].validate(&json!(5)).is_valid());
assert!(!broker_settings[1].validate(&json!(99999)).is_valid());
}
proptest! {
#[test]
fn prop_test_integer_range_always_validates_in_bounds(
value in 10i64..=100i64
) {
let metadata = SettingMetadata {
key: "value".to_string(),
label: "Value".to_string(),
description: "Test value".to_string(),
setting_type: SettingType::Integer { min: Some(10), max: Some(100) },
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
};
assert!(metadata.validate(&json!(value)).is_valid());
}
}
proptest! {
#[test]
fn prop_test_string_length_constraint(
s in "[a-z]{1,100}"
) {
let metadata = SettingMetadata {
key: "text".to_string(),
label: "Text".to_string(),
description: "Text value".to_string(),
setting_type: SettingType::String {
pattern: None,
min_length: Some(1),
max_length: Some(100),
},
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
};
assert!(metadata.validate(&json!(s)).is_valid());
}
}
proptest! {
#[test]
fn prop_test_pattern_matches_generated_strings(
s in "[0-9]+"
) {
let metadata = SettingMetadata {
key: "digits".to_string(),
label: "Digits".to_string(),
description: "Digit string".to_string(),
setting_type: SettingType::String {
pattern: Some("^[0-9]+$".to_string()),
min_length: None,
max_length: None,
},
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
};
assert!(metadata.validate(&json!(s)).is_valid());
}
}
fn make_openai_key() -> String {
"sk-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()
}
fn make_anthropic_key() -> String {
"sk-ant-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string()
}
fn turtle_openai_api_key_metadata() -> SettingMetadata {
SettingMetadata {
key: "llm.openai_api_key".to_string(),
label: "OpenAI API Key".to_string(),
description: "API key for OpenAI models (loaded from secrets source)".to_string(),
setting_type: SettingType::String {
pattern: Some("^sk-[a-zA-Z0-9]{48}$".to_string()),
min_length: Some(51),
max_length: Some(51),
},
default: None,
constraints: vec![
Constraint::Required,
Constraint::Pattern("^sk-[a-zA-Z0-9]{48}$".to_string()),
],
visibility: Visibility::Secret,
group: Some("llm".to_string()),
}
}
fn turtle_anthropic_api_key_metadata() -> SettingMetadata {
SettingMetadata {
key: "llm.anthropic_api_key".to_string(),
label: "Anthropic API Key".to_string(),
description: "API key for Anthropic models (loaded from secrets source)".to_string(),
setting_type: SettingType::String {
pattern: Some("^sk-ant-[a-zA-Z0-9]{36}$".to_string()),
min_length: Some(43),
max_length: Some(43),
},
default: None,
constraints: vec![
Constraint::Required,
Constraint::Pattern("^sk-ant-[a-zA-Z0-9]{36}$".to_string()),
],
visibility: Visibility::Secret,
group: Some("llm".to_string()),
}
}
fn turtle_db_password_metadata() -> SettingMetadata {
SettingMetadata {
key: "database.password".to_string(),
label: "Database Password".to_string(),
description: "Password for database connection (loaded from secrets source)".to_string(),
setting_type: SettingType::String {
pattern: None,
min_length: Some(8),
max_length: Some(64),
},
default: None,
constraints: vec![Constraint::Required],
visibility: Visibility::Secret,
group: Some("database".to_string()),
}
}
fn turtle_user_theme_preference_metadata() -> SettingMetadata {
SettingMetadata {
key: "tui.theme".to_string(),
label: "TUI Theme Preference".to_string(),
description: "User's preferred theme (loaded from user global config: ~/.config/turtle/)".to_string(),
setting_type: SettingType::String {
pattern: None,
min_length: Some(1),
max_length: Some(50),
},
default: Some(json!("dark")),
constraints: vec![Constraint::OneOf(vec![
"light".to_string(),
"dark".to_string(),
"auto".to_string(),
])],
visibility: Visibility::Public,
group: Some("tui".to_string()),
}
}
fn turtle_project_log_directory_metadata() -> SettingMetadata {
SettingMetadata {
key: "output.report_dir".to_string(),
label: "Report Output Directory".to_string(),
description: "Directory for analysis reports (loaded from project config: ./turtle.toml)".to_string(),
setting_type: SettingType::Path { must_exist: false, is_directory: true },
default: Some(json!("./analysis_reports")),
constraints: vec![],
visibility: Visibility::Public,
group: Some("output".to_string()),
}
}
fn turtle_default_llm_provider_metadata() -> SettingMetadata {
SettingMetadata {
key: "llm.provider".to_string(),
label: "LLM Provider".to_string(),
description: "LLM provider to use (falls back to default if not set)".to_string(),
setting_type: SettingType::String {
pattern: None,
min_length: Some(1),
max_length: Some(50),
},
default: Some(json!("ollama")),
constraints: vec![Constraint::OneOf(vec![
"ollama".to_string(),
"openai".to_string(),
"anthropic".to_string(),
])],
visibility: Visibility::Public,
group: Some("llm".to_string()),
}
}
#[test]
fn test_multi_source_secret_api_key_from_secrets_source() {
let openai_key_metadata = turtle_openai_api_key_metadata();
let valid_key = json!(make_openai_key());
assert!(openai_key_metadata.validate(&valid_key).is_valid());
let invalid_key = json!("bad-key-format");
let validation = openai_key_metadata.validate(&invalid_key);
assert!(!validation.is_valid());
if let Some(error) = validation.errors().first() {
let error_msg = error.to_string();
assert!(!error_msg.contains("bad-key-format"));
assert!(error_msg.contains("llm.openai_api_key") || error_msg.contains("REDACTED"));
}
}
#[test]
fn test_multi_source_anthropic_key_validation_with_secret_redaction() {
let anthropic_metadata = turtle_anthropic_api_key_metadata();
let valid_key = json!(make_anthropic_key());
assert!(anthropic_metadata.validate(&valid_key).is_valid());
let short_key = json!("sk-ant-short");
let validation = anthropic_metadata.validate(&short_key);
assert!(!validation.is_valid());
if let Some(error) = validation.errors().first() {
assert!(!error.to_string().contains("sk-ant-short"));
}
}
#[test]
fn test_multi_source_database_password_secret_source() {
let password_metadata = turtle_db_password_metadata();
let valid_pwd = json!("secure_password_123");
assert!(password_metadata.validate(&valid_pwd).is_valid());
let short_pwd = json!("short");
let validation = password_metadata.validate(&short_pwd);
assert!(!validation.is_valid());
if let Some(error) = validation.errors().first() {
let error_msg = error.to_string();
assert!(!error_msg.is_empty());
}
}
#[test]
fn test_multi_source_user_global_config_theme_preference() {
let theme_metadata = turtle_user_theme_preference_metadata();
assert!(theme_metadata.validate(&json!("dark")).is_valid());
assert!(theme_metadata.validate(&json!("light")).is_valid());
assert!(theme_metadata.validate(&json!("auto")).is_valid());
let invalid_theme = json!("neon-purple");
assert!(!theme_metadata.validate(&invalid_theme).is_valid());
}
#[test]
fn test_multi_source_project_local_config_output_directory() {
let output_dir_metadata = turtle_project_log_directory_metadata();
assert!(output_dir_metadata.validate(&json!("./reports")).is_valid());
assert!(output_dir_metadata
.validate(&json!("./analysis/output/reports"))
.is_valid());
assert!(output_dir_metadata.validate(&json!("/var/turtle/reports")).is_valid());
}
#[test]
fn test_multi_source_project_overrides_user_global() {
let provider_metadata = turtle_default_llm_provider_metadata();
let user_global_provider = json!("openai"); let project_local_provider = json!("anthropic");
assert!(provider_metadata.validate(&user_global_provider).is_valid());
assert!(provider_metadata.validate(&project_local_provider).is_valid());
assert!(provider_metadata.validate(&project_local_provider).is_valid());
}
#[test]
fn test_multi_source_secret_overrides_all_public_sources() {
let api_key_metadata = turtle_openai_api_key_metadata();
let valid_secret_key = json!(make_openai_key());
assert!(api_key_metadata.validate(&valid_secret_key).is_valid());
}
#[test]
fn test_multi_source_default_fallback_if_not_in_any_source() {
let theme_metadata = turtle_user_theme_preference_metadata();
let default_value = theme_metadata.default.clone().unwrap();
assert!(theme_metadata.validate(&default_value).is_valid());
assert_eq!(default_value, json!("dark"));
}
#[test]
fn test_multi_source_environment_variable_overrides_all_files() {
let provider_metadata = turtle_default_llm_provider_metadata();
let config_file_value = json!("openai");
let env_override_value = json!("anthropic");
assert!(provider_metadata.validate(&config_file_value).is_valid());
assert!(provider_metadata.validate(&env_override_value).is_valid());
assert!(provider_metadata.validate(&env_override_value).is_valid());
}
#[test]
fn test_multi_source_validation_error_shows_key_and_source_hint() {
let api_key = turtle_openai_api_key_metadata();
let invalid_key = json!("not-a-valid-key");
let result = api_key.validate(&invalid_key);
assert!(!result.is_valid());
if let Some(error) = result.errors().first() {
let error_msg = error.to_string();
assert!(
error_msg.contains("llm.openai_api_key")
|| error_msg.contains("api_key")
|| error_msg.contains("REDACTED")
);
assert!(!error_msg.contains("not-a-valid-key"));
}
}
#[test]
fn test_multi_source_config_with_all_three_scopes() {
let schema = ConfigSchema {
name: "turtle-config".to_string(),
version: "1.0.0".to_string(),
settings: vec![
turtle_default_llm_provider_metadata(),
turtle_user_theme_preference_metadata(),
turtle_project_log_directory_metadata(),
turtle_openai_api_key_metadata(),
],
groups: vec![
SettingGroup {
name: "llm".to_string(),
label: "LLM Settings".to_string(),
description: "LLM provider and authentication".to_string(),
settings: vec!["llm.provider".to_string(), "llm.openai_api_key".to_string()],
},
SettingGroup {
name: "tui".to_string(),
label: "Terminal UI".to_string(),
description: "Terminal interface preferences".to_string(),
settings: vec!["tui.theme".to_string()],
},
SettingGroup {
name: "output".to_string(),
label: "Output Configuration".to_string(),
description: "Report output settings".to_string(),
settings: vec!["output.report_dir".to_string()],
},
],
};
let merged_config = json!({
"llm": {
"provider": "openai",
"openai_api_key": make_openai_key()
},
"tui": {
"theme": "light"
},
"output": {
"report_dir": "./reports"
}
});
for setting in &schema.settings {
let key_parts: Vec<&str> = setting.key.split('.').collect();
let mut value = &merged_config;
for part in key_parts {
if let Some(v) = value.get(part) {
value = v;
} else if let Some(default) = &setting.default {
value = default;
}
}
assert!(
setting.validate(value).is_valid(),
"Setting {} should be valid",
setting.key
);
}
}
#[test]
fn test_multi_source_merged_turtle_config_scenario() {
let provider = turtle_default_llm_provider_metadata();
let theme = turtle_user_theme_preference_metadata();
let report_dir = turtle_project_log_directory_metadata();
let api_key = turtle_openai_api_key_metadata();
let effective_config = json!({
"llm": {
"provider": "openai",
"openai_api_key": make_openai_key()
},
"tui": {
"theme": "light"
},
"output": {
"report_dir": "./project_reports"
}
});
assert!(provider.validate(&effective_config["llm"]["provider"]).is_valid());
assert!(theme.validate(&effective_config["tui"]["theme"]).is_valid());
assert!(report_dir.validate(&effective_config["output"]["report_dir"]).is_valid());
assert!(api_key.validate(&effective_config["llm"]["openai_api_key"]).is_valid());
}
#[test]
fn test_multi_source_secret_not_logged_in_validation_errors() {
let api_key = turtle_openai_api_key_metadata();
let wrong_keys = vec![json!("sk-1234"), json!("wrong-prefix-12345"), json!("sk-proj-wrong")];
for wrong_key in wrong_keys {
let result = api_key.validate(&wrong_key);
assert!(!result.is_valid());
for error in result.errors() {
let msg = error.to_string();
let key_str = wrong_key.as_str().unwrap_or("");
assert!(
!msg.contains(key_str),
"Error message must not contain secret value: {}",
msg
);
assert!(
msg.contains("REDACTED") || msg.contains("api_key"),
"Error should indicate redaction: {}",
msg
);
}
}
}
#[test]
fn test_config_composition_defaults_only() {
let effective_config = json!({
"llm": {
"provider": "ollama" },
"tui": {
"theme": "dark" },
"output": {
"report_dir": "./analysis_reports" }
});
let schema = ConfigSchema {
name: "turtle-config".to_string(),
version: "1.0.0".to_string(),
settings: vec![
turtle_default_llm_provider_metadata(),
turtle_user_theme_preference_metadata(),
turtle_project_log_directory_metadata(),
],
groups: vec![],
};
for setting in &schema.settings {
let key_parts: Vec<&str> = setting.key.split('.').collect();
let mut value = &effective_config;
for part in key_parts {
if let Some(v) = value.get(part) {
value = v;
} else if let Some(default) = &setting.default {
value = default;
}
}
assert!(
setting.validate(value).is_valid(),
"Setting {} should be valid with defaults",
setting.key
);
}
}
#[test]
fn test_config_composition_user_global_overrides_defaults() {
let effective_config = json!({
"llm": {
"provider": "ollama" },
"tui": {
"theme": "light" },
"output": {
"report_dir": "./analysis_reports" }
});
let theme_metadata = turtle_user_theme_preference_metadata();
let provider_metadata = turtle_default_llm_provider_metadata();
assert!(theme_metadata.validate(&effective_config["tui"]["theme"]).is_valid());
assert_eq!(effective_config["tui"]["theme"], json!("light"));
assert!(provider_metadata.validate(&effective_config["llm"]["provider"]).is_valid());
assert_eq!(effective_config["llm"]["provider"], json!("ollama"));
}
#[test]
fn test_config_composition_project_local_overrides_user_and_defaults() {
let effective_config = json!({
"llm": {
"provider": "anthropic" },
"tui": {
"theme": "light" },
"output": {
"report_dir": "./project_reports" }
});
let provider_metadata = turtle_default_llm_provider_metadata();
let theme_metadata = turtle_user_theme_preference_metadata();
let output_metadata = turtle_project_log_directory_metadata();
assert!(provider_metadata.validate(&effective_config["llm"]["provider"]).is_valid());
assert_eq!(effective_config["llm"]["provider"], json!("anthropic"));
assert!(theme_metadata.validate(&effective_config["tui"]["theme"]).is_valid());
assert_eq!(effective_config["tui"]["theme"], json!("light"));
assert!(output_metadata
.validate(&effective_config["output"]["report_dir"])
.is_valid());
assert_eq!(effective_config["output"]["report_dir"], json!("./project_reports"));
}
#[test]
fn test_config_composition_secrets_overrides_all_files() {
let _defaults_config = json!({
"llm": {
"openai_api_key": null },
"database": {
"password": null }
});
let _secrets_config = json!({
"llm": {
"openai_api_key": make_openai_key() },
"database": {
"password": "SecureDbPassword123" }
});
let final_config = json!({
"llm": {
"openai_api_key": make_openai_key() },
"database": {
"password": "SecureDbPassword123" }
});
let api_key_metadata = turtle_openai_api_key_metadata();
let password_metadata = turtle_db_password_metadata();
assert!(api_key_metadata
.validate(&final_config["llm"]["openai_api_key"])
.is_valid());
assert!(password_metadata
.validate(&final_config["database"]["password"])
.is_valid());
assert_ne!(final_config["llm"]["openai_api_key"], json!(null));
assert_ne!(final_config["database"]["password"], json!(null));
}
#[test]
fn test_config_composition_full_turtle_scenario() {
let defaults = json!({
"llm": {
"provider": "ollama", "openai_api_key": null
},
"tui": {
"theme": "dark" },
"output": {
"report_dir": "./analysis_reports" },
"database": {
"password": null
}
});
let user_global = json!({
"tui": {
"theme": "light" }
});
let project_local = json!({
"llm": {
"provider": "openai" },
"output": {
"report_dir": "./project_analysis" }
});
let secrets = json!({
"llm": {
"openai_api_key": make_openai_key() },
"database": {
"password": "SecureDbPassword123" }
});
let mut effective = defaults.clone();
if let Some(tui) = user_global.get("tui") {
effective["tui"] = tui.clone();
}
if let Some(llm) = project_local.get("llm") {
effective["llm"]["provider"] = llm.get("provider").unwrap().clone();
}
if let Some(output) = project_local.get("output") {
effective["output"] = output.clone();
}
if let Some(llm) = secrets.get("llm") {
if let Some(key) = llm.get("openai_api_key") {
effective["llm"]["openai_api_key"] = key.clone();
}
}
if let Some(db) = secrets.get("database") {
if let Some(pwd) = db.get("password") {
effective["database"]["password"] = pwd.clone();
}
}
let _schema = ConfigSchema {
name: "turtle-config".to_string(),
version: "1.0.0".to_string(),
settings: vec![
turtle_default_llm_provider_metadata(),
turtle_user_theme_preference_metadata(),
turtle_project_log_directory_metadata(),
turtle_openai_api_key_metadata(),
turtle_db_password_metadata(),
],
groups: vec![],
};
let provider = turtle_default_llm_provider_metadata();
assert_eq!(effective["llm"]["provider"], json!("openai")); assert!(provider.validate(&effective["llm"]["provider"]).is_valid());
let theme = turtle_user_theme_preference_metadata();
assert_eq!(effective["tui"]["theme"], json!("light")); assert!(theme.validate(&effective["tui"]["theme"]).is_valid());
let output = turtle_project_log_directory_metadata();
assert_eq!(effective["output"]["report_dir"], json!("./project_analysis")); assert!(output.validate(&effective["output"]["report_dir"]).is_valid());
let api_key = turtle_openai_api_key_metadata();
assert_ne!(effective["llm"]["openai_api_key"], json!(null)); assert!(api_key.validate(&effective["llm"]["openai_api_key"]).is_valid());
let password = turtle_db_password_metadata();
assert_eq!(effective["database"]["password"], json!("SecureDbPassword123")); assert!(password.validate(&effective["database"]["password"]).is_valid());
}
#[test]
fn test_config_composition_precedence_verification() {
let default_provider = json!("ollama");
let metadata = turtle_default_llm_provider_metadata();
assert!(metadata.validate(&default_provider).is_valid());
let user_provider = json!("openai");
assert!(metadata.validate(&user_provider).is_valid());
assert_ne!(user_provider, default_provider);
let project_provider = json!("anthropic");
assert!(metadata.validate(&project_provider).is_valid());
assert_ne!(project_provider, user_provider);
let api_key_metadata = turtle_openai_api_key_metadata();
let secret_api_key = json!(make_openai_key());
assert!(api_key_metadata.validate(&secret_api_key).is_valid());
let env_override_provider = json!("anthropic");
assert!(metadata.validate(&env_override_provider).is_valid());
assert_eq!(default_provider, json!("ollama")); assert_eq!(user_provider, json!("openai")); assert_eq!(project_provider, json!("anthropic")); }
#[test]
fn test_config_composition_missing_optional_settings() {
let schema = ConfigSchema {
name: "turtle-config".to_string(),
version: "1.0.0".to_string(),
settings: vec![
turtle_default_llm_provider_metadata(), turtle_user_theme_preference_metadata(), turtle_project_log_directory_metadata(), turtle_openai_api_key_metadata(), turtle_anthropic_api_key_metadata(), ],
groups: vec![],
};
let effective_config = json!({
"llm": {
"provider": "openai",
"openai_api_key": make_openai_key(),
"anthropic_api_key": null },
"tui": {
"theme": "auto" },
"output": {
"report_dir": "./reports"
}
});
let openai_key = &schema.settings[3];
assert!(openai_key.validate(&effective_config["llm"]["openai_api_key"]).is_valid());
let anthropic_key = &schema.settings[4];
let result = anthropic_key.validate(&effective_config["llm"]["anthropic_api_key"]);
assert!(!result.is_valid(), "Required field cannot be null");
let theme = &schema.settings[1];
assert!(theme.validate(&effective_config["tui"]["theme"]).is_valid());
}
#[test]
fn test_actual_loader_composition_multiple_files() {
use serde::{Deserialize, Serialize};
use settings_loader::LayerBuilder;
use std::fs;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
struct ComposedConfig {
#[serde(default)]
name: String,
#[serde(default)]
port: u16,
#[serde(default)]
debug: bool,
}
let temp_dir = tempfile::tempdir().unwrap();
let base_path = temp_dir.path().join("base.toml");
fs::write(&base_path, "name = \"DefaultApp\"\nport = 8000\ndebug = false").unwrap();
let override_path = temp_dir.path().join("overrides.toml");
fs::write(&override_path, "port = 9000").unwrap();
let builder = LayerBuilder::new().with_path(&base_path).with_path(&override_path);
let config_builder = builder.build().expect("Failed to build layers");
let config = config_builder.build().expect("Failed to build config");
let result: ComposedConfig = config.try_deserialize().expect("Failed to deserialize");
assert_eq!(
result.name, "DefaultApp",
"Unspecified fields should come from earlier layer"
);
assert_eq!(result.port, 9000, "Layer 2 should override layer 1 value");
assert!(!result.debug, "Unspecified fields should come from earlier layer");
}
#[test]
fn test_actual_loader_composition_partial_defaults() {
use serde::{Deserialize, Serialize};
use settings_loader::{LoadingOptions, SettingsLoader};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
struct PartialConfig {
#[serde(default)]
host: String,
#[serde(default = "default_port")]
port: u16,
#[serde(default)]
verbose: bool,
}
fn default_port() -> u16 {
3306
}
impl SettingsLoader for PartialConfig {
type Options = TestPartialLoadingOptions;
}
#[derive(Debug, Clone)]
struct TestPartialLoadingOptions {
config_path: Option<PathBuf>,
}
impl LoadingOptions for TestPartialLoadingOptions {
type Error = settings_loader::SettingsError;
fn config_path(&self) -> Option<PathBuf> {
self.config_path.clone()
}
fn secrets_path(&self) -> Option<PathBuf> {
None
}
fn implicit_search_paths(&self) -> Vec<PathBuf> {
vec![]
}
}
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("config.yaml");
fs::write(&config_path, "host: localhost").unwrap();
let options = TestPartialLoadingOptions { config_path: Some(config_path) };
let loaded: PartialConfig = PartialConfig::load(&options).expect("Failed to load partial config");
assert_eq!(loaded.host, "localhost");
assert_eq!(loaded.port, 3306, "Unspecified field should use serde default function");
assert!(!loaded.verbose);
}
#[test]
fn test_actual_loader_composition_multilevel_structure() {
use serde::{Deserialize, Serialize};
use settings_loader::{LoadingOptions, SettingsLoader};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
struct DatabaseConfig {
#[serde(default)]
host: String,
#[serde(default)]
port: u16,
#[serde(default)]
username: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
struct RealNestedConfig {
#[serde(default)]
database: DatabaseConfig,
}
impl SettingsLoader for RealNestedConfig {
type Options = TestNestedLoadingOptions;
}
#[derive(Debug, Clone)]
struct TestNestedLoadingOptions {
config_path: Option<PathBuf>,
}
impl LoadingOptions for TestNestedLoadingOptions {
type Error = settings_loader::SettingsError;
fn config_path(&self) -> Option<PathBuf> {
self.config_path.clone()
}
fn secrets_path(&self) -> Option<PathBuf> {
None
}
fn implicit_search_paths(&self) -> Vec<PathBuf> {
vec![]
}
}
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("config.yaml");
fs::write(
&config_path,
"database:\n host: localhost\n port: 5432\n username: admin",
)
.unwrap();
let options = TestNestedLoadingOptions { config_path: Some(config_path) };
let loaded: RealNestedConfig = RealNestedConfig::load(&options).expect("Failed to load nested config");
assert_eq!(loaded.database.host, "localhost");
assert_eq!(loaded.database.port, 5432);
assert_eq!(loaded.database.username, "admin");
}
#[test]
fn test_layer_builder_composition_precedence() {
use serde::{Deserialize, Serialize};
use settings_loader::LayerBuilder;
use std::fs;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
struct PrecedenceConfig {
#[serde(default)]
setting_a: String,
#[serde(default)]
setting_b: String,
}
let temp_dir = tempfile::tempdir().unwrap();
let layer1_path = temp_dir.path().join("base.yaml");
fs::write(&layer1_path, "setting_a: value_from_base\nsetting_b: base_value").unwrap();
let layer2_path = temp_dir.path().join("override.yaml");
fs::write(&layer2_path, "setting_a: value_from_override").unwrap();
let builder = LayerBuilder::new().with_path(&layer1_path).with_path(&layer2_path);
let config_builder = builder.build().expect("Failed to build layers");
let config = config_builder.build().expect("Failed to build config");
let result_forward: PrecedenceConfig = config.try_deserialize().expect("Failed to deserialize");
assert_eq!(
result_forward.setting_a, "value_from_override",
"Later layer (layer 2) should override earlier layer (layer 1) for setting_a"
);
assert_eq!(
result_forward.setting_b, "base_value",
"Unoverridden fields should come from earlier layer"
);
let builder_reversed = LayerBuilder::new().with_path(&layer2_path).with_path(&layer1_path);
let config_builder = builder_reversed.build().expect("Failed to build layers");
let config = config_builder.build().expect("Failed to build config");
let result_reversed: PrecedenceConfig = config.try_deserialize().expect("Failed to deserialize");
assert_eq!(
result_reversed.setting_a, "value_from_base",
"Reversing layer order proves precedence rules: later layer wins"
);
assert_eq!(
result_reversed.setting_b, "base_value",
"Field values should respect layer order"
);
assert_ne!(
result_forward.setting_a, result_reversed.setting_a,
"Changing layer order should change the result - proves precedence works"
);
}
#[test]
fn test_actual_validation_with_loaded_config() {
use serde::{Deserialize, Serialize};
use settings_loader::LayerBuilder;
use std::fs;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
struct ValidatedConfig {
#[serde(default)]
host: String,
#[serde(default)]
port: u16,
#[serde(default)]
max_connections: u16,
}
let temp_dir = tempfile::tempdir().unwrap();
let base_path = temp_dir.path().join("base.yaml");
fs::write(&base_path, "host: localhost\nport: 8080\nmax_connections: 100").unwrap();
let env_path = temp_dir.path().join("env_overrides.yaml");
fs::write(&env_path, "port: 9000").unwrap();
let builder = LayerBuilder::new().with_path(&base_path).with_path(&env_path);
let config_builder = builder.build().expect("Failed to build layers");
let config = config_builder.build().expect("Failed to build config");
let loaded: ValidatedConfig = config.try_deserialize().expect("Failed to deserialize");
assert_eq!(loaded.host, "localhost"); assert_eq!(loaded.port, 9000); assert_eq!(loaded.max_connections, 100);
let host_metadata = SettingMetadata {
key: "host".to_string(),
label: "Host".to_string(),
description: "Server hostname".to_string(),
setting_type: SettingType::String { pattern: None, min_length: None, max_length: None },
default: None,
constraints: vec![Constraint::Required],
visibility: Visibility::Public,
group: None,
};
let port_metadata = SettingMetadata {
key: "port".to_string(),
label: "Port".to_string(),
description: "Server port number (valid range 1-65535)".to_string(),
setting_type: SettingType::Integer { min: Some(1), max: Some(65535) },
default: None,
constraints: vec![Constraint::Required],
visibility: Visibility::Public,
group: None,
};
let max_conn_metadata = SettingMetadata {
key: "max_connections".to_string(),
label: "Max Connections".to_string(),
description: "Maximum concurrent connections".to_string(),
setting_type: SettingType::Integer { min: Some(1), max: Some(10000) },
default: None,
constraints: vec![],
visibility: Visibility::Public,
group: None,
};
assert!(
host_metadata.validate(&json!(loaded.host)).is_valid(),
"Composed host value from source 1 should be valid"
);
assert!(
port_metadata.validate(&json!(loaded.port)).is_valid(),
"Composed port value from source 2 (overridden) should be valid"
);
assert!(
max_conn_metadata.validate(&json!(loaded.max_connections)).is_valid(),
"Composed max_connections from source 1 should be valid"
);
}
}