diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index 82af22a..102a064 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -5,9 +5,10 @@ mod tests { use tokio::sync::mpsc; use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES}; - use crate::app::{App, Data, RadarrConfig, DEFAULT_ROUTE}; + use crate::app::{App, Data, ServarrConfig, DEFAULT_ROUTE}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; - use crate::models::{HorizontallyScrollableText, Route, TabRoute}; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; + use crate::models::{HorizontallyScrollableText, TabRoute}; use crate::network::radarr_network::RadarrEvent; use crate::network::NetworkEvent; @@ -34,7 +35,7 @@ mod tests { }, TabRoute { title: "Sonarr", - route: Route::Sonarr, + route: ActiveSonarrBlock::Series.into(), help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)), contextual_help: None, }, @@ -126,6 +127,10 @@ mod tests { version: "test".to_owned(), ..RadarrData::default() }, + sonarr_data: SonarrData { + version: "test".to_owned(), + ..SonarrData::default() + }, }, ..App::default() }; @@ -135,6 +140,7 @@ mod tests { assert_eq!(app.tick_count, 0); assert_eq!(app.error, HorizontallyScrollableText::default()); assert!(app.data.radarr_data.version.is_empty()); + assert!(app.data.sonarr_data.version.is_empty()); } #[test] @@ -248,11 +254,11 @@ mod tests { } #[test] - fn test_radarr_config_default() { - let radarr_config = RadarrConfig::default(); + fn test_servarr_config_default() { + let radarr_config = ServarrConfig::default(); assert_eq!(radarr_config.host, Some("localhost".to_string())); - assert_eq!(radarr_config.port, Some(7878)); + assert_eq!(radarr_config.port, None); assert_eq!(radarr_config.uri, None); assert!(radarr_config.api_token.is_empty()); assert_eq!(radarr_config.ssl_cert_path, None); diff --git a/src/app/mod.rs b/src/app/mod.rs index 87faa8e..764280a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -9,6 +9,7 @@ use tokio_util::sync::CancellationToken; use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState}; use crate::network::NetworkEvent; @@ -151,7 +152,7 @@ impl<'a> Default for App<'a> { }, TabRoute { title: "Sonarr", - route: Route::Sonarr, + route: ActiveSonarrBlock::Series.into(), help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)), contextual_help: None, }, @@ -172,25 +173,24 @@ impl<'a> Default for App<'a> { #[derive(Default)] pub struct Data<'a> { pub radarr_data: RadarrData<'a>, -} - -pub trait ServarrConfig { - fn validate(&self); + pub sonarr_data: SonarrData, } #[derive(Debug, Deserialize, Serialize, Default)] pub struct AppConfig { - pub radarr: RadarrConfig, + pub radarr: ServarrConfig, + pub sonarr: ServarrConfig, } -impl ServarrConfig for AppConfig { - fn validate(&self) { +impl AppConfig { + pub fn validate(&self) { self.radarr.validate(); } } #[derive(Debug, Deserialize, Serialize)] -pub struct RadarrConfig { +#[cfg_attr(test, derive(Clone))] +pub struct ServarrConfig { pub host: Option, pub port: Option, pub uri: Option, @@ -198,20 +198,20 @@ pub struct RadarrConfig { pub ssl_cert_path: Option, } -impl ServarrConfig for RadarrConfig { +impl ServarrConfig { fn validate(&self) { if self.host.is_none() && self.uri.is_none() { - log_and_print_error("'host' or 'uri' is required for Radarr configuration".to_owned()); + log_and_print_error("'host' or 'uri' is required for configuration".to_owned()); process::exit(1); } } } -impl Default for RadarrConfig { +impl Default for ServarrConfig { fn default() -> Self { - RadarrConfig { + ServarrConfig { host: Some("localhost".to_string()), - port: Some(7878), + port: None, uri: None, api_token: "".to_string(), ssl_cert_path: None, diff --git a/src/cli/cli_tests.rs b/src/cli/cli_tests.rs index c080b00..d94e97c 100644 --- a/src/cli/cli_tests.rs +++ b/src/cli/cli_tests.rs @@ -20,9 +20,9 @@ mod tests { }; use pretty_assertions::assert_eq; - #[test] - fn test_radarr_subcommand_requires_subcommand() { - let result = Cli::command().try_get_matches_from(["managarr", "radarr"]); + #[rstest] + fn test_servarr_subcommand_requires_subcommand(#[values("radarr", "sonarr")] subcommand: &str) { + let result = Cli::command().try_get_matches_from(["managarr", subcommand]); assert!(result.is_err()); assert_eq!( @@ -39,6 +39,13 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn test_sonarr_subcommand_delegates_to_sonarr() { + let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "series"]); + + assert!(result.is_ok()); + } + #[test] fn test_completions_requires_argument() { let result = Cli::command().try_get_matches_from(["managarr", "completions"]); @@ -121,10 +128,41 @@ mod tests { ))) }); let app_arc = Arc::new(Mutex::new(App::default())); - let claer_blocklist_command = RadarrCommand::ClearBlocklist.into(); + let clear_blocklist_command = RadarrCommand::ClearBlocklist.into(); - let result = handle_command(&app_arc, claer_blocklist_command, &mut mock_network).await; + let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await; assert!(result.is_ok()); } + + // TODO: Uncomment to properly test delegation once the ClearBlocklist command is added to Sonarr + // #[tokio::test] + // async fn test_cli_handler_delegates_sonarr_commands_to_the_sonarr_cli_handler() { + // let mut mock_network = MockNetworkTrait::new(); + // mock_network + // .expect_handle_network_event() + // .with(eq::(SonarrEvent::GetBlocklist.into())) + // .times(1) + // .returning(|_| { + // Ok(Serdeable::Sonarr(SonarrSerdeable::BlocklistResponse( + // BlocklistResponse { + // records: vec![BlocklistItem::default()], + // }, + // ))) + // }); + // mock_network + // .expect_handle_network_event() + // .with(eq::(SonarrEvent::ClearBlocklist.into())) + // .times(1) + // .returning(|_| { + // Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + // json!({"testResponse": "response"}), + // ))) + // }); + // let clear_blocklist_command = SonarrCommand::ClearBlocklist.into(); + + // let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await; + + // assert!(result.is_ok()); + // } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 66c6bb2..1178276 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -4,11 +4,13 @@ use anyhow::Result; use clap::{command, Subcommand}; use clap_complete::Shell; use radarr::{RadarrCliHandler, RadarrCommand}; +use sonarr::{SonarrCliHandler, SonarrCommand}; use tokio::sync::Mutex; use crate::{app::App, network::NetworkTrait}; pub mod radarr; +pub mod sonarr; #[cfg(test)] #[path = "cli_tests.rs"] @@ -19,6 +21,9 @@ pub enum Command { #[command(subcommand, about = "Commands for manging your Radarr instance")] Radarr(RadarrCommand), + #[command(subcommand, about = "Commands for manging your Sonarr instance")] + Sonarr(SonarrCommand), + #[command( arg_required_else_help = true, about = "Generate shell completions for the Managarr CLI" @@ -45,11 +50,20 @@ pub(crate) async fn handle_command( command: Command, network: &mut dyn NetworkTrait, ) -> Result<()> { - if let Command::Radarr(radarr_command) = command { - RadarrCliHandler::with(app, radarr_command, network) - .handle() - .await? + match command { + Command::Radarr(radarr_command) => { + RadarrCliHandler::with(app, radarr_command, network) + .handle() + .await? + } + Command::Sonarr(sonarr_command) => { + SonarrCliHandler::with(app, sonarr_command, network) + .handle() + .await? + } + _ => (), } + Ok(()) } diff --git a/src/cli/radarr/get_command_handler_tests.rs b/src/cli/radarr/get_command_handler_tests.rs index 990e185..0bc629f 100644 --- a/src/cli/radarr/get_command_handler_tests.rs +++ b/src/cli/radarr/get_command_handler_tests.rs @@ -1,5 +1,5 @@ #[cfg(test)] -mod test { +mod tests { use clap::error::ErrorKind; use clap::CommandFactory; diff --git a/src/cli/radarr/list_command_handler_tests.rs b/src/cli/radarr/list_command_handler_tests.rs index 4c7446a..2252d03 100644 --- a/src/cli/radarr/list_command_handler_tests.rs +++ b/src/cli/radarr/list_command_handler_tests.rs @@ -130,7 +130,7 @@ mod tests { #[case(RadarrListCommand::Tasks, RadarrEvent::GetTasks)] #[case(RadarrListCommand::Updates, RadarrEvent::GetUpdates)] #[tokio::test] - async fn test_handle_list_blocklist_command( + async fn test_handle_list_command( #[case] list_command: RadarrListCommand, #[case] expected_radarr_event: RadarrEvent, ) { diff --git a/src/cli/sonarr/get_command_handler.rs b/src/cli/sonarr/get_command_handler.rs new file mode 100644 index 0000000..1e361b9 --- /dev/null +++ b/src/cli/sonarr/get_command_handler.rs @@ -0,0 +1,60 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::Subcommand; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + execute_network_event, + network::{sonarr_network::SonarrEvent, NetworkTrait}, +}; + +use super::SonarrCommand; + +#[cfg(test)] +#[path = "get_command_handler_tests.rs"] +mod get_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum SonarrGetCommand { + #[command(about = "Get the system status")] + SystemStatus, +} + +impl From for Command { + fn from(value: SonarrGetCommand) -> Self { + Command::Sonarr(SonarrCommand::Get(value)) + } +} + +pub(super) struct SonarrGetCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: SonarrGetCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrGetCommand> for SonarrGetCommandHandler<'a, 'b> { + fn with( + _app: &'a Arc>>, + command: SonarrGetCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + SonarrGetCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result<()> { + match self.command { + SonarrGetCommand::SystemStatus => { + execute_network_event!(self, SonarrEvent::GetStatus); + } + } + + Ok(()) + } +} diff --git a/src/cli/sonarr/get_command_handler_tests.rs b/src/cli/sonarr/get_command_handler_tests.rs new file mode 100644 index 0000000..3bddd8d --- /dev/null +++ b/src/cli/sonarr/get_command_handler_tests.rs @@ -0,0 +1,71 @@ +#[cfg(test)] +mod tests { + use crate::cli::{ + sonarr::{get_command_handler::SonarrGetCommand, SonarrCommand}, + Command, + }; + use crate::Cli; + use clap::CommandFactory; + + #[test] + fn test_sonarr_get_command_from() { + let command = SonarrGetCommand::SystemStatus; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Sonarr(SonarrCommand::Get(command))); + } + + mod cli { + use super::*; + + #[test] + fn test_system_status_has_no_arg_requirements() { + let result = + Cli::command().try_get_matches_from(["managarr", "radarr", "get", "system-status"]); + + assert!(result.is_ok()); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::{ + app::App, + cli::{ + sonarr::get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler}, + CliCommandHandler, + }, + models::{sonarr_models::SonarrSerdeable, Serdeable}, + network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent}, + }; + + #[tokio::test] + async fn test_handle_get_system_status_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(SonarrEvent::GetStatus.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let get_system_status_command = SonarrGetCommand::SystemStatus; + + let result = + SonarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + } +} diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs new file mode 100644 index 0000000..6ca0d51 --- /dev/null +++ b/src/cli/sonarr/list_command_handler.rs @@ -0,0 +1,60 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::Subcommand; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + execute_network_event, + network::{sonarr_network::SonarrEvent, NetworkTrait}, +}; + +use super::SonarrCommand; + +#[cfg(test)] +#[path = "list_command_handler_tests.rs"] +mod list_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum SonarrListCommand { + #[command(about = "List all series in your Sonarr library")] + Series, +} + +impl From for Command { + fn from(value: SonarrListCommand) -> Self { + Command::Sonarr(SonarrCommand::List(value)) + } +} + +pub(super) struct SonarrListCommandHandler<'a, 'b> { + app: &'a Arc>>, + command: SonarrListCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandHandler<'a, 'b> { + fn with( + app: &'a Arc>>, + command: SonarrListCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + SonarrListCommandHandler { + app, + command, + network, + } + } + + async fn handle(self) -> Result<()> { + match self.command { + SonarrListCommand::Series => { + execute_network_event!(self, SonarrEvent::ListSeries); + } + } + + Ok(()) + } +} diff --git a/src/cli/sonarr/list_command_handler_tests.rs b/src/cli/sonarr/list_command_handler_tests.rs new file mode 100644 index 0000000..52d6819 --- /dev/null +++ b/src/cli/sonarr/list_command_handler_tests.rs @@ -0,0 +1,77 @@ +#[cfg(test)] +mod tests { + use crate::cli::{ + sonarr::{list_command_handler::SonarrListCommand, SonarrCommand}, + Command, + }; + use crate::Cli; + use clap::CommandFactory; + + #[test] + fn test_sonarr_list_command_from() { + let command = SonarrListCommand::Series; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Sonarr(SonarrCommand::List(command))); + } + + mod cli { + use super::*; + use rstest::rstest; + + #[rstest] + fn test_list_commands_have_no_arg_requirements(#[values("series")] subcommand: &str) { + let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", subcommand]); + + assert!(result.is_ok()); + } + } + + mod handler { + + use std::sync::Arc; + + use mockall::predicate::eq; + use rstest::rstest; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::cli::sonarr::list_command_handler::SonarrListCommand; + use crate::cli::CliCommandHandler; + use crate::network::sonarr_network::SonarrEvent; + use crate::{ + app::App, + models::{radarr_models::RadarrSerdeable, Serdeable}, + network::{MockNetworkTrait, NetworkEvent}, + }; + + #[rstest] + #[case(SonarrListCommand::Series, SonarrEvent::ListSeries)] + #[tokio::test] + async fn test_handle_list_command( + #[case] list_command: SonarrListCommand, + #[case] expected_sonarr_event: SonarrEvent, + ) { + use crate::cli::sonarr::list_command_handler::SonarrListCommandHandler; + + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(expected_sonarr_event.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + + let result = SonarrListCommandHandler::with(&app_arc, list_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + } +} diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs new file mode 100644 index 0000000..6c3b9e0 --- /dev/null +++ b/src/cli/sonarr/mod.rs @@ -0,0 +1,75 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::Subcommand; +use get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler}; +use list_command_handler::{SonarrListCommand, SonarrListCommandHandler}; +use tokio::sync::Mutex; + +use crate::{app::App, network::NetworkTrait}; + +use super::{CliCommandHandler, Command}; + +mod get_command_handler; +mod list_command_handler; + +#[cfg(test)] +#[path = "sonarr_command_tests.rs"] +mod sonarr_command_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum SonarrCommand { + #[command( + subcommand, + about = "Commands to fetch details of the resources in your Sonarr instance" + )] + Get(SonarrGetCommand), + #[command( + subcommand, + about = "Commands to list attributes from your Sonarr instance" + )] + List(SonarrListCommand), +} + +impl From for Command { + fn from(sonarr_command: SonarrCommand) -> Command { + Command::Sonarr(sonarr_command) + } +} + +pub(super) struct SonarrCliHandler<'a, 'b> { + app: &'a Arc>>, + command: SonarrCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, 'b> { + fn with( + app: &'a Arc>>, + command: SonarrCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + SonarrCliHandler { + app, + command, + network, + } + } + + async fn handle(self) -> Result<()> { + match self.command { + SonarrCommand::Get(get_command) => { + SonarrGetCommandHandler::with(self.app, get_command, self.network) + .handle() + .await? + } + SonarrCommand::List(list_command) => { + SonarrListCommandHandler::with(self.app, list_command, self.network) + .handle() + .await? + } + } + + Ok(()) + } +} diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs new file mode 100644 index 0000000..a1840be --- /dev/null +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -0,0 +1,86 @@ +#[cfg(test)] +mod tests { + use crate::cli::{ + sonarr::{list_command_handler::SonarrListCommand, SonarrCommand}, + Command, + }; + + #[test] + fn test_sonarr_command_from() { + let command = SonarrCommand::List(SonarrListCommand::Series); + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Sonarr(command)); + } + + mod cli {} + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::{ + app::App, + cli::{ + sonarr::{ + get_command_handler::SonarrGetCommand, list_command_handler::SonarrListCommand, + SonarrCliHandler, SonarrCommand, + }, + CliCommandHandler, + }, + models::{ + sonarr_models::{Series, SonarrSerdeable}, + Serdeable, + }, + network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent}, + }; + + #[tokio::test] + async fn test_sonarr_cli_handler_delegates_get_commands_to_the_get_command_handler() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(SonarrEvent::GetStatus.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let get_system_status_command = SonarrCommand::Get(SonarrGetCommand::SystemStatus); + + let result = SonarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_sonarr_cli_handler_delegates_list_commands_to_the_list_command_handler() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(SonarrEvent::ListSeries.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::SeriesVec(vec![ + Series::default(), + ]))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let list_series_command = SonarrCommand::List(SonarrListCommand::Series); + + let result = SonarrCliHandler::with(&app_arc, list_series_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + } +} diff --git a/src/main.rs b/src/main.rs index 388286b..4123f5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use std::{io, panic, process}; use anyhow::anyhow; use anyhow::Result; -use app::{log_and_print_error, AppConfig, ServarrConfig}; +use app::{log_and_print_error, AppConfig}; use clap::{ command, crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser, }; @@ -112,7 +112,7 @@ async fn main() -> Result<()> { match args.command { Some(command) => match command { - Command::Radarr(_) => { + Command::Radarr(_) | Command::Sonarr(_) => { let app_nw = Arc::clone(&app); let mut network = Network::new(&app_nw, cancellation_token, reqwest_client); diff --git a/src/models/mod.rs b/src/models/mod.rs index 7193066..0845624 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -6,8 +6,11 @@ use radarr_models::RadarrSerdeable; use regex::Regex; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Number; +use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; +use sonarr_models::SonarrSerdeable; pub mod radarr_models; pub mod servarr_data; +pub mod sonarr_models; pub mod stateful_list; pub mod stateful_table; @@ -20,7 +23,7 @@ mod model_tests; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Route { Radarr(ActiveRadarrBlock, Option), - Sonarr, + Sonarr(ActiveSonarrBlock, Option), Readarr, Lidarr, Whisparr, @@ -33,6 +36,7 @@ pub enum Route { #[serde(untagged)] pub enum Serdeable { Radarr(RadarrSerdeable), + Sonarr(SonarrSerdeable), } pub trait Scrollable { @@ -359,6 +363,16 @@ where ))) } +pub fn from_f64<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let num: Number = Deserialize::deserialize(deserializer)?; + num.as_f64().ok_or(de::Error::custom(format!( + "Unable to convert Number to f64: {num:?}" + ))) +} + pub fn strip_non_search_characters(input: &str) -> String { Regex::new(r"[^a-zA-Z0-9.,/'\-:\s]") .unwrap() diff --git a/src/models/model_tests.rs b/src/models/model_tests.rs index 7cd2696..58880fc 100644 --- a/src/models/model_tests.rs +++ b/src/models/model_tests.rs @@ -10,6 +10,7 @@ mod tests { use serde::de::IntoDeserializer; use serde_json::to_string; + use crate::models::from_f64; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::{from_i64, strip_non_search_characters}; use crate::models::{ @@ -649,6 +650,13 @@ mod tests { ); } + #[test] + fn test_from_f64() { + let deserializer: F64Deserializer = 1f64.into_deserializer(); + + assert_eq!(from_f64(deserializer), Ok(1.0)); + } + #[test] fn test_horizontally_scrollable_serialize() { let text = HorizontallyScrollableText::from("Test"); diff --git a/src/models/servarr_data/mod.rs b/src/models/servarr_data/mod.rs index c430c1a..a4ec084 100644 --- a/src/models/servarr_data/mod.rs +++ b/src/models/servarr_data/mod.rs @@ -1 +1,2 @@ pub mod radarr; +pub mod sonarr; diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index a487156..1c42d9f 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -19,6 +19,14 @@ mod tests { use crate::assert_movie_info_tabs_reset; use crate::models::BlockSelectionState; + #[test] + fn test_from_active_radarr_block_to_route() { + assert_eq!( + Route::from(ActiveRadarrBlock::AddMoviePrompt), + Route::Radarr(ActiveRadarrBlock::AddMoviePrompt, None) + ); + } + #[test] fn test_from_tuple_to_route_with_context() { assert_eq!( @@ -60,7 +68,7 @@ mod tests { assert_eq!(radarr_data.disk_space_vec, Vec::new()); assert!(radarr_data.version.is_empty()); assert_eq!(radarr_data.start_time, >::default()); - assert!(radarr_data.movies.items.is_empty()); + assert!(radarr_data.movies.is_empty()); assert_eq!(radarr_data.selected_block, BlockSelectionState::default()); assert!(radarr_data.downloads.items.is_empty()); assert!(radarr_data.indexers.items.is_empty()); diff --git a/src/models/servarr_data/sonarr/mod.rs b/src/models/servarr_data/sonarr/mod.rs new file mode 100644 index 0000000..49bfe8e --- /dev/null +++ b/src/models/servarr_data/sonarr/mod.rs @@ -0,0 +1 @@ +pub mod sonarr_data; diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs new file mode 100644 index 0000000..d43f669 --- /dev/null +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -0,0 +1,43 @@ +use chrono::{DateTime, Utc}; +use strum::EnumIter; + +use crate::models::{sonarr_models::Series, stateful_table::StatefulTable, Route}; + +#[cfg(test)] +#[path = "sonarr_data_tests.rs"] +mod sonarr_data_tests; + +pub struct SonarrData { + pub version: String, + pub start_time: DateTime, + pub series: StatefulTable, +} + +impl Default for SonarrData { + fn default() -> SonarrData { + SonarrData { + version: String::new(), + start_time: DateTime::default(), + series: StatefulTable::default(), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)] +pub enum ActiveSonarrBlock { + #[default] + Series, + SeriesSortPrompt, +} + +impl From for Route { + fn from(active_sonarr_block: ActiveSonarrBlock) -> Route { + Route::Sonarr(active_sonarr_block, None) + } +} + +impl From<(ActiveSonarrBlock, Option)> for Route { + fn from(value: (ActiveSonarrBlock, Option)) -> Route { + Route::Sonarr(value.0, value.1) + } +} diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs new file mode 100644 index 0000000..cc29b79 --- /dev/null +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -0,0 +1,42 @@ +#[cfg(test)] +mod tests { + mod sonarr_data_tests { + use chrono::{DateTime, Utc}; + + use crate::models::{ + servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}, + Route, + }; + + #[test] + fn test_from_active_sonarr_block_to_route() { + assert_eq!( + Route::from(ActiveSonarrBlock::SeriesSortPrompt), + Route::Sonarr(ActiveSonarrBlock::SeriesSortPrompt, None) + ); + } + + #[test] + fn test_from_tuple_to_route_with_context() { + assert_eq!( + Route::from(( + ActiveSonarrBlock::SeriesSortPrompt, + Some(ActiveSonarrBlock::Series) + )), + Route::Sonarr( + ActiveSonarrBlock::SeriesSortPrompt, + Some(ActiveSonarrBlock::Series), + ) + ); + } + + #[test] + fn test_sonarr_data_defaults() { + let sonarr_data = SonarrData::default(); + + assert!(sonarr_data.version.is_empty()); + assert_eq!(sonarr_data.start_time, >::default()); + assert!(sonarr_data.series.is_empty()); + } + } +} diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs new file mode 100644 index 0000000..3b878eb --- /dev/null +++ b/src/models/sonarr_models.rs @@ -0,0 +1,207 @@ +use std::fmt::{Display, Formatter}; + +use chrono::{DateTime, Utc}; +use clap::ValueEnum; +use derivative::Derivative; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Number, Value}; +use strum::EnumIter; + +use crate::serde_enum_from; + +use super::{HorizontallyScrollableText, Serdeable}; + +#[cfg(test)] +#[path = "sonarr_models_tests.rs"] +mod sonarr_models_tests; + +#[derive(Derivative, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derivative(Default)] +pub struct Rating { + #[serde(deserialize_with = "super::from_i64")] + pub votes: i64, + #[serde(deserialize_with = "super::from_f64")] + pub value: f64, +} + +impl Eq for Rating {} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Season { + #[serde(deserialize_with = "super::from_i64")] + pub season_number: i64, + pub monitored: bool, + pub statistics: SeasonStatistics, +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SeasonStatistics { + pub next_airing: Option>, + pub previous_airing: Option>, + #[serde(deserialize_with = "super::from_i64")] + pub episode_file_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub episode_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub total_episode_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub size_on_disk: i64, + #[serde(deserialize_with = "super::from_f64")] + pub percent_of_episodes: f64, +} + +impl Eq for SeasonStatistics {} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Series { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub tvdb_id: i64, + pub title: HorizontallyScrollableText, + #[serde(deserialize_with = "super::from_i64")] + pub quality_profile_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub language_profile_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub runtime: i64, + #[serde(deserialize_with = "super::from_i64")] + pub year: i64, + pub monitored: bool, + pub series_type: SeriesType, + pub path: String, + pub genres: Vec, + pub tags: Vec, + pub ratings: Rating, + pub ended: bool, + pub status: SeriesStatus, + pub overview: String, + pub network: Option, + pub season_folder: bool, + pub certification: Option, + pub statistics: Option, + pub seasons: Option>, +} + +#[derive( + Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum, +)] +#[serde(rename_all = "camelCase")] +pub enum SeriesType { + #[default] + Standard, + Daily, + Anime, +} + +impl Display for SeriesType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let series_type = match self { + SeriesType::Standard => "standard", + SeriesType::Daily => "daily", + SeriesType::Anime => "anime", + }; + write!(f, "{series_type}") + } +} + +impl SeriesType { + pub fn to_display_str<'a>(self) -> &'a str { + match self { + SeriesType::Standard => "Standard", + SeriesType::Daily => "Daily", + SeriesType::Anime => "Anime", + } + } +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SeriesStatistics { + #[serde(deserialize_with = "super::from_i64")] + pub season_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub episode_file_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub episode_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub total_episode_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub size_on_disk: i64, + #[serde(deserialize_with = "super::from_f64")] + pub percent_of_episodes: f64, +} + +impl Eq for SeriesStatistics {} + +#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter)] +#[serde(rename_all = "camelCase")] +pub enum SeriesStatus { + #[default] + Continuing, + Ended, + Upcoming, + Deleted, +} + +impl Display for SeriesStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let series_status = match self { + SeriesStatus::Continuing => "continuing", + SeriesStatus::Ended => "ended", + SeriesStatus::Upcoming => "upcoming", + SeriesStatus::Deleted => "deleted", + }; + write!(f, "{series_status}") + } +} + +impl SeriesStatus { + pub fn to_display_str<'a>(self) -> &'a str { + match self { + SeriesStatus::Continuing => "Continuing", + SeriesStatus::Ended => "Ended", + SeriesStatus::Upcoming => "Upcoming", + SeriesStatus::Deleted => "Deleted", + } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[serde(untagged)] +#[allow(clippy::large_enum_variant)] +pub enum SonarrSerdeable { + Value(Value), + SeriesVec(Vec), + SystemStatus(SystemStatus), +} + +impl From for Serdeable { + fn from(value: SonarrSerdeable) -> Serdeable { + Serdeable::Sonarr(value) + } +} + +impl From<()> for SonarrSerdeable { + fn from(_: ()) -> Self { + SonarrSerdeable::Value(json!({})) + } +} + +serde_enum_from!( + SonarrSerdeable { + Value(Value), + SeriesVec(Vec), + SystemStatus(SystemStatus), + } +); + +#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct SystemStatus { + pub version: String, + pub start_time: DateTime, +} diff --git a/src/models/sonarr_models_tests.rs b/src/models/sonarr_models_tests.rs new file mode 100644 index 0000000..15387f6 --- /dev/null +++ b/src/models/sonarr_models_tests.rs @@ -0,0 +1,92 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::json; + + use crate::models::{ + sonarr_models::{Series, SeriesStatus, SeriesType, SonarrSerdeable, SystemStatus}, + Serdeable, + }; + + #[test] + fn test_series_status_display() { + assert_str_eq!(SeriesStatus::Continuing.to_string(), "continuing"); + assert_str_eq!(SeriesStatus::Ended.to_string(), "ended"); + assert_str_eq!(SeriesStatus::Upcoming.to_string(), "upcoming"); + assert_str_eq!(SeriesStatus::Deleted.to_string(), "deleted"); + } + + #[test] + fn test_series_status_to_display_str() { + assert_str_eq!(SeriesStatus::Continuing.to_display_str(), "Continuing"); + assert_str_eq!(SeriesStatus::Ended.to_display_str(), "Ended"); + assert_str_eq!(SeriesStatus::Upcoming.to_display_str(), "Upcoming"); + assert_str_eq!(SeriesStatus::Deleted.to_display_str(), "Deleted"); + } + + #[test] + fn test_series_type_display() { + assert_str_eq!(SeriesType::Standard.to_string(), "standard"); + assert_str_eq!(SeriesType::Daily.to_string(), "daily"); + assert_str_eq!(SeriesType::Anime.to_string(), "anime"); + } + + #[test] + fn test_series_type_to_display_str() { + assert_str_eq!(SeriesType::Standard.to_display_str(), "Standard"); + assert_str_eq!(SeriesType::Daily.to_display_str(), "Daily"); + assert_str_eq!(SeriesType::Anime.to_display_str(), "Anime"); + } + + #[test] + fn test_sonarr_serdeable_from() { + let sonarr_serdeable = SonarrSerdeable::Value(json!({})); + + let serdeable: Serdeable = Serdeable::from(sonarr_serdeable.clone()); + + assert_eq!(serdeable, Serdeable::Sonarr(sonarr_serdeable)); + } + + #[test] + fn test_sonarr_serdeable_from_unit() { + let sonarr_serdeable = SonarrSerdeable::from(()); + + assert_eq!(sonarr_serdeable, SonarrSerdeable::Value(json!({}))); + } + + #[test] + fn test_sonarr_serdeable_from_value() { + let value = json!({"test": "test"}); + + let sonarr_serdeable: SonarrSerdeable = value.clone().into(); + + assert_eq!(sonarr_serdeable, SonarrSerdeable::Value(value)); + } + + #[test] + fn test_sonarr_serdeable_from_series() { + let series = vec![Series { + id: 1, + ..Series::default() + }]; + + let sonarr_serdeable: SonarrSerdeable = series.clone().into(); + + assert_eq!(sonarr_serdeable, SonarrSerdeable::SeriesVec(series)); + } + + #[test] + fn test_sonarr_serdeable_from_system_status() { + let system_status = SystemStatus { + version: "1".to_owned(), + ..SystemStatus::default() + }; + + let sonarr_serdeable: SonarrSerdeable = system_status.clone().into(); + + assert_eq!( + sonarr_serdeable, + SonarrSerdeable::SystemStatus(system_status) + ); + } +} diff --git a/src/network/mod.rs b/src/network/mod.rs index e3c252e..0967a46 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -8,35 +8,42 @@ use regex::Regex; use reqwest::{Client, RequestBuilder}; use serde::de::DeserializeOwned; use serde::Serialize; +use sonarr_network::SonarrEvent; use strum_macros::Display; use tokio::select; use tokio::sync::{Mutex, MutexGuard}; use tokio_util::sync::CancellationToken; -use crate::app::App; +use crate::app::{App, ServarrConfig}; use crate::models::Serdeable; use crate::network::radarr_network::RadarrEvent; #[cfg(test)] use mockall::automock; pub mod radarr_network; +pub mod sonarr_network; mod utils; #[cfg(test)] #[path = "network_tests.rs"] mod network_tests; -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum NetworkEvent { - Radarr(RadarrEvent), -} - #[cfg_attr(test, automock)] #[async_trait] pub trait NetworkTrait { async fn handle_network_event(&mut self, network_event: NetworkEvent) -> Result; } +pub trait NetworkResource { + fn resource(&self) -> &'static str; +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum NetworkEvent { + Radarr(RadarrEvent), + Sonarr(SonarrEvent), +} + #[derive(Clone)] pub struct Network<'a, 'b> { client: Client, @@ -52,6 +59,10 @@ impl<'a, 'b> NetworkTrait for Network<'a, 'b> { .handle_radarr_event(radarr_event) .await .map(Serdeable::from), + NetworkEvent::Sonarr(sonarr_event) => self + .handle_sonarr_event(sonarr_event) + .await + .map(Serdeable::from), }; let mut app = self.app.lock().await; @@ -180,6 +191,65 @@ impl<'a, 'b> Network<'a, 'b> { .header("X-Api-Key", api_token), } } + + async fn request_props_from( + &self, + network_event: N, + method: RequestMethod, + body: Option, + path: Option, + query_params: Option, + ) -> RequestProps + where + T: Serialize + Debug, + N: Into + NetworkResource, + { + let app = self.app.lock().await; + let resource = network_event.resource(); + let ( + ServarrConfig { + host, + port, + uri, + api_token, + ssl_cert_path, + }, + default_port, + ) = match network_event.into() { + NetworkEvent::Radarr(_) => (&app.config.radarr, 7878), + NetworkEvent::Sonarr(_) => (&app.config.sonarr, 8989), + }; + let mut uri = if let Some(servarr_uri) = uri { + format!("{servarr_uri}/api/v3{resource}") + } else { + let protocol = if ssl_cert_path.is_some() { + "https" + } else { + "http" + }; + let host = host.as_ref().unwrap(); + format!( + "{protocol}://{host}:{}/api/v3{resource}", + port.unwrap_or(default_port) + ) + }; + + if let Some(path) = path { + uri = format!("{uri}{path}"); + } + + if let Some(params) = query_params { + uri = format!("{uri}?{params}"); + } + + RequestProps { + uri, + method, + body, + api_token: api_token.to_owned(), + ignore_status_code: false, + } + } } #[derive(Clone, Copy, Debug, Display, PartialEq, Eq)] diff --git a/src/network/network_tests.rs b/src/network/network_tests.rs index 5528a0f..109ac71 100644 --- a/src/network/network_tests.rs +++ b/src/network/network_tests.rs @@ -12,9 +12,11 @@ mod tests { use tokio::sync::{mpsc, Mutex}; use tokio_util::sync::CancellationToken; - use crate::app::{App, AppConfig, RadarrConfig}; + use crate::app::{App, AppConfig, ServarrConfig}; use crate::models::HorizontallyScrollableText; use crate::network::radarr_network::RadarrEvent; + use crate::network::sonarr_network::SonarrEvent; + use crate::network::NetworkResource; use crate::network::{Network, NetworkEvent, NetworkTrait, RequestMethod, RequestProps}; #[tokio::test] @@ -34,12 +36,12 @@ mod tests { ); let mut app = App::default(); app.is_loading = true; - let radarr_config = RadarrConfig { + let radarr_config = ServarrConfig { host, api_token: String::new(), port, ssl_cert_path: None, - ..RadarrConfig::default() + ..ServarrConfig::default() }; app.config.radarr = radarr_config; let app_arc = Arc::new(Mutex::new(app)); @@ -395,6 +397,214 @@ mod tests { async_server.assert_async().await; } + #[rstest] + #[case(RadarrEvent::GetMovies, 7878)] + #[case(SonarrEvent::ListSeries, 8989)] + #[tokio::test] + async fn test_request_props_from_default_config( + #[case] network_event: impl Into + NetworkResource, + #[case] default_port: u16, + ) { + let app_arc = Arc::new(Mutex::new(App::default())); + let network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + let resource = network_event.resource(); + + let request_props = network + .request_props_from(network_event, RequestMethod::Get, None::<()>, None, None) + .await; + + assert_str_eq!( + request_props.uri, + format!("http://localhost:{default_port}/api/v3{resource}") + ); + assert_eq!(request_props.method, RequestMethod::Get); + assert_eq!(request_props.body, None); + assert!(request_props.api_token.is_empty()); + } + + #[rstest] + #[tokio::test] + async fn test_request_props_from_custom_config( + #[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into + + NetworkResource, + ) { + let api_token = "testToken1234".to_owned(); + let app_arc = Arc::new(Mutex::new(App::default())); + let resource = network_event.resource(); + let servarr_config = ServarrConfig { + host: Some("192.168.0.123".to_owned()), + port: Some(8080), + api_token: api_token.clone(), + ssl_cert_path: Some("/test/cert.crt".to_owned()), + ..ServarrConfig::default() + }; + { + let mut app = app_arc.lock().await; + app.config.radarr = servarr_config.clone(); + app.config.sonarr = servarr_config; + } + let network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + 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); + } + + #[rstest] + #[tokio::test] + async fn test_request_props_from_custom_config_using_uri_instead_of_host_and_port( + #[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into + + NetworkResource, + ) { + let api_token = "testToken1234".to_owned(); + let app_arc = Arc::new(Mutex::new(App::default())); + let resource = network_event.resource(); + let servarr_config = ServarrConfig { + uri: Some("https://192.168.0.123:8080".to_owned()), + api_token: api_token.clone(), + ..ServarrConfig::default() + }; + { + let mut app = app_arc.lock().await; + app.config.radarr = servarr_config.clone(); + app.config.sonarr = servarr_config; + } + let network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + 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); + } + + #[rstest] + #[case(RadarrEvent::GetMovies, 7878)] + #[case(SonarrEvent::ListSeries, 8989)] + #[tokio::test] + async fn test_request_props_from_default_config_with_path_and_query_params( + #[case] network_event: impl Into + NetworkResource, + #[case] default_port: u16, + ) { + let app_arc = Arc::new(Mutex::new(App::default())); + let network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + let resource = network_event.resource(); + + let request_props = network + .request_props_from( + network_event, + RequestMethod::Get, + None::<()>, + Some("/test".to_owned()), + Some("id=1".to_owned()), + ) + .await; + + assert_str_eq!( + request_props.uri, + format!("http://localhost:{default_port}/api/v3{resource}/test?id=1") + ); + assert_eq!(request_props.method, RequestMethod::Get); + assert_eq!(request_props.body, None); + assert!(request_props.api_token.is_empty()); + } + + #[rstest] + #[tokio::test] + async fn test_request_props_from_custom_config_with_path_and_query_params( + #[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into + + NetworkResource, + ) { + let api_token = "testToken1234".to_owned(); + let app_arc = Arc::new(Mutex::new(App::default())); + let resource = network_event.resource(); + let servarr_config = ServarrConfig { + host: Some("192.168.0.123".to_owned()), + port: Some(8080), + api_token: api_token.clone(), + ssl_cert_path: Some("/test/cert.crt".to_owned()), + ..ServarrConfig::default() + }; + { + let mut app = app_arc.lock().await; + app.config.radarr = servarr_config.clone(); + app.config.sonarr = servarr_config; + } + let network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + let request_props = network + .request_props_from( + network_event, + RequestMethod::Get, + None::<()>, + Some("/test".to_owned()), + Some("id=1".to_owned()), + ) + .await; + + assert_str_eq!( + request_props.uri, + format!("https://192.168.0.123:8080/api/v3{resource}/test?id=1") + ); + assert_eq!(request_props.method, RequestMethod::Get); + assert_eq!(request_props.body, None); + assert_str_eq!(request_props.api_token, api_token); + } + + #[rstest] + #[tokio::test] + async fn test_request_props_from_custom_config_using_uri_instead_of_host_and_port_with_path_and_query_params( + #[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into + + NetworkResource, + ) { + let api_token = "testToken1234".to_owned(); + let app_arc = Arc::new(Mutex::new(App::default())); + let resource = network_event.resource(); + let servarr_config = ServarrConfig { + uri: Some("https://192.168.0.123:8080".to_owned()), + api_token: api_token.clone(), + ..ServarrConfig::default() + }; + { + let mut app = app_arc.lock().await; + app.config.radarr = servarr_config.clone(); + app.config.sonarr = servarr_config; + } + let network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + let request_props = network + .request_props_from( + network_event, + RequestMethod::Get, + None::<()>, + Some("/test".to_owned()), + Some("id=1".to_owned()), + ) + .await; + + assert_str_eq!( + request_props.uri, + format!("https://192.168.0.123:8080/api/v3{resource}/test?id=1") + ); + assert_eq!(request_props.method, RequestMethod::Get); + assert_eq!(request_props.body, None); + assert_str_eq!(request_props.api_token, api_token); + } + #[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)] struct Test { pub value: String, @@ -425,3 +635,78 @@ mod tests { (async_server, app_arc, server) } } + +#[cfg(test)] +pub(in crate::network) mod test_utils { + use std::sync::Arc; + + use mockito::{Matcher, Mock, Server, ServerGuard}; + use serde_json::Value; + use tokio::sync::Mutex; + + use crate::{ + app::{App, ServarrConfig}, + network::{NetworkEvent, NetworkResource, RequestMethod}, + }; + + pub async fn mock_servarr_api<'a>( + method: RequestMethod, + request_body: Option, + response_body: Option, + response_status: Option, + network_event: impl Into + NetworkResource, + path: Option<&str>, + query_params: Option<&str>, + ) -> (Mock, Arc>>, ServerGuard) { + let status = response_status.unwrap_or(200); + let resource = network_event.resource(); + let mut server = Server::new_async().await; + let mut uri = format!("/api/v3{resource}"); + + if let Some(path) = path { + uri = format!("{uri}{path}"); + } + + if let Some(params) = query_params { + uri = format!("{uri}?{params}"); + } + + let mut async_server = server + .mock(&method.to_string().to_uppercase(), uri.as_str()) + .match_header("X-Api-Key", "test1234") + .with_status(status); + + if let Some(body) = request_body { + async_server = async_server.match_body(Matcher::Json(body)); + } + + if let Some(body) = response_body { + async_server = async_server.with_body(body.to_string()); + } + + async_server = async_server.create_async().await; + + let host = Some(server.host_with_port().split(':').collect::>()[0].to_owned()); + let port = Some( + server.host_with_port().split(':').collect::>()[1] + .parse() + .unwrap(), + ); + let mut app = App::default(); + let servarr_config = ServarrConfig { + host, + port, + api_token: "test1234".to_owned(), + ..ServarrConfig::default() + }; + + match network_event.into() { + NetworkEvent::Radarr(_) => app.config.radarr = servarr_config, + NetworkEvent::Sonarr(_) => app.config.sonarr = servarr_config, + } + + let app_arc = Arc::new(Mutex::new(app)); + + (async_server, app_arc, server) + } +} diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 078dae0..2c13156 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -3,11 +3,9 @@ use std::fmt::Debug; use indoc::formatdoc; use log::{debug, info, warn}; -use serde::Serialize; use serde_json::{json, Value}; use urlencoding::encode; -use crate::app::RadarrConfig; use crate::models::radarr_models::{ AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, BlocklistResponse, Collection, CollectionMovie, CommandBody, Credit, CreditType, DeleteMovieParams, DiskSpace, DownloadRecord, @@ -23,9 +21,11 @@ use crate::models::servarr_data::radarr::modals::{ use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::stateful_table::StatefulTable; use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText}; -use crate::network::{Network, NetworkEvent, RequestMethod, RequestProps}; +use crate::network::{Network, NetworkEvent, RequestMethod}; use crate::utils::{convert_runtime, convert_to_gb}; +use super::NetworkResource; + #[cfg(test)] #[path = "radarr_network_tests.rs"] mod radarr_network_tests; @@ -80,8 +80,8 @@ pub enum RadarrEvent { UpdateDownloads, } -impl RadarrEvent { - const fn resource(&self) -> &'static str { +impl NetworkResource for RadarrEvent { + fn resource(&self) -> &'static str { match &self { RadarrEvent::ClearBlocklist => "/blocklist/bulk", RadarrEvent::DeleteBlocklistItem(_) => "/blocklist", @@ -214,11 +214,14 @@ impl<'a, 'b> Network<'a, 'b> { } RadarrEvent::GetRootFolders => self.get_root_folders().await.map(RadarrSerdeable::from), RadarrEvent::GetSecurityConfig => self.get_security_config().await.map(RadarrSerdeable::from), - RadarrEvent::GetStatus => self.get_status().await.map(RadarrSerdeable::from), + RadarrEvent::GetStatus => self.get_radarr_status().await.map(RadarrSerdeable::from), RadarrEvent::GetTags => self.get_tags().await.map(RadarrSerdeable::from), RadarrEvent::GetTasks => self.get_tasks().await.map(RadarrSerdeable::from), RadarrEvent::GetUpdates => self.get_updates().await.map(RadarrSerdeable::from), - RadarrEvent::HealthCheck => self.get_healthcheck().await.map(RadarrSerdeable::from), + RadarrEvent::HealthCheck => self + .get_radarr_healthcheck() + .await + .map(RadarrSerdeable::from), RadarrEvent::SearchNewMovie(query) => { self.search_movie(query).await.map(RadarrSerdeable::from) } @@ -246,6 +249,7 @@ impl<'a, 'b> Network<'a, 'b> { async fn add_movie(&mut self, add_movie_body_option: Option) -> Result { info!("Adding new movie to Radarr"); + let event = RadarrEvent::AddMovie(None); let body = if let Some(add_movie_body) = add_movie_body_option { add_movie_body } else { @@ -337,11 +341,7 @@ impl<'a, 'b> Network<'a, 'b> { debug!("Add movie body: {body:?}"); let request_props = self - .radarr_request_props_from( - RadarrEvent::AddMovie(None).resource(), - RequestMethod::Post, - Some(body), - ) + .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self @@ -351,6 +351,7 @@ impl<'a, 'b> Network<'a, 'b> { async fn add_root_folder(&mut self, root_folder: Option) -> Result { info!("Adding new root folder to Radarr"); + let event = RadarrEvent::AddRootFolder(None); let body = if let Some(path) = root_folder { AddRootFolderBody { path } } else { @@ -372,11 +373,7 @@ impl<'a, 'b> Network<'a, 'b> { debug!("Add root folder body: {body:?}"); let request_props = self - .radarr_request_props_from( - RadarrEvent::AddRootFolder(None).resource(), - RequestMethod::Post, - Some(body), - ) + .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self @@ -386,12 +383,15 @@ impl<'a, 'b> Network<'a, 'b> { async fn add_tag(&mut self, tag: String) -> Result { info!("Adding a new Radarr tag"); + let event = RadarrEvent::AddTag(String::new()); let request_props = self - .radarr_request_props_from( - RadarrEvent::AddTag(String::new()).resource(), + .request_props_from( + event, RequestMethod::Post, Some(json!({ "label": tag })), + None, + None, ) .await; @@ -404,12 +404,15 @@ impl<'a, 'b> Network<'a, 'b> { async fn delete_tag(&mut self, id: i64) -> Result<()> { info!("Deleting Radarr tag with id: {id}"); + let event = RadarrEvent::DeleteTag(id); let request_props = self - .radarr_request_props_from( - format!("{}/{id}", RadarrEvent::DeleteTag(id).resource()).as_str(), + .request_props_from( + event, RequestMethod::Delete, None::<()>, + Some(format!("/{id}")), + None, ) .await; @@ -420,6 +423,7 @@ impl<'a, 'b> Network<'a, 'b> { async fn clear_blocklist(&mut self) -> Result<()> { info!("Clearing Radarr blocklist"); + let event = RadarrEvent::ClearBlocklist; let ids = self .app @@ -434,10 +438,12 @@ impl<'a, 'b> Network<'a, 'b> { .collect::>(); let request_props = self - .radarr_request_props_from( - RadarrEvent::ClearBlocklist.resource(), + .request_props_from( + event, RequestMethod::Delete, Some(json!({"ids": ids})), + None, + None, ) .await; @@ -447,6 +453,7 @@ impl<'a, 'b> Network<'a, 'b> { } async fn delete_blocklist_item(&mut self, blocklist_item_id: Option) -> Result<()> { + let event = RadarrEvent::DeleteBlocklistItem(None); let id = if let Some(b_id) = blocklist_item_id { b_id } else { @@ -464,10 +471,12 @@ impl<'a, 'b> Network<'a, 'b> { info!("Deleting Radarr blocklist item for item with id: {id}"); let request_props = self - .radarr_request_props_from( - format!("{}/{id}", RadarrEvent::DeleteBlocklistItem(None).resource()).as_str(), + .request_props_from( + event, RequestMethod::Delete, None::<()>, + Some(format!("/{id}")), + None, ) .await; @@ -477,6 +486,7 @@ impl<'a, 'b> Network<'a, 'b> { } async fn delete_download(&mut self, download_id: Option) -> Result<()> { + let event = RadarrEvent::DeleteDownload(None); let id = if let Some(dl_id) = download_id { dl_id } else { @@ -494,10 +504,12 @@ impl<'a, 'b> Network<'a, 'b> { info!("Deleting Radarr download for download with id: {id}"); let request_props = self - .radarr_request_props_from( - format!("{}/{id}", RadarrEvent::DeleteDownload(None).resource()).as_str(), + .request_props_from( + event, RequestMethod::Delete, None::<()>, + Some(format!("/{id}")), + None, ) .await; @@ -507,6 +519,7 @@ impl<'a, 'b> Network<'a, 'b> { } async fn delete_indexer(&mut self, indexer_id: Option) -> Result<()> { + let event = RadarrEvent::DeleteIndexer(None); let id = if let Some(i_id) = indexer_id { i_id } else { @@ -524,10 +537,12 @@ impl<'a, 'b> Network<'a, 'b> { info!("Deleting Radarr indexer for indexer with id: {id}"); let request_props = self - .radarr_request_props_from( - format!("{}/{id}", RadarrEvent::DeleteIndexer(None).resource()).as_str(), + .request_props_from( + event, RequestMethod::Delete, None::<()>, + Some(format!("/{id}")), + None, ) .await; @@ -537,6 +552,7 @@ impl<'a, 'b> Network<'a, 'b> { } async fn delete_movie(&mut self, delete_movie_params: Option) -> Result<()> { + let event = RadarrEvent::DeleteMovie(None); let (movie_id, delete_files, add_import_exclusion) = if let Some(params) = delete_movie_params { ( params.id, @@ -544,7 +560,7 @@ impl<'a, 'b> Network<'a, 'b> { params.add_list_exclusion, ) } else { - let movie_id = self.extract_movie_id().await; + let (movie_id, _) = self.extract_movie_id(None).await; let delete_files = self.app.lock().await.data.radarr_data.delete_movie_files; let add_import_exclusion = self.app.lock().await.data.radarr_data.add_list_exclusion; @@ -554,14 +570,14 @@ impl<'a, 'b> Network<'a, 'b> { info!("Deleting Radarr movie with ID: {movie_id} with deleteFiles={delete_files} and addImportExclusion={add_import_exclusion}"); let request_props = self - .radarr_request_props_from( - format!( - "{}/{movie_id}?deleteFiles={delete_files}&addImportExclusion={add_import_exclusion}", - RadarrEvent::DeleteMovie(None).resource() - ) - .as_str(), + .request_props_from( + event, RequestMethod::Delete, None::<()>, + Some(format!("/{movie_id}")), + Some(format!( + "deleteFiles={delete_files}&addImportExclusion={add_import_exclusion}" + )), ) .await; @@ -581,6 +597,7 @@ impl<'a, 'b> Network<'a, 'b> { } async fn delete_root_folder(&mut self, root_folder_id: Option) -> Result<()> { + let event = RadarrEvent::DeleteRootFolder(None); let id = if let Some(rf_id) = root_folder_id { rf_id } else { @@ -598,10 +615,12 @@ impl<'a, 'b> Network<'a, 'b> { info!("Deleting Radarr root folder for folder with id: {id}"); let request_props = self - .radarr_request_props_from( - format!("{}/{id}", RadarrEvent::DeleteRootFolder(None).resource()).as_str(), + .request_props_from( + event, RequestMethod::Delete, None::<()>, + Some(format!("/{id}")), + None, ) .await; @@ -611,11 +630,12 @@ impl<'a, 'b> Network<'a, 'b> { } async fn download_release(&mut self, params: Option) -> Result { + let event = RadarrEvent::DownloadRelease(None); let body = if let Some(release_download_body) = params { info!("Downloading release with params: {release_download_body:?}"); release_download_body } else { - let movie_id = self.extract_movie_id().await; + let (movie_id, _) = self.extract_movie_id(None).await; let (guid, title, indexer_id) = { let app = self.app.lock().await; let Release { @@ -645,11 +665,7 @@ impl<'a, 'b> Network<'a, 'b> { }; let request_props = self - .radarr_request_props_from( - RadarrEvent::DownloadRelease(None).resource(), - RequestMethod::Post, - Some(body), - ) + .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self @@ -659,6 +675,7 @@ impl<'a, 'b> Network<'a, 'b> { async fn edit_all_indexer_settings(&mut self, params: Option) -> Result { info!("Updating Radarr indexer settings"); + let event = RadarrEvent::EditAllIndexerSettings(None); let body = if let Some(indexer_settings) = params { indexer_settings @@ -678,11 +695,7 @@ impl<'a, 'b> Network<'a, 'b> { debug!("Indexer settings body: {body:?}"); let request_props = self - .radarr_request_props_from( - RadarrEvent::EditAllIndexerSettings(None).resource(), - RequestMethod::Put, - Some(body), - ) + .request_props_from(event, RequestMethod::Put, Some(body), None, None) .await; let resp = self @@ -699,18 +712,22 @@ impl<'a, 'b> Network<'a, 'b> { edit_collection_params: Option, ) -> Result<()> { info!("Editing Radarr collection"); - + let detail_event = RadarrEvent::GetCollections; + let event = RadarrEvent::EditCollection(None); info!("Fetching collection details"); + let collection_id = if let Some(ref params) = edit_collection_params { params.collection_id } else { self.extract_collection_id().await }; let request_props = self - .radarr_request_props_from( - format!("{}/{collection_id}", RadarrEvent::GetCollections.resource()).as_str(), + .request_props_from( + detail_event, RequestMethod::Get, None::<()>, + Some(format!("/{collection_id}")), + None, ) .await; @@ -811,14 +828,12 @@ impl<'a, 'b> Network<'a, 'b> { debug!("Edit collection body: {detailed_collection_body:?}"); let request_props = self - .radarr_request_props_from( - format!( - "{}/{collection_id}", - RadarrEvent::EditCollection(None).resource() - ) - .as_str(), + .request_props_from( + event, RequestMethod::Put, Some(detailed_collection_body), + Some(format!("/{collection_id}")), + None, ) .await; @@ -828,6 +843,8 @@ impl<'a, 'b> Network<'a, 'b> { } async fn edit_indexer(&mut self, edit_indexer_params: Option) -> Result<()> { + let detail_event = RadarrEvent::GetIndexers; + let event = RadarrEvent::EditIndexer(None); let id = if let Some(ref params) = edit_indexer_params { params.indexer_id } else { @@ -846,10 +863,12 @@ impl<'a, 'b> Network<'a, 'b> { info!("Fetching indexer details for indexer with ID: {id}"); let request_props = self - .radarr_request_props_from( - format!("{}/{id}", RadarrEvent::GetIndexers.resource()).as_str(), + .request_props_from( + detail_event, RequestMethod::Get, None::<()>, + Some(format!("/{id}")), + None, ) .await; @@ -1061,10 +1080,12 @@ impl<'a, 'b> Network<'a, 'b> { debug!("Edit indexer body: {detailed_indexer_body:?}"); let request_props = self - .radarr_request_props_from( - format!("{}/{id}", RadarrEvent::EditIndexer(None).resource()).as_str(), + .request_props_from( + event, RequestMethod::Put, Some(detailed_indexer_body), + Some(format!("/{id}")), + None, ) .await; @@ -1075,23 +1096,23 @@ impl<'a, 'b> Network<'a, 'b> { async fn edit_movie(&mut self, edit_movie_params: Option) -> Result<()> { info!("Editing Radarr movie"); + let detail_event = RadarrEvent::GetMovieDetails(None); + let event = RadarrEvent::EditMovie(None); - let movie_id = if let Some(ref params) = edit_movie_params { - params.movie_id + let (movie_id, _) = if let Some(ref params) = edit_movie_params { + self.extract_movie_id(Some(params.movie_id)).await } else { - self.extract_movie_id().await + self.extract_movie_id(None).await }; info!("Fetching movie details for movie with ID: {movie_id}"); let request_props = self - .radarr_request_props_from( - format!( - "{}/{movie_id}", - RadarrEvent::GetMovieDetails(None).resource() - ) - .as_str(), + .request_props_from( + detail_event, RequestMethod::Get, None::<()>, + Some(format!("/{movie_id}")), + None, ) .await; @@ -1209,10 +1230,12 @@ impl<'a, 'b> Network<'a, 'b> { debug!("Edit movie body: {detailed_movie_body:?}"); let request_props = self - .radarr_request_props_from( - format!("{}/{movie_id}", RadarrEvent::EditMovie(None).resource()).as_str(), + .request_props_from( + event, RequestMethod::Put, Some(detailed_movie_body), + Some(format!("/{movie_id}")), + None, ) .await; @@ -1223,13 +1246,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_blocklist(&mut self) -> Result { info!("Fetching blocklist"); + let event = RadarrEvent::GetBlocklist; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetBlocklist.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1249,13 +1269,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_collections(&mut self) -> Result> { info!("Fetching Radarr collections"); + let event = RadarrEvent::GetCollections; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetCollections.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1274,12 +1291,17 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_credits(&mut self, movie_id: Option) -> Result> { info!("Fetching Radarr movie credits"); + let event = RadarrEvent::GetMovieCredits(None); + let (_, movie_id_param) = self.extract_movie_id(movie_id).await; - let request_uri = self - .append_movie_id_param(RadarrEvent::GetMovieCredits(None).resource(), movie_id) - .await; let request_props = self - .radarr_request_props_from(request_uri.as_str(), RequestMethod::Get, None::<()>) + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(movie_id_param), + ) .await; self @@ -1321,13 +1343,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_diskspace(&mut self) -> Result> { info!("Fetching Radarr disk space"); + let event = RadarrEvent::GetOverview; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetOverview.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1339,13 +1358,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_downloads(&mut self) -> Result { info!("Fetching Radarr downloads"); + let event = RadarrEvent::GetDownloads; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetDownloads.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1361,13 +1377,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_host_config(&mut self) -> Result { info!("Fetching Radarr host config"); + let event = RadarrEvent::GetHostConfig; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetHostConfig.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1377,13 +1390,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_indexers(&mut self) -> Result> { info!("Fetching Radarr indexers"); + let event = RadarrEvent::GetIndexers; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetIndexers.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1395,13 +1405,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_all_indexer_settings(&mut self) -> Result { info!("Fetching Radarr indexer settings"); + let event = RadarrEvent::GetAllIndexerSettings; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetAllIndexerSettings.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1415,15 +1422,12 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_healthcheck(&mut self) -> Result<()> { + async fn get_radarr_healthcheck(&mut self) -> Result<()> { info!("Performing Radarr health check"); + let event = RadarrEvent::HealthCheck; let request_props = self - .radarr_request_props_from( - RadarrEvent::HealthCheck.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1433,14 +1437,14 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_logs(&mut self, events: Option) -> Result { info!("Fetching Radarr logs"); + let event = RadarrEvent::GetLogs(events); - let resource = format!( - "{}?pageSize={}&sortDirection=descending&sortKey=time", - RadarrEvent::GetLogs(events).resource(), + let params = format!( + "pageSize={}&sortDirection=descending&sortKey=time", events.unwrap_or(500) ); let request_props = self - .radarr_request_props_from(&resource, RequestMethod::Get, None::<()>) + .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) .await; self @@ -1480,19 +1484,18 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_movie_details(&mut self, movie_id: Option) -> Result { info!("Fetching Radarr movie details"); + let event = RadarrEvent::GetMovieDetails(None); + let (id, _) = self.extract_movie_id(movie_id).await; - let id = if let Some(m_id) = movie_id { - m_id - } else { - self.extract_movie_id().await - }; info!("Fetching movie details for movie with ID: {id}"); let request_props = self - .radarr_request_props_from( - format!("{}/{id}", RadarrEvent::GetMovieDetails(None).resource()).as_str(), + .request_props_from( + event, RequestMethod::Get, None::<()>, + Some(format!("/{id}")), + None, ) .await; @@ -1637,12 +1640,17 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_movie_history(&mut self, movie_id: Option) -> Result> { info!("Fetching Radarr movie history"); + let event = RadarrEvent::GetMovieHistory(None); - let request_uri = self - .append_movie_id_param(RadarrEvent::GetMovieHistory(None).resource(), movie_id) - .await; + let (_, movie_id_param) = self.extract_movie_id(movie_id).await; let request_props = self - .radarr_request_props_from(request_uri.as_str(), RequestMethod::Get, None::<()>) + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(movie_id_param), + ) .await; self @@ -1668,13 +1676,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_movies(&mut self) -> Result> { info!("Fetching Radarr library"); + let event = RadarrEvent::GetMovies; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetMovies.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1693,13 +1698,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_quality_profiles(&mut self) -> Result> { info!("Fetching Radarr quality profiles"); + let event = RadarrEvent::GetQualityProfiles; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetQualityProfiles.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1714,13 +1716,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_queued_events(&mut self) -> Result> { info!("Fetching Radarr queued events"); + let event = RadarrEvent::GetQueuedEvents; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetQueuedEvents.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1735,18 +1734,17 @@ impl<'a, 'b> Network<'a, 'b> { } async fn get_releases(&mut self, movie_id: Option) -> Result> { - let id = if let Some(m_id) = movie_id { - m_id - } else { - self.extract_movie_id().await - }; + let (id, movie_id_param) = self.extract_movie_id(movie_id).await; info!("Fetching releases for movie with ID: {id}"); + let event = RadarrEvent::GetReleases(None); let request_props = self - .radarr_request_props_from( - format!("{}?movieId={id}", RadarrEvent::GetReleases(None).resource()).as_str(), + .request_props_from( + event, RequestMethod::Get, None::<()>, + None, + Some(movie_id_param), ) .await; @@ -1770,13 +1768,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_root_folders(&mut self) -> Result> { info!("Fetching Radarr root folders"); + let event = RadarrEvent::GetRootFolders; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetRootFolders.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1788,13 +1783,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_security_config(&mut self) -> Result { info!("Fetching Radarr security config"); + let event = RadarrEvent::GetSecurityConfig; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetSecurityConfig.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1802,15 +1794,12 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_status(&mut self) -> Result { + async fn get_radarr_status(&mut self) -> Result { info!("Fetching Radarr system status"); + let event = RadarrEvent::GetStatus; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetStatus.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1823,13 +1812,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_tags(&mut self) -> Result> { info!("Fetching Radarr tags"); + let event = RadarrEvent::GetTags; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetTags.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1844,13 +1830,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_tasks(&mut self) -> Result> { info!("Fetching Radarr tasks"); + let event = RadarrEvent::GetTasks; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetTasks.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1862,13 +1845,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_updates(&mut self) -> Result> { info!("Fetching Radarr updates"); + let event = RadarrEvent::GetUpdates; let request_props = self - .radarr_request_props_from( - RadarrEvent::GetUpdates.resource(), - RequestMethod::Get, - None::<()>, - ) + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self @@ -1943,6 +1923,7 @@ impl<'a, 'b> Network<'a, 'b> { async fn search_movie(&mut self, query: Option) -> Result> { info!("Searching for specific Radarr movie"); + let event = RadarrEvent::SearchNewMovie(None); let search = if let Some(search_query) = query { Ok(search_query.into()) } else { @@ -1960,15 +1941,12 @@ impl<'a, 'b> Network<'a, 'b> { match search { Ok(search_string) => { let request_props = self - .radarr_request_props_from( - format!( - "{}?term={}", - RadarrEvent::SearchNewMovie(None).resource(), - encode(&search_string.text) - ) - .as_str(), + .request_props_from( + event, RequestMethod::Get, None::<()>, + None, + Some(format!("term={}", encode(&search_string.text))), ) .await; @@ -2002,6 +1980,7 @@ impl<'a, 'b> Network<'a, 'b> { } async fn start_task(&mut self, task: Option) -> Result { + let event = RadarrEvent::StartTask(None); let task_name = if let Some(t_name) = task { t_name } else { @@ -2022,11 +2001,7 @@ impl<'a, 'b> Network<'a, 'b> { let body = CommandBody { name: task_name }; let request_props = self - .radarr_request_props_from( - RadarrEvent::StartTask(None).resource(), - RequestMethod::Post, - Some(body), - ) + .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self @@ -2035,6 +2010,8 @@ impl<'a, 'b> Network<'a, 'b> { } async fn test_indexer(&mut self, indexer_id: Option) -> Result { + let detail_event = RadarrEvent::GetIndexers; + let event = RadarrEvent::TestIndexer(None); let id = if let Some(i_id) = indexer_id { i_id } else { @@ -2053,10 +2030,12 @@ impl<'a, 'b> Network<'a, 'b> { info!("Fetching indexer details for indexer with ID: {id}"); let request_props = self - .radarr_request_props_from( - format!("{}/{id}", RadarrEvent::GetIndexers.resource()).as_str(), + .request_props_from( + detail_event, RequestMethod::Get, None::<()>, + Some(format!("/{id}")), + None, ) .await; @@ -2071,11 +2050,7 @@ impl<'a, 'b> Network<'a, 'b> { info!("Testing indexer"); let mut request_props = self - .radarr_request_props_from( - RadarrEvent::TestIndexer(None).resource(), - RequestMethod::Post, - Some(test_body), - ) + .request_props_from(event, RequestMethod::Post, Some(test_body), None, None) .await; request_props.ignore_status_code = true; @@ -2095,13 +2070,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn test_all_indexers(&mut self) -> Result> { info!("Testing all indexers"); + let event = RadarrEvent::TestAllIndexers; let mut request_props = self - .radarr_request_props_from( - RadarrEvent::TestAllIndexers.resource(), - RequestMethod::Post, - None, - ) + .request_props_from(event, RequestMethod::Post, None, None, None) .await; request_props.ignore_status_code = true; @@ -2144,11 +2116,8 @@ impl<'a, 'b> Network<'a, 'b> { } async fn trigger_automatic_search(&mut self, movie_id: Option) -> Result { - let id = if let Some(m_id) = movie_id { - m_id - } else { - self.extract_movie_id().await - }; + let event = RadarrEvent::TriggerAutomaticSearch(None); + let (id, _) = self.extract_movie_id(movie_id).await; info!("Searching indexers for movie with ID: {id}"); let body = MovieCommandBody { name: "MoviesSearch".to_owned(), @@ -2156,11 +2125,7 @@ impl<'a, 'b> Network<'a, 'b> { }; let request_props = self - .radarr_request_props_from( - RadarrEvent::TriggerAutomaticSearch(None).resource(), - RequestMethod::Post, - Some(body), - ) + .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self @@ -2170,17 +2135,14 @@ impl<'a, 'b> Network<'a, 'b> { async fn update_all_movies(&mut self) -> Result { info!("Updating all movies"); + let event = RadarrEvent::UpdateAllMovies; let body = MovieCommandBody { name: "RefreshMovie".to_owned(), movie_ids: Vec::new(), }; let request_props = self - .radarr_request_props_from( - RadarrEvent::UpdateAllMovies.resource(), - RequestMethod::Post, - Some(body), - ) + .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self @@ -2189,11 +2151,8 @@ impl<'a, 'b> Network<'a, 'b> { } async fn update_and_scan(&mut self, movie_id: Option) -> Result { - let id = if let Some(m_id) = movie_id { - m_id - } else { - self.extract_movie_id().await - }; + let (id, _) = self.extract_movie_id(movie_id).await; + let event = RadarrEvent::UpdateAndScan(None); info!("Updating and scanning movie with ID: {id}"); let body = MovieCommandBody { name: "RefreshMovie".to_owned(), @@ -2201,11 +2160,7 @@ impl<'a, 'b> Network<'a, 'b> { }; let request_props = self - .radarr_request_props_from( - RadarrEvent::UpdateAndScan(None).resource(), - RequestMethod::Post, - Some(body), - ) + .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self @@ -2215,16 +2170,13 @@ impl<'a, 'b> Network<'a, 'b> { async fn update_collections(&mut self) -> Result { info!("Updating collections"); + let event = RadarrEvent::UpdateCollections; let body = CommandBody { name: "RefreshCollections".to_owned(), }; let request_props = self - .radarr_request_props_from( - RadarrEvent::UpdateCollections.resource(), - RequestMethod::Post, - Some(body), - ) + .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self @@ -2234,16 +2186,13 @@ impl<'a, 'b> Network<'a, 'b> { async fn update_downloads(&mut self) -> Result { info!("Updating downloads"); + let event = RadarrEvent::UpdateDownloads; let body = CommandBody { name: "RefreshMonitoredDownloads".to_owned(), }; let request_props = self - .radarr_request_props_from( - RadarrEvent::UpdateDownloads.resource(), - RequestMethod::Post, - Some(body), - ) + .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self @@ -2251,44 +2200,6 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn radarr_request_props_from( - &self, - resource: &str, - method: RequestMethod, - body: Option, - ) -> RequestProps { - let app = self.app.lock().await; - let RadarrConfig { - host, - port, - uri, - api_token, - ssl_cert_path, - } = &app.config.radarr; - let uri = if let Some(radarr_uri) = uri { - format!("{radarr_uri}/api/v3{resource}") - } else { - let protocol = if ssl_cert_path.is_some() { - "https" - } else { - "http" - }; - let host = host.as_ref().unwrap(); - format!( - "{protocol}://{host}:{}/api/v3{resource}", - port.unwrap_or(7878) - ) - }; - - RequestProps { - uri, - method, - body, - api_token: api_token.to_owned(), - ignore_status_code: false, - } - } - async fn extract_and_add_tag_ids_vec(&mut self, edit_tags: String) -> Vec { let tags_map = self.app.lock().await.data.radarr_data.tags_map.clone(); let tags = edit_tags.clone(); @@ -2319,16 +2230,21 @@ impl<'a, 'b> Network<'a, 'b> { .collect() } - async fn extract_movie_id(&mut self) -> i64 { - self - .app - .lock() - .await - .data - .radarr_data - .movies - .current_selection() - .id + async fn extract_movie_id(&mut self, movie_id: Option) -> (i64, String) { + let movie_id = if let Some(id) = movie_id { + id + } else { + self + .app + .lock() + .await + .data + .radarr_data + .movies + .current_selection() + .id + }; + (movie_id, format!("movieId={movie_id}")) } async fn extract_collection_id(&mut self) -> i64 { @@ -2342,15 +2258,6 @@ impl<'a, 'b> Network<'a, 'b> { .current_selection() .id } - - async fn append_movie_id_param(&mut self, resource: &str, movie_id: Option) -> String { - let movie_id = if let Some(id) = movie_id { - id - } else { - self.extract_movie_id().await - }; - format!("{resource}?movieId={movie_id}") - } } fn get_movie_status(has_file: bool, downloads_vec: &[DownloadRecord], movie_id: i64) -> String { diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 67500b6..d1dfff6 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -4,7 +4,7 @@ mod test { use bimap::BiMap; use chrono::{DateTime, Utc}; - use mockito::{Matcher, Mock, Server, ServerGuard}; + use mockito::{Matcher, Server}; use pretty_assertions::{assert_eq, assert_str_eq}; use reqwest::Client; use rstest::rstest; @@ -13,6 +13,7 @@ mod test { use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; + use crate::app::ServarrConfig; use crate::models::radarr_models::{ BlocklistItem, BlocklistItemMovie, CollectionMovie, IndexerField, Language, MediaInfo, MinimumAvailability, Monitor, MovieCollection, MovieFile, Quality, QualityWrapper, Rating, @@ -21,6 +22,7 @@ mod test { use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::stateful_table::SortOption; use crate::models::HorizontallyScrollableText; + use crate::network::network_tests::test_utils::mock_servarr_api; use crate::App; use super::super::*; @@ -228,13 +230,15 @@ mod test { } #[tokio::test] - async fn test_handle_get_healthcheck_event() { - let (async_server, app_arc, _server) = mock_radarr_api( + async fn test_handle_get_radarr_healthcheck_event() { + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, None, None, - RadarrEvent::HealthCheck.resource(), + RadarrEvent::HealthCheck, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -246,7 +250,7 @@ mod test { #[tokio::test] async fn test_handle_get_diskspace_event() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(json!([ @@ -260,7 +264,9 @@ mod test { } ])), None, - RadarrEvent::GetOverview.resource(), + RadarrEvent::GetOverview, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -291,7 +297,7 @@ mod test { #[tokio::test] async fn test_handle_get_status_event() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(json!({ @@ -299,7 +305,9 @@ mod test { "startTime": "2023-02-25T20:16:43Z" })), None, - RadarrEvent::GetStatus.resource(), + RadarrEvent::GetStatus, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -357,12 +365,14 @@ mod test { ..movie() }, ]; - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(json!([movie_1, movie_2])), None, - RadarrEvent::GetMovies.resource(), + RadarrEvent::GetMovies, + None, + None, ) .await; app_arc.lock().await.data.radarr_data.movies.sort_asc = true; @@ -411,12 +421,14 @@ mod test { *movie_1.get_mut("title").unwrap() = json!("z test"); *movie_2.get_mut("id").unwrap() = json!(2); *movie_2.get_mut("title").unwrap() = json!("A test"); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(json!([movie_1, movie_2])), None, - RadarrEvent::GetMovies.resource(), + RadarrEvent::GetMovies, + None, + None, ) .await; app_arc @@ -477,13 +489,14 @@ mod test { "languages": [ { "name": "English" } ], "quality": { "quality": { "name": "HD - 1080p" }} }]); - let resource = format!("{}?movieId=1", RadarrEvent::GetReleases(None).resource()); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(release_json), None, - &resource, + RadarrEvent::GetReleases(None), + None, + Some("movieId=1"), ) .await; app_arc @@ -536,13 +549,14 @@ mod test { "languages": [ { "name": "English" } ], "quality": { "quality": { "name": "HD - 1080p" }} }]); - let resource = format!("{}?movieId=1", RadarrEvent::GetReleases(None).resource()); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(release_json), None, - &resource, + RadarrEvent::GetReleases(None), + None, + Some("movieId=1"), ) .await; app_arc @@ -598,16 +612,14 @@ mod test { } } }]); - let resource = format!( - "{}?term=test%20term", - RadarrEvent::SearchNewMovie(None).resource() - ); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(add_movie_search_result_json), None, - &resource, + RadarrEvent::SearchNewMovie(None), + None, + Some("term=test%20term"), ) .await; app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into()); @@ -665,16 +677,14 @@ mod test { } } }]); - let resource = format!( - "{}?term=test%20term", - RadarrEvent::SearchNewMovie(None).resource() - ); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(add_movie_search_result_json), None, - &resource, + RadarrEvent::SearchNewMovie(None), + None, + Some("term=test%20term"), ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -692,14 +702,16 @@ mod test { #[tokio::test] async fn test_handle_start_task_event() { let response = json!({ "test": "test"}); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "name": "ApplicationCheckUpdate" })), Some(response.clone()), None, - RadarrEvent::StartTask(None).resource(), + RadarrEvent::StartTask(None), + None, + None, ) .await; app_arc @@ -727,14 +739,16 @@ mod test { #[tokio::test] async fn test_handle_start_task_event_uses_provided_task_name() { let response = json!({ "test": "test"}); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "name": "ApplicationCheckUpdate" })), Some(response.clone()), None, - RadarrEvent::StartTask(None).resource(), + RadarrEvent::StartTask(None), + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -751,12 +765,16 @@ mod test { #[tokio::test] async fn test_handle_search_new_movie_event_no_results() { - let resource = format!( - "{}?term=test%20term", - RadarrEvent::SearchNewMovie(None).resource() - ); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Get, None, Some(json!([])), None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!([])), + None, + RadarrEvent::SearchNewMovie(None), + None, + Some("term=test%20term"), + ) + .await; app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into()); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -801,11 +819,11 @@ mod test { .unwrap(), ); let mut app = App::default(); - let radarr_config = RadarrConfig { + let radarr_config = ServarrConfig { host, port, api_token: "test1234".to_owned(), - ..RadarrConfig::default() + ..ServarrConfig::default() }; app.config.radarr = radarr_config; let app_arc = Arc::new(Mutex::new(app)); @@ -861,13 +879,14 @@ mod test { "errorMessage": "test failure", "severity": "error" }]); - let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(indexer_details_json.clone()), None, - &resource, + RadarrEvent::GetIndexers, + Some("/1"), + None, ) .await; let async_test_server = server @@ -929,13 +948,14 @@ mod test { "tags": [1], "id": 1 }); - let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(indexer_details_json.clone()), None, - &resource, + RadarrEvent::GetIndexers, + Some("/1"), + None, ) .await; let async_test_server = server @@ -997,13 +1017,14 @@ mod test { "tags": [1], "id": 1 }); - let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(indexer_details_json.clone()), None, - &resource, + RadarrEvent::GetIndexers, + Some("/1"), + None, ) .await; let async_test_server = server @@ -1079,12 +1100,14 @@ mod test { ] }]); let response: Vec = serde_json::from_value(response_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, None, Some(response_json), Some(400), - RadarrEvent::TestAllIndexers.resource(), + RadarrEvent::TestAllIndexers, + None, + None, ) .await; app_arc @@ -1127,7 +1150,7 @@ mod test { #[tokio::test] async fn test_handle_trigger_automatic_search_event() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "name": "MoviesSearch", @@ -1135,7 +1158,9 @@ mod test { })), Some(json!({})), None, - RadarrEvent::TriggerAutomaticSearch(None).resource(), + RadarrEvent::TriggerAutomaticSearch(None), + None, + None, ) .await; app_arc @@ -1157,7 +1182,7 @@ mod test { #[tokio::test] async fn test_handle_trigger_automatic_search_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "name": "MoviesSearch", @@ -1165,7 +1190,9 @@ mod test { })), Some(json!({})), None, - RadarrEvent::TriggerAutomaticSearch(None).resource(), + RadarrEvent::TriggerAutomaticSearch(None), + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -1180,7 +1207,7 @@ mod test { #[tokio::test] async fn test_handle_update_and_scan_event() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "name": "RefreshMovie", @@ -1188,7 +1215,9 @@ mod test { })), Some(json!({})), None, - RadarrEvent::UpdateAndScan(None).resource(), + RadarrEvent::UpdateAndScan(None), + None, + None, ) .await; app_arc @@ -1210,7 +1239,7 @@ mod test { #[tokio::test] async fn test_handle_update_and_scan_event_uses_provied_movie_id() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "name": "RefreshMovie", @@ -1218,7 +1247,9 @@ mod test { })), Some(json!({})), None, - RadarrEvent::UpdateAndScan(None).resource(), + RadarrEvent::UpdateAndScan(None), + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -1233,7 +1264,7 @@ mod test { #[tokio::test] async fn test_handle_update_all_movies_event() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "name": "RefreshMovie", @@ -1241,7 +1272,9 @@ mod test { })), Some(json!({})), None, - RadarrEvent::UpdateAllMovies.resource(), + RadarrEvent::UpdateAllMovies, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -1256,14 +1289,16 @@ mod test { #[tokio::test] async fn test_handle_update_downloads_event() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "name": "RefreshMonitoredDownloads" })), Some(json!({})), None, - RadarrEvent::UpdateDownloads.resource(), + RadarrEvent::UpdateDownloads, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -1278,14 +1313,16 @@ mod test { #[tokio::test] async fn test_handle_update_collections_event() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "name": "RefreshCollections" })), Some(json!({})), None, - RadarrEvent::UpdateCollections.resource(), + RadarrEvent::UpdateCollections, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -1300,14 +1337,15 @@ mod test { #[tokio::test] async fn test_handle_get_movie_details_event() { - let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); let response: Movie = serde_json::from_str(MOVIE_JSON).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - &resource, + RadarrEvent::GetMovieDetails(None), + Some("/1"), + None, ) .await; app_arc @@ -1394,14 +1432,15 @@ mod test { #[tokio::test] async fn test_handle_get_movie_details_event_uses_provided_id() { - let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); let response: Movie = serde_json::from_str(MOVIE_JSON).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - &resource, + RadarrEvent::GetMovieDetails(None), + Some("/1"), + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -1440,13 +1479,14 @@ mod test { "minimumAvailability": "released", "ratings": {} }); - let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(movie_json_with_missing_fields), None, - &resource, + RadarrEvent::GetMovieDetails(None), + Some("/1"), + None, ) .await; app_arc @@ -1512,16 +1552,14 @@ mod test { }]); let response: Vec = serde_json::from_value(movie_history_item_json.clone()).unwrap(); - let resource = format!( - "{}?movieId=1", - RadarrEvent::GetMovieHistory(None).resource() - ); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(movie_history_item_json), None, - &resource, + RadarrEvent::GetMovieHistory(None), + None, + Some("movieId=1"), ) .await; app_arc @@ -1568,16 +1606,14 @@ mod test { }]); let response: Vec = serde_json::from_value(movie_history_item_json.clone()).unwrap(); - let resource = format!( - "{}?movieId=1", - RadarrEvent::GetMovieHistory(None).resource() - ); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(movie_history_item_json), None, - &resource, + RadarrEvent::GetMovieHistory(None), + None, + Some("movieId=1"), ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -1601,16 +1637,14 @@ mod test { "date": "2022-12-30T07:37:56Z", "eventType": "grabbed" }]); - let resource = format!( - "{}?movieId=1", - RadarrEvent::GetMovieHistory(None).resource() - ); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(movie_history_item_json), None, - &resource, + RadarrEvent::GetMovieHistory(None), + None, + Some("movieId=1"), ) .await; app_arc @@ -1740,12 +1774,14 @@ mod test { ..blocklist_item() }, ]; - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(blocklist_json), None, - RadarrEvent::GetBlocklist.resource(), + RadarrEvent::GetBlocklist, + None, + None, ) .await; app_arc.lock().await.data.radarr_data.blocklist.sort_asc = true; @@ -1861,12 +1897,14 @@ mod test { }, }, }]}); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(blocklist_json), None, - RadarrEvent::GetBlocklist.resource(), + RadarrEvent::GetBlocklist, + None, + None, ) .await; app_arc.lock().await.data.radarr_data.blocklist.sort_asc = true; @@ -1983,12 +2021,14 @@ mod test { ..collection() }, ]; - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(collections_json), None, - RadarrEvent::GetCollections.resource(), + RadarrEvent::GetCollections, + None, + None, ) .await; app_arc.lock().await.data.radarr_data.collections.sort_asc = true; @@ -2090,12 +2130,14 @@ mod test { } }], }]); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(collections_json), None, - RadarrEvent::GetCollections.resource(), + RadarrEvent::GetCollections, + None, + None, ) .await; app_arc.lock().await.data.radarr_data.collections.sort_asc = true; @@ -2156,12 +2198,14 @@ mod test { }); let response: DownloadsResponse = serde_json::from_value(downloads_response_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(downloads_response_json), None, - RadarrEvent::GetDownloads.resource(), + RadarrEvent::GetDownloads, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2194,12 +2238,14 @@ mod test { "sslCertPassword": "test" }); let response: HostConfig = serde_json::from_value(host_config_response.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(host_config_response), None, - RadarrEvent::GetHostConfig.resource(), + RadarrEvent::GetHostConfig, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2247,12 +2293,14 @@ mod test { "id": 1 }]); let response: Vec = serde_json::from_value(indexers_response_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(indexers_response_json), None, - RadarrEvent::GetIndexers.resource(), + RadarrEvent::GetIndexers, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2286,12 +2334,14 @@ mod test { }); let response: IndexerSettings = serde_json::from_value(indexer_settings_response_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(indexer_settings_response_json), None, - RadarrEvent::GetAllIndexerSettings.resource(), + RadarrEvent::GetAllIndexerSettings, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2323,12 +2373,14 @@ mod test { "whitelistedHardcodedSubs": "", "id": 1 }); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(indexer_settings_response_json), None, - RadarrEvent::GetAllIndexerSettings.resource(), + RadarrEvent::GetAllIndexerSettings, + None, + None, ) .await; app_arc.lock().await.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); @@ -2371,12 +2423,14 @@ mod test { trigger: "scheduled".to_owned(), }; - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(queued_events_json), None, - RadarrEvent::GetQueuedEvents.resource(), + RadarrEvent::GetQueuedEvents, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2397,10 +2451,6 @@ mod test { #[tokio::test] async fn test_handle_get_logs_event() { - let resource = format!( - "{}?pageSize=500&sortDirection=descending&sortKey=time", - RadarrEvent::GetLogs(None).resource() - ); let expected_logs = vec![ HorizontallyScrollableText::from( "2023-05-20 21:29:16 UTC|FATAL|RadarrError|Some.Big.Bad.Exception|test exception", @@ -2432,12 +2482,14 @@ mod test { ] }); let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(logs_response_json), None, - &resource, + RadarrEvent::GetLogs(None), + None, + Some("pageSize=500&sortDirection=descending&sortKey=time"), ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2467,10 +2519,6 @@ mod test { #[tokio::test] async fn test_handle_get_logs_event_uses_provided_events() { - let resource = format!( - "{}?pageSize=1000&sortDirection=descending&sortKey=time", - RadarrEvent::GetLogs(Some(1000)).resource() - ); let expected_logs = vec![ HorizontallyScrollableText::from( "2023-05-20 21:29:16 UTC|FATAL|RadarrError|Some.Big.Bad.Exception|test exception", @@ -2502,12 +2550,14 @@ mod test { ] }); let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(logs_response_json), None, - &resource, + RadarrEvent::GetLogs(Some(1000)), + None, + Some("pageSize=1000&sortDirection=descending&sortKey=time"), ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2543,12 +2593,14 @@ mod test { }]); let response: Vec = serde_json::from_value(quality_profile_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(quality_profile_json), None, - RadarrEvent::GetQualityProfiles.resource(), + RadarrEvent::GetQualityProfiles, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2574,12 +2626,14 @@ mod test { "label": "usenet" }]); let response: Vec = serde_json::from_value(tags_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(tags_json), None, - RadarrEvent::GetTags.resource(), + RadarrEvent::GetTags, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2636,12 +2690,14 @@ mod test { last_duration: "00:00:00.5111547".to_owned(), }, ]; - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(tasks_json), None, - RadarrEvent::GetTasks.resource(), + RadarrEvent::GetTasks, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2729,12 +2785,14 @@ mod test { * Killed bug 1 * Fixed bug 2" )); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(updates_json), None, - RadarrEvent::GetUpdates.resource(), + RadarrEvent::GetUpdates, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2757,12 +2815,14 @@ mod test { async fn test_handle_add_tag() { let tag_json = json!({ "id": 3, "label": "testing" }); let response: Tag = serde_json::from_value(tag_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "label": "testing" })), Some(tag_json), None, - RadarrEvent::GetTags.resource(), + RadarrEvent::GetTags, + None, + None, ) .await; app_arc.lock().await.data.radarr_data.tags_map = @@ -2789,9 +2849,16 @@ mod test { #[tokio::test] async fn test_handle_delete_tag_event() { - let resource = format!("{}/1", RadarrEvent::DeleteTag(1).resource()); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteTag(1), + Some("/1"), + None, + ) + .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network @@ -2811,12 +2878,14 @@ mod test { "freeSpace": 219902325555200u64, }]); let response: Vec = serde_json::from_value(root_folder_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(root_folder_json), None, - RadarrEvent::GetRootFolders.resource(), + RadarrEvent::GetRootFolders, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2847,12 +2916,14 @@ mod test { }); let response: SecurityConfig = serde_json::from_value(security_config_response.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(security_config_response), None, - RadarrEvent::GetSecurityConfig.resource(), + RadarrEvent::GetSecurityConfig, + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2883,16 +2954,14 @@ mod test { } ]); let response: Vec = serde_json::from_value(credits_json.clone()).unwrap(); - let resource = format!( - "{}?movieId=1", - RadarrEvent::GetMovieCredits(None).resource() - ); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(credits_json), None, - &resource, + RadarrEvent::GetMovieCredits(None), + None, + Some("movieId=1"), ) .await; app_arc @@ -2936,16 +3005,14 @@ mod test { } ]); let response: Vec = serde_json::from_value(credits_json.clone()).unwrap(); - let resource = format!( - "{}?movieId=1", - RadarrEvent::GetMovieCredits(None).resource() - ); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(credits_json), None, - &resource, + RadarrEvent::GetMovieCredits(None), + None, + Some("movieId=1"), ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -2975,16 +3042,14 @@ mod test { "type": "crew", } ]); - let resource = format!( - "{}?movieId=1", - RadarrEvent::GetMovieCredits(None).resource() - ); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, Some(credits_json), None, - &resource, + RadarrEvent::GetMovieCredits(None), + None, + Some("movieId=1"), ) .await; app_arc @@ -3011,12 +3076,16 @@ mod test { #[tokio::test] async fn test_handle_delete_movie_event() { - let resource = format!( - "{}/1?deleteFiles=true&addImportExclusion=true", - RadarrEvent::DeleteMovie(None).resource() - ); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteMovie(None), + Some("/1"), + Some("deleteFiles=true&addImportExclusion=true"), + ) + .await; { let mut app = app_arc.lock().await; app.data.radarr_data.movies.set_items(vec![movie()]); @@ -3037,12 +3106,16 @@ mod test { #[tokio::test] async fn test_handle_delete_movie_event_use_provided_params() { - let resource = format!( - "{}/1?deleteFiles=true&addImportExclusion=true", - RadarrEvent::DeleteMovie(None).resource() - ); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteMovie(None), + Some("/1"), + Some("deleteFiles=true&addImportExclusion=true"), + ) + .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); let delete_movie_params = DeleteMovieParams { id: 1, @@ -3077,12 +3150,14 @@ mod test { }, ]; let expected_request_json = json!({ "ids": [1, 2, 3]}); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Delete, Some(expected_request_json), None, None, - RadarrEvent::ClearBlocklist.resource(), + RadarrEvent::ClearBlocklist, + None, + None, ) .await; app_arc @@ -3104,9 +3179,16 @@ mod test { #[tokio::test] async fn test_handle_delete_blocklist_item_event() { - let resource = format!("{}/1", RadarrEvent::DeleteBlocklistItem(None).resource()); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteBlocklistItem(None), + Some("/1"), + None, + ) + .await; app_arc .lock() .await @@ -3126,9 +3208,16 @@ mod test { #[tokio::test] async fn test_handle_delete_blocklist_item_event_uses_provided_id() { - let resource = format!("{}/1", RadarrEvent::DeleteBlocklistItem(None).resource()); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteBlocklistItem(None), + Some("/1"), + None, + ) + .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network @@ -3141,9 +3230,16 @@ mod test { #[tokio::test] async fn test_handle_delete_download_event() { - let resource = format!("{}/1", RadarrEvent::DeleteDownload(None).resource()); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteDownload(None), + Some("/1"), + None, + ) + .await; app_arc .lock() .await @@ -3163,9 +3259,16 @@ mod test { #[tokio::test] async fn test_handle_delete_download_event_uses_provided_id() { - let resource = format!("{}/1", RadarrEvent::DeleteDownload(None).resource()); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteDownload(None), + Some("/1"), + None, + ) + .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network @@ -3178,9 +3281,16 @@ mod test { #[tokio::test] async fn test_handle_delete_indexer_event() { - let resource = format!("{}/1", RadarrEvent::DeleteIndexer(None).resource()); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteIndexer(None), + Some("/1"), + None, + ) + .await; app_arc .lock() .await @@ -3200,9 +3310,16 @@ mod test { #[tokio::test] async fn test_handle_delete_indexer_event_uses_provided_id() { - let resource = format!("{}/1", RadarrEvent::DeleteIndexer(None).resource()); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteIndexer(None), + Some("/1"), + None, + ) + .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network @@ -3215,9 +3332,16 @@ mod test { #[tokio::test] async fn test_handle_delete_root_folder_event() { - let resource = format!("{}/1", RadarrEvent::DeleteRootFolder(None).resource()); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteRootFolder(None), + Some("/1"), + None, + ) + .await; app_arc .lock() .await @@ -3237,9 +3361,16 @@ mod test { #[tokio::test] async fn test_handle_delete_root_folder_event_uses_provided_id() { - let resource = format!("{}/1", RadarrEvent::DeleteRootFolder(None).resource()); - let (async_server, app_arc, _server) = - mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteRootFolder(None), + Some("/1"), + None, + ) + .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network @@ -3253,7 +3384,7 @@ mod test { #[rstest] #[tokio::test] async fn test_handle_add_movie_event(#[values(true, false)] movie_details_context: bool) { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "tmdbId": 1234, @@ -3270,7 +3401,9 @@ mod test { })), Some(json!({})), None, - RadarrEvent::AddMovie(None).resource(), + RadarrEvent::AddMovie(None), + None, + None, ) .await; @@ -3343,7 +3476,7 @@ mod test { #[tokio::test] async fn test_handle_add_movie_event_uses_provided_body() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "tmdbId": 1234, @@ -3360,7 +3493,9 @@ mod test { })), Some(json!({})), None, - RadarrEvent::AddMovie(None).resource(), + RadarrEvent::AddMovie(None), + None, + None, ) .await; let body = AddMovieBody { @@ -3396,7 +3531,7 @@ mod test { #[tokio::test] async fn test_handle_add_movie_event_reuse_existing_table_if_search_already_performed() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "tmdbId": 5678, @@ -3413,7 +3548,9 @@ mod test { })), Some(json!({})), None, - RadarrEvent::AddMovie(None).resource(), + RadarrEvent::AddMovie(None), + None, + None, ) .await; @@ -3495,14 +3632,16 @@ mod test { #[tokio::test] async fn test_handle_add_root_folder_event() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "path": "/nfs/test" })), Some(json!({})), None, - RadarrEvent::AddRootFolder(None).resource(), + RadarrEvent::AddRootFolder(None), + None, + None, ) .await; @@ -3526,14 +3665,16 @@ mod test { #[tokio::test] async fn test_handle_add_root_folder_event_uses_provided_path() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "path": "/test/test" })), Some(json!({})), None, - RadarrEvent::AddRootFolder(None).resource(), + RadarrEvent::AddRootFolder(None), + None, + None, ) .await; @@ -3567,12 +3708,14 @@ mod test { "whitelistedHardcodedSubs": "", "id": 1 }); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Put, Some(indexer_settings_json), None, None, - RadarrEvent::EditAllIndexerSettings(None).resource(), + RadarrEvent::EditAllIndexerSettings(None), + None, + None, ) .await; @@ -3607,12 +3750,14 @@ mod test { "whitelistedHardcodedSubs": "", "id": 1 }); - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Put, Some(indexer_settings_json), None, None, - RadarrEvent::EditAllIndexerSettings(None).resource(), + RadarrEvent::EditAllIndexerSettings(None), + None, + None, ) .await; @@ -3668,13 +3813,14 @@ mod test { *expected_body.get_mut("rootFolderPath").unwrap() = json!("/nfs/Test Path"); *expected_body.get_mut("searchOnAdd").unwrap() = json!(false); - let resource = format!("{}/123", RadarrEvent::GetCollections.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(detailed_collection_body), None, - &resource, + RadarrEvent::GetCollections, + Some("/123"), + None, ) .await; let async_edit_server = server @@ -3768,13 +3914,14 @@ mod test { *expected_body.get_mut("rootFolderPath").unwrap() = json!("/nfs/Test Path"); *expected_body.get_mut("searchOnAdd").unwrap() = json!(false); - let resource = format!("{}/123", RadarrEvent::GetCollections.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(detailed_collection_body), None, - &resource, + RadarrEvent::GetCollections, + Some("/123"), + None, ) .await; let async_edit_server = server @@ -3851,13 +3998,14 @@ mod test { *expected_body.get_mut("rootFolderPath").unwrap() = json!("/nfs/movies"); *expected_body.get_mut("searchOnAdd").unwrap() = json!(true); - let resource = format!("{}/123", RadarrEvent::GetCollections.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(detailed_collection_body), None, - &resource, + RadarrEvent::GetCollections, + Some("/123"), + None, ) .await; let async_edit_server = server @@ -3938,13 +4086,14 @@ mod test { "id": 1 }); - let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(indexer_details_json), None, - &resource, + RadarrEvent::GetIndexers, + Some("/1"), + None, ) .await; let async_edit_server = server @@ -4030,13 +4179,14 @@ mod test { "id": 1 }); - let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(indexer_details_json), None, - &resource, + RadarrEvent::GetIndexers, + Some("/1"), + None, ) .await; let async_edit_server = server @@ -4138,13 +4288,14 @@ mod test { "id": 1 }); - let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(indexer_details_json), None, - &resource, + RadarrEvent::GetIndexers, + Some("/1"), + None, ) .await; let async_edit_server = server @@ -4266,13 +4417,14 @@ mod test { ..EditIndexerParams::default() }; - let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(indexer_details_json), None, - &resource, + RadarrEvent::GetIndexers, + Some("/1"), + None, ) .await; let async_edit_server = server @@ -4326,13 +4478,14 @@ mod test { ..EditIndexerParams::default() }; - let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(indexer_details_json.clone()), None, - &resource, + RadarrEvent::GetIndexers, + Some("/1"), + None, ) .await; let async_edit_server = server @@ -4411,13 +4564,14 @@ mod test { ..EditIndexerParams::default() }; - let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(indexer_details_json), None, - &resource, + RadarrEvent::GetIndexers, + Some("/1"), + None, ) .await; let async_edit_server = server @@ -4450,13 +4604,14 @@ mod test { *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); *expected_body.get_mut("tags").unwrap() = json!([1, 2]); - let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - &resource, + RadarrEvent::GetMovieDetails(None), + Some("/1"), + None, ) .await; let async_edit_server = server @@ -4516,13 +4671,14 @@ mod test { *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); *expected_body.get_mut("tags").unwrap() = json!([1, 2]); - let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - &resource, + RadarrEvent::GetMovieDetails(None), + Some("/1"), + None, ) .await; let async_edit_server = server @@ -4558,13 +4714,14 @@ mod test { #[tokio::test] async fn test_handle_edit_movie_event_uses_provided_parameters_defaults_to_previous_values() { let expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); - let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - &resource, + RadarrEvent::GetMovieDetails(None), + Some("/1"), + None, ) .await; let async_edit_server = server @@ -4598,13 +4755,14 @@ mod test { let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); *expected_body.get_mut("tags").unwrap() = json!([]); - let resource = format!("{}/1", RadarrEvent::GetMovieDetails(None).resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( + let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - &resource, + RadarrEvent::GetMovieDetails(None), + Some("/1"), + None, ) .await; let async_edit_server = server @@ -4635,7 +4793,7 @@ mod test { #[tokio::test] async fn test_handle_download_release_event() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "guid": "1234", @@ -4644,7 +4802,9 @@ mod test { })), Some(json!({})), None, - RadarrEvent::DownloadRelease(None).resource(), + RadarrEvent::DownloadRelease(None), + None, + None, ) .await; let mut movie_details_modal = MovieDetailsModal::default(); @@ -4671,7 +4831,7 @@ mod test { #[tokio::test] async fn test_handle_download_release_event_uses_provided_params() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "guid": "1234", @@ -4680,7 +4840,9 @@ mod test { })), Some(json!({})), None, - RadarrEvent::DownloadRelease(None).resource(), + RadarrEvent::DownloadRelease(None), + None, + None, ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); @@ -4720,12 +4882,14 @@ mod test { #[tokio::test] async fn test_extract_and_add_tag_ids_vec_add_missing_tags_first() { - let (async_server, app_arc, _server) = mock_radarr_api( + let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ "label": "testing" })), Some(json!({ "id": 3, "label": "testing" })), None, - RadarrEvent::GetTags.resource(), + RadarrEvent::GetTags, + None, + None, ) .await; let tags = "usenet, test, testing".to_owned(); @@ -4769,7 +4933,31 @@ mod test { }]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - assert_eq!(network.extract_movie_id().await, 1); + let (id, movie_id_param) = network.extract_movie_id(None).await; + + assert_eq!(id, 1); + assert_str_eq!(movie_id_param, "movieId=1"); + } + + #[tokio::test] + async fn test_extract_movie_id_uses_provided_id() { + let app_arc = Arc::new(Mutex::new(App::default())); + app_arc + .lock() + .await + .data + .radarr_data + .movies + .set_items(vec![Movie { + id: 1, + ..Movie::default() + }]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + let (id, movie_id_param) = network.extract_movie_id(Some(2)).await; + + assert_eq!(id, 2); + assert_str_eq!(movie_id_param, "movieId=2"); } #[tokio::test] @@ -4783,7 +4971,10 @@ mod test { app_arc.lock().await.data.radarr_data.movies = filtered_movies; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - assert_eq!(network.extract_movie_id().await, 1); + let (id, movie_id_param) = network.extract_movie_id(None).await; + + assert_eq!(id, 1); + assert_str_eq!(movie_id_param, "movieId=1"); } #[tokio::test] @@ -4818,115 +5009,6 @@ mod test { assert_eq!(network.extract_collection_id().await, 1); } - #[tokio::test] - async fn test_append_movie_id_param() { - let app_arc = Arc::new(Mutex::new(App::default())); - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![Movie { - id: 1, - ..Movie::default() - }]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert_str_eq!( - network.append_movie_id_param("/test", None).await, - "/test?movieId=1" - ); - } - - #[tokio::test] - async fn test_append_movie_id_param_uses_provided_id() { - let app_arc = Arc::new(Mutex::new(App::default())); - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![Movie { - id: 1, - ..Movie::default() - }]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert_str_eq!( - network.append_movie_id_param("/test", Some(11)).await, - "/test?movieId=11" - ); - } - - #[tokio::test] - async fn test_radarr_request_props_from_default_radarr_config() { - let app_arc = Arc::new(Mutex::new(App::default())); - let network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let request_props = network - .radarr_request_props_from("/test", RequestMethod::Get, None::<()>) - .await; - - assert_str_eq!(request_props.uri, "http://localhost:7878/api/v3/test"); - assert_eq!(request_props.method, RequestMethod::Get); - assert_eq!(request_props.body, None); - assert!(request_props.api_token.is_empty()); - - app_arc.lock().await.config.radarr = RadarrConfig { - host: Some("192.168.0.123".to_owned()), - port: Some(8080), - api_token: "testToken1234".to_owned(), - ..RadarrConfig::default() - }; - } - - #[tokio::test] - async fn test_radarr_request_props_from_custom_radarr_config() { - let api_token = "testToken1234".to_owned(); - let app_arc = Arc::new(Mutex::new(App::default())); - app_arc.lock().await.config.radarr = RadarrConfig { - host: Some("192.168.0.123".to_owned()), - port: Some(8080), - api_token: api_token.clone(), - ssl_cert_path: Some("/test/cert.crt".to_owned()), - ..RadarrConfig::default() - }; - let network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let request_props = network - .radarr_request_props_from("/test", RequestMethod::Get, None::<()>) - .await; - - assert_str_eq!(request_props.uri, "https://192.168.0.123:8080/api/v3/test"); - assert_eq!(request_props.method, RequestMethod::Get); - assert_eq!(request_props.body, None); - assert_str_eq!(request_props.api_token, api_token); - } - - #[tokio::test] - async fn test_radarr_request_props_from_custom_radarr_config_using_uri_instead_of_host_and_port() - { - let api_token = "testToken1234".to_owned(); - let app_arc = Arc::new(Mutex::new(App::default())); - app_arc.lock().await.config.radarr = RadarrConfig { - uri: Some("https://192.168.0.123:8080".to_owned()), - api_token: api_token.clone(), - ..RadarrConfig::default() - }; - let network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let request_props = network - .radarr_request_props_from("/test", RequestMethod::Get, None::<()>) - .await; - - assert_str_eq!(request_props.uri, "https://192.168.0.123:8080/api/v3/test"); - assert_eq!(request_props.method, RequestMethod::Get); - assert_eq!(request_props.body, None); - assert_str_eq!(request_props.api_token, api_token); - } - #[test] fn test_get_movie_status_downloaded() { assert_str_eq!(get_movie_status(true, &[], 0), "Downloaded"); @@ -4979,52 +5061,6 @@ mod test { ); } - async fn mock_radarr_api( - method: RequestMethod, - request_body: Option, - response_body: Option, - response_status: Option, - resource: &str, - ) -> (Mock, Arc>>, ServerGuard) { - let status = response_status.unwrap_or(200); - let mut server = Server::new_async().await; - let mut async_server = server - .mock( - &method.to_string().to_uppercase(), - format!("/api/v3{resource}").as_str(), - ) - .match_header("X-Api-Key", "test1234") - .with_status(status); - - if let Some(body) = request_body { - async_server = async_server.match_body(Matcher::Json(body)); - } - - if let Some(body) = response_body { - async_server = async_server.with_body(body.to_string()); - } - - async_server = async_server.create_async().await; - - let host = Some(server.host_with_port().split(':').collect::>()[0].to_owned()); - let port = Some( - server.host_with_port().split(':').collect::>()[1] - .parse() - .unwrap(), - ); - let mut app = App::default(); - let radarr_config = RadarrConfig { - host, - port, - api_token: "test1234".to_owned(), - ..RadarrConfig::default() - }; - app.config.radarr = radarr_config; - let app_arc = Arc::new(Mutex::new(app)); - - (async_server, app_arc, server) - } - fn language() -> Language { Language { name: "English".to_owned(), diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs new file mode 100644 index 0000000..f155763 --- /dev/null +++ b/src/network/sonarr_network.rs @@ -0,0 +1,106 @@ +use anyhow::Result; +use log::info; + +use crate::{ + models::{ + servarr_data::sonarr::sonarr_data::ActiveSonarrBlock, + sonarr_models::{Series, SonarrSerdeable, SystemStatus}, + Route, + }, + network::RequestMethod, +}; + +use super::{Network, NetworkEvent, NetworkResource}; +#[cfg(test)] +#[path = "sonarr_network_tests.rs"] +mod sonarr_network_tests; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum SonarrEvent { + GetStatus, + HealthCheck, + ListSeries, +} + +impl NetworkResource for SonarrEvent { + fn resource(&self) -> &'static str { + match &self { + SonarrEvent::GetStatus => "/system/status", + SonarrEvent::HealthCheck => "/health", + SonarrEvent::ListSeries => "/series", + } + } +} + +impl From for NetworkEvent { + fn from(sonarr_event: SonarrEvent) -> Self { + NetworkEvent::Sonarr(sonarr_event) + } +} + +impl<'a, 'b> Network<'a, 'b> { + pub async fn handle_sonarr_event( + &mut self, + sonarr_event: SonarrEvent, + ) -> Result { + match sonarr_event { + SonarrEvent::GetStatus => self.get_sonarr_status().await.map(SonarrSerdeable::from), + SonarrEvent::HealthCheck => self + .get_sonarr_healthcheck() + .await + .map(SonarrSerdeable::from), + SonarrEvent::ListSeries => self.list_series().await.map(SonarrSerdeable::from), + } + } + + async fn get_sonarr_healthcheck(&mut self) -> Result<()> { + info!("Performing Sonarr health check"); + let event = SonarrEvent::HealthCheck; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + async fn list_series(&mut self) -> Result> { + info!("Fetching Sonarr library"); + let event = SonarrEvent::ListSeries; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |mut series_vec, mut app| { + if !matches!( + app.get_current_route(), + Route::Sonarr(ActiveSonarrBlock::SeriesSortPrompt, _) + ) { + series_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app.data.sonarr_data.series.set_items(series_vec); + app.data.sonarr_data.series.apply_sorting_toggle(false); + } + }) + .await + } + + async fn get_sonarr_status(&mut self) -> Result { + info!("Fetching Sonarr system status"); + let event = SonarrEvent::GetStatus; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), SystemStatus>(request_props, |system_status, mut app| { + app.data.sonarr_data.version = system_status.version; + app.data.sonarr_data.start_time = system_status.start_time; + }) + .await + } +} diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs new file mode 100644 index 0000000..0de2bb0 --- /dev/null +++ b/src/network/sonarr_network_tests.rs @@ -0,0 +1,351 @@ +#[cfg(test)] +mod test { + use chrono::{DateTime, Utc}; + use pretty_assertions::{assert_eq, assert_str_eq}; + use reqwest::Client; + use rstest::rstest; + use serde_json::json; + use serde_json::{Number, Value}; + use tokio_util::sync::CancellationToken; + + use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; + use crate::models::sonarr_models::SystemStatus; + use crate::models::{sonarr_models::SonarrSerdeable, stateful_table::SortOption}; + + use crate::{ + models::sonarr_models::{ + Rating, Season, SeasonStatistics, Series, SeriesStatistics, SeriesStatus, SeriesType, + }, + network::{ + network_tests::test_utils::mock_servarr_api, sonarr_network::SonarrEvent, Network, + NetworkEvent, NetworkResource, RequestMethod, + }, + }; + + const SERIES_JSON: &str = r#"{ + "title": "Test", + "status": "continuing", + "ended": false, + "overview": "Blah blah blah", + "network": "HBO", + "seasons": [ + { + "seasonNumber": 1, + "monitored": true, + "statistics": { + "previousAiring": "2022-10-24T01:00:00Z", + "episodeFileCount": 10, + "episodeCount": 10, + "totalEpisodeCount": 10, + "sizeOnDisk": 36708563419, + "percentOfEpisodes": 100.0 + } + } + ], + "year": 2022, + "path": "/nfs/tv/Test", + "qualityProfileId": 6, + "languageProfileId": 1, + "seasonFolder": true, + "monitored": true, + "runtime": 63, + "tvdbId": 371572, + "seriesType": "standard", + "certification": "TV-MA", + "genres": ["cool", "family", "fun"], + "tags": [3], + "ratings": {"votes": 406744, "value": 8.4}, + "statistics": { + "seasonCount": 2, + "episodeFileCount": 18, + "episodeCount": 18, + "totalEpisodeCount": 50, + "sizeOnDisk": 63894022699, + "percentOfEpisodes": 100.0 + }, + "id": 1 + } +"#; + + #[rstest] + fn test_resource_series(#[values(SonarrEvent::ListSeries)] event: SonarrEvent) { + assert_str_eq!(event.resource(), "/series"); + } + + #[rstest] + #[case(SonarrEvent::HealthCheck, "/health")] + #[case(SonarrEvent::GetStatus, "/system/status")] + fn test_resource(#[case] event: SonarrEvent, #[case] expected_uri: String) { + assert_str_eq!(event.resource(), expected_uri); + } + + #[test] + fn test_from_sonarr_event() { + assert_eq!( + NetworkEvent::Sonarr(SonarrEvent::HealthCheck), + NetworkEvent::from(SonarrEvent::HealthCheck) + ); + } + + #[tokio::test] + async fn test_handle_get_sonarr_healthcheck_event() { + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + None, + None, + SonarrEvent::HealthCheck, + None, + None, + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + let _ = network.handle_sonarr_event(SonarrEvent::HealthCheck).await; + + async_server.assert_async().await; + } + + #[rstest] + #[tokio::test] + async fn test_handle_get_series_event(#[values(true, false)] use_custom_sorting: bool) { + let mut series_1: Value = serde_json::from_str(SERIES_JSON).unwrap(); + let mut series_2: Value = serde_json::from_str(SERIES_JSON).unwrap(); + *series_1.get_mut("id").unwrap() = json!(1); + *series_1.get_mut("title").unwrap() = json!("z test"); + *series_2.get_mut("id").unwrap() = json!(2); + *series_2.get_mut("title").unwrap() = json!("A test"); + let expected_series = vec![ + Series { + id: 1, + title: "z test".into(), + ..series() + }, + Series { + id: 2, + title: "A test".into(), + ..series() + }, + ]; + let mut expected_sorted_series = vec![ + Series { + id: 1, + title: "z test".into(), + ..series() + }, + Series { + id: 2, + title: "A test".into(), + ..series() + }, + ]; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!([series_1, series_2])), + None, + SonarrEvent::ListSeries, + None, + None, + ) + .await; + app_arc.lock().await.data.sonarr_data.series.sort_asc = true; + if use_custom_sorting { + let cmp_fn = |a: &Series, b: &Series| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; + expected_sorted_series.sort_by(cmp_fn); + let title_sort_option = SortOption { + name: "Title", + cmp_fn: Some(cmp_fn), + }; + app_arc + .lock() + .await + .data + .sonarr_data + .series + .sorting(vec![title_sort_option]); + } + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::SeriesVec(series) = network + .handle_sonarr_event(SonarrEvent::ListSeries) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.sonarr_data.series.items, + expected_sorted_series + ); + assert!(app_arc.lock().await.data.sonarr_data.series.sort_asc); + assert_eq!(series, expected_series); + } + } + + #[tokio::test] + async fn test_handle_get_series_event_no_op_while_user_is_selecting_sort_options() { + let mut series_1: Value = serde_json::from_str(SERIES_JSON).unwrap(); + let mut series_2: Value = serde_json::from_str(SERIES_JSON).unwrap(); + *series_1.get_mut("id").unwrap() = json!(1); + *series_1.get_mut("title").unwrap() = json!("z test"); + *series_2.get_mut("id").unwrap() = json!(2); + *series_2.get_mut("title").unwrap() = json!("A test"); + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!([series_1, series_2])), + None, + SonarrEvent::ListSeries, + None, + None, + ) + .await; + app_arc + .lock() + .await + .push_navigation_stack(ActiveSonarrBlock::SeriesSortPrompt.into()); + app_arc.lock().await.data.sonarr_data.series.sort_asc = true; + let cmp_fn = |a: &Series, b: &Series| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; + let title_sort_option = SortOption { + name: "Title", + cmp_fn: Some(cmp_fn), + }; + app_arc + .lock() + .await + .data + .sonarr_data + .series + .sorting(vec![title_sort_option]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::ListSeries) + .await + .is_ok()); + + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .sonarr_data + .series + .items + .is_empty()); + assert!(app_arc.lock().await.data.sonarr_data.series.sort_asc); + } + + #[tokio::test] + async fn test_handle_get_status_event() { + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!({ + "version": "v1", + "startTime": "2023-02-25T20:16:43Z" + })), + None, + SonarrEvent::GetStatus, + None, + None, + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap()) + as DateTime; + + if let SonarrSerdeable::SystemStatus(status) = network + .handle_sonarr_event(SonarrEvent::GetStatus) + .await + .unwrap() + { + async_server.assert_async().await; + assert_str_eq!(app_arc.lock().await.data.sonarr_data.version, "v1"); + assert_eq!(app_arc.lock().await.data.sonarr_data.start_time, date_time); + assert_eq!( + status, + SystemStatus { + version: "v1".to_owned(), + start_time: date_time + } + ); + } + } + + fn rating() -> Rating { + Rating { + votes: 406744, + value: 8.4, + } + } + + fn season() -> Season { + Season { + season_number: 1, + monitored: true, + statistics: season_statistics(), + } + } + + fn season_statistics() -> SeasonStatistics { + SeasonStatistics { + previous_airing: Some(DateTime::from( + DateTime::parse_from_rfc3339("2022-10-24T01:00:00Z").unwrap(), + )), + next_airing: None, + episode_file_count: 10, + episode_count: 10, + total_episode_count: 10, + size_on_disk: 36708563419, + percent_of_episodes: 100.0, + } + } + + fn series() -> Series { + Series { + title: "Test".to_owned().into(), + status: SeriesStatus::Continuing, + ended: false, + overview: "Blah blah blah".into(), + network: Some("HBO".to_owned()), + seasons: Some(vec![season()]), + year: 2022, + path: "/nfs/tv/Test".to_owned(), + quality_profile_id: 6, + language_profile_id: 1, + season_folder: true, + monitored: true, + runtime: 63, + tvdb_id: 371572, + series_type: SeriesType::Standard, + certification: Some("TV-MA".to_owned()), + genres: vec!["cool".to_owned(), "family".to_owned(), "fun".to_owned()], + tags: vec![Number::from(3)], + ratings: rating(), + statistics: Some(series_statistics()), + id: 1, + } + } + + fn series_statistics() -> SeriesStatistics { + SeriesStatistics { + season_count: 2, + episode_file_count: 18, + episode_count: 18, + total_episode_count: 50, + size_on_disk: 63894022699, + percent_of_episodes: 100.0, + } + } +}