dead_man_switch/
config.rs1use crate::app;
4use crate::error::ConfigError;
5
6use serde::{Deserialize, Serialize};
7use std::env;
8use std::fs::{self, File};
9use std::io::Write;
10use std::path::PathBuf;
11use uuid::Uuid;
12use zeroize::{Zeroize, ZeroizeOnDrop};
13
14#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
21pub struct Config {
22 pub username: String,
24 pub password: String,
26 pub smtp_server: String,
28 pub smtp_port: u16,
30 pub smtp_check_timeout: Option<u64>,
32 pub message: String,
36 pub message_warning: String,
38 pub subject: String,
42 pub subject_warning: String,
44 pub to: String,
46 pub from: String,
48 pub attachment: Option<String>,
50 pub timer_warning: u64,
52 pub timer_dead_man: u64,
54 pub web_password: String,
56 pub cookie_exp_days: u64,
58 pub log_level: Option<String>,
60}
61
62impl Default for Config {
63 fn default() -> Self {
64 let web_password = env::var("WEB_PASSWORD")
69 .ok()
70 .unwrap_or_else(|| Uuid::new_v4().to_string());
71 Self {
72 username: "me@example.com".to_string(),
73 password: "".to_string(),
74 smtp_server: "smtp.example.com".to_string(),
75 smtp_port: 587,
76 smtp_check_timeout: Some(5),
77 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(),
78 message_warning: "Hey, you haven't checked in for a while. Are you okay?".to_string(),
79 subject: "[URGENT] Something Happened to Me!".to_string(),
80 subject_warning: "[URGENT] You need to check in!".to_string(),
81 to: "someone@example.com".to_string(),
82 from: "me@example.com".to_string(),
83 attachment: None,
84 timer_warning: 60 * 60 * 24 * 14, timer_dead_man: 60 * 60 * 24 * 7, web_password,
87 cookie_exp_days: 7,
88 log_level: None,
89 }
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum Email {
96 Warning,
98 DeadMan,
100}
101
102fn file_name() -> &'static str {
104 "config.toml"
105}
106
107pub fn file_path() -> Result<PathBuf, ConfigError> {
114 let path = app::file_path(file_name())?;
115 Ok(path)
116}
117
118pub fn save(config: &Config) -> Result<(), ConfigError> {
126 let file_path = file_path()?;
127 let mut file = File::create(file_path)?;
128 let config = toml::to_string(config)?;
129
130 file.write_all(config.as_bytes())?;
131
132 Ok(())
133}
134
135pub fn load_or_initialize() -> Result<Config, ConfigError> {
150 let file_path = file_path()?;
151 if !file_path.exists() {
152 let config = Config::default();
153 save(&config)?;
154
155 Ok(config)
156 } else {
157 let config_str = fs::read_to_string(&file_path)?;
158 let config: Config = toml::from_str(&config_str)?;
159
160 Ok(config)
161 }
162}
163
164pub fn attachment_path(config: &Config) -> Result<PathBuf, ConfigError> {
172 let attachment_path = config
173 .attachment
174 .as_ref()
175 .ok_or(ConfigError::AttachmentNotFound)?;
176 Ok(PathBuf::from(attachment_path))
177}
178
179#[cfg(test)]
180mod test {
181 use super::*;
182 use std::path::Path;
183
184 struct TestGuard;
185 impl TestGuard {
186 fn new(c: &Config) -> Self {
187 let file_path = file_path().expect("setup: failed file_path()");
190
191 if let Some(parent) = file_path.parent() {
193 fs::create_dir_all(parent).expect("setup: failed to create dir");
194 }
195 let mut file = File::create(file_path).expect("setup: failed to create file");
196 let c_str = toml::to_string(c).expect("setup: failed to convert data");
197 file.write_all(c_str.as_bytes())
198 .expect("setup: failed to write data");
199 file.sync_all()
200 .expect("setup: failed to ensure file written to disk");
201
202 TestGuard
203 }
204 }
205 impl Drop for TestGuard {
206 fn drop(&mut self) {
207 let file_path = file_path().expect("teardown: failed file_path()");
209 cleanup_test_dir_parent(file_path.as_path());
210 }
211 }
212
213 fn cleanup_test_dir(dir: &Path) {
215 if let Some(parent) = dir.parent() {
216 let _ = fs::remove_dir_all(parent);
217 }
218 }
219
220 fn cleanup_test_dir_parent(dir: &Path) {
222 if let Some(parent) = dir.parent() {
223 cleanup_test_dir(parent)
224 }
225 }
226
227 fn load_config_from_path(path: &PathBuf) -> Config {
229 let config_str = fs::read_to_string(path).expect("helper: error reading config data");
230 let config: Config =
231 toml::from_str(&config_str).expect("helper: error parsing config data");
232 config
233 }
234
235 #[test]
236 fn file_path_in_test_mode() {
237 let result = file_path();
239 assert!(result.is_ok());
240
241 let result = result.unwrap();
242 let expected = format!("{}_test", app::name());
243 assert!(result.to_string_lossy().contains(expected.as_str()));
244
245 let expected = Path::new(app::name()).join(file_name());
247 assert!(result
248 .to_string_lossy()
249 .contains(expected.to_string_lossy().as_ref()));
250
251 cleanup_test_dir_parent(&result);
253 }
254
255 #[test]
256 fn save_config() {
257 let mut config = Config::default();
259 config.message = "test save".to_string();
260
261 let result = save(&config);
262 assert!(result.is_ok());
263
264 let test_path = file_path().unwrap();
265 let loaded_config = load_config_from_path(&test_path);
268 assert_eq!(loaded_config, config);
270
271 cleanup_test_dir_parent(&test_path);
273 }
274
275 #[test]
276 fn timer_guard_ok() {
277 let mut config = Config::default();
282 config.message = "test guard".to_string();
283 let _guard = TestGuard::new(&config);
284
285 let test_path = file_path().unwrap();
287 let loaded_config = load_config_from_path(&test_path);
288 assert_eq!(loaded_config, config);
289 }
290
291 #[test]
292 fn load_or_initialize_with_existing_file() {
293 let mut existing_config = Config::default();
295 existing_config.message = "test load".to_string();
296 let _guard = TestGuard::new(&existing_config);
297
298 let config = load_or_initialize().unwrap();
300
301 assert_eq!(config, existing_config);
304 }
305
306 #[test]
307 fn load_or_initialize_with_no_existing_file() {
308 let mut config_default = Config::default();
309
310 let mut config = load_or_initialize().unwrap();
312
313 config_default.web_password = "".to_string();
315 config.web_password = "".to_string();
316
317 assert_eq!(config, config_default);
318
319 let test_path = file_path().unwrap();
321 cleanup_test_dir_parent(&test_path);
322 }
323
324 #[test]
325 fn example_config_is_valid() {
326 let example_config = fs::read_to_string("../../config.example.toml").unwrap();
327 let config: Result<Config, toml::de::Error> = toml::from_str(&example_config);
328 assert!(config.is_ok());
329 }
330}