use std::fmt;
use super::{CaptchaConfig, EmailConfig, MariadbConfig, MediaLibraryConfig, SpacetimeDbConfig};
use super::{Result, variables};
#[derive(Clone, Default)]
pub struct AppConfig {
pub mariadb: MariadbConfig,
pub spacetimedb: SpacetimeDbConfig,
pub media_library: MediaLibraryConfig,
pub captcha: CaptchaConfig,
pub email: EmailConfig,
}
impl fmt::Debug for AppConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut output = formatter.debug_struct("AppConfig");
output
.field("mariadb", &self.mariadb)
.field("spacetimedb", &self.spacetimedb)
.field("media_library", &self.media_library)
.field("captcha", &self.captcha)
.field("email", &self.email);
output.finish()
}
}
impl AppConfig {
pub fn from_env(mut defaults: Self) -> Result<Self> {
defaults.apply_environment(&mut variables::value)?;
defaults.validate()?;
Ok(defaults)
}
fn apply_environment(
&mut self,
value: &mut impl FnMut(&'static str) -> Result<Option<String>>,
) -> Result<()> {
self.mariadb.apply_environment(value)?;
self.spacetimedb.apply_environment(value)?;
self.media_library.apply_environment(value)?;
self.captcha.apply_environment(value)?;
self.email.apply_environment(value)?;
Ok(())
}
pub fn validate(&self) -> Result<()> {
self.mariadb.validate()?;
self.spacetimedb.validate()?;
self.media_library.validate()?;
self.captcha.validate()?;
self.email.validate()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[test]
fn empty_defaults_are_valid() {
AppConfig::default()
.validate()
.expect("an application may opt into only the sections it uses");
}
#[test]
fn environment_values_override_only_supplied_defaults() {
let mut values = HashMap::from([
("DATABASE_URL", "mysql://database.invalid/app".to_string()),
("CAPTCHA_BYPASS", "true".to_string()),
]);
let mut config = AppConfig {
captcha: CaptchaConfig::standard(),
..AppConfig::default()
};
config
.apply_environment(&mut |name| Ok(values.remove(name)))
.expect("environment values should resolve");
config
.validate()
.expect("resolved defaults should validate");
assert_eq!(
config.mariadb.connection_url().expect("database URL"),
"mysql://database.invalid/app"
);
assert!(config.captcha.bypass);
}
#[test]
fn debug_output_redacts_credentials() {
let config = AppConfig {
mariadb: MariadbConfig::new("mysql://user:password@database.invalid/schema"),
email: EmailConfig::smtp(
"sender@example.invalid",
"Example",
"mail.example.invalid",
2525,
super::super::EmailSmtpSecurity::StartTls,
),
..AppConfig::default()
};
let mut config = config;
config.email.smtp.username = Some("account".to_string());
config.email.smtp.password = Some("secret".to_string());
let debug = format!("{config:?}");
assert!(!debug.contains("user:password"));
assert!(!debug.contains("secret"));
assert!(!debug.contains("account"));
assert!(debug.contains("[REDACTED]"));
}
}