diff --git a/Cargo.lock b/Cargo.lock index 7ad358a..c6efad9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,6 +395,7 @@ dependencies = [ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)", "simple_logger 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1279,6 +1280,14 @@ name = "regex-syntax" version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "remove_dir_all" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "resolv-conf" version = "0.6.2" @@ -1443,6 +1452,19 @@ dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "term_size" version = "0.3.1" @@ -1846,6 +1868,7 @@ dependencies = [ "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd" "checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716" +"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" "checksum resolv-conf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b263b4aa1b5de9ffc0054a2386f96992058bb6870aab516f8cdeb8a667d56dcb" "checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" "checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" @@ -1866,6 +1889,7 @@ dependencies = [ "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238" "checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" +"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" "checksum term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9e5b9a66db815dcfd2da92db471106457082577c3c278d4138ab3e3b4e189327" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" diff --git a/Cargo.toml b/Cargo.toml index d0988eb..876d261 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,5 +21,8 @@ log = "0.4" simple_logger = "1.3" clap = { version = "2.33", features = ["yaml", "wrap_help"] } +[dev-dependencies] +tempfile = "3.1" + [profile.release] lto = true diff --git a/src/error.rs b/src/error.rs index ea184c8..022e2cc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +use std::error::Error; use std::fmt; #[derive(Debug)] @@ -9,6 +10,8 @@ pub enum BunBunError { LoggerInitError(log::SetLoggerError), } +impl Error for BunBunError {} + impl fmt::Display for BunBunError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/src/main.rs b/src/main.rs index caf4fc2..025495b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -95,7 +95,7 @@ fn init_logger( Ok(()) } -#[derive(Deserialize)] +#[derive(Deserialize, Debug, PartialEq)] struct Config { bind_address: String, public_address: String, @@ -103,7 +103,7 @@ struct Config { groups: Vec, } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] struct RouteGroup { name: String, description: Option, @@ -234,3 +234,175 @@ fn start_watch( Ok(watch) } + +#[cfg(test)] +mod init_logger { + use super::*; + + #[test] + fn defaults_to_warn() -> Result<(), BunBunError> { + init_logger(0, 0)?; + assert_eq!(log::max_level(), log::Level::Warn); + Ok(()) + } + + #[test] + #[ignore] + fn caps_to_2_when_log_level_is_lt_2() -> Result<(), BunBunError> { + init_logger(0, 3)?; + assert_eq!(log::max_level(), log::LevelFilter::Off); + Ok(()) + } + + #[test] + #[ignore] + fn caps_to_3_when_log_level_is_gt_3() -> Result<(), BunBunError> { + init_logger(4, 0)?; + assert_eq!(log::max_level(), log::Level::Trace); + Ok(()) + } +} + +#[cfg(test)] +mod read_config { + use super::*; + use tempfile::NamedTempFile; + + #[test] + fn returns_default_config_if_path_does_not_exist() { + assert_eq!( + read_config("/this_is_a_non_existent_file").unwrap(), + serde_yaml::from_slice(DEFAULT_CONFIG).unwrap() + ); + } + + #[test] + fn returns_error_if_given_empty_config() { + assert_eq!( + read_config("/dev/null").unwrap_err().to_string(), + "EOF while parsing a value" + ); + } + + #[test] + fn returns_error_if_given_invalid_config() -> Result<(), std::io::Error> { + let mut tmp_file = NamedTempFile::new()?; + tmp_file.write_all(b"g")?; + assert_eq!( + read_config(tmp_file.path().to_str().unwrap()) + .unwrap_err() + .to_string(), + r#"invalid type: string "g", expected struct Config at line 1 column 1"# + ); + Ok(()) + } + + #[test] + fn returns_error_if_config_missing_field() -> Result<(), std::io::Error> { + let mut tmp_file = NamedTempFile::new()?; + tmp_file.write_all( + br#" + bind_address: "localhost" + public_address: "localhost" + "#, + )?; + assert_eq!( + read_config(tmp_file.path().to_str().unwrap()) + .unwrap_err() + .to_string(), + "missing field `groups` at line 2 column 19" + ); + Ok(()) + } + + #[test] + fn returns_ok_if_valid_config() -> Result<(), std::io::Error> { + let mut tmp_file = NamedTempFile::new()?; + tmp_file.write_all( + br#" + bind_address: "a" + public_address: "b" + groups: []"#, + )?; + assert_eq!( + read_config(tmp_file.path().to_str().unwrap()).unwrap(), + Config { + bind_address: String::from("a"), + public_address: String::from("b"), + groups: vec![], + default_route: None, + } + ); + Ok(()) + } +} + +#[cfg(test)] +mod cache_routes { + use super::*; + use std::iter::FromIterator; + + fn generate_routes(routes: &[(&str, &str)]) -> HashMap { + HashMap::from_iter( + routes + .iter() + .map(|(k, v)| (String::from(*k), String::from(*v))), + ) + } + + #[test] + fn empty_groups_yield_empty_routes() { + assert_eq!(cache_routes(&[]), HashMap::new()); + } + + #[test] + fn disjoint_groups_yield_summed_routes() { + let group1 = RouteGroup { + name: String::from("x"), + description: Some(String::from("y")), + routes: generate_routes(&[("a", "b"), ("c", "d")]), + }; + + let group2 = RouteGroup { + name: String::from("5"), + description: Some(String::from("6")), + routes: generate_routes(&[("1", "2"), ("3", "4")]), + }; + + assert_eq!( + cache_routes(&[group1, group2]), + generate_routes(&[("a", "b"), ("c", "d"), ("1", "2"), ("3", "4")]) + ); + } + + #[test] + fn overlapping_groups_use_latter_routes() { + let group1 = RouteGroup { + name: String::from("x"), + description: Some(String::from("y")), + routes: generate_routes(&[("a", "b"), ("c", "d")]), + }; + + let group2 = RouteGroup { + name: String::from("5"), + description: Some(String::from("6")), + routes: generate_routes(&[("a", "1"), ("c", "2")]), + }; + + assert_eq!( + cache_routes(&[group1.clone(), group2]), + generate_routes(&[("a", "1"), ("c", "2")]) + ); + + let group3 = RouteGroup { + name: String::from("5"), + description: Some(String::from("6")), + routes: generate_routes(&[("a", "1"), ("b", "2")]), + }; + + assert_eq!( + cache_routes(&[group1, group3]), + generate_routes(&[("a", "1"), ("b", "2"), ("c", "d")]) + ); + } +} diff --git a/src/routes.rs b/src/routes.rs index f094ded..06b20ab 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -112,7 +112,10 @@ fn resolve_hop( (None, Some(route)) => { let args = split_args.join(" "); debug!("Using default route {} with args {}", route, args); - (routes.get(route).cloned(), args) + match routes.get(route) { + Some(v) => (Some(v.to_owned()), args), + None => (None, String::new()), + } } // No default route and no match (None, None) => { @@ -156,3 +159,65 @@ pub async fn opensearch(data: StateData, req: HttpRequest) -> impl Responder { .unwrap(), ) } + +#[cfg(test)] +mod resolve_hop { + use super::*; + + fn generate_route_result( + keyword: &str, + args: &str, + ) -> (Option, String) { + (Some(String::from(keyword)), String::from(args)) + } + + #[test] + fn empty_routes_no_default_yields_failed_hop() { + assert_eq!( + resolve_hop("hello world", &HashMap::new(), &None), + (None, String::new()) + ); + } + + #[test] + fn empty_routes_some_default_yields_failed_hop() { + assert_eq!( + resolve_hop( + "hello world", + &HashMap::new(), + &Some(String::from("google")) + ), + (None, String::new()) + ); + } + + #[test] + fn only_default_routes_some_default_yields_default_hop() { + let mut map = HashMap::new(); + map.insert(String::from("google"), String::from("https://example.com")); + assert_eq!( + resolve_hop("hello world", &map, &Some(String::from("google"))), + generate_route_result("https://example.com", "hello world"), + ); + } + + #[test] + fn non_default_routes_some_default_yields_non_default_hop() { + let mut map = HashMap::new(); + map.insert(String::from("google"), String::from("https://example.com")); + assert_eq!( + resolve_hop("google hello world", &map, &Some(String::from("a"))), + generate_route_result("https://example.com", "hello world"), + ); + } + + #[test] + fn non_default_routes_no_default_yields_non_default_hop() { + let mut map = HashMap::new(); + map.insert(String::from("google"), String::from("https://example.com")); + assert_eq!( + resolve_hop("google hello world", &map, &None), + generate_route_result("https://example.com", "hello world"), + ); + } +}