diff --git a/README.md b/README.md index eb6cb34..3cf9234 100644 --- a/README.md +++ b/README.md @@ -364,7 +364,13 @@ radarr: - host: 192.168.0.78 port: 7878 api_token: someApiToken1234567890 - ssl_cert_path: /path/to/radarr.crt # Required to enable SSL + ssl_cert_path: /path/to/radarr.crt # Use a self-signed SSL certificate to connect to this Servarr + # Enables SSL regardless of the value of the 'ssl' + + - host: 192.168.0.79 + port: 7878 + api_token: someApiToken1234567890 + ssl: true # Use SSL to connect to this Servarr (public certs) - uri: http://htpc.local/radarr # Example of using the 'uri' key instead of 'host' and 'port' api_token: someApiToken1234567890 diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index 8b8201e..aea2490 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -447,6 +447,78 @@ mod tests { assert_none!(config.port); } + #[test] + #[serial] + fn test_deserialize_optional_env_var_bool_is_bool() { + let yaml_data = r#" + host: localhost + api_token: "test123" + ssl: true + "#; + + let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_some_eq_x!(&config.ssl, &true); + } + + #[test] + #[serial] + fn test_deserialize_optional_env_var_bool_is_string() { + let yaml_data = r#" + host: localhost + api_token: "test123" + ssl: "true" + "#; + + let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_some_eq_x!(&config.ssl, &true); + } + + #[test] + #[serial] + fn test_deserialize_optional_env_var_bool_is_present() { + unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_BOOL", "true") }; + let yaml_data = r#" + host: localhost + api_token: "test123" + ssl: ${TEST_VAR_DESERIALIZE_OPTION_BOOL} + "#; + + let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_some_eq_x!(&config.ssl, &true); + unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_BOOL") }; + } + + #[test] + #[serial] + fn test_deserialize_optional_env_var_bool_defaults_to_false() { + unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY", "test") }; + let yaml_data = r#" + host: localhost + api_token: "test123" + ssl: ${TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY} + "#; + + let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_some_eq_x!(&config.ssl, &false); + unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY") }; + } + + #[test] + fn test_deserialize_optional_env_var_bool_empty() { + let yaml_data = r#" + host: localhost + api_token: "test123" + "#; + + let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_none!(config.ssl); + } + #[test] #[serial] fn test_deserialize_optional_env_var_header_map_is_present() { @@ -674,7 +746,7 @@ mod tests { let mut custom_headers = HeaderMap::new(); custom_headers.insert("X-Custom-Header", "value".parse().unwrap()); let expected_str = format!( - "ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}), monitored_storage_paths: Some([\"/path1\", \"/path2\"]) }}" + "ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl: Some(true), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}), monitored_storage_paths: Some([\"/path1\", \"/path2\"]) }}" ); let servarr_config = ServarrConfig { name: Some(name), @@ -685,6 +757,7 @@ mod tests { api_token: Some(api_token), api_token_file: Some(api_token_file), ssl_cert_path: Some(ssl_cert_path), + ssl: Some(true), custom_headers: Some(custom_headers), monitored_storage_paths: Some(monitored_storage), }; diff --git a/src/app/mod.rs b/src/app/mod.rs index 2ab870d..fc0b234 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -431,6 +431,8 @@ pub struct ServarrConfig { pub api_token: Option, #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub api_token_file: Option, + #[serde(default, deserialize_with = "deserialize_optional_env_var_bool")] + pub ssl: Option, #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub ssl_cert_path: Option, #[serde( @@ -486,6 +488,7 @@ impl Default for ServarrConfig { api_token: Some(String::new()), api_token_file: None, ssl_cert_path: None, + ssl: None, custom_headers: None, monitored_storage_paths: None, } @@ -532,6 +535,29 @@ where } } +fn deserialize_optional_env_var_bool<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrBool { + Bool(bool), + String(String), + } + + match StringOrBool::deserialize(deserializer)? { + StringOrBool::Bool(b) => Ok(Some(b)), + StringOrBool::String(s) => { + let val = interpolate_env_vars(&s) + .to_lowercase() + .parse() + .unwrap_or(false); + Ok(Some(val)) + } + } +} + fn deserialize_optional_env_var_header_map<'de, D>( deserializer: D, ) -> Result, D::Error> diff --git a/src/cli/lidarr/add_command_handler.rs b/src/cli/lidarr/add_command_handler.rs index b52d13f..d29cc52 100644 --- a/src/cli/lidarr/add_command_handler.rs +++ b/src/cli/lidarr/add_command_handler.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use anyhow::Result; -use clap::{ArgAction, Subcommand, arg}; +use clap::{ArgAction, Subcommand}; use tokio::sync::Mutex; use super::LidarrCommand; diff --git a/src/cli/lidarr/list_command_handler.rs b/src/cli/lidarr/list_command_handler.rs index e09658f..c9150b2 100644 --- a/src/cli/lidarr/list_command_handler.rs +++ b/src/cli/lidarr/list_command_handler.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use anyhow::Result; -use clap::{Subcommand, arg}; +use clap::Subcommand; use serde_json::json; use tokio::sync::Mutex; diff --git a/src/cli/lidarr/mod.rs b/src/cli/lidarr/mod.rs index bbae67f..7ef4c25 100644 --- a/src/cli/lidarr/mod.rs +++ b/src/cli/lidarr/mod.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler}; use anyhow::Result; -use clap::{Subcommand, arg}; +use clap::Subcommand; use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler}; use edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler}; use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler}; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index bb5acd1..858952b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use anyhow::Result; -use clap::{Subcommand, command}; +use clap::Subcommand; use clap_complete::Shell; use indoc::indoc; use lidarr::{LidarrCliHandler, LidarrCommand}; diff --git a/src/cli/radarr/add_command_handler.rs b/src/cli/radarr/add_command_handler.rs index e30bad3..c884fe5 100644 --- a/src/cli/radarr/add_command_handler.rs +++ b/src/cli/radarr/add_command_handler.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use anyhow::Result; -use clap::{ArgAction, Subcommand, arg, command}; +use clap::{ArgAction, Subcommand}; use tokio::sync::Mutex; use super::RadarrCommand; diff --git a/src/cli/radarr/get_command_handler.rs b/src/cli/radarr/get_command_handler.rs index 7233146..19e192c 100644 --- a/src/cli/radarr/get_command_handler.rs +++ b/src/cli/radarr/get_command_handler.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use anyhow::Result; -use clap::{Subcommand, command}; +use clap::Subcommand; use tokio::sync::Mutex; use crate::{ diff --git a/src/cli/radarr/list_command_handler.rs b/src/cli/radarr/list_command_handler.rs index 15dd974..cd8c875 100644 --- a/src/cli/radarr/list_command_handler.rs +++ b/src/cli/radarr/list_command_handler.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use anyhow::Result; -use clap::{Subcommand, command}; +use clap::Subcommand; use tokio::sync::Mutex; use crate::{ diff --git a/src/network/mod.rs b/src/network/mod.rs index dc8359f..4be177b 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -229,6 +229,7 @@ impl<'a, 'b> Network<'a, 'b> { uri, api_token, ssl_cert_path, + ssl, custom_headers: custom_headers_option, .. } = app @@ -245,7 +246,7 @@ impl<'a, 'b> Network<'a, 'b> { let mut uri = if let Some(servarr_uri) = uri { format!("{servarr_uri}/api/{api_version}{resource}") } else { - let protocol = if ssl_cert_path.is_some() { + let protocol = if ssl_cert_path.is_some() || ssl.unwrap_or(false) { "https" } else { "http" diff --git a/src/network/network_tests.rs b/src/network/network_tests.rs index cbf7073..7dd4c20 100644 --- a/src/network/network_tests.rs +++ b/src/network/network_tests.rs @@ -409,7 +409,7 @@ mod tests { #[tokio::test] #[should_panic(expected = "Servarr config is undefined")] #[rstest] - async fn test_request_props_from_requires_radarr_config_to_be_present_for_all_network_events( + async fn test_request_props_from_requires_config_to_be_present_for_all_network_events( #[values(RadarrEvent::HealthCheck, SonarrEvent::HealthCheck)] network_event: impl Into + NetworkResource, ) { @@ -492,6 +492,82 @@ mod tests { assert!(request_props.custom_headers.is_empty()); } + #[rstest] + #[tokio::test] + async fn test_request_props_from_custom_config_ssl_doesnt_affect_ssl_cert_path( + #[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into + + NetworkResource, + #[values(Some(true), Some(false), None)] ssl_option: Option, + ) { + let api_token = "testToken1234".to_owned(); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let resource = network_event.resource(); + let servarr_config = ServarrConfig { + host: Some("192.168.0.123".to_owned()), + port: Some(8080), + api_token: Some(api_token.clone()), + ssl_cert_path: Some("/test/cert.crt".to_owned()), + ssl: ssl_option, + ..ServarrConfig::default() + }; + { + let mut app = app_arc.lock().await; + app.server_tabs.tabs[0].config = Some(servarr_config.clone()); + app.server_tabs.tabs[1].config = Some(servarr_config); + } + let network = test_network(&app_arc); + + let request_props = network + .request_props_from(network_event, RequestMethod::Get, None::<()>, None, None) + .await; + + assert_str_eq!( + request_props.uri, + format!("https://192.168.0.123:8080/api/v3{resource}") + ); + assert_eq!(request_props.method, RequestMethod::Get); + assert_eq!(request_props.body, None); + assert_str_eq!(request_props.api_token, api_token); + assert!(request_props.custom_headers.is_empty()); + } + + #[rstest] + #[tokio::test] + async fn test_request_props_uses_ssl_property( + #[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into + + NetworkResource, + ) { + let api_token = "testToken1234".to_owned(); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let resource = network_event.resource(); + let servarr_config = ServarrConfig { + host: Some("192.168.0.123".to_owned()), + port: Some(8080), + api_token: Some(api_token.clone()), + ssl: Some(true), + ..ServarrConfig::default() + }; + { + let mut app = app_arc.lock().await; + app.server_tabs.tabs[0].config = Some(servarr_config.clone()); + app.server_tabs.tabs[1].config = Some(servarr_config); + } + let network = test_network(&app_arc); + + let request_props = network + .request_props_from(network_event, RequestMethod::Get, None::<()>, None, None) + .await; + + assert_str_eq!( + request_props.uri, + format!("https://192.168.0.123:8080/api/v3{resource}") + ); + assert_eq!(request_props.method, RequestMethod::Get); + assert_eq!(request_props.body, None); + assert_str_eq!(request_props.api_token, api_token); + assert!(request_props.custom_headers.is_empty()); + } + #[rstest] #[tokio::test] async fn test_request_props_from_custom_config_custom_headers(