Compare commits

..

2 Commits

14 changed files with 211 additions and 11 deletions
@@ -0,0 +1,11 @@
### AI assistance (if any):
- List tools here and files touched by them
### Authorship & Understanding
- [ ] I wrote or heavily modified this code myself
- [ ] I understand how it works end-to-end
- [ ] I can maintain this code in the future
- [ ] No undisclosed AI-generated code was used
- [ ] If AI assistance was used, it is documented below
+7
View File
@@ -91,5 +91,12 @@ Then, you can run workflows locally without having to commit and see if the GitH
act -W .github/workflows/release.yml --input_type bump=minor
```
## Authorship Policy
All code in this repository is written and reviewed by humans. AI-generated code (e.g., Copilot, ChatGPT,
Claude, etc.) is not permitted unless explicitly disclosed and approved.
Submissions must certify that the contributor understands and can maintain the code they submit.
## Questions? Reach out to me!
If you encounter any questions while developing Managarr, please don't hesitate to reach out to me at alex.j.tusa@gmail.com. I'm happy to help contributors, new and experienced in any way I can!
+7 -1
View File
@@ -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
+74 -1
View File
@@ -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),
};
+26
View File
@@ -431,6 +431,8 @@ pub struct ServarrConfig {
pub api_token: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub api_token_file: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_env_var_bool")]
pub ssl: Option<bool>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub ssl_cert_path: Option<String>,
#[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<Option<bool>, 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<Option<HeaderMap>, D::Error>
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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};
+1 -1
View File
@@ -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};
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{Subcommand, command};
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
+1 -1
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{Subcommand, command};
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
+2 -1
View File
@@ -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"
+77 -1
View File
@@ -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<NetworkEvent>
+ 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<NetworkEvent>
+ NetworkResource,
#[values(Some(true), Some(false), None)] ssl_option: Option<bool>,
) {
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<NetworkEvent>
+ 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(