From d7ce374a1741fdbb5c3aeef1218058a3d1060e88 Mon Sep 17 00:00:00 2001 From: Gus Power Date: Wed, 21 May 2025 16:23:55 +0100 Subject: config fallback w/ tests. introduced a macro to remove some test boilerplate around Result and having return Ok(()) --- src/config.rs | 183 ++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 108 insertions(+), 75 deletions(-) (limited to 'src/config.rs') diff --git a/src/config.rs b/src/config.rs index 2c5427e..881759c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,8 +5,9 @@ use fqdn::FQDN; use serde::{Deserialize, Serialize}; use serde_with::DisplayFromStr; use serde_with::serde_as; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::{fs, io}; +use log::warn; use strum::Display; #[derive(Debug, Deserialize, Serialize)] @@ -16,17 +17,38 @@ pub struct Config { } impl Config { - pub fn load>(path: P) -> AppResult { - let path = path.as_ref(); - let content = fs::read_to_string(path).map_err(|e| match e.kind() { - io::ErrorKind::NotFound => AppError::FileNotFound(path.to_path_buf()), - _ => AppError::IoError(e), - })?; - - serde_json::from_str(&content).map_err(|e| AppError::ConfigParseError { - source: e, - path: path.to_path_buf(), - }) + pub fn load(paths: Vec) -> AppResult { + let mut index = 1; + for path in &paths { + let content = fs::read_to_string(&path); + match content { + Ok(value) => { + match serde_json::from_str(&value) { + Ok(config) => { return Ok(config) }, + Err(error) => { + if index == paths.len() { return Err(AppError::ConfigParseError { + source: error, + path: path.to_path_buf(), + })} + warn!("{}", error); + } + } + } + Err(e) => { + let error = match e.kind() { + io::ErrorKind::NotFound => AppError::ConfigFileNotFound(path.clone()), + _ => AppError::IoError(e), + }; + if index == paths.len() { + return Err(error); + } + warn!("{}", error); + }, + } + index += 1; + } + + Err(AppError::ConfigFileNotProvided) } } @@ -70,91 +92,102 @@ mod tests { use super::*; use crate::dyndns_service::DynDnsProvider::Gandi; use crate::dyndns_service::gandi::GandiConfig; - use crate::{assert_config_parse_error, assert_error, assert_file_not_found, assert_io_error}; + use crate::{ + assert_config_file_not_found, assert_config_parse_error, assert_error, assert_io_error, + test, + }; use serde_json::json; - use std::error::Error; - use std::fs::{File, read_dir}; - use std::io::BufReader; + use std::fs::read_dir; + use std::path::PathBuf; use std::str::FromStr; - #[test] - fn check_minimal_config() { - let input = json!({ - "fqdn": "dyn.domain.com", - "providers": [ { - "type": "GANDI", - "api_key": "SOME-API-KEY", - } ] - }); - - let wan_config = serde_json::from_value::(input).unwrap(); - - assert_eq!( - wan_config.dns_record.fqdn, - FQDN::from_str("dyn.domain.com").unwrap() - ); - assert_eq!(wan_config.interface, None); - - assert_eq!(wan_config.providers.len(), 1); - let expected = GandiConfig::new("SOME-API-KEY".to_string()); - let actual = wan_config.providers.get(0).unwrap(); - assert_eq!(&Gandi(expected), actual); + test! { + fn check_minimal_config() { + let input = json!({ + "fqdn": "dyn.domain.com", + "providers": [ { + "type": "GANDI", + "api_key": "SOME-API-KEY", + } ] + }); + + let wan_config = serde_json::from_value::(input)?; + + assert_eq!( + wan_config.dns_record.fqdn, + FQDN::from_str("dyn.domain.com")? + ); + assert_eq!(wan_config.interface, None); + + assert_eq!(wan_config.providers.len(), 1); + let expected = GandiConfig::new("SOME-API-KEY".to_string()); + let actual = wan_config.providers.get(0).unwrap(); + assert_eq!(&Gandi(expected), actual); + } } - #[test] - fn check_defaults_on_dns_record_deserialization() { - let input = json!({ - "fqdn": "dyn.mydomain.com", - }); + test! { + fn check_defaults_on_dns_record_deserialization() { + let input = json!({ + "fqdn": "dyn.mydomain.com", + }); - let dns_record = serde_json::from_value::(input).unwrap(); + let dns_record = serde_json::from_value::(input)?; - assert_eq!(dns_record.record_type, default_record_type()); - assert_eq!(dns_record.ttl, default_ttl()); + assert_eq!(dns_record.record_type, default_record_type()); + assert_eq!(dns_record.ttl, default_ttl()); - assert_eq!(dns_record.fqdn, FQDN::from_str("dyn.mydomain.com").unwrap()); + assert_eq!(dns_record.fqdn, FQDN::from_str("dyn.mydomain.com")?); + } } - #[test] - fn check_file_configs() -> Result<(), Box> { - let path = Path::new("test"); - for entry in read_dir(path)? { - let entry = entry?; - let test_file_path = entry.path(); - if test_file_path.extension().unwrap_or_default() != "json" { - continue; + test! { + fn check_test_file_configs() { + let path = Path::new("test"); + for entry in read_dir(path)? { + let entry = entry?; + let test_file_path = entry.path(); + if test_file_path.extension().unwrap_or_default() != "json" { + continue; + } + + Config::load(vec![test_file_path])?; } + } + } - let file = File::open(test_file_path)?; - let reader = BufReader::new(file); + test! { + fn check_chooses_fallback_config() { + let configs = vec![PathBuf::from_str("test/unknown.json")?, + PathBuf::from_str("test/also-unknown.json")?, + PathBuf::from_str("test/config.bork")?, + PathBuf::from_str("test/noop.json")?]; - let actual: Config = serde_json::from_reader(reader)?; - assert_eq!(actual.networks.len(), 2); + Config::load(configs)?; } - Ok(()) } - #[test] - fn check_missing_config() -> Result<(), Box> { - let path = Path::new("test/unknown.yaml"); + test! { + fn missing_config() { + let path = Path::new("test/unknown.yaml"); - assert_file_not_found!(Config::load(path), path); - Ok(()) + assert_config_file_not_found!(Config::load(vec![path.to_path_buf()]), path); + } } - #[test] - fn check_broken_config() -> Result<(), Box> { - let path = Path::new("test/config.bork"); + test! { + fn broken_config() { + let path = Path::new("test/config.bork"); - assert_config_parse_error!(Config::load(path), path); - Ok(()) + assert_config_parse_error!(Config::load(vec![path.to_path_buf()]), path); + } } - #[test] - fn check_inaccessible_config() -> Result<(), Box> { - let path = Path::new("/root/secure-config.json"); + test! { + fn inaccessible_config() { + let path = Path::new("/root/secure-config.json"); - assert_io_error!(Config::load(path)); - Ok(()) + assert_io_error!(Config::load(vec![path.to_path_buf()])); + } } } -- cgit v1.2.3