diff --git a/src/cli/sonarr/edit_command_handler.rs b/src/cli/sonarr/edit_command_handler.rs index adeeafb..bd879c3 100644 --- a/src/cli/sonarr/edit_command_handler.rs +++ b/src/cli/sonarr/edit_command_handler.rs @@ -9,7 +9,7 @@ use crate::{ cli::{mutex_flags_or_option, CliCommandHandler, Command}, models::{ servarr_models::EditIndexerParams, - sonarr_models::{IndexerSettings, SonarrSerdeable}, + sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable}, Serdeable, }, network::{sonarr_network::SonarrEvent, NetworkTrait}, @@ -148,6 +148,82 @@ pub enum SonarrEditCommand { #[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")] clear_tags: bool, }, + #[command( + about = "Edit preferences for the specified series", + group( + ArgGroup::new("edit_series") + .args([ + "enable_monitoring", + "disable_monitoring", + "enable_season_folders", + "disable_season_folders", + "series_type", + "quality_profile_id", + "language_profile_id", + "root_folder_path", + "tag", + "clear_tags" + ]).required(true) + .multiple(true)) + )] + Series { + #[arg( + long, + help = "The ID of the series whose settings you want to edit", + required = true + )] + series_id: i64, + #[arg( + long, + help = "Enable monitoring of this series in Sonarr so Sonarr will automatically download this series if it is available", + conflicts_with = "disable_monitoring" + )] + enable_monitoring: bool, + #[arg( + long, + help = "Disable monitoring of this series so Sonarr does not automatically download the series if it is found to be available", + conflicts_with = "enable_monitoring" + )] + disable_monitoring: bool, + #[arg( + long, + help = "The minimum availability to monitor for this film", + value_enum + )] + #[arg( + long, + help = "Enable sorting episodes of this series into season folders", + conflicts_with = "disable_season_folders" + )] + enable_season_folders: bool, + #[arg( + long, + help = "Disable sorting episodes of this series into season folders", + conflicts_with = "enable_season_folders" + )] + disable_season_folders: bool, + #[arg(long, help = "The type of series", value_enum)] + series_type: Option, + #[arg(long, help = "The ID of the quality profile to use for this series")] + quality_profile_id: Option, + #[arg(long, help = "The ID of the language profile to use for this series")] + language_profile_id: Option, + #[arg( + long, + help = "The root folder path where all film data and metadata should live" + )] + root_folder_path: Option, + #[arg( + long, + help = "Tag IDs to tag this series with", + value_parser, + action = ArgAction::Append, + conflicts_with = "clear_tags" + )] + tag: Option>, + #[arg(long, help = "Clear all tags on this series", conflicts_with = "tag")] + clear_tags: bool, + }, } impl From for Command { @@ -246,6 +322,40 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandH .await?; "Indexer updated".to_owned() } + SonarrEditCommand::Series { + series_id, + enable_monitoring, + disable_monitoring, + enable_season_folders, + disable_season_folders, + series_type, + quality_profile_id, + language_profile_id, + root_folder_path, + tag, + clear_tags, + } => { + let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring); + let season_folders_value = + mutex_flags_or_option(enable_season_folders, disable_season_folders); + let edit_series_params = EditSeriesParams { + series_id, + monitored: monitored_value, + use_season_folders: season_folders_value, + series_type, + quality_profile_id, + language_profile_id, + root_folder_path, + tags: tag, + clear_tags, + }; + + self + .network + .handle_network_event(SonarrEvent::EditSeries(Some(edit_series_params)).into()) + .await?; + "Series Updated".to_owned() + } }; Ok(result) diff --git a/src/cli/sonarr/edit_command_handler_tests.rs b/src/cli/sonarr/edit_command_handler_tests.rs index 352e373..eaef63f 100644 --- a/src/cli/sonarr/edit_command_handler_tests.rs +++ b/src/cli/sonarr/edit_command_handler_tests.rs @@ -20,7 +20,7 @@ mod tests { } mod cli { - use crate::Cli; + use crate::{models::sonarr_models::SeriesType, Cli}; use super::*; use clap::{error::ErrorKind, CommandFactory, Parser}; @@ -358,6 +358,245 @@ mod tests { assert_eq!(edit_command, expected_args); } } + + #[test] + fn test_edit_series_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "series"]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_series_with_series_id_still_requires_arguments() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "series", + "--series-id", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_series_monitoring_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "series", + "--series-id", + "1", + "--enable-monitoring", + "--disable-monitoring", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_series_season_folders_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "series", + "--series-id", + "1", + "--enable-season-folders", + "--disable-season-folders", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_series_tag_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "series", + "--series-id", + "1", + "--tag", + "1", + "--clear-tags", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[rstest] + fn test_edit_series_assert_argument_flags_require_args( + #[values( + "--series-type", + "--quality-profile-id", + "--language-profile-id", + "--root-folder-path", + "--tag" + )] + flag: &str, + ) { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "series", + "--series-id", + "1", + flag, + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_series_series_type_validation() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "edit", + "series", + "--series-id", + "1", + "--series-type", + "test", + ]); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_series_only_requires_at_least_one_argument_plus_series_id() { + let expected_args = SonarrEditCommand::Series { + series_id: 1, + enable_monitoring: false, + disable_monitoring: false, + enable_season_folders: false, + disable_season_folders: false, + series_type: None, + quality_profile_id: None, + language_profile_id: None, + root_folder_path: Some("/nfs/test".to_owned()), + tag: None, + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "sonarr", + "edit", + "series", + "--series-id", + "1", + "--root-folder-path", + "/nfs/test", + ]); + + 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_series_tag_argument_is_repeatable() { + let expected_args = SonarrEditCommand::Series { + series_id: 1, + enable_monitoring: false, + disable_monitoring: false, + enable_season_folders: false, + disable_season_folders: false, + series_type: None, + quality_profile_id: None, + language_profile_id: None, + root_folder_path: None, + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "sonarr", + "edit", + "series", + "--series-id", + "1", + "--tag", + "1", + "--tag", + "2", + ]); + + 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_series_all_arguments_defined() { + let expected_args = SonarrEditCommand::Series { + series_id: 1, + enable_monitoring: true, + disable_monitoring: false, + enable_season_folders: true, + disable_season_folders: false, + series_type: Some(SeriesType::Anime), + quality_profile_id: Some(1), + language_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "sonarr", + "edit", + "series", + "--series-id", + "1", + "--enable-monitoring", + "--enable-season-folders", + "--series-type", + "anime", + "--quality-profile-id", + "1", + "--language-profile-id", + "1", + "--root-folder-path", + "/nfs/test", + "--tag", + "1", + "--tag", + "2", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command { + assert_eq!(edit_command, expected_args); + } + } } mod handler { @@ -375,7 +614,7 @@ mod tests { }, models::{ servarr_models::EditIndexerParams, - sonarr_models::{IndexerSettings, SonarrSerdeable}, + sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable}, Serdeable, }, network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent}, @@ -490,5 +729,146 @@ mod tests { assert!(result.is_ok()); } + + #[tokio::test] + async fn test_handle_edit_series_command() { + let expected_edit_series_params = EditSeriesParams { + series_id: 1, + monitored: Some(true), + use_season_folders: Some(true), + series_type: Some(SeriesType::Anime), + quality_profile_id: Some(1), + language_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_series_command = SonarrEditCommand::Series { + series_id: 1, + enable_monitoring: true, + disable_monitoring: false, + enable_season_folders: true, + disable_season_folders: false, + series_type: Some(SeriesType::Anime), + quality_profile_id: Some(1), + language_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_series_command_handles_disable_monitoring_flag_properly() { + let expected_edit_series_params = EditSeriesParams { + series_id: 1, + monitored: Some(false), + use_season_folders: Some(false), + series_type: Some(SeriesType::Anime), + quality_profile_id: Some(1), + language_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_series_command = SonarrEditCommand::Series { + series_id: 1, + enable_monitoring: false, + disable_monitoring: true, + enable_season_folders: false, + disable_season_folders: true, + series_type: Some(SeriesType::Anime), + quality_profile_id: Some(1), + language_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_edit_series_command_no_monitoring_boolean_flags_returns_none_value() { + let expected_edit_series_params = EditSeriesParams { + series_id: 1, + monitored: None, + use_season_folders: None, + series_type: Some(SeriesType::Anime), + quality_profile_id: Some(1), + language_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let edit_series_command = SonarrEditCommand::Series { + series_id: 1, + enable_monitoring: false, + disable_monitoring: false, + enable_season_folders: false, + disable_season_folders: false, + series_type: Some(SeriesType::Anime), + quality_profile_id: Some(1), + language_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } } }