use crate::app;
use crate::error::ConfigError;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use uuid::Uuid;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct Config {
pub username: String,
pub password: String,
pub smtp_server: String,
pub smtp_port: u16,
pub smtp_check_timeout: Option<u64>,
pub message: String,
pub message_warning: String,
pub subject: String,
pub subject_warning: String,
pub to: String,
pub from: String,
pub attachment: Option<String>,
pub timer_warning: u64,
pub timer_dead_man: u64,
pub web_password: String,
pub cookie_exp_days: u64,
pub log_level: Option<String>,
}
impl Default for Config {
fn default() -> Self {
let web_password = env::var("WEB_PASSWORD")
.ok()
.unwrap_or_else(|| Uuid::new_v4().to_string());
Self {
username: "me@example.com".to_string(),
password: "".to_string(),
smtp_server: "smtp.example.com".to_string(),
smtp_port: 587,
smtp_check_timeout: Some(5),
message: "I'm probably dead, go to Central Park NY under bench #137 you'll find an age-encrypted drive. Password is our favorite music in Pascal case.".to_string(),
message_warning: "Hey, you haven't checked in for a while. Are you okay?".to_string(),
subject: "[URGENT] Something Happened to Me!".to_string(),
subject_warning: "[URGENT] You need to check in!".to_string(),
to: "someone@example.com".to_string(),
from: "me@example.com".to_string(),
attachment: None,
timer_warning: 60 * 60 * 24 * 14, timer_dead_man: 60 * 60 * 24 * 7, web_password,
cookie_exp_days: 7,
log_level: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Email {
Warning,
DeadMan,
}
fn file_name() -> &'static str {
"config.toml"
}
pub fn file_path() -> Result<PathBuf, ConfigError> {
let path = app::file_path(file_name())?;
Ok(path)
}
pub fn save(config: &Config) -> Result<(), ConfigError> {
let file_path = file_path()?;
let mut file = File::create(file_path)?;
let config = toml::to_string(config)?;
file.write_all(config.as_bytes())?;
Ok(())
}
pub fn load_or_initialize() -> Result<Config, ConfigError> {
let file_path = file_path()?;
if !file_path.exists() {
let config = Config::default();
save(&config)?;
Ok(config)
} else {
let config_str = fs::read_to_string(&file_path)?;
let config: Config = toml::from_str(&config_str)?;
Ok(config)
}
}
pub fn attachment_path(config: &Config) -> Result<PathBuf, ConfigError> {
let attachment_path = config
.attachment
.as_ref()
.ok_or(ConfigError::AttachmentNotFound)?;
Ok(PathBuf::from(attachment_path))
}
#[cfg(test)]
mod test {
use super::*;
use std::path::Path;
struct TestGuard;
impl TestGuard {
fn new(c: &Config) -> Self {
let file_path = file_path().expect("setup: failed file_path()");
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).expect("setup: failed to create dir");
}
let mut file = File::create(file_path).expect("setup: failed to create file");
let c_str = toml::to_string(c).expect("setup: failed to convert data");
file.write_all(c_str.as_bytes())
.expect("setup: failed to write data");
file.sync_all()
.expect("setup: failed to ensure file written to disk");
TestGuard
}
}
impl Drop for TestGuard {
fn drop(&mut self) {
let file_path = file_path().expect("teardown: failed file_path()");
cleanup_test_dir_parent(file_path.as_path());
}
}
fn cleanup_test_dir(dir: &Path) {
if let Some(parent) = dir.parent() {
let _ = fs::remove_dir_all(parent);
}
}
fn cleanup_test_dir_parent(dir: &Path) {
if let Some(parent) = dir.parent() {
cleanup_test_dir(parent)
}
}
fn load_config_from_path(path: &PathBuf) -> Config {
let config_str = fs::read_to_string(path).expect("helper: error reading config data");
let config: Config =
toml::from_str(&config_str).expect("helper: error parsing config data");
config
}
#[test]
fn file_path_in_test_mode() {
let result = file_path();
assert!(result.is_ok());
let result = result.unwrap();
let expected = format!("{}_test", app::name());
assert!(result.to_string_lossy().contains(expected.as_str()));
let expected = Path::new(app::name()).join(file_name());
assert!(
result
.to_string_lossy()
.contains(expected.to_string_lossy().as_ref())
);
cleanup_test_dir_parent(&result);
}
#[test]
fn save_config() {
let mut config = Config::default();
config.message = "test save".to_string();
let result = save(&config);
assert!(result.is_ok());
let test_path = file_path().unwrap();
let loaded_config = load_config_from_path(&test_path);
assert_eq!(loaded_config, config);
cleanup_test_dir_parent(&test_path);
}
#[test]
fn timer_guard_ok() {
let mut config = Config::default();
config.message = "test guard".to_string();
let _guard = TestGuard::new(&config);
let test_path = file_path().unwrap();
let loaded_config = load_config_from_path(&test_path);
assert_eq!(loaded_config, config);
}
#[test]
fn load_or_initialize_with_existing_file() {
let mut existing_config = Config::default();
existing_config.message = "test load".to_string();
let _guard = TestGuard::new(&existing_config);
let config = load_or_initialize().unwrap();
assert_eq!(config, existing_config);
}
#[test]
fn load_or_initialize_with_no_existing_file() {
let mut config_default = Config::default();
let mut config = load_or_initialize().unwrap();
config_default.web_password = "".to_string();
config.web_password = "".to_string();
assert_eq!(config, config_default);
let test_path = file_path().unwrap();
cleanup_test_dir_parent(&test_path);
}
#[test]
fn example_config_is_valid() {
let example_config = fs::read_to_string("../../config.example.toml").unwrap();
let config: Result<Config, toml::de::Error> = toml::from_str(&example_config);
assert!(config.is_ok());
}
}