diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index 88e541b..5fd14a1 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -31,7 +31,7 @@ mod tests { assert!(result.is_ok()); } - #[rstest] + #[test] fn test_download_release_requires_movie_id() { let result = Cli::command().try_get_matches_from([ "managarr", @@ -50,7 +50,7 @@ mod tests { ); } - #[rstest] + #[test] fn test_download_release_requires_guid() { let result = Cli::command().try_get_matches_from([ "managarr", @@ -69,7 +69,7 @@ mod tests { ); } - #[rstest] + #[test] fn test_download_release_requires_indexer_id() { let result = Cli::command().try_get_matches_from([ "managarr", @@ -105,7 +105,7 @@ mod tests { assert!(result.is_ok()); } - #[rstest] + #[test] fn test_manual_search_requires_movie_id() { let result = Cli::command().try_get_matches_from(["managarr", "radarr", "manual-search"]); @@ -164,7 +164,7 @@ mod tests { ); } - #[rstest] + #[test] fn test_start_task_task_name_validation() { let result = Cli::command().try_get_matches_from([ "managarr", @@ -191,7 +191,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", "radarr", "test-indexer"]); @@ -215,7 +215,7 @@ mod tests { assert!(result.is_ok()); } - #[rstest] + #[test] fn test_trigger_automatic_search_requires_movie_id() { let result = Cli::command().try_get_matches_from(["managarr", "radarr", "trigger-automatic-search"]); diff --git a/src/cli/sonarr/edit_command_handler.rs b/src/cli/sonarr/edit_command_handler.rs new file mode 100644 index 0000000..fe04b26 --- /dev/null +++ b/src/cli/sonarr/edit_command_handler.rs @@ -0,0 +1,120 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::{ArgGroup, Subcommand}; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + models::{ + sonarr_models::{IndexerSettings, SonarrSerdeable}, + Serdeable, + }, + network::{sonarr_network::SonarrEvent, NetworkTrait}, +}; + +use super::SonarrCommand; + +#[cfg(test)] +#[path = "edit_command_handler_tests.rs"] +mod edit_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum SonarrEditCommand { + #[command( + about = "Edit and indexer settings that apply to all indexers", + group( + ArgGroup::new("edit_settings") + .args([ + "maximum_size", + "minimum_age", + "retention", + "rss_sync_interval", + ]).required(true) + .multiple(true)) + )] + AllIndexerSettings { + #[arg( + long, + help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited" + )] + maximum_size: Option, + #[arg( + long, + help = "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider." + )] + minimum_age: Option, + #[arg( + long, + help = "Usenet only: The retention time in days to retain releases. Set to zero to set for unlimited retention" + )] + retention: Option, + #[arg( + long, + help = "The RSS sync interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)" + )] + rss_sync_interval: Option, + }, +} + +impl From for Command { + fn from(value: SonarrEditCommand) -> Self { + Command::Sonarr(SonarrCommand::Edit(value)) + } +} + +pub(super) struct SonarrEditCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: SonarrEditCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandHandler<'a, 'b> { + fn with( + _app: &'a Arc>>, + command: SonarrEditCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + SonarrEditCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + SonarrEditCommand::AllIndexerSettings { + maximum_size, + minimum_age, + retention, + rss_sync_interval, + } => { + if let Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(previous_indexer_settings)) = self + .network + .handle_network_event(SonarrEvent::GetAllIndexerSettings.into()) + .await? + { + let params = IndexerSettings { + id: 1, + maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size), + minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age), + retention: retention.unwrap_or(previous_indexer_settings.retention), + rss_sync_interval: rss_sync_interval + .unwrap_or(previous_indexer_settings.rss_sync_interval), + }; + self + .network + .handle_network_event(SonarrEvent::EditAllIndexerSettings(Some(params)).into()) + .await?; + "All indexer settings updated".to_owned() + } else { + String::new() + } + } + }; + + Ok(result) + } +} diff --git a/src/cli/sonarr/edit_command_handler_tests.rs b/src/cli/sonarr/edit_command_handler_tests.rs new file mode 100644 index 0000000..e8bb470 --- /dev/null +++ b/src/cli/sonarr/edit_command_handler_tests.rs @@ -0,0 +1,196 @@ +#[cfg(test)] +mod tests { + use crate::cli::{ + sonarr::{edit_command_handler::SonarrEditCommand, SonarrCommand}, + Command, + }; + + #[test] + fn test_sonarr_edit_command_from() { + let command = SonarrEditCommand::AllIndexerSettings { + maximum_size: None, + minimum_age: None, + retention: None, + rss_sync_interval: None, + }; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Sonarr(SonarrCommand::Edit(command))); + } + + mod cli { + use crate::Cli; + + use super::*; + use clap::{error::ErrorKind, CommandFactory, Parser}; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[test] + fn test_edit_all_indexer_settings_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "all-indexer-settings"]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[rstest] + fn test_edit_all_indexer_settings_assert_argument_flags_require_args( + #[values( + "--maximum-size", + "--minimum-age", + "--retention", + "--rss-sync-interval" + )] + flag: &str, + ) { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "all-indexer-settings", + flag, + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_all_indexer_settings_only_requires_at_least_one_argument() { + let expected_args = SonarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: None, + retention: None, + rss_sync_interval: None, + }; + let result = Cli::try_parse_from([ + "managarr", + "sonarr", + "edit", + "all-indexer-settings", + "--maximum-size", + "1", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + + #[test] + fn test_edit_all_indexer_settings_all_arguments_defined() { + let expected_args = SonarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: Some(1), + retention: Some(1), + rss_sync_interval: Some(1), + }; + let result = Cli::try_parse_from([ + "managarr", + "sonarr", + "edit", + "all-indexer-settings", + "--maximum-size", + "1", + "--minimum-age", + "1", + "--retention", + "1", + "--rss-sync-interval", + "1", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::{ + app::App, + cli::{ + sonarr::edit_command_handler::{SonarrEditCommand, SonarrEditCommandHandler}, + CliCommandHandler, + }, + models::{ + sonarr_models::{IndexerSettings, SonarrSerdeable}, + Serdeable, + }, + network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent}, + }; + + #[tokio::test] + async fn test_handle_edit_all_indexer_settings_command() { + let expected_edit_all_indexer_settings = IndexerSettings { + id: 1, + maximum_size: 1, + minimum_age: 1, + retention: 1, + rss_sync_interval: 1, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::GetAllIndexerSettings.into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::IndexerSettings( + IndexerSettings { + id: 1, + maximum_size: 2, + minimum_age: 2, + retention: 2, + rss_sync_interval: 2, + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_all_indexer_settings_command = SonarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: Some(1), + retention: Some(1), + rss_sync_interval: Some(1), + }; + + let result = SonarrEditCommandHandler::with( + &app_arc, + edit_all_indexer_settings_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + } +} diff --git a/src/cli/sonarr/manual_search_command_handler_tests.rs b/src/cli/sonarr/manual_search_command_handler_tests.rs index 26dc760..53b26ad 100644 --- a/src/cli/sonarr/manual_search_command_handler_tests.rs +++ b/src/cli/sonarr/manual_search_command_handler_tests.rs @@ -24,9 +24,8 @@ mod tests { use super::*; use clap::error::ErrorKind; use pretty_assertions::assert_eq; - use rstest::rstest; - #[rstest] + #[test] fn test_manual_season_search_requires_series_id() { let result = Cli::command().try_get_matches_from([ "managarr", @@ -44,7 +43,7 @@ mod tests { ); } - #[rstest] + #[test] fn test_manual_season_search_requires_season_number() { let result = Cli::command().try_get_matches_from([ "managarr", diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index 9d9818d..1cf214d 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -5,6 +5,7 @@ use anyhow::Result; use clap::Subcommand; use delete_command_handler::{SonarrDeleteCommand, SonarrDeleteCommandHandler}; use download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler}; +use edit_command_handler::{SonarrEditCommand, SonarrEditCommandHandler}; use get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler}; use list_command_handler::{SonarrListCommand, SonarrListCommandHandler}; use manual_search_command_handler::{SonarrManualSearchCommand, SonarrManualSearchCommandHandler}; @@ -25,6 +26,7 @@ use super::{CliCommandHandler, Command}; mod add_command_handler; mod delete_command_handler; mod download_command_handler; +mod edit_command_handler; mod get_command_handler; mod list_command_handler; mod manual_search_command_handler; @@ -47,6 +49,11 @@ pub enum SonarrCommand { about = "Commands to delete resources from your Sonarr instance" )] Delete(SonarrDeleteCommand), + #[command( + subcommand, + about = "Commands to edit resources in your Sonarr instance" + )] + Edit(SonarrEditCommand), #[command( subcommand, about = "Commands to fetch details of the resources in your Sonarr instance" @@ -152,6 +159,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' .handle() .await? } + SonarrCommand::Edit(edit_command) => { + SonarrEditCommandHandler::with(self.app, edit_command, self.network) + .handle() + .await? + } SonarrCommand::Download(download_command) => { SonarrDownloadCommandHandler::with(self.app, download_command, self.network) .handle() diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index 60ccec5..4490d23 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -156,8 +156,8 @@ mod tests { cli::{ sonarr::{ add_command_handler::SonarrAddCommand, delete_command_handler::SonarrDeleteCommand, - download_command_handler::SonarrDownloadCommand, get_command_handler::SonarrGetCommand, - list_command_handler::SonarrListCommand, + download_command_handler::SonarrDownloadCommand, edit_command_handler::SonarrEditCommand, + get_command_handler::SonarrGetCommand, list_command_handler::SonarrListCommand, manual_search_command_handler::SonarrManualSearchCommand, refresh_command_handler::SonarrRefreshCommand, trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand, @@ -167,8 +167,8 @@ mod tests { }, models::{ sonarr_models::{ - BlocklistItem, BlocklistResponse, Series, SonarrReleaseDownloadBody, SonarrSerdeable, - SonarrTaskName, + BlocklistItem, BlocklistResponse, IndexerSettings, Series, SonarrReleaseDownloadBody, + SonarrSerdeable, SonarrTaskName, }, Serdeable, }, @@ -330,6 +330,64 @@ mod tests { assert!(result.is_ok()); } + #[tokio::test] + async fn test_sonarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() { + let expected_edit_all_indexer_settings = IndexerSettings { + id: 1, + maximum_size: 1, + minimum_age: 1, + retention: 1, + rss_sync_interval: 1, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::GetAllIndexerSettings.into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::IndexerSettings( + IndexerSettings { + id: 1, + maximum_size: 2, + minimum_age: 2, + retention: 2, + rss_sync_interval: 2, + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_all_indexer_settings_command = + SonarrCommand::Edit(SonarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: Some(1), + retention: Some(1), + rss_sync_interval: Some(1), + }); + + let result = SonarrCliHandler::with( + &app_arc, + edit_all_indexer_settings_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + #[tokio::test] async fn test_sonarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler( ) {