From 3be9321df61aa6489453339454bbd39fc5f44153 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 22 Nov 2024 20:42:34 -0700 Subject: [PATCH] refactor(cli): the trigger-automatic-search commands now all have their own dedicated subcommand to keep things cleaner. Now they look like 'trigger-automatic-search episode/series/season' and their corresponding flags --- src/cli/sonarr/mod.rs | 75 ++--- src/cli/sonarr/sonarr_command_tests.rs | 236 +++------------- ...rigger_automatic_search_command_handler.rs | 113 ++++++++ ..._automatic_search_command_handler_tests.rs | 259 ++++++++++++++++++ 4 files changed, 423 insertions(+), 260 deletions(-) create mode 100644 src/cli/sonarr/trigger_automatic_search_command_handler.rs create mode 100644 src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index 8ed7ecc..573697b 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -10,6 +10,9 @@ use list_command_handler::{SonarrListCommand, SonarrListCommandHandler}; use manual_search_command_handler::{SonarrManualSearchCommand, SonarrManualSearchCommandHandler}; use refresh_command_handler::{SonarrRefreshCommand, SonarrRefreshCommandHandler}; use tokio::sync::Mutex; +use trigger_automatic_search_command_handler::{ + SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler, +}; use crate::{ app::App, @@ -26,6 +29,7 @@ mod get_command_handler; mod list_command_handler; mod manual_search_command_handler; mod refresh_command_handler; +mod trigger_automatic_search_command_handler; #[cfg(test)] #[path = "sonarr_command_tests.rs"] @@ -65,6 +69,11 @@ pub enum SonarrCommand { Refresh(SonarrRefreshCommand), #[command(subcommand, about = "Commands to manually search for releases")] ManualSearch(SonarrManualSearchCommand), + #[command( + subcommand, + about = "Commands to trigger automatic searches for releases of different resources in your Sonarr instance" + )] + TriggerAutomaticSearch(SonarrTriggerAutomaticSearchCommand), #[command(about = "Clear the blocklist")] ClearBlocklist, #[command(about = "Mark the Sonarr history item with the given ID as 'failed'")] @@ -95,37 +104,6 @@ pub enum SonarrCommand { }, #[command(about = "Test all Radarr indexers")] TestAllIndexers, - #[command(about = "Trigger an automatic search for the series with the specified ID")] - TriggerAutomaticSeriesSearch { - #[arg( - long, - help = "The ID of the series you want to trigger an automatic search for", - required = true - )] - series_id: i64, - }, - #[command( - about = "Trigger an automatic search for the given season corresponding to the series with the given ID" - )] - TriggerAutomaticSeasonSearch { - #[arg( - long, - help = "The Sonarr ID of the series whose season you wish to trigger an automatic search for", - required = true - )] - series_id: i64, - #[arg(long, help = "The season number to search for", required = true)] - season_number: i64, - }, - #[command(about = "Trigger an automatic search for the episode with the specified ID")] - TriggerAutomaticEpisodeSearch { - #[arg( - long, - help = "The ID of the episode you want to trigger an automatic search for", - required = true - )] - episode_id: i64, - }, } impl From for Command { @@ -190,6 +168,15 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' .handle() .await? } + SonarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => { + SonarrTriggerAutomaticSearchCommandHandler::with( + self.app, + trigger_automatic_search_command, + self.network, + ) + .handle() + .await? + } SonarrCommand::ClearBlocklist => { self .network @@ -230,32 +217,6 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' .await?; serde_json::to_string_pretty(&resp)? } - SonarrCommand::TriggerAutomaticSeriesSearch { series_id } => { - let resp = self - .network - .handle_network_event(SonarrEvent::TriggerAutomaticSeriesSearch(Some(series_id)).into()) - .await?; - serde_json::to_string_pretty(&resp)? - } - SonarrCommand::TriggerAutomaticSeasonSearch { - series_id, - season_number, - } => { - let resp = self - .network - .handle_network_event( - SonarrEvent::TriggerAutomaticSeasonSearch(Some((series_id, season_number))).into(), - ) - .await?; - serde_json::to_string_pretty(&resp)? - } - SonarrCommand::TriggerAutomaticEpisodeSearch { episode_id } => { - let resp = self - .network - .handle_network_event(SonarrEvent::TriggerAutomaticEpisodeSearch(Some(episode_id)).into()) - .await?; - serde_json::to_string_pretty(&resp)? - } }; Ok(result) diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index 6a23746..822aa69 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -118,111 +118,6 @@ mod tests { assert!(result.is_ok()); } - - #[test] - fn test_trigger_automatic_series_search_requires_series_id() { - let result = Cli::command().try_get_matches_from([ - "managarr", - "sonarr", - "trigger-automatic-series-search", - ]); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().kind(), - ErrorKind::MissingRequiredArgument - ); - } - - #[test] - fn test_trigger_automatic_series_search_requirements_satisfied() { - let result = Cli::command().try_get_matches_from([ - "managarr", - "sonarr", - "trigger-automatic-series-search", - "--series-id", - "1", - ]); - - assert!(result.is_ok()); - } - - #[test] - fn test_trigger_automatic_season_search_requires_series_id() { - let result = Cli::command().try_get_matches_from([ - "managarr", - "sonarr", - "trigger-automatic-season-search", - "--season-number", - "1", - ]); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().kind(), - ErrorKind::MissingRequiredArgument - ); - } - - #[test] - fn test_trigger_automatic_season_search_requires_season_number() { - let result = Cli::command().try_get_matches_from([ - "managarr", - "sonarr", - "trigger-automatic-season-search", - "--series-id", - "1", - ]); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().kind(), - ErrorKind::MissingRequiredArgument - ); - } - - #[test] - fn test_trigger_automatic_season_search_requirements_satisfied() { - let result = Cli::command().try_get_matches_from([ - "managarr", - "sonarr", - "trigger-automatic-season-search", - "--series-id", - "1", - "--season-number", - "1", - ]); - - assert!(result.is_ok()); - } - - #[test] - fn test_trigger_automatic_episode_search_requires_episode_id() { - let result = Cli::command().try_get_matches_from([ - "managarr", - "sonarr", - "trigger-automatic-episode-search", - ]); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().kind(), - ErrorKind::MissingRequiredArgument - ); - } - - #[test] - fn test_trigger_automatic_episode_search_requirements_satisfied() { - let result = Cli::command().try_get_matches_from([ - "managarr", - "sonarr", - "trigger-automatic-episode-search", - "--episode-id", - "1", - ]); - - assert!(result.is_ok()); - } } mod handler { @@ -240,7 +135,9 @@ mod tests { download_command_handler::SonarrDownloadCommand, get_command_handler::SonarrGetCommand, list_command_handler::SonarrListCommand, manual_search_command_handler::SonarrManualSearchCommand, - refresh_command_handler::SonarrRefreshCommand, SonarrCliHandler, SonarrCommand, + refresh_command_handler::SonarrRefreshCommand, + trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand, + SonarrCliHandler, SonarrCommand, }, CliCommandHandler, }, @@ -437,6 +334,36 @@ mod tests { assert!(result.is_ok()); } + #[tokio::test] + async fn test_sonarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler( + ) { + let expected_episode_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let manual_episode_search_command = + SonarrCommand::TriggerAutomaticSearch(SonarrTriggerAutomaticSearchCommand::Episode { + episode_id: 1, + }); + + let result = + SonarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + #[tokio::test] async fn test_sonarr_cli_handler_delegates_get_commands_to_the_get_command_handler() { let mut mock_network = MockNetworkTrait::new(); @@ -580,102 +507,5 @@ mod tests { assert!(result.is_ok()); } - - #[tokio::test] - async fn test_trigger_automatic_series_search_command() { - let expected_series_id = 1; - let mut mock_network = MockNetworkTrait::new(); - mock_network - .expect_handle_network_event() - .with(eq::( - SonarrEvent::TriggerAutomaticSeriesSearch(Some(expected_series_id)).into(), - )) - .times(1) - .returning(|_| { - Ok(Serdeable::Sonarr(SonarrSerdeable::Value( - json!({"testResponse": "response"}), - ))) - }); - let app_arc = Arc::new(Mutex::new(App::default())); - let trigger_automatic_series_search_command = - SonarrCommand::TriggerAutomaticSeriesSearch { series_id: 1 }; - - let result = SonarrCliHandler::with( - &app_arc, - trigger_automatic_series_search_command, - &mut mock_network, - ) - .handle() - .await; - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_trigger_automatic_season_search_command() { - let expected_series_id = 1; - let expected_season_number = 1; - let mut mock_network = MockNetworkTrait::new(); - mock_network - .expect_handle_network_event() - .with(eq::( - SonarrEvent::TriggerAutomaticSeasonSearch(Some(( - expected_series_id, - expected_season_number, - ))) - .into(), - )) - .times(1) - .returning(|_| { - Ok(Serdeable::Sonarr(SonarrSerdeable::Value( - json!({"testResponse": "response"}), - ))) - }); - let app_arc = Arc::new(Mutex::new(App::default())); - let trigger_automatic_season_search_command = SonarrCommand::TriggerAutomaticSeasonSearch { - series_id: 1, - season_number: 1, - }; - - let result = SonarrCliHandler::with( - &app_arc, - trigger_automatic_season_search_command, - &mut mock_network, - ) - .handle() - .await; - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_trigger_automatic_episode_search_command() { - let expected_episode_id = 1; - let mut mock_network = MockNetworkTrait::new(); - mock_network - .expect_handle_network_event() - .with(eq::( - SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(), - )) - .times(1) - .returning(|_| { - Ok(Serdeable::Sonarr(SonarrSerdeable::Value( - json!({"testResponse": "response"}), - ))) - }); - let app_arc = Arc::new(Mutex::new(App::default())); - let trigger_automatic_episode_search_command = - SonarrCommand::TriggerAutomaticEpisodeSearch { episode_id: 1 }; - - let result = SonarrCliHandler::with( - &app_arc, - trigger_automatic_episode_search_command, - &mut mock_network, - ) - .handle() - .await; - - assert!(result.is_ok()); - } } } diff --git a/src/cli/sonarr/trigger_automatic_search_command_handler.rs b/src/cli/sonarr/trigger_automatic_search_command_handler.rs new file mode 100644 index 0000000..e87a5a5 --- /dev/null +++ b/src/cli/sonarr/trigger_automatic_search_command_handler.rs @@ -0,0 +1,113 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::Subcommand; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + network::{sonarr_network::SonarrEvent, NetworkTrait}, +}; + +use super::SonarrCommand; + +#[cfg(test)] +#[path = "trigger_automatic_search_command_handler_tests.rs"] +mod trigger_automatic_search_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum SonarrTriggerAutomaticSearchCommand { + #[command(about = "Trigger an automatic search for the series with the specified ID")] + Series { + #[arg( + long, + help = "The ID of the series you want to trigger an automatic search for", + required = true + )] + series_id: i64, + }, + #[command( + about = "Trigger an automatic search for the given season corresponding to the series with the given ID" + )] + Season { + #[arg( + long, + help = "The Sonarr ID of the series whose season you wish to trigger an automatic search for", + required = true + )] + series_id: i64, + #[arg(long, help = "The season number to search for", required = true)] + season_number: i64, + }, + #[command(about = "Trigger an automatic search for the episode with the specified ID")] + Episode { + #[arg( + long, + help = "The ID of the episode you want to trigger an automatic search for", + required = true + )] + episode_id: i64, + }, +} + +impl From for Command { + fn from(value: SonarrTriggerAutomaticSearchCommand) -> Self { + Command::Sonarr(SonarrCommand::TriggerAutomaticSearch(value)) + } +} + +pub(super) struct SonarrTriggerAutomaticSearchCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: SonarrTriggerAutomaticSearchCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand> + for SonarrTriggerAutomaticSearchCommandHandler<'a, 'b> +{ + fn with( + _app: &'a Arc>>, + command: SonarrTriggerAutomaticSearchCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + SonarrTriggerAutomaticSearchCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + SonarrTriggerAutomaticSearchCommand::Series { series_id } => { + let resp = self + .network + .handle_network_event(SonarrEvent::TriggerAutomaticSeriesSearch(Some(series_id)).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + SonarrTriggerAutomaticSearchCommand::Season { + series_id, + season_number, + } => { + let resp = self + .network + .handle_network_event( + SonarrEvent::TriggerAutomaticSeasonSearch(Some((series_id, season_number))).into(), + ) + .await?; + serde_json::to_string_pretty(&resp)? + } + SonarrTriggerAutomaticSearchCommand::Episode { episode_id } => { + let resp = self + .network + .handle_network_event(SonarrEvent::TriggerAutomaticEpisodeSearch(Some(episode_id)).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs b/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs new file mode 100644 index 0000000..03c4057 --- /dev/null +++ b/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs @@ -0,0 +1,259 @@ +#[cfg(test)] +mod tests { + use crate::cli::{ + sonarr::{ + trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand, SonarrCommand, + }, + Command, + }; + use crate::Cli; + use clap::CommandFactory; + use pretty_assertions::assert_eq; + + #[test] + fn test_sonarr_trigger_automatic_search_command_from() { + let command = SonarrTriggerAutomaticSearchCommand::Episode { episode_id: 1 }; + + let result = Command::from(command.clone()); + + assert_eq!( + result, + Command::Sonarr(SonarrCommand::TriggerAutomaticSearch(command)) + ); + } + + mod cli { + use super::*; + use clap::error::ErrorKind; + use pretty_assertions::assert_eq; + + #[test] + fn test_trigger_automatic_series_search_requires_series_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "trigger-automatic-search", + "series", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_trigger_automatic_series_search_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "trigger-automatic-search", + "series", + "--series-id", + "1", + ]); + + assert!(result.is_ok()); + } + + #[test] + fn test_trigger_automatic_season_search_requires_series_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "trigger-automatic-search", + "season", + "--season-number", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_trigger_automatic_season_search_requires_season_number() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "trigger-automatic-search", + "season", + "--series-id", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_trigger_automatic_season_search_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "trigger-automatic-search", + "season", + "--series-id", + "1", + "--season-number", + "1", + ]); + + assert!(result.is_ok()); + } + + #[test] + fn test_trigger_automatic_episode_search_requires_episode_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "trigger-automatic-search", + "episode", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_trigger_automatic_episode_search_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "trigger-automatic-search", + "episode", + "--episode-id", + "1", + ]); + + 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::trigger_automatic_search_command_handler::{ + SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler, + }, + CliCommandHandler, + }, + models::{sonarr_models::SonarrSerdeable, Serdeable}, + network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent}, + }; + + #[tokio::test] + async fn test_trigger_automatic_series_search_command() { + let expected_series_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::TriggerAutomaticSeriesSearch(Some(expected_series_id)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let trigger_automatic_series_search_command = + SonarrTriggerAutomaticSearchCommand::Series { series_id: 1 }; + + let result = SonarrTriggerAutomaticSearchCommandHandler::with( + &app_arc, + trigger_automatic_series_search_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_trigger_automatic_season_search_command() { + let expected_series_id = 1; + let expected_season_number = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::TriggerAutomaticSeasonSearch(Some(( + expected_series_id, + expected_season_number, + ))) + .into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let trigger_automatic_season_search_command = SonarrTriggerAutomaticSearchCommand::Season { + series_id: 1, + season_number: 1, + }; + + let result = SonarrTriggerAutomaticSearchCommandHandler::with( + &app_arc, + trigger_automatic_season_search_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_trigger_automatic_episode_search_command() { + let expected_episode_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let trigger_automatic_episode_search_command = + SonarrTriggerAutomaticSearchCommand::Episode { episode_id: 1 }; + + let result = SonarrTriggerAutomaticSearchCommandHandler::with( + &app_arc, + trigger_automatic_episode_search_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + } +}