use premortem::prelude::*;
use serde::Deserialize;
#[derive(Debug, Deserialize, DeriveValidate, PartialEq)]
pub struct AppConfig {
#[validate(non_empty)]
pub host: String,
#[validate(range(1..=65535))]
pub port: u16,
#[serde(default)]
pub debug: bool,
}
fn main() {
println!("This example is meant to be run with `cargo test`");
println!();
println!("Available tests:");
println!(" - test_load_from_toml: Load config from mock TOML file");
println!(" - test_env_override: Environment variable overrides");
println!(" - test_validation_errors_accumulate: All errors collected");
println!(" - test_missing_required_file: Error on missing file");
println!(" - test_optional_file_missing: Optional file skipped gracefully");
println!(" - test_permission_denied: I/O error handling");
println!();
println!("Run: cargo test");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_from_toml() {
let env = MockEnv::new().with_file(
"config.toml",
r#"
host = "localhost"
port = 8080
"#,
);
let config = Config::<AppConfig>::builder()
.source(Toml::file("config.toml"))
.build_with_env(&env)
.expect("should load successfully");
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 8080);
assert!(!config.debug);
}
#[test]
fn test_env_override() {
let env = MockEnv::new()
.with_file(
"config.toml",
r#"
host = "localhost"
port = 8080
"#,
)
.with_env("APP_PORT", "9000")
.with_env("APP_DEBUG", "true");
let config = Config::<AppConfig>::builder()
.source(Toml::file("config.toml"))
.source(Env::prefix("APP_"))
.build_with_env(&env)
.expect("should load successfully");
assert_eq!(config.host, "localhost"); assert_eq!(config.port, 9000); assert!(config.debug); }
#[test]
fn test_validation_errors_accumulate() {
let env = MockEnv::new().with_file(
"config.toml",
r#"
host = ""
port = 0
"#,
);
let result = Config::<AppConfig>::builder()
.source(Toml::file("config.toml"))
.build_with_env(&env);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 2, "Expected 2 errors, got: {:?}", errors);
}
#[test]
fn test_missing_required_file() {
let env = MockEnv::new();
let result = Config::<AppConfig>::builder()
.source(Toml::file("missing.toml"))
.build_with_env(&env);
assert!(result.is_err());
let errors = result.unwrap_err();
match errors.first() {
ConfigError::SourceError { kind, .. } => {
assert!(
matches!(kind, SourceErrorKind::NotFound { .. }),
"Expected NotFound, got: {:?}",
kind
);
}
other => panic!("Expected SourceError, got: {:?}", other),
}
}
#[test]
fn test_optional_file_missing() {
let env = MockEnv::new()
.with_env("APP_HOST", "localhost")
.with_env("APP_PORT", "8080");
let config = Config::<AppConfig>::builder()
.source(Toml::file("config.toml").optional())
.source(Env::prefix("APP_"))
.build_with_env(&env)
.expect("should load from env only");
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 8080);
}
#[test]
fn test_permission_denied() {
let env = MockEnv::new().with_unreadable_file("secret.toml");
let result = Config::<AppConfig>::builder()
.source(Toml::file("secret.toml"))
.build_with_env(&env);
assert!(result.is_err());
let errors = result.unwrap_err();
match errors.first() {
ConfigError::SourceError { kind, .. } => {
assert!(
matches!(kind, SourceErrorKind::IoError { .. }),
"Expected IoError, got: {:?}",
kind
);
}
other => panic!("Expected SourceError, got: {:?}", other),
}
}
#[test]
fn test_nested_config() {
#[derive(Debug, Deserialize, DeriveValidate)]
struct ServerConfig {
#[validate(non_empty)]
host: String,
#[validate(range(1..=65535))]
port: u16,
}
#[derive(Debug, Deserialize, DeriveValidate)]
struct NestedConfig {
#[validate(nested)]
server: ServerConfig,
}
let env = MockEnv::new().with_file(
"config.toml",
r#"
[server]
host = "localhost"
port = 8080
"#,
);
let config = Config::<NestedConfig>::builder()
.source(Toml::file("config.toml"))
.build_with_env(&env)
.expect("should load successfully");
assert_eq!(config.server.host, "localhost");
assert_eq!(config.server.port, 8080);
}
#[test]
fn test_nested_validation_errors_have_paths() {
#[derive(Debug, Deserialize, DeriveValidate)]
struct ServerConfig {
#[validate(non_empty)]
host: String,
}
#[derive(Debug, Deserialize, DeriveValidate)]
struct NestedConfig {
#[validate(nested)]
server: ServerConfig,
}
let env = MockEnv::new().with_file(
"config.toml",
r#"
[server]
host = ""
"#,
);
let result = Config::<NestedConfig>::builder()
.source(Toml::file("config.toml"))
.build_with_env(&env);
assert!(result.is_err());
let errors = result.unwrap_err();
let paths: Vec<_> = errors.iter().filter_map(|e| e.path()).collect();
assert!(
paths.iter().any(|p| p.contains("server")),
"Expected path to include 'server', got: {:?}",
paths
);
}
#[test]
fn test_dynamic_file_changes() {
let env = MockEnv::new().with_file(
"config.toml",
r#"
host = "localhost"
port = 8080
"#,
);
let config1 = Config::<AppConfig>::builder()
.source(Toml::file("config.toml"))
.build_with_env(&env)
.expect("should load");
assert_eq!(config1.port, 8080);
env.set_file(
"config.toml",
r#"
host = "production"
port = 9000
"#,
);
let config2 = Config::<AppConfig>::builder()
.source(Toml::file("config.toml"))
.build_with_env(&env)
.expect("should load");
assert_eq!(config2.port, 9000);
assert_eq!(config2.host, "production");
}
}