diff --git a/src/cli/sonarr/download_command_handler.rs b/src/cli/sonarr/download_command_handler.rs index 88055b8..f990ab2 100644 --- a/src/cli/sonarr/download_command_handler.rs +++ b/src/cli/sonarr/download_command_handler.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use anyhow::Result; use clap::Subcommand; use tokio::sync::Mutex; @@ -106,7 +107,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDownloadCommand> } } - async fn handle(self) -> anyhow::Result { + async fn handle(self) -> Result { let result = match self.command { SonarrDownloadCommand::Series { guid, diff --git a/src/cli/sonarr/manual_search_command_handler.rs b/src/cli/sonarr/manual_search_command_handler.rs new file mode 100644 index 0000000..5164526 --- /dev/null +++ b/src/cli/sonarr/manual_search_command_handler.rs @@ -0,0 +1,99 @@ +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 = "manual_search_command_handler_tests.rs"] +mod manual_search_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum SonarrManualSearchCommand { + #[command(about = "Trigger a manual search of releases for the episode with the given ID")] + Episode { + #[arg( + long, + help = "The Sonarr ID of the episode whose releases you wish to fetch and list", + required = true + )] + episode_id: i64, + }, + #[command( + about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID" + )] + Season { + #[arg( + long, + help = "The Sonarr ID of the series whose releases you wish to fetch and list", + required = true + )] + series_id: i64, + #[arg(long, help = "The season number to search for", required = true)] + season_number: i64, + }, +} + +impl From for Command { + fn from(value: SonarrManualSearchCommand) -> Self { + Command::Sonarr(SonarrCommand::ManualSearch(value)) + } +} + +pub(super) struct SonarrManualSearchCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: SonarrManualSearchCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand> + for SonarrManualSearchCommandHandler<'a, 'b> +{ + fn with( + _app: &'a Arc>>, + command: SonarrManualSearchCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + SonarrManualSearchCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + SonarrManualSearchCommand::Episode { episode_id } => { + println!("Searching for episode releases. This may take a minute..."); + let resp = self + .network + .handle_network_event(SonarrEvent::GetEpisodeReleases(Some(episode_id)).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + SonarrManualSearchCommand::Season { + series_id, + season_number, + } => { + println!("Searching for season releases. This may take a minute..."); + let resp = self + .network + .handle_network_event( + SonarrEvent::GetSeasonReleases(Some((series_id, season_number))).into(), + ) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/sonarr/manual_search_command_handler_tests.rs b/src/cli/sonarr/manual_search_command_handler_tests.rs new file mode 100644 index 0000000..26dc760 --- /dev/null +++ b/src/cli/sonarr/manual_search_command_handler_tests.rs @@ -0,0 +1,189 @@ +#[cfg(test)] +mod tests { + use crate::cli::{ + sonarr::{manual_search_command_handler::SonarrManualSearchCommand, SonarrCommand}, + Command, + }; + use crate::Cli; + use clap::CommandFactory; + use pretty_assertions::assert_eq; + + #[test] + fn test_sonarr_manual_search_command_from() { + let command = SonarrManualSearchCommand::Episode { episode_id: 1 }; + + let result = Command::from(command.clone()); + + assert_eq!( + result, + Command::Sonarr(SonarrCommand::ManualSearch(command)) + ); + } + + mod cli { + use super::*; + use clap::error::ErrorKind; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_manual_season_search_requires_series_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "manual-search", + "season", + "--season-number", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[rstest] + fn test_manual_season_search_requires_season_number() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "manual-search", + "season", + "--series-id", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_manual_season_search_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "manual-search", + "season", + "--series-id", + "1", + "--season-number", + "1", + ]); + + assert!(result.is_ok()); + } + + #[test] + fn test_manual_episode_search_requires_episode_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "sonarr", "manual-search", "episode"]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_manual_episode_search_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "manual-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::manual_search_command_handler::{ + SonarrManualSearchCommand, SonarrManualSearchCommandHandler, + }, + CliCommandHandler, + }, + models::{sonarr_models::SonarrSerdeable, Serdeable}, + network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent}, + }; + + #[tokio::test] + async fn test_manual_episode_search_command() { + let expected_episode_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::GetEpisodeReleases(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 = SonarrManualSearchCommand::Episode { episode_id: 1 }; + + let result = SonarrManualSearchCommandHandler::with( + &app_arc, + manual_episode_search_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_manual_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::GetSeasonReleases(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 manual_season_search_command = SonarrManualSearchCommand::Season { + series_id: 1, + season_number: 1, + }; + + let result = SonarrManualSearchCommandHandler::with( + &app_arc, + manual_season_search_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + } +} diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index bc1fdcd..8ed7ecc 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -7,6 +7,7 @@ use delete_command_handler::{SonarrDeleteCommand, SonarrDeleteCommandHandler}; use download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler}; use get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler}; use list_command_handler::{SonarrListCommand, SonarrListCommandHandler}; +use manual_search_command_handler::{SonarrManualSearchCommand, SonarrManualSearchCommandHandler}; use refresh_command_handler::{SonarrRefreshCommand, SonarrRefreshCommandHandler}; use tokio::sync::Mutex; @@ -23,6 +24,7 @@ mod delete_command_handler; mod download_command_handler; mod get_command_handler; mod list_command_handler; +mod manual_search_command_handler; mod refresh_command_handler; #[cfg(test)] @@ -61,6 +63,8 @@ pub enum SonarrCommand { about = "Commands to refresh the data in your Sonarr instance" )] Refresh(SonarrRefreshCommand), + #[command(subcommand, about = "Commands to manually search for releases")] + ManualSearch(SonarrManualSearchCommand), #[command(about = "Clear the blocklist")] ClearBlocklist, #[command(about = "Mark the Sonarr history item with the given ID as 'failed'")] @@ -72,28 +76,6 @@ pub enum SonarrCommand { )] history_item_id: i64, }, - #[command(about = "Trigger a manual search of releases for the episode with the given ID")] - ManualEpisodeSearch { - #[arg( - long, - help = "The Sonarr ID of the episode whose releases you wish to fetch and list", - required = true - )] - episode_id: i64, - }, - #[command( - about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID" - )] - ManualSeasonSearch { - #[arg( - long, - help = "The Sonarr ID of the series whose releases you wish to fetch and list", - required = true - )] - series_id: i64, - #[arg(long, help = "The season number to search for", required = true)] - season_number: i64, - }, #[command(about = "Start the specified Sonarr task")] StartTask { #[arg( @@ -198,8 +180,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' .handle() .await? } - SonarrCommand::Refresh(update_command) => { - SonarrRefreshCommandHandler::with(self.app, update_command, self.network) + SonarrCommand::Refresh(refresh_command) => { + SonarrRefreshCommandHandler::with(self.app, refresh_command, self.network) + .handle() + .await? + } + SonarrCommand::ManualSearch(manual_search_command) => { + SonarrManualSearchCommandHandler::with(self.app, manual_search_command, self.network) .handle() .await? } @@ -221,27 +208,6 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' .await?; "Sonarr history item marked as 'failed'".to_owned() } - SonarrCommand::ManualEpisodeSearch { episode_id } => { - println!("Searching for episode releases. This may take a minute..."); - let resp = self - .network - .handle_network_event(SonarrEvent::GetEpisodeReleases(Some(episode_id)).into()) - .await?; - serde_json::to_string_pretty(&resp)? - } - SonarrCommand::ManualSeasonSearch { - series_id, - season_number, - } => { - println!("Searching for season releases. This may take a minute..."); - let resp = self - .network - .handle_network_event( - SonarrEvent::GetSeasonReleases(Some((series_id, season_number))).into(), - ) - .await?; - serde_json::to_string_pretty(&resp)? - } SonarrCommand::StartTask { task_name } => { let resp = self .network diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index 9a98496..ef68518 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -32,7 +32,7 @@ mod tests { assert!(result.is_ok()); } - #[rstest] + #[test] fn test_mark_history_item_as_failed_requires_history_item_id() { let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "mark-history-item-as-failed"]); @@ -44,7 +44,7 @@ mod tests { ); } - #[rstest] + #[test] fn test_mark_history_item_as_failed_requirements_satisfied() { let result = Cli::command().try_get_matches_from([ "managarr", @@ -57,81 +57,7 @@ mod tests { assert!(result.is_ok()); } - #[rstest] - fn test_manual_season_search_requires_series_id() { - let result = Cli::command().try_get_matches_from([ - "managarr", - "sonarr", - "manual-season-search", - "--season-number", - "1", - ]); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().kind(), - ErrorKind::MissingRequiredArgument - ); - } - - #[rstest] - fn test_manual_season_search_requires_season_number() { - let result = Cli::command().try_get_matches_from([ - "managarr", - "sonarr", - "manual-season-search", - "--series-id", - "1", - ]); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().kind(), - ErrorKind::MissingRequiredArgument - ); - } - #[test] - fn test_manual_season_search_requirements_satisfied() { - let result = Cli::command().try_get_matches_from([ - "managarr", - "sonarr", - "manual-season-search", - "--series-id", - "1", - "--season-number", - "1", - ]); - - assert!(result.is_ok()); - } - - #[rstest] - fn test_manual_episode_search_requires_episode_id() { - let result = - Cli::command().try_get_matches_from(["managarr", "sonarr", "manual-episode-search"]); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().kind(), - ErrorKind::MissingRequiredArgument - ); - } - - #[test] - fn test_manual_episode_search_requirements_satisfied() { - let result = Cli::command().try_get_matches_from([ - "managarr", - "sonarr", - "manual-episode-search", - "--episode-id", - "1", - ]); - - assert!(result.is_ok()); - } - - #[rstest] fn test_start_task_requires_task_name() { let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "start-task"]); @@ -142,7 +68,7 @@ mod tests { ); } - #[rstest] + #[test] fn test_start_task_task_name_validation() { let result = Cli::command().try_get_matches_from([ "managarr", @@ -169,7 +95,7 @@ mod tests { assert!(result.is_ok()); } - #[rstest] + #[test] fn test_test_indexer_requires_indexer_id() { let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "test-indexer"]); @@ -193,7 +119,7 @@ mod tests { assert!(result.is_ok()); } - #[rstest] + #[test] fn test_trigger_automatic_series_search_requires_series_id() { let result = Cli::command().try_get_matches_from([ "managarr", @@ -221,7 +147,7 @@ mod tests { assert!(result.is_ok()); } - #[rstest] + #[test] fn test_trigger_automatic_season_search_requires_series_id() { let result = Cli::command().try_get_matches_from([ "managarr", @@ -238,7 +164,7 @@ mod tests { ); } - #[rstest] + #[test] fn test_trigger_automatic_season_search_requires_season_number() { let result = Cli::command().try_get_matches_from([ "managarr", @@ -270,7 +196,7 @@ mod tests { assert!(result.is_ok()); } - #[rstest] + #[test] fn test_trigger_automatic_episode_search_requires_episode_id() { let result = Cli::command().try_get_matches_from([ "managarr", @@ -390,62 +316,6 @@ mod tests { assert!(result.is_ok()); } - #[tokio::test] - async fn test_manual_episode_search_command() { - let expected_episode_id = 1; - let mut mock_network = MockNetworkTrait::new(); - mock_network - .expect_handle_network_event() - .with(eq::( - SonarrEvent::GetEpisodeReleases(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::ManualEpisodeSearch { 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_manual_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::GetSeasonReleases(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 manual_season_search_command = SonarrCommand::ManualSeasonSearch { - series_id: 1, - season_number: 1, - }; - - let result = - SonarrCliHandler::with(&app_arc, manual_season_search_command, &mut mock_network) - .handle() - .await; - - assert!(result.is_ok()); - } - #[tokio::test] async fn test_sonarr_cli_handler_delegates_add_commands_to_the_add_command_handler() { let expected_tag_name = "test".to_owned();