#![cfg(all(feature = "validation", feature = "interpolation"))]
#![allow(dead_code)]
mod common;
use std::path::PathBuf;
#[test]
fn test_config_error_accessible() {
use confers::ConfigError;
let _err = ConfigError::FileNotFound {
filename: PathBuf::from("test.toml"),
source: None,
};
let _err = ConfigError::InvalidValue {
key: "test".to_string(),
expected_type: "string".to_string(),
message: "test message".to_string(),
};
}
#[test]
fn test_error_code_variants() {
use confers::error::ErrorCode;
assert_eq!(ErrorCode::FileNotFound as u16, 1);
assert_eq!(ErrorCode::FilePermission as u16, 2);
assert_eq!(ErrorCode::FileParseError as u16, 3);
assert_eq!(ErrorCode::IoError as u16, 10);
assert_eq!(ErrorCode::ValidationFailed as u16, 100);
assert_eq!(ErrorCode::TypeMismatch as u16, 101);
assert_eq!(ErrorCode::InvalidValue as u16, 102);
assert_eq!(ErrorCode::SchemaValidationFailed as u16, 103);
assert_eq!(ErrorCode::DecryptionFailed as u16, 200);
assert_eq!(ErrorCode::KeyRotationFailed as u16, 203);
assert_eq!(ErrorCode::RemoteUnavailable as u16, 300);
assert_eq!(ErrorCode::Timeout as u16, 900);
assert_eq!(ErrorCode::SizeLimitExceeded as u16, 500);
assert_eq!(ErrorCode::InterpolationError as u16, 402);
assert_eq!(ErrorCode::CircularReference as u16, 400);
assert_eq!(ErrorCode::ConcurrencyConflict as u16, 901);
assert_eq!(ErrorCode::WatcherError as u16, 501);
assert_eq!(ErrorCode::OverrideBlocked as u16, 401);
}
#[test]
fn test_error_code_display() {
use confers::error::ErrorCode;
assert_eq!(format!("{}", ErrorCode::FileNotFound), "FILE_NOT_FOUND");
assert_eq!(format!("{}", ErrorCode::FileParseError), "FILE_PARSE_ERROR");
assert_eq!(
format!("{}", ErrorCode::ValidationFailed),
"VALIDATION_FAILED"
);
assert_eq!(
format!("{}", ErrorCode::CircularReference),
"CIRCULAR_REFERENCE"
);
}
#[test]
fn test_invalid_toml_format_error() {
let temp_file = common::create_temp_config(
r#"
invalid toml content {{{
"#,
".toml",
);
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct TestConfig {
value: String,
}
let result = TestConfig::load_file_with_env(temp_file.path());
assert!(result.is_err());
}
#[test]
fn test_invalid_json_format_error() {
let temp_file = common::create_temp_config(
r#"
{ invalid json content }
"#,
".json",
);
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct TestConfig {
value: String,
}
let result = TestConfig::load_file_with_env(temp_file.path());
assert!(result.is_err());
}
#[cfg(feature = "yaml")]
#[test]
fn test_invalid_yaml_format_error() {
let temp_file = common::create_temp_config(
r#"
invalid: yaml: content:
- item
extra: invalid: indent
"#,
".yaml",
);
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct TestConfig {
value: String,
}
let result = TestConfig::load_file_with_env(temp_file.path());
assert!(result.is_err());
}
#[test]
fn test_parse_error_location() {
use confers::error::ParseLocation;
let loc = ParseLocation::new("test.toml", 10, 5);
assert_eq!(loc.line, 10);
assert_eq!(loc.column, 5);
let loc_str = format!("{}", loc);
assert!(loc_str.contains("test.toml"));
assert!(loc_str.contains("10"));
assert!(loc_str.contains("5"));
}
#[test]
fn test_parse_error_from_path() {
use confers::error::ParseLocation;
let loc = ParseLocation::from_path(
std::path::Path::new("/home/user/project/config.toml"),
15,
10,
);
assert_eq!(loc.line, 15);
assert_eq!(loc.column, 10);
assert!(loc.source_name.contains("config.toml"));
}
#[test]
fn test_config_validation_failure() {
use garde::Validate;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize, Validate)]
struct ValidatedConfig {
#[garde(length(min = 1, max = 10))]
name: String,
#[garde(range(min = 1, max = 100))]
count: u32,
}
let config = ValidatedConfig {
name: "".to_string(), count: 200, };
let result = config.validate();
assert!(result.is_err());
}
#[test]
fn test_validation_error_custom_rule() {
use confers::ConfigError;
let err = ConfigError::validation("field_name", "length(min=1)", "Field cannot be empty");
if let confers::ConfigError::ValidationFailed {
field,
rule,
message,
} = err
{
assert_eq!(field, "field_name");
assert_eq!(rule, "length(min=1)");
assert_eq!(message, "Field cannot be empty");
} else {
panic!("Expected ValidationFailed error");
}
}
#[test]
fn test_validation_error_from_garde_report() {
use confers::ConfigError;
let err = ConfigError::validation("test_field", "required", "test error");
let code = err.code();
assert!(matches!(code, confers::error::ErrorCode::ValidationFailed));
}
#[test]
fn test_validation_error_user_message() {
let err = confers::ConfigError::ValidationFailed {
field: "email".to_string(),
rule: "email".to_string(),
message: "Invalid email format".to_string(),
};
let user_msg = err.user_message();
assert!(user_msg.contains("email"));
assert!(user_msg.contains("Invalid email format"));
}
#[test]
fn test_circular_reference_detection() {
let temp_file = common::create_temp_config(
r#"
a = "${b}"
b = "${a}"
"#,
".toml",
);
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct CircularConfig {
a: String,
b: String,
}
let result = CircularConfig::load_file_with_env(temp_file.path());
assert!(result.is_err());
}
#[test]
fn test_self_reference_error() {
let temp_file = common::create_temp_config(
r#"
value = "${value}"
"#,
".toml",
);
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct SelfRefConfig {
value: String,
}
let result = SelfRefConfig::load_file_with_env(temp_file.path());
assert!(result.is_err());
}
#[test]
fn test_undefined_variable_error() {
let temp_file = common::create_temp_config(
r#"
value = "${undefined_var}"
"#,
".toml",
);
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct UndefinedConfig {
value: String,
}
let result = UndefinedConfig::load_file_with_env(temp_file.path());
match result {
Ok(_config) => {
}
Err(_) => {
}
}
}
#[test]
fn test_nested_circular_reference() {
let temp_file = common::create_temp_config(
r#"
a = "${b}"
b = "${c}"
c = "${a}"
"#,
".toml",
);
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct NestedCircularConfig {
a: String,
b: String,
c: String,
}
let result = NestedCircularConfig::load_file_with_env(temp_file.path());
assert!(result.is_err());
}
#[test]
fn test_missing_required_field_error() {
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct RequiredConfig {
required_field: String,
}
let temp_file = common::create_temp_config(
r#"
other_field = "value"
"#,
".toml",
);
let result = RequiredConfig::load_file_with_env(temp_file.path());
assert!(result.is_err());
}
#[test]
fn test_missing_nested_required_field() {
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct NestedConfig {
db: DatabaseConfig,
}
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct DatabaseConfig {
host: String,
port: u16,
}
let temp_file = common::create_temp_config(
r#"
[db]
# missing host and port
"#,
".toml",
);
let result = NestedConfig::load_file_with_env(temp_file.path());
match result {
Ok(config) => {
assert_eq!(config.db.host, "");
}
Err(_) => {
}
}
}
#[test]
fn test_type_mismatch_error() {
let temp_file = common::create_temp_config(
r#"
port = "not_a_number"
"#,
".toml",
);
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct PortConfig {
port: u16,
}
let result = PortConfig::load_file_with_env(temp_file.path());
assert!(result.is_err());
}
#[test]
fn test_integer_to_string_mismatch() {
let temp_file = common::create_temp_config(
r#"
name = 12345
"#,
".toml",
);
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct NameConfig {
name: String,
}
let result = NameConfig::load_file_with_env(temp_file.path());
match result {
Ok(config) => {
assert_eq!(config.name, "12345");
}
Err(_) => {
}
}
}
#[test]
fn test_string_to_integer_mismatch() {
let temp_file = common::create_temp_config(
r#"
count = "not_an_integer"
"#,
".toml",
);
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct CountConfig {
count: i32,
}
let result = CountConfig::load_file_with_env(temp_file.path());
assert!(result.is_err());
}
#[test]
fn test_object_to_primitive_mismatch() {
let temp_file = common::create_temp_config(
r#"
value = { nested = "object" }
"#,
".toml",
);
#[derive(Debug, Clone, confers::Config, serde::Deserialize)]
#[config()]
struct SimpleConfig {
value: String,
}
let result = SimpleConfig::load_file_with_env(temp_file.path());
assert!(result.is_err());
}
#[test]
fn test_retryable_error_detection() {
use confers::ConfigError;
let err = ConfigError::Timeout { duration_ms: 5000 };
assert!(err.is_retryable());
let err = ConfigError::ValidationFailed {
field: "test".to_string(),
rule: "required".to_string(),
message: "missing".to_string(),
};
assert!(!err.is_retryable());
let err = ConfigError::RemoteUnavailable {
error_type: "timeout".to_string(),
retryable: true,
};
assert!(err.is_retryable());
let err = ConfigError::RemoteUnavailable {
error_type: "auth".to_string(),
retryable: false,
};
assert!(!err.is_retryable());
}
#[test]
fn test_io_error_retryability() {
use confers::ConfigError;
use std::io;
let io_err = io::Error::new(io::ErrorKind::ConnectionRefused, "connection refused");
let err = ConfigError::IoError(io_err);
assert!(err.is_retryable());
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let err = ConfigError::IoError(io_err);
assert!(!err.is_retryable());
let io_err = io::Error::new(io::ErrorKind::TimedOut, "timed out");
let err = ConfigError::IoError(io_err);
assert!(err.is_retryable());
}
#[test]
fn test_watcher_error_retryability() {
use confers::ConfigError;
let recoverable_err = ConfigError::WatcherError {
message: "temporary error".to_string(),
path: None,
recoverable: true,
};
assert!(recoverable_err.is_retryable());
let non_recoverable_err = ConfigError::WatcherError {
message: "fatal error".to_string(),
path: None,
recoverable: false,
};
assert!(!non_recoverable_err.is_retryable());
}
#[test]
fn test_error_user_message_formatting() {
use confers::ConfigError;
let err = ConfigError::FileNotFound {
filename: PathBuf::from("/path/to/config.toml"),
source: None,
};
let msg = err.user_message();
assert!(msg.contains("config.toml"));
assert!(msg.contains("not found"));
let err = ConfigError::VersionMismatch {
found: 1,
expected: 2,
};
let msg = err.user_message();
assert!(msg.contains("1"));
assert!(msg.contains("2"));
let err = ConfigError::Timeout { duration_ms: 30000 };
let msg = err.user_message();
assert!(msg.contains("30000"));
}
#[test]
fn test_error_audit_message() {
use confers::ConfigError;
let err = ConfigError::FileNotFound {
filename: PathBuf::from("test.toml"),
source: None,
};
let audit_msg = err.audit_message();
assert!(audit_msg.contains("error_code"));
assert!(audit_msg.contains("FILE_NOT_FOUND"));
assert!(audit_msg.contains("operation=config"));
}
#[test]
fn test_error_sanitized_chain() {
use confers::ConfigError;
let err = ConfigError::DecryptionFailed {
message: "key mismatch".to_string(),
};
let chain = err.sanitized_chain();
assert!(!chain.is_empty());
}
#[test]
fn test_multi_source_error() {
use confers::error::MultiSourceError;
use confers::ConfigError;
let errors: Vec<(&str, ConfigError)> = vec![
("source_1", ConfigError::Timeout { duration_ms: 1000 }),
(
"source_2",
ConfigError::RemoteUnavailable {
error_type: "connection".to_string(),
retryable: true,
},
),
];
let multi_err = MultiSourceError::new(5, errors);
assert_eq!(multi_err.failed_count, 2);
assert_eq!(multi_err.total_count, 5);
let display = format!("{}", multi_err);
assert!(display.contains("2/5"));
}
#[test]
fn test_multi_source_error_partial_config() {
use confers::error::MultiSourceError;
use confers::ConfigError;
let errors: Vec<(&str, ConfigError)> = vec![(
"source_1",
ConfigError::FileNotFound {
filename: PathBuf::from("missing.toml"),
source: None,
},
)];
let multi_err =
MultiSourceError::with_partial(3, errors, serde_json::json!({ "partial": true }));
assert_eq!(multi_err.failed_count, 1);
assert_eq!(multi_err.total_count, 3);
assert!(multi_err.partial_config().is_some());
}
#[test]
fn test_build_result_ok() {
use confers::error::BuildResult;
let result: BuildResult<i32> = BuildResult::ok(42);
assert!(!result.degraded);
assert!(!result.has_warnings());
assert_eq!(result.config, 42);
}
#[test]
fn test_build_result_with_warnings() {
use confers::error::{BuildResult, SourceWarning, WarningCode};
let warnings = vec![SourceWarning {
message: "unused key".to_string(),
source: Some("config.toml".to_string()),
code: WarningCode::UnusedKey,
}];
let result: BuildResult<i32> = BuildResult::with_warnings(42, warnings);
assert!(!result.degraded);
assert!(result.has_warnings());
}
#[test]
fn test_build_result_degraded() {
use confers::error::BuildResult;
let result: BuildResult<i32> = BuildResult::degraded(42, "remote source unavailable");
assert!(result.degraded);
assert_eq!(
result.degraded_reason,
Some("remote source unavailable".to_string())
);
}
#[test]
fn test_build_result_map() {
use confers::error::BuildResult;
let result: BuildResult<i32> = BuildResult::ok(21);
let mapped = result.map(|v| v * 2);
assert_eq!(mapped.config, 42);
}
#[test]
fn test_warning_code_display() {
use confers::error::WarningCode;
assert_eq!(
format!("{}", WarningCode::OptionalSourceSkipped),
"OPTIONAL_SOURCE_SKIPPED"
);
assert_eq!(format!("{}", WarningCode::DeprecatedKey), "DEPRECATED_KEY");
assert_eq!(format!("{}", WarningCode::DefaultUsed), "DEFAULT_USED");
assert_eq!(format!("{}", WarningCode::UnusedKey), "UNUSED_KEY");
}
#[test]
fn test_file_not_found_with_source() {
use confers::ConfigError;
use std::io;
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let err = ConfigError::FileNotFound {
filename: PathBuf::from("/path/to/file.toml"),
source: Some(io_err),
};
let msg = err.user_message();
assert!(msg.contains("file.toml"));
let code = err.code();
assert!(matches!(code, confers::error::ErrorCode::FileNotFound));
}
#[test]
fn test_migration_error() {
use confers::ConfigError;
let err = ConfigError::MigrationFailed {
from: 1,
to: 2,
reason: "invalid transformation".to_string(),
source: None,
};
let msg = err.user_message();
assert!(msg.contains("v1"));
assert!(msg.contains("v2"));
assert!(msg.contains("invalid transformation"));
}
#[test]
fn test_module_not_found_error() {
use confers::ConfigError;
let err = ConfigError::ModuleNotFound {
group: "database".to_string(),
module: "postgresql".to_string(),
};
let msg = err.user_message();
assert!(msg.contains("postgresql"));
assert!(msg.contains("database"));
}
#[test]
fn test_size_limit_exceeded_error() {
use confers::ConfigError;
let err = ConfigError::SizeLimitExceeded {
actual: 10485760, limit: 1048576, };
let msg = err.user_message();
assert!(msg.contains("10485760"));
assert!(msg.contains("1048576"));
}
#[test]
fn test_key_rotation_failed_error() {
use confers::ConfigError;
let err = ConfigError::KeyRotationFailed {
from_version: "v1".to_string(),
to_version: "v2".to_string(),
reason: "invalid key format".to_string(),
};
let msg = err.user_message();
assert!(msg.contains("v1"));
assert!(msg.contains("v2"));
}
#[test]
fn test_override_blocked_error() {
use confers::ConfigError;
let err = ConfigError::OverrideBlocked {
key: "api_key".to_string(),
reason: "protected field".to_string(),
override_source: Some("cli".to_string()),
};
let msg = err.user_message();
assert!(msg.contains("api_key"));
assert!(msg.contains("cli"));
}
#[test]
fn test_concurrency_conflict_error() {
use confers::ConfigError;
let err = ConfigError::ConcurrencyConflict {
key: "counter".to_string(),
message: "value changed during read".to_string(),
expected_type: Some("i32".to_string()),
};
assert!(err.is_retryable());
let msg = err.user_message();
assert!(msg.contains("counter"));
}