From c74d5936d23ede3fdc8b07ac464d07877e5c90f7 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 14 Jan 2026 13:30:51 -0700 Subject: [PATCH] feat: Full CLI and TUI support for the Lidarr Indexers tab --- src/app/lidarr/lidarr_context_clues.rs | 10 +- src/app/lidarr/lidarr_context_clues_tests.rs | 8 +- src/app/lidarr/lidarr_tests.rs | 298 ++- src/app/lidarr/mod.rs | 31 +- src/cli/lidarr/delete_command_handler.rs | 12 + .../lidarr/delete_command_handler_tests.rs | 59 + src/cli/lidarr/edit_command_handler.rs | 201 +- src/cli/lidarr/edit_command_handler_tests.rs | 449 ++++ src/cli/lidarr/get_command_handler.rs | 9 + src/cli/lidarr/get_command_handler_tests.rs | 36 + src/cli/lidarr/lidarr_command_tests.rs | 169 +- src/cli/lidarr/list_command_handler.rs | 9 + src/cli/lidarr/list_command_handler_tests.rs | 2 + src/cli/lidarr/mod.rs | 24 + src/cli/sonarr/edit_command_handler.rs | 4 +- src/cli/sonarr/edit_command_handler_tests.rs | 4 +- src/cli/sonarr/sonarr_command_tests.rs | 5 +- .../indexers/edit_indexer_handler.rs | 533 +++++ .../indexers/edit_indexer_handler_tests.rs | 1916 +++++++++++++++++ .../indexers/edit_indexer_settings_handler.rs | 209 ++ .../edit_indexer_settings_handler_tests.rs | 609 ++++++ .../indexers/indexers_handler_tests.rs | 717 ++++++ src/handlers/lidarr_handlers/indexers/mod.rs | 217 ++ .../indexers/test_all_indexers_handler.rs | 108 + .../test_all_indexers_handler_tests.rs | 133 ++ .../lidarr_handlers/lidarr_handler_tests.rs | 32 +- src/handlers/lidarr_handlers/mod.rs | 7 +- .../root_folders_handler_tests.rs | 4 +- .../indexers/edit_indexer_handler.rs | 2 +- .../indexers/edit_indexer_handler_tests.rs | 8 +- .../indexers/edit_indexer_handler.rs | 2 +- .../indexers/edit_indexer_handler_tests.rs | 8 +- .../indexers/edit_indexer_settings_handler.rs | 2 +- .../edit_indexer_settings_handler_tests.rs | 8 +- .../sonarr_handler_test_utils.rs | 5 +- src/models/lidarr_models.rs | 20 +- src/models/lidarr_models_tests.rs | 45 +- src/models/servarr_data/lidarr/lidarr_data.rs | 156 +- .../servarr_data/lidarr/lidarr_data_tests.rs | 187 +- src/models/servarr_data/lidarr/modals.rs | 72 + .../servarr_data/lidarr/modals_tests.rs | 108 +- src/models/servarr_data/modals.rs | 22 +- src/models/servarr_data/modals_tests.rs | 20 + src/models/servarr_data/sonarr/sonarr_data.rs | 11 +- src/models/servarr_models.rs | 39 +- src/models/servarr_models_tests.rs | 23 +- src/models/sonarr_models.rs | 20 +- src/models/sonarr_models_tests.rs | 10 +- .../indexers/lidarr_indexers_network_tests.rs | 901 ++++++++ src/network/lidarr_network/indexers/mod.rs | 419 ++++ .../lidarr_network_test_utils.rs | 50 +- .../lidarr_network/lidarr_network_tests.rs | 27 +- src/network/lidarr_network/mod.rs | 43 +- src/network/servarr_test_utils.rs | 12 +- src/network/sonarr_network/indexers/mod.rs | 2 +- .../indexers/sonarr_indexers_network_tests.rs | 43 +- src/network/sonarr_network/mod.rs | 8 +- .../sonarr_network_test_utils.rs | 17 +- .../sonarr_network/sonarr_network_tests.rs | 6 +- src/ui/lidarr_ui/indexers/edit_indexer_ui.rs | 169 ++ .../indexers/edit_indexer_ui_tests.rs | 81 + .../lidarr_ui/indexers/indexer_settings_ui.rs | 117 + .../indexers/indexer_settings_ui_tests.rs | 44 + .../lidarr_ui/indexers/indexers_ui_tests.rs | 156 ++ src/ui/lidarr_ui/indexers/mod.rs | 184 ++ ...dexer_ui_renders_edit_torrent_indexer.snap | 42 + ...ndexer_ui_renders_edit_usenet_indexer.snap | 42 + ...ests__edit_indexer_ui_renders_loading.snap | 42 + ..._settings_ui_renders_indexer_settings.snap | 40 + ...ests__indexers_ui_DeleteIndexerPrompt.snap | 38 + ..._snapshot_tests__indexers_ui_Indexers.snap | 7 + ...apshot_tests__indexers_ui_TestIndexer.snap | 35 + ..._tests__indexers_ui_TestIndexerPrompt.snap | 31 + ...rs_edit_torrent_indexer_over_indexers.snap | 42 + ...ers_edit_usenet_indexer_over_indexers.snap | 42 + ...s__indexers_ui_renders_empty_indexers.snap | 5 + ...ot_tests__indexers_ui_renders_loading.snap | 8 + ...exers_ui_renders_loading_test_results.snap | 35 + ...ults_when_indexer_test_errors_is_none.snap | 35 + ...ers_ui_renders_test_all_over_indexers.snap | 48 + ...t_tests__test_all_indexers_ui_renders.snap | 48 + ..._test_all_indexers_ui_renders_loading.snap | 48 + .../indexers/test_all_indexers_ui.rs | 79 + .../indexers/test_all_indexers_ui_tests.rs | 52 + src/ui/lidarr_ui/lidarr_ui_tests.rs | 1 + src/ui/lidarr_ui/mod.rs | 3 + ...__snapshot_tests__lidarr_tabs_Artists.snap | 2 +- ...snapshot_tests__lidarr_tabs_Downloads.snap | 2 +- ...__snapshot_tests__lidarr_tabs_History.snap | 2 +- ..._snapshot_tests__lidarr_tabs_Indexers.snap | 54 + ...apshot_tests__lidarr_tabs_RootFolders.snap | 2 +- 91 files changed, 9481 insertions(+), 166 deletions(-) create mode 100644 src/handlers/lidarr_handlers/indexers/edit_indexer_handler.rs create mode 100644 src/handlers/lidarr_handlers/indexers/edit_indexer_handler_tests.rs create mode 100644 src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler.rs create mode 100644 src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler_tests.rs create mode 100644 src/handlers/lidarr_handlers/indexers/indexers_handler_tests.rs create mode 100644 src/handlers/lidarr_handlers/indexers/mod.rs create mode 100644 src/handlers/lidarr_handlers/indexers/test_all_indexers_handler.rs create mode 100644 src/handlers/lidarr_handlers/indexers/test_all_indexers_handler_tests.rs create mode 100644 src/models/servarr_data/modals_tests.rs create mode 100644 src/network/lidarr_network/indexers/lidarr_indexers_network_tests.rs create mode 100644 src/network/lidarr_network/indexers/mod.rs create mode 100644 src/ui/lidarr_ui/indexers/edit_indexer_ui.rs create mode 100644 src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs create mode 100644 src/ui/lidarr_ui/indexers/indexer_settings_ui.rs create mode 100644 src/ui/lidarr_ui/indexers/indexer_settings_ui_tests.rs create mode 100644 src/ui/lidarr_ui/indexers/indexers_ui_tests.rs create mode 100644 src/ui/lidarr_ui/indexers/mod.rs create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_torrent_indexer.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_usenet_indexer.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_loading.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexer_settings_ui__indexer_settings_ui_tests__tests__snapshot_tests__indexer_settings_ui_renders_indexer_settings.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_DeleteIndexerPrompt.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_Indexers.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexer.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexerPrompt.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_torrent_indexer_over_indexers.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_usenet_indexer_over_indexers.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_empty_indexers.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results_when_indexer_test_errors_is_none.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_test_all_over_indexers.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders.snap create mode 100644 src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders_loading.snap create mode 100644 src/ui/lidarr_ui/indexers/test_all_indexers_ui.rs create mode 100644 src/ui/lidarr_ui/indexers/test_all_indexers_ui_tests.rs create mode 100644 src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Indexers.snap diff --git a/src/app/lidarr/lidarr_context_clues.rs b/src/app/lidarr/lidarr_context_clues.rs index 7fdc69e..ba9abd6 100644 --- a/src/app/lidarr/lidarr_context_clues.rs +++ b/src/app/lidarr/lidarr_context_clues.rs @@ -6,7 +6,7 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::models::Route; use crate::models::servarr_data::lidarr::lidarr_data::{ ADD_ARTIST_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, - EDIT_ARTIST_BLOCKS, + EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, }; #[cfg(test)] @@ -71,10 +71,12 @@ impl ContextClueProvider for LidarrContextClueProvider { .lidarr_data .artist_info_tabs .get_active_route_contextual_help(), - ActiveLidarrBlock::AddArtistSearchInput | ActiveLidarrBlock::AddArtistEmptySearchResults => { - Some(&BARE_POPUP_CONTEXT_CLUES) - } + ActiveLidarrBlock::AddArtistSearchInput + | ActiveLidarrBlock::AddArtistEmptySearchResults + | ActiveLidarrBlock::TestAllIndexers => Some(&BARE_POPUP_CONTEXT_CLUES), _ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) + || EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block) + || INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block) || ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) => { Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES) diff --git a/src/app/lidarr/lidarr_context_clues_tests.rs b/src/app/lidarr/lidarr_context_clues_tests.rs index e0250f4..2c7704b 100644 --- a/src/app/lidarr/lidarr_context_clues_tests.rs +++ b/src/app/lidarr/lidarr_context_clues_tests.rs @@ -10,7 +10,8 @@ mod tests { LidarrContextClueProvider, }; use crate::models::servarr_data::lidarr::lidarr_data::{ - ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, LidarrData, + ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, + INDEXER_SETTINGS_BLOCKS, LidarrData, }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use rstest::rstest; @@ -204,7 +205,8 @@ mod tests { fn test_lidarr_context_clue_provider_bare_popup_context_clues( #[values( ActiveLidarrBlock::AddArtistSearchInput, - ActiveLidarrBlock::AddArtistEmptySearchResults + ActiveLidarrBlock::AddArtistEmptySearchResults, + ActiveLidarrBlock::TestAllIndexers )] active_lidarr_block: ActiveLidarrBlock, ) { @@ -220,6 +222,8 @@ mod tests { fn test_lidarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks() { let mut blocks = EDIT_ARTIST_BLOCKS.to_vec(); blocks.extend(ADD_ROOT_FOLDER_BLOCKS); + blocks.extend(INDEXER_SETTINGS_BLOCKS); + blocks.extend(EDIT_INDEXER_BLOCKS); for active_lidarr_block in blocks { let mut app = App::test_default(); diff --git a/src/app/lidarr/lidarr_tests.rs b/src/app/lidarr/lidarr_tests.rs index 918dba6..1c4cd4a 100644 --- a/src/app/lidarr/lidarr_tests.rs +++ b/src/app/lidarr/lidarr_tests.rs @@ -2,6 +2,7 @@ mod tests { use crate::app::App; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::models::servarr_models::Indexer; use crate::network::NetworkEvent; use crate::network::lidarr_network::LidarrEvent; use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist; @@ -30,7 +31,7 @@ mod tests { ); assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into()); assert_eq!(rx.recv().await.unwrap(), LidarrEvent::ListArtists.into()); - assert!(!app.data.sonarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); } @@ -48,7 +49,7 @@ mod tests { assert!(app.is_loading); assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetAlbums(1).into()); - assert!(!app.data.sonarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); } @@ -130,6 +131,280 @@ mod tests { assert_eq!(app.tick_count, 0); } + #[tokio::test] + async fn test_dispatch_by_indexers_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::Indexers) + .await; + + assert!(app.is_loading); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetIndexers.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_all_indexer_settings_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::AllIndexerSettingsPrompt) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetAllIndexerSettings.into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_test_indexer_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.data.lidarr_data.indexers.set_items(vec![Indexer { + id: 1, + ..Indexer::default() + }]); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::TestIndexer) + .await; + + assert!(app.is_loading); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::TestIndexer(1).into()); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_test_all_indexers_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::TestAllIndexers) + .await; + + assert!(app.is_loading); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::TestAllIndexers.into() + ); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_check_for_lidarr_prompt_action_no_prompt_confirm() { + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = false; + + app.check_for_lidarr_prompt_action().await; + + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.should_refresh); + } + + #[tokio::test] + async fn test_check_for_lidarr_prompt_action() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::GetStatus); + + app.check_for_lidarr_prompt_action().await; + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into()); + assert!(app.should_refresh); + assert_eq!(app.data.lidarr_data.prompt_confirm_action, None); + } + + #[tokio::test] + async fn test_lidarr_refresh_metadata() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.is_routing = true; + + app.refresh_lidarr_metadata().await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetMetadataProfiles.into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into()); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetDiskSpace.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into()); + assert!(app.is_loading); + } + + #[tokio::test] + async fn test_lidarr_on_tick_first_render() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.is_first_render = true; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetMetadataProfiles.into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into()); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetDiskSpace.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into()); + assert!(app.is_loading); + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.is_first_render); + } + + #[tokio::test] + async fn test_lidarr_on_tick_routing() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.is_routing = true; + app.should_refresh = true; + app.is_first_render = false; + app.tick_count = 1; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[tokio::test] + async fn test_lidarr_on_tick_routing_while_long_request_is_running_should_cancel_request() { + let (tx, _) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.is_routing = true; + app.should_refresh = false; + app.is_first_render = false; + app.tick_count = 1; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert!(app.cancellation_token.is_cancelled()); + } + + #[tokio::test] + async fn test_lidarr_on_tick_should_refresh() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.should_refresh = true; + app.is_first_render = false; + app.tick_count = 1; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert!(app.should_refresh); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[tokio::test] + async fn test_lidarr_on_tick_should_refresh_does_not_cancel_prompt_requests() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.is_loading = true; + app.is_routing = true; + app.should_refresh = true; + app.is_first_render = false; + app.tick_count = 1; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert!(app.is_loading); + assert!(app.should_refresh); + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.cancellation_token.is_cancelled()); + } + + #[tokio::test] + async fn test_lidarr_on_tick_network_tick_frequency() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.tick_count = 2; + app.tick_until_poll = 2; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetMetadataProfiles.into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into()); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert!(app.is_loading); + } + #[tokio::test] async fn test_extract_add_new_artist_search_query() { let app = App::test_default_fully_populated(); @@ -139,6 +414,14 @@ mod tests { assert_str_eq!(query, "Test Artist"); } + #[tokio::test] + #[should_panic(expected = "Add artist search is empty")] + async fn test_extract_add_new_artist_search_query_panics_when_the_query_is_not_set() { + let app = App::test_default(); + + app.extract_add_new_artist_search_query().await; + } + #[tokio::test] async fn test_extract_artist_id() { let mut app = App::test_default(); @@ -146,4 +429,15 @@ mod tests { assert_eq!(app.extract_artist_id().await, 1); } + + #[tokio::test] + async fn test_extract_lidarr_indexer_id() { + let mut app = App::test_default(); + app.data.lidarr_data.indexers.set_items(vec![Indexer { + id: 1, + ..Indexer::default() + }]); + + assert_eq!(app.extract_lidarr_indexer_id().await, 1); + } } diff --git a/src/app/lidarr/mod.rs b/src/app/lidarr/mod.rs index 2f1db7d..b4522c3 100644 --- a/src/app/lidarr/mod.rs +++ b/src/app/lidarr/mod.rs @@ -55,6 +55,31 @@ impl App<'_> { .dispatch_network_event(LidarrEvent::GetRootFolders.into()) .await; } + ActiveLidarrBlock::Indexers => { + self + .dispatch_network_event(LidarrEvent::GetTags.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetIndexers.into()) + .await; + } + ActiveLidarrBlock::AllIndexerSettingsPrompt => { + self + .dispatch_network_event(LidarrEvent::GetAllIndexerSettings.into()) + .await; + } + ActiveLidarrBlock::TestIndexer => { + self + .dispatch_network_event( + LidarrEvent::TestIndexer(self.extract_lidarr_indexer_id().await).into(), + ) + .await; + } + ActiveLidarrBlock::TestAllIndexers => { + self + .dispatch_network_event(LidarrEvent::TestAllIndexers.into()) + .await; + } _ => (), } @@ -68,7 +93,7 @@ impl App<'_> { .lidarr_data .add_artist_search .as_ref() - .expect("add_artist_search should be set") + .expect("Add artist search is empty") .text .clone() } @@ -77,6 +102,10 @@ impl App<'_> { self.data.lidarr_data.artists.current_selection().id } + async fn extract_lidarr_indexer_id(&self) -> i64 { + self.data.lidarr_data.indexers.current_selection().id + } + async fn check_for_lidarr_prompt_action(&mut self) { if self.data.lidarr_data.prompt_confirm { self.data.lidarr_data.prompt_confirm = false; diff --git a/src/cli/lidarr/delete_command_handler.rs b/src/cli/lidarr/delete_command_handler.rs index 789c718..a0bf22a 100644 --- a/src/cli/lidarr/delete_command_handler.rs +++ b/src/cli/lidarr/delete_command_handler.rs @@ -42,6 +42,11 @@ pub enum LidarrDeleteCommand { #[arg(long, help = "The ID of the download to delete", required = true)] download_id: i64, }, + #[command(about = "Delete the indexer with the given ID")] + Indexer { + #[arg(long, help = "The ID of the indexer to delete", required = true)] + indexer_id: i64, + }, #[command(about = "Delete the root folder with the given ID")] RootFolder { #[arg(long, help = "The ID of the root folder to delete", required = true)] @@ -120,6 +125,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteComm .await?; serde_json::to_string_pretty(&resp)? } + LidarrDeleteCommand::Indexer { indexer_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteIndexer(indexer_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrDeleteCommand::RootFolder { root_folder_id } => { let resp = self .network diff --git a/src/cli/lidarr/delete_command_handler_tests.rs b/src/cli/lidarr/delete_command_handler_tests.rs index f951906..02dae9c 100644 --- a/src/cli/lidarr/delete_command_handler_tests.rs +++ b/src/cli/lidarr/delete_command_handler_tests.rs @@ -179,6 +179,39 @@ mod tests { assert_eq!(delete_command, expected_args); } + #[test] + fn test_delete_indexer_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "indexer"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_indexer_success() { + let expected_args = LidarrDeleteCommand::Indexer { indexer_id: 1 }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "indexer", + "--indexer-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + #[test] fn test_delete_root_folder_requires_arguments() { let result = @@ -354,6 +387,32 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_handle_delete_indexer_command() { + let expected_indexer_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteIndexer(expected_indexer_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_indexer_command = LidarrDeleteCommand::Indexer { indexer_id: 1 }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_indexer_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_delete_root_folder_command() { let expected_root_folder_id = 1; diff --git a/src/cli/lidarr/edit_command_handler.rs b/src/cli/lidarr/edit_command_handler.rs index 2fc0bd5..50d052f 100644 --- a/src/cli/lidarr/edit_command_handler.rs +++ b/src/cli/lidarr/edit_command_handler.rs @@ -4,6 +4,10 @@ use anyhow::Result; use clap::{ArgAction, ArgGroup, Subcommand}; use tokio::sync::Mutex; +use super::LidarrCommand; +use crate::models::Serdeable; +use crate::models::lidarr_models::LidarrSerdeable; +use crate::models::servarr_models::{EditIndexerParams, IndexerSettings}; use crate::{ app::App, cli::{CliCommandHandler, Command, mutex_flags_or_option}, @@ -11,14 +15,46 @@ use crate::{ network::{NetworkTrait, lidarr_network::LidarrEvent}, }; -use super::LidarrCommand; - #[cfg(test)] #[path = "edit_command_handler_tests.rs"] mod edit_command_handler_tests; #[derive(Debug, Clone, PartialEq, Eq, Subcommand)] pub enum LidarrEditCommand { + #[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, + }, #[command( about = "Edit preferences for the specified artist", group( @@ -80,6 +116,97 @@ pub enum LidarrEditCommand { #[arg(long, help = "Clear all tags on this artist", conflicts_with = "tag")] clear_tags: bool, }, + #[command( + about = "Edit preferences for the specified indexer", + group( + ArgGroup::new("edit_indexer") + .args([ + "name", + "enable_rss", + "disable_rss", + "enable_automatic_search", + "disable_automatic_search", + "enable_interactive_search", + "disable_automatic_search", + "url", + "api_key", + "seed_ratio", + "tag", + "priority", + "clear_tags" + ]).required(true) + .multiple(true)) + )] + Indexer { + #[arg( + long, + help = "The ID of the indexer whose settings you wish to edit", + required = true + )] + indexer_id: i64, + #[arg(long, help = "The name of the indexer")] + name: Option, + #[arg( + long, + help = "Indicate to Lidarr that this indexer should be used when Lidarr periodically looks for releases via RSS Sync", + conflicts_with = "disable_rss" + )] + enable_rss: bool, + #[arg( + long, + help = "Disable using this indexer when Lidarr periodically looks for releases via RSS Sync", + conflicts_with = "enable_rss" + )] + disable_rss: bool, + #[arg( + long, + help = "Indicate to Lidarr that this indexer should be used when automatic searches are performed via the UI or by Lidarr", + conflicts_with = "disable_automatic_search" + )] + enable_automatic_search: bool, + #[arg( + long, + help = "Disable using this indexer whenever automatic searches are performed via the UI or by Lidarr", + conflicts_with = "enable_automatic_search" + )] + disable_automatic_search: bool, + #[arg( + long, + help = "Indicate to Lidarr that this indexer should be used when an interactive search is used", + conflicts_with = "disable_interactive_search" + )] + enable_interactive_search: bool, + #[arg( + long, + help = "Disable using this indexer whenever an interactive search is performed", + conflicts_with = "enable_interactive_search" + )] + disable_interactive_search: bool, + #[arg(long, help = "The URL of the indexer")] + url: Option, + #[arg(long, help = "The API key used to access the indexer's API")] + api_key: Option, + #[arg( + long, + help = "The ratio a torrent should reach before stopping; Empty uses the download client's default. Ratio should be at least 1.0 and follow the indexer's rules" + )] + seed_ratio: Option, + #[arg( + long, + help = "Only use this indexer for series with at least one matching tag ID. Leave blank to use with all series.", + value_parser, + action = ArgAction::Append, + conflicts_with = "clear_tags" + )] + tag: Option>, + #[arg( + long, + help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Lidarr will still use all enabled indexers for RSS Sync and Searching" + )] + priority: Option, + #[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")] + clear_tags: bool, + }, } impl From for Command { @@ -109,6 +236,34 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrEditCommand> for LidarrEditCommandH async fn handle(self) -> Result { let result = match self.command { + LidarrEditCommand::AllIndexerSettings { + maximum_size, + minimum_age, + retention, + rss_sync_interval, + } => { + if let Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(previous_indexer_settings)) = self + .network + .handle_network_event(LidarrEvent::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(LidarrEvent::EditAllIndexerSettings(params).into()) + .await?; + "All indexer settings updated".to_owned() + } else { + String::new() + } + } LidarrEditCommand::Artist { artist_id, enable_monitoring, @@ -139,6 +294,48 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrEditCommand> for LidarrEditCommandH .await?; "Artist Updated".to_owned() } + LidarrEditCommand::Indexer { + indexer_id, + name, + enable_rss, + disable_rss, + enable_automatic_search, + disable_automatic_search, + enable_interactive_search, + disable_interactive_search, + url, + api_key, + seed_ratio, + tag, + priority, + clear_tags, + } => { + let rss_value = mutex_flags_or_option(enable_rss, disable_rss); + let automatic_search_value = + mutex_flags_or_option(enable_automatic_search, disable_automatic_search); + let interactive_search_value = + mutex_flags_or_option(enable_interactive_search, disable_interactive_search); + let edit_indexer_params = EditIndexerParams { + indexer_id, + name, + enable_rss: rss_value, + enable_automatic_search: automatic_search_value, + enable_interactive_search: interactive_search_value, + url, + api_key, + seed_ratio, + tags: tag, + tag_input_string: None, + priority, + clear_tags, + }; + + self + .network + .handle_network_event(LidarrEvent::EditIndexer(edit_indexer_params).into()) + .await?; + "Indexer updated".to_owned() + } }; Ok(result) diff --git a/src/cli/lidarr/edit_command_handler_tests.rs b/src/cli/lidarr/edit_command_handler_tests.rs index bed34f0..ab44b31 100644 --- a/src/cli/lidarr/edit_command_handler_tests.rs +++ b/src/cli/lidarr/edit_command_handler_tests.rs @@ -32,6 +32,96 @@ mod tests { 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", "lidarr", "edit", "all-indexer-settings"]); + + assert_err!(&result); + 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", + "lidarr", + "edit", + "all-indexer-settings", + flag, + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_all_indexer_settings_only_requires_at_least_one_argument() { + let expected_args = LidarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: None, + retention: None, + rss_sync_interval: None, + }; + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "all-indexer-settings", + "--maximum-size", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_all_indexer_settings_all_arguments_defined() { + let expected_args = LidarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: Some(1), + retention: Some(1), + rss_sync_interval: Some(1), + }; + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "all-indexer-settings", + "--maximum-size", + "1", + "--minimum-age", + "1", + "--retention", + "1", + "--rss-sync-interval", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + #[test] fn test_edit_artist_requires_arguments() { let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "artist"]); @@ -249,6 +339,253 @@ mod tests { }; assert_eq!(edit_command, expected_args); } + + #[test] + fn test_edit_indexer_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "indexer"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_indexer_with_indexer_id_still_requires_arguments() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_indexer_rss_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--enable-rss", + "--disable-rss", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_indexer_automatic_search_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--enable-automatic-search", + "--disable-automatic-search", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_indexer_interactive_search_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--enable-interactive-search", + "--disable-interactive-search", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_indexer_tag_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--tag", + "1", + "--clear-tags", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[rstest] + fn test_edit_indexer_assert_argument_flags_require_args( + #[values("--name", "--url", "--api-key", "--seed-ratio", "--tag", "--priority")] flag: &str, + ) { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + flag, + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_indexer_only_requires_at_least_one_argument_plus_indexer_id() { + let expected_args = LidarrEditCommand::Indexer { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: false, + disable_rss: false, + enable_automatic_search: false, + disable_automatic_search: false, + enable_interactive_search: false, + disable_interactive_search: false, + url: None, + api_key: None, + seed_ratio: None, + tag: None, + priority: None, + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--name", + "Test", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_indexer_tag_argument_is_repeatable() { + let expected_args = LidarrEditCommand::Indexer { + indexer_id: 1, + name: None, + enable_rss: false, + disable_rss: false, + enable_automatic_search: false, + disable_automatic_search: false, + enable_interactive_search: false, + disable_interactive_search: false, + url: None, + api_key: None, + seed_ratio: None, + tag: Some(vec![1, 2]), + priority: None, + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--tag", + "1", + "--tag", + "2", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_indexer_all_arguments_defined() { + let expected_args = LidarrEditCommand::Indexer { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: true, + disable_rss: false, + enable_automatic_search: true, + disable_automatic_search: false, + enable_interactive_search: true, + disable_interactive_search: false, + url: Some("http://test.com".to_owned()), + api_key: Some("testKey".to_owned()), + seed_ratio: Some("1.2".to_owned()), + tag: Some(vec![1, 2]), + priority: Some(25), + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--name", + "Test", + "--enable-rss", + "--enable-automatic-search", + "--enable-interactive-search", + "--url", + "http://test.com", + "--api-key", + "testKey", + "--seed-ratio", + "1.2", + "--tag", + "1", + "--tag", + "2", + "--priority", + "25", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } } mod handler { @@ -258,6 +595,7 @@ mod tests { use serde_json::json; use tokio::sync::Mutex; + use crate::models::servarr_models::{EditIndexerParams, IndexerSettings}; use crate::{ app::App, cli::{ @@ -271,6 +609,63 @@ mod tests { network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent}, }; + #[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::( + LidarrEvent::GetAllIndexerSettings.into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::IndexerSettings( + IndexerSettings { + id: 1, + maximum_size: 2, + minimum_age: 2, + retention: 2, + rss_sync_interval: 2, + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_all_indexer_settings_command = LidarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: Some(1), + retention: Some(1), + rss_sync_interval: Some(1), + }; + + let result = LidarrEditCommandHandler::with( + &app_arc, + edit_all_indexer_settings_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_edit_artist_command() { let expected_edit_artist_params = EditArtistParams { @@ -405,5 +800,59 @@ mod tests { assert_ok!(&result); } + + #[tokio::test] + async fn test_handle_edit_indexer_command() { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: Some(true), + enable_automatic_search: Some(true), + enable_interactive_search: Some(true), + url: Some("http://test.com".to_owned()), + api_key: Some("testKey".to_owned()), + seed_ratio: Some("1.2".to_owned()), + tags: Some(vec![1, 2]), + tag_input_string: None, + priority: Some(25), + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditIndexer(expected_edit_indexer_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_indexer_command = LidarrEditCommand::Indexer { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: true, + disable_rss: false, + enable_automatic_search: true, + disable_automatic_search: false, + enable_interactive_search: true, + disable_interactive_search: false, + url: Some("http://test.com".to_owned()), + api_key: Some("testKey".to_owned()), + seed_ratio: Some("1.2".to_owned()), + tag: Some(vec![1, 2]), + priority: Some(25), + clear_tags: false, + }; + + let result = + LidarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } } } diff --git a/src/cli/lidarr/get_command_handler.rs b/src/cli/lidarr/get_command_handler.rs index b33ce37..a6335d0 100644 --- a/src/cli/lidarr/get_command_handler.rs +++ b/src/cli/lidarr/get_command_handler.rs @@ -27,6 +27,8 @@ pub enum LidarrGetCommand { )] album_id: i64, }, + #[command(about = "Get the shared settings for all indexers")] + AllIndexerSettings, #[command(about = "Get detailed information for the artist with the given ID")] ArtistDetails { #[arg( @@ -78,6 +80,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHan .await?; serde_json::to_string_pretty(&resp)? } + LidarrGetCommand::AllIndexerSettings => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetAllIndexerSettings.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrGetCommand::ArtistDetails { artist_id } => { let resp = self .network diff --git a/src/cli/lidarr/get_command_handler_tests.rs b/src/cli/lidarr/get_command_handler_tests.rs index 6c6e6a5..35b8930 100644 --- a/src/cli/lidarr/get_command_handler_tests.rs +++ b/src/cli/lidarr/get_command_handler_tests.rs @@ -49,6 +49,14 @@ mod tests { assert_ok!(&result); } + #[test] + fn test_all_indexer_settings_has_no_arg_requirements() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "all-indexer-settings"]); + + assert_ok!(&result); + } + #[test] fn test_artist_details_requires_artist_id() { let result = @@ -143,6 +151,34 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_handle_get_all_indexer_settings_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetAllIndexerSettings.into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let get_all_indexer_settings_command = LidarrGetCommand::AllIndexerSettings; + + let result = LidarrGetCommandHandler::with( + &app_arc, + get_all_indexer_settings_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_get_artist_details_command() { let expected_artist_id = 1; diff --git a/src/cli/lidarr/lidarr_command_tests.rs b/src/cli/lidarr/lidarr_command_tests.rs index bad1fbe..dc7133f 100644 --- a/src/cli/lidarr/lidarr_command_tests.rs +++ b/src/cli/lidarr/lidarr_command_tests.rs @@ -21,6 +21,16 @@ mod tests { use super::*; use clap::error::ErrorKind; use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_commands_that_have_no_arg_requirements( + #[values("test-all-indexers")] subcommand: &str, + ) { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", subcommand]); + + assert_ok!(&result); + } #[test] fn test_list_artists_has_no_arg_requirements() { @@ -148,6 +158,30 @@ mod tests { assert_ok!(&result); } + + #[test] + fn test_test_indexer_requires_indexer_id() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "test-indexer"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_test_indexer_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "test-indexer", + "--indexer-id", + "1", + ]); + + assert_ok!(&result); + } } mod handler { @@ -158,9 +192,11 @@ mod tests { use tokio::sync::Mutex; use crate::cli::lidarr::add_command_handler::LidarrAddCommand; + use crate::cli::lidarr::edit_command_handler::LidarrEditCommand; use crate::cli::lidarr::get_command_handler::LidarrGetCommand; use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand; use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand; + use crate::models::servarr_models::IndexerSettings; use crate::{ app::App, cli::{ @@ -259,6 +295,64 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_lidarr_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::( + LidarrEvent::GetAllIndexerSettings.into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::IndexerSettings( + IndexerSettings { + id: 1, + maximum_size: 2, + minimum_age: 2, + retention: 2, + rss_sync_interval: 2, + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_all_indexer_settings_command = + LidarrCommand::Edit(LidarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: Some(1), + retention: Some(1), + rss_sync_interval: Some(1), + }); + + let result = LidarrCliHandler::with( + &app_arc, + edit_all_indexer_settings_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_lidarr_cli_handler_delegates_list_commands_to_the_list_command_handler() { let mut mock_network = MockNetworkTrait::new(); @@ -303,6 +397,38 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler() + { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::TriggerAutomaticArtistSearch(1).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let trigger_automatic_search_command = + LidarrCommand::TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand::Artist { + artist_id: 1, + }); + + let result = LidarrCliHandler::with( + &app_arc, + trigger_automatic_search_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_toggle_artist_monitoring_command() { let mut mock_network = MockNetworkTrait::new(); @@ -359,13 +485,13 @@ mod tests { } #[tokio::test] - async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler() - { + async fn test_test_indexer_command() { + let expected_indexer_id = 1; let mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() .with(eq::( - LidarrEvent::TriggerAutomaticArtistSearch(1).into(), + LidarrEvent::TestIndexer(expected_indexer_id).into(), )) .times(1) .returning(|_| { @@ -374,18 +500,33 @@ mod tests { ))) }); let app_arc = Arc::new(Mutex::new(App::test_default())); - let trigger_automatic_search_command = - LidarrCommand::TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand::Artist { - artist_id: 1, - }); + let test_indexer_command = LidarrCommand::TestIndexer { indexer_id: 1 }; - let result = LidarrCliHandler::with( - &app_arc, - trigger_automatic_search_command, - &mut mock_network, - ) - .handle() - .await; + let result = LidarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_test_all_indexers_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::TestAllIndexers.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let test_all_indexers_command = LidarrCommand::TestAllIndexers; + + let result = LidarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network) + .handle() + .await; assert_ok!(&result); } diff --git a/src/cli/lidarr/list_command_handler.rs b/src/cli/lidarr/list_command_handler.rs index a3d1ff1..b072c11 100644 --- a/src/cli/lidarr/list_command_handler.rs +++ b/src/cli/lidarr/list_command_handler.rs @@ -39,6 +39,8 @@ pub enum LidarrListCommand { #[arg(long, help = "How many history events to fetch", default_value_t = 500)] events: u64, }, + #[command(about = "List all Lidarr indexers")] + Indexers, #[command(about = "List all Lidarr metadata profiles")] MetadataProfiles, #[command(about = "List all Lidarr quality profiles")] @@ -104,6 +106,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH .await?; serde_json::to_string_pretty(&resp)? } + LidarrListCommand::Indexers => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetIndexers.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrListCommand::MetadataProfiles => { let resp = self .network diff --git a/src/cli/lidarr/list_command_handler_tests.rs b/src/cli/lidarr/list_command_handler_tests.rs index 2aa1b7c..e18a95d 100644 --- a/src/cli/lidarr/list_command_handler_tests.rs +++ b/src/cli/lidarr/list_command_handler_tests.rs @@ -27,6 +27,7 @@ mod tests { fn test_list_commands_have_no_arg_requirements( #[values( "artists", + "indexers", "metadata-profiles", "quality-profiles", "tags", @@ -132,6 +133,7 @@ mod tests { #[rstest] #[case(LidarrListCommand::Artists, LidarrEvent::ListArtists)] + #[case(LidarrListCommand::Indexers, LidarrEvent::GetIndexers)] #[case(LidarrListCommand::MetadataProfiles, LidarrEvent::GetMetadataProfiles)] #[case(LidarrListCommand::QualityProfiles, LidarrEvent::GetQualityProfiles)] #[case(LidarrListCommand::RootFolders, LidarrEvent::GetRootFolders)] diff --git a/src/cli/lidarr/mod.rs b/src/cli/lidarr/mod.rs index f032635..9116820 100644 --- a/src/cli/lidarr/mod.rs +++ b/src/cli/lidarr/mod.rs @@ -86,6 +86,15 @@ pub enum LidarrCommand { )] query: String, }, + #[command( + about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'" + )] + TestIndexer { + #[arg(long, help = "The ID of the indexer to test", required = true)] + indexer_id: i64, + }, + #[command(about = "Test all Lidarr indexers")] + TestAllIndexers, #[command( about = "Toggle monitoring for the specified album corresponding to the given album ID" )] @@ -190,6 +199,21 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, ' .await?; serde_json::to_string_pretty(&resp)? } + LidarrCommand::TestIndexer { indexer_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::TestIndexer(indexer_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrCommand::TestAllIndexers => { + println!("Testing all Lidarr indexers. This may take a minute..."); + let resp = self + .network + .handle_network_event(LidarrEvent::TestAllIndexers.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrCommand::ToggleAlbumMonitoring { album_id } => { let resp = self .network diff --git a/src/cli/sonarr/edit_command_handler.rs b/src/cli/sonarr/edit_command_handler.rs index 6ba16ca..43cfc07 100644 --- a/src/cli/sonarr/edit_command_handler.rs +++ b/src/cli/sonarr/edit_command_handler.rs @@ -9,8 +9,8 @@ use crate::{ cli::{CliCommandHandler, Command, mutex_flags_or_option}, models::{ Serdeable, - servarr_models::EditIndexerParams, - sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable}, + servarr_models::{EditIndexerParams, IndexerSettings}, + sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable}, }, network::{NetworkTrait, sonarr_network::SonarrEvent}, }; diff --git a/src/cli/sonarr/edit_command_handler_tests.rs b/src/cli/sonarr/edit_command_handler_tests.rs index 58cc2ac..6964451 100644 --- a/src/cli/sonarr/edit_command_handler_tests.rs +++ b/src/cli/sonarr/edit_command_handler_tests.rs @@ -622,8 +622,8 @@ mod tests { }, models::{ Serdeable, - servarr_models::EditIndexerParams, - sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable}, + servarr_models::{EditIndexerParams, IndexerSettings}, + sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable}, }, network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent}, }; diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index ac48082..c43c7ab 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -266,9 +266,10 @@ mod tests { }, models::{ Serdeable, + servarr_models::IndexerSettings, sonarr_models::{ - BlocklistItem, BlocklistResponse, IndexerSettings, Series, SonarrReleaseDownloadBody, - SonarrSerdeable, SonarrTaskName, + BlocklistItem, BlocklistResponse, Series, SonarrReleaseDownloadBody, SonarrSerdeable, + SonarrTaskName, }, }, network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent}, diff --git a/src/handlers/lidarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/lidarr_handlers/indexers/edit_indexer_handler.rs new file mode 100644 index 0000000..a1aea6e --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/edit_indexer_handler.rs @@ -0,0 +1,533 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_INDEXER_BLOCKS}; +use crate::models::servarr_data::modals::EditIndexerModal; +use crate::models::servarr_models::EditIndexerParams; +use crate::network::lidarr_network::LidarrEvent; +use crate::{ + handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key, +}; + +#[cfg(test)] +#[path = "edit_indexer_handler_tests.rs"] +mod edit_indexer_handler_tests; + +pub(super) struct EditIndexerHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl EditIndexerHandler<'_, '_> { + fn build_edit_indexer_params(&mut self) -> EditIndexerParams { + let edit_indexer_modal = self + .app + .data + .lidarr_data + .edit_indexer_modal + .take() + .expect("EditIndexerModal is None"); + let indexer_id = self.app.data.lidarr_data.indexers.current_selection().id; + let tags = edit_indexer_modal.tags.text; + let EditIndexerModal { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + priority, + .. + } = edit_indexer_modal; + + EditIndexerParams { + indexer_id, + name: Some(name.text), + enable_rss, + enable_automatic_search, + enable_interactive_search, + url: Some(url.text), + api_key: Some(api_key.text), + seed_ratio: Some(seed_ratio.text), + tags: None, + tag_input_string: Some(tags), + priority: Some(priority), + clear_tags: false, + } + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditIndexerHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + EDIT_INDEXER_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + _context: Option, + ) -> EditIndexerHandler<'a, 'b> { + EditIndexerHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.lidarr_data.edit_indexer_modal.is_some() + } + + fn handle_scroll_up(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerPrompt => { + self.app.data.lidarr_data.selected_block.up(); + } + ActiveLidarrBlock::EditIndexerPriorityInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .priority += 1; + } + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerPrompt => { + self.app.data.lidarr_data.selected_block.down(); + } + ActiveLidarrBlock::EditIndexerPriorityInput => { + let edit_indexer_modal = self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + if edit_indexer_modal.priority > 1 { + edit_indexer_modal.priority -= 1; + } + } + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerNameInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + .scroll_home(); + } + ActiveLidarrBlock::EditIndexerUrlInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + .scroll_home(); + } + ActiveLidarrBlock::EditIndexerApiKeyInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + .scroll_home(); + } + ActiveLidarrBlock::EditIndexerSeedRatioInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + .scroll_home(); + } + ActiveLidarrBlock::EditIndexerTagsInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + .scroll_home(); + } + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerNameInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + .reset_offset(); + } + ActiveLidarrBlock::EditIndexerUrlInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + .reset_offset(); + } + ActiveLidarrBlock::EditIndexerApiKeyInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + .reset_offset(); + } + ActiveLidarrBlock::EditIndexerSeedRatioInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + .reset_offset(); + } + ActiveLidarrBlock::EditIndexerTagsInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + .reset_offset(); + } + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerPrompt => { + handle_prompt_left_right_keys!( + self, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + lidarr_data + ); + } + ActiveLidarrBlock::EditIndexerNameInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + ); + } + ActiveLidarrBlock::EditIndexerUrlInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + ); + } + ActiveLidarrBlock::EditIndexerApiKeyInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + ); + } + ActiveLidarrBlock::EditIndexerSeedRatioInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + ); + } + ActiveLidarrBlock::EditIndexerTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + ); + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerPrompt => { + let selected_block = self.app.data.lidarr_data.selected_block.get_active_block(); + match selected_block { + ActiveLidarrBlock::EditIndexerConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::EditIndexer(self.build_edit_indexer_params())); + self.app.should_refresh = true; + } else { + self.app.data.lidarr_data.edit_indexer_modal = None; + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::EditIndexerNameInput + | ActiveLidarrBlock::EditIndexerUrlInput + | ActiveLidarrBlock::EditIndexerApiKeyInput + | ActiveLidarrBlock::EditIndexerSeedRatioInput + | ActiveLidarrBlock::EditIndexerTagsInput => { + self.app.push_navigation_stack(selected_block.into()); + self.app.ignore_special_keys_for_textbox_input = true; + } + ActiveLidarrBlock::EditIndexerPriorityInput => self + .app + .push_navigation_stack(ActiveLidarrBlock::EditIndexerPriorityInput.into()), + ActiveLidarrBlock::EditIndexerToggleEnableRss => { + let indexer = self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_rss = Some(!indexer.enable_rss.unwrap_or_default()); + } + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch => { + let indexer = self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_automatic_search = + Some(!indexer.enable_automatic_search.unwrap_or_default()); + } + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch => { + let indexer = self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_interactive_search = + Some(!indexer.enable_interactive_search.unwrap_or_default()); + } + _ => (), + } + } + ActiveLidarrBlock::EditIndexerNameInput + | ActiveLidarrBlock::EditIndexerUrlInput + | ActiveLidarrBlock::EditIndexerApiKeyInput + | ActiveLidarrBlock::EditIndexerSeedRatioInput + | ActiveLidarrBlock::EditIndexerTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + ActiveLidarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + self.app.data.lidarr_data.edit_indexer_modal = None; + } + ActiveLidarrBlock::EditIndexerNameInput + | ActiveLidarrBlock::EditIndexerUrlInput + | ActiveLidarrBlock::EditIndexerApiKeyInput + | ActiveLidarrBlock::EditIndexerSeedRatioInput + | ActiveLidarrBlock::EditIndexerPriorityInput + | ActiveLidarrBlock::EditIndexerTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + _ => self.app.pop_navigation_stack(), + } + } + + fn handle_char_key_event(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerNameInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + ); + } + ActiveLidarrBlock::EditIndexerUrlInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + ); + } + ActiveLidarrBlock::EditIndexerApiKeyInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + ); + } + ActiveLidarrBlock::EditIndexerSeedRatioInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + ); + } + ActiveLidarrBlock::EditIndexerTagsInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + ); + } + ActiveLidarrBlock::EditIndexerPrompt => { + if self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::EditIndexerConfirmPrompt + && matches_key!(confirm, self.key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::EditIndexer(self.build_edit_indexer_params())); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/lidarr_handlers/indexers/edit_indexer_handler_tests.rs new file mode 100644 index 0000000..6dd6d67 --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -0,0 +1,1916 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; + use crate::assert_modal_present; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_INDEXER_BLOCKS}; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_models::EditIndexerParams; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + mod test_handle_scroll_up_and_down { + use crate::app::App; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS; + use crate::models::servarr_data::modals::EditIndexerModal; + + use super::*; + + #[rstest] + fn test_edit_indexer_priority_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + key, + &mut app, + ActiveLidarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + + if key == Key::Up { + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 2 + ); + } else { + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 1 + ); + + EditIndexerHandler::new( + Key::Up, + &mut app, + ActiveLidarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 2 + ); + + EditIndexerHandler::new( + key, + &mut app, + ActiveLidarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 1 + ); + } + } + + #[rstest] + fn test_edit_indexer_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerNameInput + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch + ); + } + } + + #[rstest] + fn test_edit_indexer_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerToggleEnableRss + ); + } + } + + mod test_handle_home_end { + use std::sync::atomic::Ordering; + + use crate::app::App; + use crate::models::servarr_data::modals::EditIndexerModal; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_edit_indexer_name_input_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_url_input_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_api_key_input_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_tags_input_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_left_right_action { + use std::sync::atomic::Ordering; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::models::servarr_data::modals::EditIndexerModal; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.y = EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1; + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[rstest] + #[case( + 0, + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput + )] + #[case( + 1, + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput + )] + #[case( + 2, + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerSeedRatioInput + )] + #[case( + 3, + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerTagsInput + )] + fn test_left_right_block_toggle_torrents( + #[values(Key::Left, Key::Right)] key: Key, + #[case] starting_y_index: usize, + #[case] left_block: ActiveLidarrBlock, + #[case] right_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.y = starting_y_index; + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + left_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + right_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + left_block + ); + } + + #[rstest] + #[case( + 0, + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput + )] + #[case( + 1, + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput + )] + #[case( + 2, + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerTagsInput + )] + #[case( + 3, + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerPriorityInput + )] + fn test_left_right_block_toggle_nzb( + #[values(Key::Left, Key::Right)] key: Key, + #[case] starting_y_index: usize, + #[case] left_block: ActiveLidarrBlock, + #[case] right_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.y = starting_y_index; + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + left_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + right_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + left_block + ); + } + + #[rstest] + fn test_left_right_block_toggle_torren_empty_row_to_prompt_confirm( + #[values(Key::Left, Key::Right)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.y = 4; + app.data.lidarr_data.prompt_confirm = false; + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerPriorityInput + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerConfirmPrompt + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerConfirmPrompt + ); + assert!(app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_edit_indexer_name_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_url_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_api_key_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_tags_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::app::App; + use crate::assert_navigation_popped; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::{ + BlockSelectionState, servarr_data::lidarr::lidarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_edit_indexer_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + assert_none!(app.data.lidarr_data.edit_indexer_modal); + } + + #[test] + fn test_edit_indexer_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 0, + }; + app.data.lidarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + app.data.lidarr_data.prompt_confirm = true; + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_modal_absent!(app.data.lidarr_data.edit_indexer_modal); + assert!(app.should_refresh); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + ); + } + + #[test] + fn test_edit_indexer_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.prompt_confirm = true; + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert_modal_present!(app.data.lidarr_data.edit_indexer_modal); + assert!(!app.should_refresh); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + } + + #[rstest] + #[case(0, 0, ActiveLidarrBlock::EditIndexerNameInput)] + #[case(0, 1, ActiveLidarrBlock::EditIndexerUrlInput)] + #[case(1, 1, ActiveLidarrBlock::EditIndexerApiKeyInput)] + #[case(2, 1, ActiveLidarrBlock::EditIndexerSeedRatioInput)] + #[case(3, 1, ActiveLidarrBlock::EditIndexerTagsInput)] + fn test_edit_indexer_prompt_submit_input_fields( + #[case] starting_y: usize, + #[case] starting_x: usize, + #[case] block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(starting_x, starting_y); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_pushed!(app, block.into()); + assert!(app.ignore_special_keys_for_textbox_input); + } + + #[test] + fn test_edit_indexer_priority_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, 4); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::EditIndexerPriorityInput.into()); + assert!(!app.ignore_special_keys_for_textbox_input); + } + + #[test] + fn test_edit_indexer_toggle_enable_rss_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, 1); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_rss + .unwrap() + ); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_rss + .unwrap() + ); + } + + #[test] + fn test_edit_indexer_toggle_enable_automatic_search_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, 2); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_automatic_search + .unwrap() + ); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_automatic_search + .unwrap() + ); + } + + #[test] + fn test_edit_indexer_toggle_enable_interactive_search_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, 3); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_interactive_search + .unwrap() + ); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_interactive_search + .unwrap() + ); + } + + #[test] + fn test_edit_indexer_name_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerNameInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_url_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerUrlInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_api_key_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerApiKeyInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerSeedRatioInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_tags_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerTagsInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + } + } + + mod test_handle_esc { + use super::*; + use crate::app::App; + use crate::assert_navigation_popped; + use crate::event::Key; + use crate::models::servarr_data::modals::EditIndexerModal; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_edit_indexer_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.edit_indexer_modal); + } + + #[rstest] + fn test_edit_indexer_input_fields_esc( + #[values( + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + ActiveLidarrBlock::EditIndexerTagsInput, + ActiveLidarrBlock::EditIndexerPriorityInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.ignore_special_keys_for_textbox_input = true; + + EditIndexerHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert!(!app.ignore_special_keys_for_textbox_input); + assert_some_eq_x!( + &app.data.lidarr_data.edit_indexer_modal, + &EditIndexerModal::default() + ); + } + } + + mod test_handle_key_char { + use super::*; + use crate::app::App; + use crate::assert_navigation_popped; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use pretty_assertions::{assert_eq, assert_str_eq}; + + #[test] + fn test_edit_indexer_name_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_url_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_api_key_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_tags_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_name_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text, + "a" + ); + } + + #[test] + fn test_edit_indexer_url_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text, + "a" + ); + } + + #[test] + fn test_edit_indexer_api_key_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text, + "a" + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text, + "a" + ); + } + + #[test] + fn test_edit_indexer_tags_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text, + "a" + ); + } + + #[test] + fn test_edit_indexer_prompt_prompt_confirmation_confirm() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 0, + }; + app.data.lidarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_modal_absent!(app.data.lidarr_data.edit_indexer_modal); + assert!(app.should_refresh); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + ); + } + } + + #[test] + fn test_edit_indexer_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block) { + assert!(EditIndexerHandler::accepts(active_lidarr_block)); + } else { + assert!(!EditIndexerHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_edit_indexer_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_build_edit_indexer_params() { + let mut app = App::test_default(); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 0, + }; + app.data.lidarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + + let params = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .build_edit_indexer_params(); + + assert_eq!(params, expected_edit_indexer_params); + assert_modal_absent!(app.data.lidarr_data.edit_indexer_modal); + } + + #[test] + fn test_edit_indexer_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_handler_is_not_ready_when_edit_indexer_modal_is_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_handler_is_ready_when_edit_indexer_modal_is_some() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + let handler = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler.rs new file mode 100644 index 0000000..0de6cc1 --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -0,0 +1,209 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS, +}; +use crate::models::servarr_models::IndexerSettings; +use crate::network::lidarr_network::LidarrEvent; +use crate::{handle_prompt_left_right_keys, matches_key}; + +#[cfg(test)] +#[path = "edit_indexer_settings_handler_tests.rs"] +mod edit_indexer_settings_handler_tests; + +pub(super) struct IndexerSettingsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl IndexerSettingsHandler<'_, '_> { + fn build_edit_indexer_settings_params(&mut self) -> IndexerSettings { + self + .app + .data + .lidarr_data + .indexer_settings + .take() + .expect("IndexerSettings is None") + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for IndexerSettingsHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + INDEXER_SETTINGS_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + _context: Option, + ) -> IndexerSettingsHandler<'a, 'b> { + IndexerSettingsHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.lidarr_data.indexer_settings.is_some() + } + + fn handle_scroll_up(&mut self) { + let indexer_settings = self.app.data.lidarr_data.indexer_settings.as_mut().unwrap(); + match self.active_lidarr_block { + ActiveLidarrBlock::AllIndexerSettingsPrompt => { + self.app.data.lidarr_data.selected_block.up(); + } + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput => { + indexer_settings.minimum_age += 1; + } + ActiveLidarrBlock::IndexerSettingsRetentionInput => { + indexer_settings.retention += 1; + } + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput => { + indexer_settings.maximum_size += 1; + } + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => { + indexer_settings.rss_sync_interval += 1; + } + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + let indexer_settings = self.app.data.lidarr_data.indexer_settings.as_mut().unwrap(); + match self.active_lidarr_block { + ActiveLidarrBlock::AllIndexerSettingsPrompt => { + self.app.data.lidarr_data.selected_block.down() + } + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput => { + if indexer_settings.minimum_age > 0 { + indexer_settings.minimum_age -= 1; + } + } + ActiveLidarrBlock::IndexerSettingsRetentionInput => { + if indexer_settings.retention > 0 { + indexer_settings.retention -= 1; + } + } + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput => { + if indexer_settings.maximum_size > 0 { + indexer_settings.maximum_size -= 1; + } + } + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => { + if indexer_settings.rss_sync_interval > 0 { + indexer_settings.rss_sync_interval -= 1; + } + } + _ => (), + } + } + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::AllIndexerSettingsPrompt { + handle_prompt_left_right_keys!( + self, + ActiveLidarrBlock::IndexerSettingsConfirmPrompt, + lidarr_data + ); + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AllIndexerSettingsPrompt => { + match self.app.data.lidarr_data.selected_block.get_active_block() { + ActiveLidarrBlock::IndexerSettingsConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = Some( + LidarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_params()), + ); + self.app.should_refresh = true; + } else { + self.app.data.lidarr_data.indexer_settings = None; + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput + | ActiveLidarrBlock::IndexerSettingsRetentionInput + | ActiveLidarrBlock::IndexerSettingsMaximumSizeInput + | ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => { + self.app.push_navigation_stack( + ( + self.app.data.lidarr_data.selected_block.get_active_block(), + None, + ) + .into(), + ) + } + + _ => (), + } + } + + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput + | ActiveLidarrBlock::IndexerSettingsRetentionInput + | ActiveLidarrBlock::IndexerSettingsMaximumSizeInput + | ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AllIndexerSettingsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + self.app.data.lidarr_data.indexer_settings = None; + } + _ => self.app.pop_navigation_stack(), + } + } + + fn handle_char_key_event(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::AllIndexerSettingsPrompt + && self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::IndexerSettingsConfirmPrompt + && matches_key!(confirm, self.key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::EditAllIndexerSettings( + self.build_edit_indexer_settings_params(), + )); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler_tests.rs new file mode 100644 index 0000000..c7e6385 --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -0,0 +1,609 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS, + }; + use crate::models::servarr_models::IndexerSettings; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer_settings; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS; + use crate::models::servarr_models::IndexerSettings; + + use super::*; + + macro_rules! test_i64_counter_scroll_value { + ($block:expr, $key:expr, $data_ref:ident, $negatives:literal) => { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new($key, &mut app, $block, None).handle(); + + if $key == Key::Up { + assert_eq!( + app + .data + .lidarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 1 + ); + } else { + if $negatives { + assert_eq!( + app + .data + .lidarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + -1 + ); + } else { + assert_eq!( + app + .data + .lidarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 0 + ); + + IndexerSettingsHandler::new(Key::Up, &mut app, $block, None).handle(); + + assert_eq!( + app + .data + .lidarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 1 + ); + + IndexerSettingsHandler::new($key, &mut app, $block, None).handle(); + assert_eq!( + app + .data + .lidarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 0 + ); + } + } + }; + } + + #[rstest] + fn test_edit_indexer_settings_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput + ); + } + } + + #[rstest] + fn test_edit_indexer_settings_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::IndexerSettingsRetentionInput + ); + } + + #[rstest] + fn test_edit_indexer_settings_minimum_age_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + key, + minimum_age, + false + ); + } + + #[rstest] + fn test_edit_indexer_settings_retention_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveLidarrBlock::IndexerSettingsRetentionInput, + key, + retention, + false + ); + } + + #[rstest] + fn test_edit_indexer_settings_maximum_size_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + key, + maximum_size, + false + ); + } + + #[rstest] + fn test_edit_indexer_settings_rss_sync_interval_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput, + key, + rss_sync_interval, + false + ); + } + } + + mod test_handle_left_right_action { + use crate::models::servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS; + + use crate::models::BlockSelectionState; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.y = INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1; + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::{ + assert_navigation_popped, + models::{ + BlockSelectionState, servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + servarr_models::IndexerSettings, + }, + network::lidarr_network::LidarrEvent, + }; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_edit_indexer_settings_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + assert_none!(app.data.lidarr_data.indexer_settings); + } + + #[test] + fn test_edit_indexer_settings_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.indexer_settings = Some(indexer_settings()); + app.data.lidarr_data.prompt_confirm = true; + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::EditAllIndexerSettings(indexer_settings()) + ); + assert_modal_absent!(app.data.lidarr_data.indexer_settings); + assert!(app.should_refresh); + } + + #[test] + fn test_edit_indexer_settings_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.lidarr_data.prompt_confirm = true; + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AllIndexerSettingsPrompt.into() + ); + assert!(!app.should_refresh); + } + + #[rstest] + #[case(ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, 0)] + #[case(ActiveLidarrBlock::IndexerSettingsRetentionInput, 1)] + #[case(ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, 2)] + #[case(ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput, 3)] + fn test_edit_indexer_settings_prompt_submit_selected_block( + #[case] selected_block: ActiveLidarrBlock, + #[case] y_index: usize, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, y_index); + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_pushed!(app, selected_block.into()); + } + + #[rstest] + fn test_edit_indexer_settings_prompt_submit_selected_block_no_op_when_not_ready( + #[values(0, 1, 2, 3, 4)] y_index: usize, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, y_index); + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AllIndexerSettingsPrompt.into() + ); + } + + #[rstest] + fn test_edit_indexer_settings_selected_block_submit( + #[values( + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + ActiveLidarrBlock::IndexerSettingsRetentionInput, + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + IndexerSettingsHandler::new(SUBMIT_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + } + } + + mod test_handle_esc { + use rstest::rstest; + + use crate::models::servarr_models::IndexerSettings; + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_edit_indexer_settings_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.indexer_settings); + } + + #[rstest] + fn test_edit_indexer_settings_selected_blocks_esc( + #[values( + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + ActiveLidarrBlock::IndexerSettingsRetentionInput, + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_some_eq_x!( + &app.data.lidarr_data.indexer_settings, + &IndexerSettings::default() + ); + } + } + + mod test_handle_key_char { + use crate::{ + assert_navigation_popped, + models::{ + BlockSelectionState, servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + }, + network::lidarr_network::LidarrEvent, + }; + + use super::*; + + #[test] + fn test_edit_indexer_settings_prompt_prompt_confirmation_confirm() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.indexer_settings = Some(indexer_settings()); + + IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::EditAllIndexerSettings(indexer_settings()) + ); + assert_modal_absent!(app.data.lidarr_data.indexer_settings); + assert!(app.should_refresh); + } + } + + #[test] + fn test_indexer_settings_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block) { + assert!(IndexerSettingsHandler::accepts(active_lidarr_block)); + } else { + assert!(!IndexerSettingsHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_indexer_settings_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_build_edit_indexer_settings_params() { + let mut app = App::test_default(); + app.data.lidarr_data.indexer_settings = Some(indexer_settings()); + + let actual_indexer_settings = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .build_edit_indexer_settings_params(); + + assert_eq!(actual_indexer_settings, indexer_settings()); + assert_modal_absent!(app.data.lidarr_data.indexer_settings); + } + + #[test] + fn test_edit_indexer_settings_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_settings_handler_not_ready_when_indexer_settings_is_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_settings_handler_ready_when_not_loading_and_indexer_settings_is_some() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + + let handler = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/lidarr_handlers/indexers/indexers_handler_tests.rs new file mode 100644 index 0000000..ff6ef88 --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/indexers_handler_tests.rs @@ -0,0 +1,717 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::indexers::IndexersHandler; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXERS_BLOCKS, + }; + use crate::models::servarr_models::Indexer; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use crate::test_handler_delegation; + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_indexer_prompt() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteIndexerPrompt.into()); + } + + #[test] + fn test_delete_indexer_prompt_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_indexers_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(4); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::RootFolders.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::RootFolders.into()); + } + + #[rstest] + fn test_indexers_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(4); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Artists.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into()); + } + + #[rstest] + fn test_left_right_delete_indexer_prompt_toggle( + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + IndexersHandler::new(key, &mut app, ActiveLidarrBlock::DeleteIndexerPrompt, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + IndexersHandler::new(key, &mut app, ActiveLidarrBlock::DeleteIndexerPrompt, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use super::*; + use crate::assert_navigation_popped; + use crate::models::servarr_data::lidarr::lidarr_data::{ + EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, LidarrData, + }; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_models::{Indexer, IndexerField}; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use bimap::BiMap; + use pretty_assertions::assert_eq; + use serde_json::{Number, Value}; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[rstest] + fn test_edit_indexer_submit(#[values(true, false)] torrent_protocol: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + let protocol = if torrent_protocol { + "torrent".to_owned() + } else { + "usenet".to_owned() + }; + let mut expected_edit_indexer_modal = EditIndexerModal { + name: "Test".into(), + enable_rss: Some(true), + enable_automatic_search: Some(true), + enable_interactive_search: Some(true), + url: "https://test.com".into(), + api_key: "1234".into(), + tags: "usenet, test".into(), + ..EditIndexerModal::default() + }; + let mut lidarr_data = LidarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..LidarrData::default() + }; + let mut fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + ]; + + if torrent_protocol { + fields.push(IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(Value::from(1.2f64)), + }); + expected_edit_indexer_modal.seed_ratio = "1.2".into(); + } + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + protocol, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + ..Indexer::default() + }; + lidarr_data.indexers.set_items(vec![indexer]); + app.data.lidarr_data = lidarr_data; + + IndexersHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + assert_some_eq_x!( + &app.data.lidarr_data.edit_indexer_modal, + &EditIndexerModal::from(&app.data.lidarr_data) + ); + assert_some_eq_x!( + &app.data.lidarr_data.edit_indexer_modal, + &expected_edit_indexer_modal + ); + if torrent_protocol { + assert_eq!( + app.data.lidarr_data.selected_block.blocks, + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.blocks, + EDIT_INDEXER_NZB_SELECTION_BLOCKS + ); + } + } + + #[test] + fn test_edit_indexer_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + assert_none!(app.data.lidarr_data.edit_indexer_modal); + } + + #[test] + fn test_delete_indexer_prompt_confirm_submit() { + let mut app = App::test_default(); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::DeleteIndexer(1) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + } + + #[test] + fn test_prompt_decline_submit() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + } + } + + mod test_handle_esc { + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_delete_indexer_prompt_block_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + + IndexersHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[rstest] + fn test_test_indexer_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.data.lidarr_data.indexer_test_errors = Some("test result".to_owned()); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::TestIndexer.into()); + + IndexersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::TestIndexer, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_none!(app.data.lidarr_data.indexer_test_errors); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + IndexersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + + use super::*; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use crate::{ + assert_navigation_popped, + models::servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + network::lidarr_network::LidarrEvent, + }; + + #[test] + fn test_refresh_indexers_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::Indexers.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_indexers_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_indexer_settings_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.settings.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + assert_eq!( + app.data.lidarr_data.selected_block.blocks, + INDEXER_SETTINGS_SELECTION_BLOCKS + ); + } + + #[test] + fn test_indexer_settings_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.settings.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + } + + #[test] + fn test_test_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::TestIndexer.into()); + } + + #[test] + fn test_test_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + } + + #[test] + fn test_test_all_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test_all.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::TestAllIndexers.into()); + } + + #[test] + fn test_test_all_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test_all.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + } + + #[test] + fn test_delete_indexer_prompt_confirm() { + let mut app = App::test_default(); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::DeleteIndexer(1) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + } + } + + #[rstest] + fn test_delegates_edit_indexer_blocks_to_edit_indexer_handler( + #[values( + ActiveLidarrBlock::EditIndexerPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerUrlInput, + ActiveLidarrBlock::EditIndexerTagsInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + IndexersHandler, + ActiveLidarrBlock::Indexers, + active_lidarr_block + ); + } + + #[rstest] + fn test_delegates_indexer_settings_blocks_to_indexer_settings_handler( + #[values( + ActiveLidarrBlock::AllIndexerSettingsPrompt, + ActiveLidarrBlock::IndexerSettingsConfirmPrompt, + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + ActiveLidarrBlock::IndexerSettingsRetentionInput, + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + IndexersHandler, + ActiveLidarrBlock::Indexers, + active_lidarr_block + ); + } + + #[test] + fn test_delegates_test_all_indexers_block_to_test_all_indexers_handler() { + test_handler_delegation!( + IndexersHandler, + ActiveLidarrBlock::Indexers, + ActiveLidarrBlock::TestAllIndexers + ); + } + + #[test] + fn test_indexers_handler_accepts() { + let mut indexers_blocks = Vec::new(); + indexers_blocks.extend(INDEXERS_BLOCKS); + indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS); + indexers_blocks.extend(EDIT_INDEXER_BLOCKS); + indexers_blocks.push(ActiveLidarrBlock::TestAllIndexers); + + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if indexers_blocks.contains(&active_lidarr_block) { + assert!(IndexersHandler::accepts(active_lidarr_block)); + } else { + assert!(!IndexersHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_indexers_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_extract_indexer_id() { + let mut app = App::test_default(); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + + let indexer_id = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .extract_indexer_id(); + + assert_eq!(indexer_id, 1); + } + + #[test] + fn test_indexers_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_indexers_handler_not_ready_when_indexers_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_indexers_handler_ready_when_not_loading_and_indexers_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + let handler = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/indexers/mod.rs b/src/handlers/lidarr_handlers/indexers/mod.rs new file mode 100644 index 0000000..653fcdb --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/mod.rs @@ -0,0 +1,217 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::lidarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; +use crate::handlers::lidarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; +use crate::handlers::lidarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, +}; +use crate::models::{BlockSelectionState, Route}; +use crate::network::lidarr_network::LidarrEvent; + +mod edit_indexer_handler; +mod edit_indexer_settings_handler; +mod test_all_indexers_handler; + +#[cfg(test)] +#[path = "indexers_handler_tests.rs"] +mod indexers_handler_tests; + +pub(super) struct IndexersHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + context: Option, +} + +impl IndexersHandler<'_, '_> { + fn extract_indexer_id(&self) -> i64 { + self.app.data.lidarr_data.indexers.current_selection().id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for IndexersHandler<'a, 'b> { + fn handle(&mut self) { + let indexers_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::Indexers.into()); + + if !handle_table( + self, + |app| &mut app.data.lidarr_data.indexers, + indexers_table_handling_config, + ) { + match self.active_lidarr_block { + _ if EditIndexerHandler::accepts(self.active_lidarr_block) => { + EditIndexerHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle() + } + _ if IndexerSettingsHandler::accepts(self.active_lidarr_block) => { + IndexerSettingsHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle() + } + _ if TestAllIndexersHandler::accepts(self.active_lidarr_block) => { + TestAllIndexersHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle() + } + _ => self.handle_key_event(), + } + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + EditIndexerHandler::accepts(active_block) + || IndexerSettingsHandler::accepts(active_block) + || TestAllIndexersHandler::accepts(active_block) + || INDEXERS_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> IndexersHandler<'a, 'b> { + IndexersHandler { + key, + app, + active_lidarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.lidarr_data.indexers.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::Indexers { + self + .app + .push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into()); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::Indexers => handle_change_tab_left_right_keys(self.app, self.key), + ActiveLidarrBlock::DeleteIndexerPrompt => handle_prompt_toggle(self.app, self.key), + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteIndexerPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteIndexer(self.extract_indexer_id())); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::Indexers => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + self.app.data.lidarr_data.edit_indexer_modal = Some((&self.app.data.lidarr_data).into()); + let protocol = &self + .app + .data + .lidarr_data + .indexers + .current_selection() + .protocol; + if protocol == "torrent" { + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + } else { + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + } + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteIndexerPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + ActiveLidarrBlock::TestIndexer => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.indexer_test_errors = None; + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::Indexers => match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ if matches_key!(test, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::TestIndexer.into()); + } + _ if matches_key!(test_all, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + } + _ if matches_key!(settings, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + } + _ => (), + }, + ActiveLidarrBlock::DeleteIndexerPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteIndexer(self.extract_indexer_id())); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler.rs b/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler.rs new file mode 100644 index 0000000..c1fdb67 --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler.rs @@ -0,0 +1,108 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::KeyEventHandler; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + +#[cfg(test)] +#[path = "test_all_indexers_handler_tests.rs"] +mod test_all_indexers_handler_tests; + +pub(super) struct TestAllIndexersHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl TestAllIndexersHandler<'_, '_> {} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for TestAllIndexersHandler<'a, 'b> { + fn handle(&mut self) { + let indexer_test_all_results_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::TestAllIndexers.into()); + + if !handle_table( + self, + |app| { + app + .data + .lidarr_data + .indexer_test_all_results + .as_mut() + .unwrap() + }, + indexer_test_all_results_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + active_block == ActiveLidarrBlock::TestAllIndexers + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + _context: Option, + ) -> TestAllIndexersHandler<'a, 'b> { + TestAllIndexersHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + let table_is_ready = if let Some(table) = &self.app.data.lidarr_data.indexer_test_all_results { + !table.is_empty() + } else { + false + }; + + !self.app.is_loading && table_is_ready + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) {} + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::TestAllIndexers { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.indexer_test_all_results = None; + } + } + + fn handle_char_key_event(&mut self) {} + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler_tests.rs b/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler_tests.rs new file mode 100644 index 0000000..6379541 --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler_tests.rs @@ -0,0 +1,133 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_popped; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::models::servarr_data::modals::IndexerTestResultModalItem; + use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + mod test_handle_esc { + use super::*; + + const ESC_KEY: crate::event::Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_test_all_indexers_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + app.data.lidarr_data.indexer_test_all_results = Some(StatefulTable::default()); + + TestAllIndexersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::TestAllIndexers, None) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_none!(app.data.lidarr_data.indexer_test_all_results); + } + } + + #[test] + fn test_test_all_indexers_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if active_lidarr_block == ActiveLidarrBlock::TestAllIndexers { + assert!(TestAllIndexersHandler::accepts(active_lidarr_block)); + } else { + assert!(!TestAllIndexersHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_test_all_indexers_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_test_all_indexers_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + app.is_loading = true; + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TestAllIndexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_test_all_indexers_handler_not_ready_when_results_is_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + app.is_loading = false; + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TestAllIndexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_test_all_indexers_handler_not_ready_when_results_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + app.is_loading = false; + app.data.lidarr_data.indexer_test_all_results = Some(StatefulTable::default()); + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TestAllIndexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_test_all_indexers_handler_ready_when_not_loading_and_results_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + app.is_loading = false; + let mut results = StatefulTable::default(); + results.set_items(vec![IndexerTestResultModalItem::default()]); + app.data.lidarr_data.indexer_test_all_results = Some(results); + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TestAllIndexers, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/lidarr_handler_tests.rs b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs index 14176f0..83b555f 100644 --- a/src/handlers/lidarr_handlers/lidarr_handler_tests.rs +++ b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs @@ -52,10 +52,11 @@ mod tests { } #[rstest] - #[case(0, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Downloads)] + #[case(0, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Downloads)] #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)] #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)] - #[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Artists)] + #[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)] + #[case(4, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Artists)] fn test_lidarr_handler_change_tab_left_right_keys( #[case] index: usize, #[case] left_block: ActiveLidarrBlock, @@ -84,10 +85,11 @@ mod tests { } #[rstest] - #[case(0, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Downloads)] + #[case(0, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Downloads)] #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)] #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)] - #[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Artists)] + #[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)] + #[case(4, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Artists)] fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation( #[case] index: usize, #[case] left_block: ActiveLidarrBlock, @@ -120,6 +122,7 @@ mod tests { #[case(1, ActiveLidarrBlock::Downloads)] #[case(2, ActiveLidarrBlock::History)] #[case(3, ActiveLidarrBlock::RootFolders)] + #[case(4, ActiveLidarrBlock::Indexers)] fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key( #[case] index: usize, #[case] block: ActiveLidarrBlock, @@ -226,4 +229,25 @@ mod tests { active_lidarr_block ); } + + #[rstest] + fn test_delegates_indexers_blocks_to_indexers_handler( + #[values( + ActiveLidarrBlock::DeleteIndexerPrompt, + ActiveLidarrBlock::Indexers, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + ActiveLidarrBlock::IndexerSettingsConfirmPrompt, + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + ActiveLidarrBlock::IndexerSettingsRetentionInput, + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_sonarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::Indexers, + active_sonarr_block + ); + } } diff --git a/src/handlers/lidarr_handlers/mod.rs b/src/handlers/lidarr_handlers/mod.rs index e03b857..1735b32 100644 --- a/src/handlers/lidarr_handlers/mod.rs +++ b/src/handlers/lidarr_handlers/mod.rs @@ -1,4 +1,5 @@ use history::HistoryHandler; +use indexers::IndexersHandler; use library::LibraryHandler; use super::KeyEventHandler; @@ -9,10 +10,11 @@ use crate::{ app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock, }; +mod downloads; mod history; +mod indexers; mod library; -mod downloads; #[cfg(test)] #[path = "lidarr_handler_tests.rs"] mod lidarr_handler_tests; @@ -41,6 +43,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b RootFoldersHandler::new(self.key, self.app, self.active_lidarr_block, self.context) .handle(); } + _ if IndexersHandler::accepts(self.active_lidarr_block) => { + IndexersHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); + } _ => self.handle_key_event(), } } diff --git a/src/handlers/lidarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/lidarr_handlers/root_folders/root_folders_handler_tests.rs index 13e5a2e..8e37557 100644 --- a/src/handlers/lidarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/lidarr_handlers/root_folders/root_folders_handler_tests.rs @@ -105,9 +105,9 @@ mod tests { assert_eq!( app.data.lidarr_data.main_tabs.get_active_route(), - ActiveLidarrBlock::Artists.into() + ActiveLidarrBlock::Indexers.into() ); - assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into()); + assert_navigation_pushed!(app, ActiveLidarrBlock::Indexers.into()); } #[rstest] diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs index c22dad1..d83ab1b 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs @@ -125,7 +125,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' .edit_indexer_modal .as_mut() .unwrap(); - if edit_indexer_modal.priority > 0 { + if edit_indexer_modal.priority > 1 { edit_indexer_modal.priority -= 1; } } diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs index 179e7fe..adc8454 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -50,7 +50,7 @@ mod tests { .as_ref() .unwrap() .priority, - 1 + 2 ); } else { assert_eq!( @@ -61,7 +61,7 @@ mod tests { .as_ref() .unwrap() .priority, - 0 + 1 ); EditIndexerHandler::new( @@ -80,7 +80,7 @@ mod tests { .as_ref() .unwrap() .priority, - 1 + 2 ); EditIndexerHandler::new( @@ -98,7 +98,7 @@ mod tests { .as_ref() .unwrap() .priority, - 0 + 1 ); } } diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs index 4e2d3c9..8d1394f 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs @@ -124,7 +124,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<' .edit_indexer_modal .as_mut() .unwrap(); - if edit_indexer_modal.priority > 0 { + if edit_indexer_modal.priority > 1 { edit_indexer_modal.priority -= 1; } } diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs index 824ecae..6ebe7fb 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -50,7 +50,7 @@ mod tests { .as_ref() .unwrap() .priority, - 1 + 2 ); } else { assert_eq!( @@ -61,7 +61,7 @@ mod tests { .as_ref() .unwrap() .priority, - 0 + 1 ); EditIndexerHandler::new( @@ -80,7 +80,7 @@ mod tests { .as_ref() .unwrap() .priority, - 1 + 2 ); EditIndexerHandler::new( @@ -98,7 +98,7 @@ mod tests { .as_ref() .unwrap() .priority, - 0 + 1 ); } } diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs index e2185ff..a6e3bb0 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -5,7 +5,7 @@ use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, }; -use crate::models::sonarr_models::IndexerSettings; +use crate::models::servarr_models::IndexerSettings; use crate::network::sonarr_network::SonarrEvent; use crate::{handle_prompt_left_right_keys, matches_key}; diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs index c8464f7..8e7a0b8 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -15,7 +15,7 @@ mod tests { use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, }; - use crate::models::sonarr_models::IndexerSettings; + use crate::models::servarr_models::IndexerSettings; mod test_handle_scroll_up_and_down { use pretty_assertions::assert_eq; @@ -23,7 +23,7 @@ mod tests { use crate::models::BlockSelectionState; use crate::models::servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS; - use crate::models::sonarr_models::IndexerSettings; + use crate::models::servarr_models::IndexerSettings; use super::*; @@ -242,7 +242,7 @@ mod tests { assert_navigation_popped, models::{ BlockSelectionState, servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, - sonarr_models::IndexerSettings, + servarr_models::IndexerSettings, }, network::sonarr_network::SonarrEvent, }; @@ -415,7 +415,7 @@ mod tests { mod test_handle_esc { use rstest::rstest; - use crate::models::sonarr_models::IndexerSettings; + use crate::models::servarr_models::IndexerSettings; use super::*; use crate::assert_navigation_popped; diff --git a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs index 08cefc2..39e5435 100644 --- a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs +++ b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs @@ -2,13 +2,14 @@ #[macro_use] pub(in crate::handlers::sonarr_handlers) mod utils { use crate::models::HorizontallyScrollableText; + use crate::models::servarr_models::IndexerSettings; use crate::models::servarr_models::{ Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder, }; use crate::models::sonarr_models::{ AddSeriesSearchResult, AddSeriesSearchResultStatistics, DownloadRecord, DownloadStatus, - Episode, EpisodeFile, IndexerSettings, MediaInfo, Rating, Season, SeasonStatistics, Series, - SeriesStatistics, SeriesStatus, SeriesType, + Episode, EpisodeFile, MediaInfo, Rating, Season, SeasonStatistics, Series, SeriesStatistics, + SeriesStatus, SeriesType, }; use chrono::DateTime; use serde_json::{Number, json}; diff --git a/src/models/lidarr_models.rs b/src/models/lidarr_models.rs index e528170..db612eb 100644 --- a/src/models/lidarr_models.rs +++ b/src/models/lidarr_models.rs @@ -1,3 +1,12 @@ +use super::{ + HorizontallyScrollableText, Serdeable, + servarr_models::{ + DiskSpace, HostConfig, Indexer, IndexerTestResult, QualityProfile, QualityWrapper, RootFolder, + SecurityConfig, Tag, + }, +}; +use crate::models::servarr_models::IndexerSettings; +use crate::serde_enum_from; use chrono::{DateTime, Utc}; use derivative::Derivative; use enum_display_style_derive::EnumDisplayStyle; @@ -5,14 +14,6 @@ use serde::{Deserialize, Serialize}; use serde_json::{Number, Value}; use strum::{Display, EnumIter}; -use super::{ - HorizontallyScrollableText, Serdeable, - servarr_models::{ - DiskSpace, HostConfig, QualityProfile, QualityWrapper, RootFolder, SecurityConfig, Tag, - }, -}; -use crate::serde_enum_from; - #[cfg(test)] #[path = "lidarr_models_tests.rs"] mod lidarr_models_tests; @@ -442,6 +443,9 @@ serde_enum_from!( DownloadsResponse(DownloadsResponse), HistoryWrapper(LidarrHistoryWrapper), HostConfig(HostConfig), + IndexerSettings(IndexerSettings), + Indexers(Vec), + IndexerTestResults(Vec), MetadataProfiles(Vec), QualityProfiles(Vec), RootFolders(Vec), diff --git a/src/models/lidarr_models_tests.rs b/src/models/lidarr_models_tests.rs index 06b3083..9053e59 100644 --- a/src/models/lidarr_models_tests.rs +++ b/src/models/lidarr_models_tests.rs @@ -10,7 +10,8 @@ mod tests { MonitorType, NewItemMonitorType, SystemStatus, }; use crate::models::servarr_models::{ - DiskSpace, HostConfig, QualityProfile, RootFolder, SecurityConfig, Tag, + DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, QualityProfile, RootFolder, + SecurityConfig, Tag, }; use crate::models::{ Serdeable, @@ -320,6 +321,48 @@ mod tests { ); } + #[test] + fn test_lidarr_serdeable_from_indexers() { + let indexers = vec![Indexer { + id: 1, + ..Indexer::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = indexers.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Indexers(indexers)); + } + + #[test] + fn test_lidarr_serdeable_from_indexer_settings() { + let indexer_settings = IndexerSettings { + id: 1, + ..IndexerSettings::default() + }; + + let lidarr_serdeable: LidarrSerdeable = indexer_settings.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::IndexerSettings(indexer_settings) + ); + } + + #[test] + fn test_lidarr_serdeable_from_indexer_test_results() { + let indexer_test_results = vec![IndexerTestResult { + id: 1, + ..IndexerTestResult::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = indexer_test_results.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::IndexerTestResults(indexer_test_results) + ); + } + #[test] fn test_lidarr_serdeable_from_metadata_profiles() { let metadata_profiles = vec![MetadataProfile { diff --git a/src/models/servarr_data/lidarr/lidarr_data.rs b/src/models/servarr_data/lidarr/lidarr_data.rs index 470acae..0671396 100644 --- a/src/models/servarr_data/lidarr/lidarr_data.rs +++ b/src/models/servarr_data/lidarr/lidarr_data.rs @@ -2,15 +2,19 @@ use serde_json::Number; use super::modals::{AddArtistModal, AddRootFolderModal, EditArtistModal}; use crate::app::context_clues::{ - DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, + DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, }; use crate::app::lidarr::lidarr_context_clues::{ ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, }; +use crate::models::servarr_data::modals::EditIndexerModal; +use crate::models::servarr_models::IndexerSettings; use crate::models::{ BlockSelectionState, HorizontallyScrollableText, Route, TabRoute, TabState, lidarr_models::{AddArtistSearchResult, Album, Artist, DownloadRecord, LidarrHistoryItem}, - servarr_models::{DiskSpace, RootFolder}, + servarr_data::modals::IndexerTestResultModalItem, + servarr_models::{DiskSpace, Indexer, RootFolder}, stateful_table::StatefulTable, }; use crate::network::lidarr_network::LidarrEvent; @@ -22,12 +26,14 @@ use strum::EnumIter; use { crate::models::lidarr_models::{MonitorType, NewItemMonitorType}, crate::models::stateful_table::SortOption, + crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer_settings, crate::network::lidarr_network::lidarr_network_test_utils::test_utils::quality_profile_map, crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ - add_artist_search_result, album, artist, download_record, lidarr_history_item, + add_artist_search_result, album, artist, download_record, indexer, lidarr_history_item, metadata_profile, metadata_profile_map, quality_profile, root_folder, tags_map, }, crate::network::servarr_test_utils::diskspace, + crate::network::servarr_test_utils::indexer_test_result, strum::{Display, EnumString, IntoEnumIterator}, }; @@ -48,7 +54,12 @@ pub struct LidarrData<'a> { pub disk_space_vec: Vec, pub downloads: StatefulTable, pub edit_artist_modal: Option, + pub edit_indexer_modal: Option, pub history: StatefulTable, + pub indexers: StatefulTable, + pub indexer_settings: Option, + pub indexer_test_all_results: Option>, + pub indexer_test_errors: Option, pub main_tabs: TabState, pub metadata_profile_map: BiMap, pub prompt_confirm: bool, @@ -118,7 +129,12 @@ impl<'a> Default for LidarrData<'a> { disk_space_vec: Vec::new(), downloads: StatefulTable::default(), edit_artist_modal: None, + edit_indexer_modal: None, history: StatefulTable::default(), + indexers: StatefulTable::default(), + indexer_settings: None, + indexer_test_all_results: None, + indexer_test_errors: None, metadata_profile_map: BiMap::new(), prompt_confirm: false, prompt_confirm_action: None, @@ -153,6 +169,12 @@ impl<'a> Default for LidarrData<'a> { contextual_help: Some(&ROOT_FOLDERS_CONTEXT_CLUES), config: None, }, + TabRoute { + title: "Indexers".to_string(), + route: ActiveLidarrBlock::Indexers.into(), + contextual_help: Some(&INDEXERS_CONTEXT_CLUES), + config: None, + }, ]), artist_info_tabs: TabState::new(vec![TabRoute { title: "Albums".to_string(), @@ -221,14 +243,33 @@ impl LidarrData<'_> { .metadata_profile_list .set_items(vec![metadata_profile().name]); + let edit_indexer_modal = EditIndexerModal { + name: "DrunkenSlug".into(), + enable_rss: Some(true), + enable_automatic_search: Some(true), + enable_interactive_search: Some(true), + url: "http://127.0.0.1:9696/1/".into(), + api_key: "someApiKey".into(), + seed_ratio: "ratio".into(), + tags: "25".into(), + priority: 1, + }; + + let mut indexer_test_all_results = StatefulTable::default(); + indexer_test_all_results.set_items(vec![indexer_test_result()]); + let mut lidarr_data = LidarrData { delete_files: true, disk_space_vec: vec![diskspace()], quality_profile_map: quality_profile_map(), metadata_profile_map: metadata_profile_map(), edit_artist_modal: Some(edit_artist_modal), + edit_indexer_modal: Some(edit_indexer_modal), add_root_folder_modal: Some(add_root_folder_modal), add_artist_modal: Some(add_artist_modal), + indexer_settings: Some(indexer_settings()), + indexer_test_all_results: Some(indexer_test_all_results), + indexer_test_errors: Some("error".to_string()), tags_map: tags_map(), ..LidarrData::default() }; @@ -250,6 +291,7 @@ impl LidarrData<'_> { lidarr_data.history.search = Some("test search".into()); lidarr_data.history.filter = Some("test filter".into()); lidarr_data.root_folders.set_items(vec![root_folder()]); + lidarr_data.indexers.set_items(vec![indexer()]); lidarr_data.version = "1.0.0".to_owned(); lidarr_data.add_artist_search = Some("Test Artist".into()); let mut add_searched_artists = StatefulTable::default(); @@ -288,6 +330,7 @@ pub enum ActiveLidarrBlock { AddRootFolderSelectQualityProfile, AddRootFolderSelectMetadataProfile, AddRootFolderTagsInput, + AllIndexerSettingsPrompt, AutomaticallySearchArtistPrompt, DeleteAlbumPrompt, DeleteAlbumConfirmPrompt, @@ -308,6 +351,18 @@ pub enum ActiveLidarrBlock { EditArtistSelectQualityProfile, EditArtistTagsInput, EditArtistToggleMonitored, + EditIndexerPrompt, + EditIndexerConfirmPrompt, + EditIndexerApiKeyInput, + EditIndexerNameInput, + EditIndexerSeedRatioInput, + EditIndexerToggleEnableRss, + EditIndexerToggleEnableAutomaticSearch, + EditIndexerToggleEnableInteractiveSearch, + EditIndexerUrlInput, + EditIndexerPriorityInput, + EditIndexerTagsInput, + DeleteIndexerPrompt, FilterArtists, FilterArtistsError, FilterHistory, @@ -315,6 +370,14 @@ pub enum ActiveLidarrBlock { History, HistoryItemDetails, HistorySortPrompt, + Indexers, + IndexerSettingsConfirmPrompt, + IndexerSettingsMaximumSizeInput, + IndexerSettingsMinimumAgeInput, + IndexerSettingsRetentionInput, + IndexerSettingsRssSyncIntervalInput, + TestAllIndexers, + TestIndexer, RootFolders, SearchAlbums, SearchAlbumsError, @@ -461,6 +524,93 @@ pub const ADD_ROOT_FOLDER_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ &[ActiveLidarrBlock::AddRootFolderConfirmPrompt], ]; +pub static EDIT_INDEXER_BLOCKS: [ActiveLidarrBlock; 11] = [ + ActiveLidarrBlock::EditIndexerPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerPriorityInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ActiveLidarrBlock::EditIndexerTagsInput, +]; + +pub const EDIT_INDEXER_TORRENT_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerTagsInput, + ], + &[ + ActiveLidarrBlock::EditIndexerPriorityInput, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ], + &[ + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ], +]; + +pub const EDIT_INDEXER_NZB_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerTagsInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerPriorityInput, + ], + &[ + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ], +]; + +pub static INDEXER_SETTINGS_BLOCKS: [ActiveLidarrBlock; 6] = [ + ActiveLidarrBlock::AllIndexerSettingsPrompt, + ActiveLidarrBlock::IndexerSettingsConfirmPrompt, + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + ActiveLidarrBlock::IndexerSettingsRetentionInput, + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput, +]; + +pub const INDEXER_SETTINGS_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ActiveLidarrBlock::IndexerSettingsMinimumAgeInput], + &[ActiveLidarrBlock::IndexerSettingsRetentionInput], + &[ActiveLidarrBlock::IndexerSettingsMaximumSizeInput], + &[ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput], + &[ActiveLidarrBlock::IndexerSettingsConfirmPrompt], +]; + +pub static INDEXERS_BLOCKS: [ActiveLidarrBlock; 3] = [ + ActiveLidarrBlock::Indexers, + ActiveLidarrBlock::DeleteIndexerPrompt, + ActiveLidarrBlock::TestIndexer, +]; + impl From for Route { fn from(active_lidarr_block: ActiveLidarrBlock) -> Route { Route::Lidarr(active_lidarr_block, None) diff --git a/src/models/servarr_data/lidarr/lidarr_data_tests.rs b/src/models/servarr_data/lidarr/lidarr_data_tests.rs index a508753..8bc7c94 100644 --- a/src/models/servarr_data/lidarr/lidarr_data_tests.rs +++ b/src/models/servarr_data/lidarr/lidarr_data_tests.rs @@ -1,17 +1,20 @@ #[cfg(test)] mod tests { use crate::app::context_clues::{ - DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, + DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, }; use crate::app::lidarr::lidarr_context_clues::{ ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, }; use crate::models::lidarr_models::Album; use crate::models::servarr_data::lidarr::lidarr_data::{ - ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ARTIST_DETAILS_BLOCKS, DELETE_ALBUM_BLOCKS, - DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, - DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, HISTORY_BLOCKS, - ROOT_FOLDERS_BLOCKS, + ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ARTIST_DETAILS_BLOCKS, + DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS, + DELETE_ARTIST_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS, + EDIT_ARTIST_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS, + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS, + INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, ROOT_FOLDERS_BLOCKS, }; use crate::models::{ BlockSelectionState, Route, @@ -150,7 +153,7 @@ mod tests { assert_is_empty!(lidarr_data.tags_map); assert_is_empty!(lidarr_data.version); - assert_eq!(lidarr_data.main_tabs.tabs.len(), 4); + assert_eq!(lidarr_data.main_tabs.tabs.len(), 5); assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library"); assert_eq!( @@ -196,6 +199,17 @@ mod tests { ); assert_none!(lidarr_data.main_tabs.tabs[3].config); + assert_str_eq!(lidarr_data.main_tabs.tabs[4].title, "Indexers"); + assert_eq!( + lidarr_data.main_tabs.tabs[4].route, + ActiveLidarrBlock::Indexers.into() + ); + assert_some_eq_x!( + &lidarr_data.main_tabs.tabs[4].contextual_help, + &INDEXERS_CONTEXT_CLUES + ); + assert_none!(lidarr_data.main_tabs.tabs[4].config); + assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 1); assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums"); assert_eq!( @@ -414,9 +428,168 @@ mod tests { assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveLidarrBlock::DeleteRootFolderPrompt)); } + #[test] + fn test_edit_indexer_blocks_contents() { + assert_eq!(EDIT_INDEXER_BLOCKS.len(), 11); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerPrompt)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerConfirmPrompt)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerApiKeyInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerNameInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerSeedRatioInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerToggleEnableRss)); + assert!( + EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch) + ); + assert!( + EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch) + ); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerUrlInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerTagsInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerPriorityInput)); + } + + #[test] + fn test_edit_indexer_nzb_selection_blocks_ordering() { + let mut edit_indexer_nzb_selection_block_iter = EDIT_INDEXER_NZB_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerTagsInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerPriorityInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ] + ); + assert_eq!(edit_indexer_nzb_selection_block_iter.next(), None); + } + + #[test] + fn test_edit_indexer_torrent_selection_blocks_ordering() { + let mut edit_indexer_torrent_selection_block_iter = + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerTagsInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerPriorityInput, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ] + ); + assert_eq!(edit_indexer_torrent_selection_block_iter.next(), None); + } + + #[test] + fn test_indexer_settings_blocks_contents() { + assert_eq!(INDEXER_SETTINGS_BLOCKS.len(), 6); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::AllIndexerSettingsPrompt)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsConfirmPrompt)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsMaximumSizeInput)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsMinimumAgeInput)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsRetentionInput)); + assert!( + INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput) + ); + } + + #[test] + fn test_indexer_settings_selection_blocks_ordering() { + let mut indexer_settings_block_iter = INDEXER_SETTINGS_SELECTION_BLOCKS.iter(); + + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveLidarrBlock::IndexerSettingsMinimumAgeInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveLidarrBlock::IndexerSettingsRetentionInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveLidarrBlock::IndexerSettingsMaximumSizeInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveLidarrBlock::IndexerSettingsConfirmPrompt,] + ); + assert_eq!(indexer_settings_block_iter.next(), None); + } + + #[test] + fn test_indexers_blocks_contents() { + assert_eq!(INDEXERS_BLOCKS.len(), 3); + assert!(INDEXERS_BLOCKS.contains(&ActiveLidarrBlock::Indexers)); + assert!(INDEXERS_BLOCKS.contains(&ActiveLidarrBlock::DeleteIndexerPrompt)); + assert!(INDEXERS_BLOCKS.contains(&ActiveLidarrBlock::TestIndexer)); + } + #[test] fn test_add_root_folder_blocks_contents() { - use crate::models::servarr_data::lidarr::lidarr_data::ADD_ROOT_FOLDER_BLOCKS; assert_eq!(ADD_ROOT_FOLDER_BLOCKS.len(), 9); assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderPrompt)); assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderConfirmPrompt)); diff --git a/src/models/servarr_data/lidarr/modals.rs b/src/models/servarr_data/lidarr/modals.rs index aa7fef3..9453045 100644 --- a/src/models/servarr_data/lidarr/modals.rs +++ b/src/models/servarr_data/lidarr/modals.rs @@ -1,6 +1,8 @@ use strum::IntoEnumIterator; use super::lidarr_data::LidarrData; +use crate::models::servarr_data::modals::EditIndexerModal; +use crate::models::servarr_models::Indexer; use crate::models::{ HorizontallyScrollableText, lidarr_models::{MonitorType, NewItemMonitorType}, @@ -114,6 +116,76 @@ impl From<&LidarrData<'_>> for EditArtistModal { } } +impl From<&LidarrData<'_>> for EditIndexerModal { + fn from(lidarr_data: &LidarrData<'_>) -> EditIndexerModal { + let mut edit_indexer_modal = EditIndexerModal::default(); + let Indexer { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + tags, + fields, + priority, + .. + } = lidarr_data.indexers.current_selection(); + let seed_ratio_field_option = fields + .as_ref() + .expect("indexer fields must exist") + .iter() + .find(|field| { + field.name.as_ref().expect("indexer field name must exist") == "seedCriteria.seedRatio" + }); + let seed_ratio_value_option = if let Some(seed_ratio_field) = seed_ratio_field_option { + seed_ratio_field.value.clone() + } else { + None + }; + + edit_indexer_modal.name = name.clone().expect("indexer name must exist").into(); + edit_indexer_modal.enable_rss = Some(*enable_rss); + edit_indexer_modal.enable_automatic_search = Some(*enable_automatic_search); + edit_indexer_modal.enable_interactive_search = Some(*enable_interactive_search); + edit_indexer_modal.priority = *priority; + edit_indexer_modal.url = fields + .as_ref() + .expect("indexer fields must exist") + .iter() + .find(|field| field.name.as_ref().expect("indexer field name must exist") == "baseUrl") + .expect("baseUrl field must exist") + .value + .clone() + .expect("baseUrl field value must exist") + .as_str() + .expect("baseUrl field value must be a string") + .into(); + edit_indexer_modal.api_key = fields + .as_ref() + .expect("indexer fields must exist") + .iter() + .find(|field| field.name.as_ref().expect("indexer field name must exist") == "apiKey") + .expect("apiKey field must exist") + .value + .clone() + .expect("apiKey field value must exist") + .as_str() + .expect("apiKey field value must be a string") + .into(); + + if let Some(seed_ratio_value) = seed_ratio_value_option { + edit_indexer_modal.seed_ratio = seed_ratio_value + .as_f64() + .expect("Seed ratio value must be a valid f64") + .to_string() + .into(); + } + + edit_indexer_modal.tags = lidarr_data.tag_ids_to_display(tags).into(); + + edit_indexer_modal + } +} + #[derive(Default)] #[cfg_attr(test, derive(Debug))] pub struct AddRootFolderModal { diff --git a/src/models/servarr_data/lidarr/modals_tests.rs b/src/models/servarr_data/lidarr/modals_tests.rs index 5977faf..279c63f 100644 --- a/src/models/servarr_data/lidarr/modals_tests.rs +++ b/src/models/servarr_data/lidarr/modals_tests.rs @@ -1,12 +1,14 @@ #[cfg(test)] mod tests { - use bimap::BiMap; - use pretty_assertions::{assert_eq, assert_str_eq}; - use crate::models::lidarr_models::{Artist, MonitorType, NewItemMonitorType}; use crate::models::servarr_data::lidarr::lidarr_data::LidarrData; use crate::models::servarr_data::lidarr::modals::{AddArtistModal, EditArtistModal}; - use crate::models::servarr_models::RootFolder; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_models::{Indexer, IndexerField, RootFolder}; + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use serde_json::{Number, Value}; #[test] fn test_add_artist_modal_from_lidarr_data() { @@ -108,4 +110,102 @@ mod tests { assert_str_eq!(edit_artist_modal.path.text, "/nfs/music/test_artist"); assert_str_eq!(edit_artist_modal.tags.text, "usenet"); } + + #[rstest] + fn test_edit_indexer_modal_from_lidarr_data(#[values(true, false)] seed_ratio_present: bool) { + let mut lidarr_data = LidarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..LidarrData::default() + }; + let mut fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + ]; + + if seed_ratio_present { + fields.push(IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(Value::from(1.2f64)), + }); + } + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + priority: 1, + ..Indexer::default() + }; + lidarr_data.indexers.set_items(vec![indexer]); + + let edit_indexer_modal = EditIndexerModal::from(&lidarr_data); + + assert_str_eq!(edit_indexer_modal.name.text, "Test"); + assert_eq!(edit_indexer_modal.enable_rss, Some(true)); + assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true)); + assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true)); + assert_eq!(edit_indexer_modal.priority, 1); + assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); + assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); + + if seed_ratio_present { + assert_str_eq!(edit_indexer_modal.seed_ratio.text, "1.2"); + } else { + assert!(edit_indexer_modal.seed_ratio.text.is_empty()); + } + } + + #[test] + fn test_edit_indexer_modal_from_lidarr_data_seed_ratio_value_is_none() { + let mut lidarr_data = LidarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..LidarrData::default() + }; + let fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: None, + }, + ]; + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + priority: 1, + ..Indexer::default() + }; + lidarr_data.indexers.set_items(vec![indexer]); + + let edit_indexer_modal = EditIndexerModal::from(&lidarr_data); + + assert_str_eq!(edit_indexer_modal.name.text, "Test"); + assert_eq!(edit_indexer_modal.enable_rss, Some(true)); + assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true)); + assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true)); + assert_eq!(edit_indexer_modal.priority, 1); + assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); + assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); + assert!(edit_indexer_modal.seed_ratio.text.is_empty()); + } } diff --git a/src/models/servarr_data/modals.rs b/src/models/servarr_data/modals.rs index 46d329b..2755e80 100644 --- a/src/models/servarr_data/modals.rs +++ b/src/models/servarr_data/modals.rs @@ -1,6 +1,10 @@ use crate::models::HorizontallyScrollableText; -#[derive(Default, Debug, PartialEq, Eq)] +#[cfg(test)] +#[path = "modals_tests.rs"] +mod modals_tests; + +#[derive(Debug, PartialEq, Eq)] pub struct EditIndexerModal { pub name: HorizontallyScrollableText, pub enable_rss: Option, @@ -13,6 +17,22 @@ pub struct EditIndexerModal { pub priority: i64, } +impl Default for EditIndexerModal { + fn default() -> Self { + Self { + name: Default::default(), + enable_rss: None, + enable_automatic_search: None, + enable_interactive_search: None, + url: Default::default(), + api_key: Default::default(), + seed_ratio: Default::default(), + tags: Default::default(), + priority: 1, + } + } +} + #[derive(Default, Clone, Eq, PartialEq, Debug)] pub struct IndexerTestResultModalItem { pub name: String, diff --git a/src/models/servarr_data/modals_tests.rs b/src/models/servarr_data/modals_tests.rs new file mode 100644 index 0000000..26ce17a --- /dev/null +++ b/src/models/servarr_data/modals_tests.rs @@ -0,0 +1,20 @@ +#[cfg(test)] +mod tests { + use crate::models::servarr_data::modals::EditIndexerModal; + use pretty_assertions::assert_eq; + + #[test] + fn test_edit_indexer_modal_default() { + let edit_indexer_modal = EditIndexerModal::default(); + + assert_is_empty!(edit_indexer_modal.name.text); + assert_none!(&edit_indexer_modal.enable_rss); + assert_none!(&edit_indexer_modal.enable_automatic_search); + assert_none!(&edit_indexer_modal.enable_interactive_search); + assert_is_empty!(edit_indexer_modal.url.text); + assert_is_empty!(edit_indexer_modal.api_key.text); + assert_is_empty!(edit_indexer_modal.seed_ratio.text); + assert_is_empty!(edit_indexer_modal.tags.text); + assert_eq!(edit_indexer_modal.priority, 1); + } +} diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index b22ec09..6d5aa55 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -12,10 +12,10 @@ use crate::{ models::{ BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, TabRoute, TabState, servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem}, - servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder}, + servarr_models::{DiskSpace, Indexer, IndexerSettings, QueueEvent, RootFolder}, sonarr_models::{ - AddSeriesSearchResult, BlocklistItem, DownloadRecord, IndexerSettings, Season, Series, - SonarrHistoryItem, SonarrTask, + AddSeriesSearchResult, BlocklistItem, DownloadRecord, Season, Series, SonarrHistoryItem, + SonarrTask, }, stateful_list::StatefulList, stateful_table::StatefulTable, @@ -33,11 +33,12 @@ use { crate::models::sonarr_models::{SeriesMonitor, SeriesType}, crate::models::stateful_table::SortOption, crate::network::servarr_test_utils::diskspace, + crate::network::servarr_test_utils::indexer_settings, crate::network::servarr_test_utils::indexer_test_result, crate::network::servarr_test_utils::queued_event, crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{ - add_series_search_result, blocklist_item, download_record, history_item, indexer, - indexer_settings, log_line, root_folder, + add_series_search_result, blocklist_item, download_record, history_item, indexer, log_line, + root_folder, }, crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{ episode, episode_file, language_profiles_map, quality_profile_map, season, series, tags_map, diff --git a/src/models/servarr_models.rs b/src/models/servarr_models.rs index 8c039bb..477e04c 100644 --- a/src/models/servarr_models.rs +++ b/src/models/servarr_models.rs @@ -89,6 +89,21 @@ pub struct DiskSpace { pub total_space: i64, } +#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct IndexerSettings { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub minimum_age: i64, + #[serde(deserialize_with = "super::from_i64")] + pub retention: i64, + #[serde(deserialize_with = "super::from_i64")] + pub maximum_size: i64, + #[serde(deserialize_with = "super::from_i64")] + pub rss_sync_interval: i64, +} + #[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct EditIndexerParams { @@ -130,7 +145,7 @@ pub struct HostConfig { pub ssl_cert_password: Option, } -#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Indexer { #[serde(deserialize_with = "super::from_i64")] @@ -153,6 +168,28 @@ pub struct Indexer { pub tags: Vec, } +impl Default for Indexer { + fn default() -> Self { + Self { + id: 0, + name: None, + implementation: None, + implementation_name: None, + config_contract: None, + supports_rss: false, + supports_search: false, + fields: None, + enable_rss: false, + enable_automatic_search: false, + enable_interactive_search: false, + protocol: "".to_string(), + priority: 1, + download_client_id: 0, + tags: vec![], + } + } +} + #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct IndexerTestResult { diff --git a/src/models/servarr_models_tests.rs b/src/models/servarr_models_tests.rs index dfe4cc9..e57dac7 100644 --- a/src/models/servarr_models_tests.rs +++ b/src/models/servarr_models_tests.rs @@ -3,9 +3,30 @@ mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; use crate::models::servarr_models::{ - AuthenticationMethod, AuthenticationRequired, CertificateValidation, QualityProfile, + AuthenticationMethod, AuthenticationRequired, CertificateValidation, Indexer, QualityProfile, }; + #[test] + fn test_indexer_default() { + let indexer = Indexer::default(); + + assert_eq!(indexer.id, 0); + assert_none!(indexer.name); + assert_none!(indexer.implementation); + assert_none!(indexer.implementation_name); + assert_none!(indexer.config_contract); + assert!(!indexer.supports_rss); + assert!(!indexer.supports_search); + assert_none!(indexer.fields); + assert!(!indexer.enable_rss); + assert!(!indexer.enable_automatic_search); + assert!(!indexer.enable_interactive_search); + assert_is_empty!(indexer.protocol); + assert_eq!(indexer.priority, 1); + assert_eq!(indexer.download_client_id, 0); + assert_is_empty!(indexer.tags); + } + #[test] fn test_authentication_method_display() { assert_str_eq!(AuthenticationMethod::Basic.to_string(), "basic"); diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index 115cf35..73a4453 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -1,6 +1,9 @@ use std::fmt::{Display, Formatter}; -use crate::{models::servarr_models::IndexerTestResult, serde_enum_from}; +use crate::{ + models::servarr_models::{IndexerSettings, IndexerTestResult}, + serde_enum_from, +}; use chrono::{DateTime, Utc}; use clap::ValueEnum; use derivative::Derivative; @@ -221,21 +224,6 @@ pub struct EpisodeFile { pub media_info: Option, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IndexerSettings { - #[serde(deserialize_with = "super::from_i64")] - pub id: i64, - #[serde(deserialize_with = "super::from_i64")] - pub minimum_age: i64, - #[serde(deserialize_with = "super::from_i64")] - pub retention: i64, - #[serde(deserialize_with = "super::from_i64")] - pub maximum_size: i64, - #[serde(deserialize_with = "super::from_i64")] - pub rss_sync_interval: i64, -} - #[derive(Serialize, Deserialize, Derivative, Debug, Clone, PartialEq, Eq)] #[derivative(Default)] #[serde(rename_all = "camelCase")] diff --git a/src/models/sonarr_models_tests.rs b/src/models/sonarr_models_tests.rs index 30c96fb..0d4c0cf 100644 --- a/src/models/sonarr_models_tests.rs +++ b/src/models/sonarr_models_tests.rs @@ -6,14 +6,14 @@ mod tests { use crate::models::{ Serdeable, servarr_models::{ - DiskSpace, HostConfig, Indexer, IndexerTestResult, Language, Log, LogResponse, - QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update, + DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Language, Log, + LogResponse, QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update, }, sonarr_models::{ AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadStatus, - DownloadsResponse, Episode, EpisodeFile, IndexerSettings, Series, SeriesMonitor, - SeriesStatus, SeriesType, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, - SonarrSerdeable, SonarrTask, SonarrTaskName, SystemStatus, + DownloadsResponse, Episode, EpisodeFile, Series, SeriesMonitor, SeriesStatus, SeriesType, + SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrSerdeable, SonarrTask, + SonarrTaskName, SystemStatus, }, }; diff --git a/src/network/lidarr_network/indexers/lidarr_indexers_network_tests.rs b/src/network/lidarr_network/indexers/lidarr_indexers_network_tests.rs new file mode 100644 index 0000000..3ad41fd --- /dev/null +++ b/src/network/lidarr_network/indexers/lidarr_indexers_network_tests.rs @@ -0,0 +1,901 @@ +#[cfg(test)] +mod tests { + use crate::models::HorizontallyScrollableText; + use crate::models::lidarr_models::LidarrSerdeable; + use crate::models::servarr_data::modals::IndexerTestResultModalItem; + use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult}; + use crate::network::NetworkResource; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ + indexer, indexer_settings, + }; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use bimap::BiMap; + use mockito::Matcher; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[tokio::test] + async fn test_handle_delete_lidarr_indexer_event() { + let (mock, app, _server) = MockServarrApi::delete() + .path("/1") + .build_for(LidarrEvent::DeleteIndexer(1)) + .await; + app + .lock() + .await + .data + .lidarr_data + .indexers + .set_items(vec![indexer()]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteIndexer(1)) + .await + .is_ok() + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_all_indexer_settings_event() { + let indexer_settings_json = json!({ + "id": 1, + "minimumAge": 1, + "maximumSize": 12345, + "retention": 1, + "rssSyncInterval": 60 + }); + let (mock, app, _server) = MockServarrApi::put() + .with_request_body(indexer_settings_json) + .build_for(LidarrEvent::EditAllIndexerSettings(indexer_settings())) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditAllIndexerSettings(indexer_settings())) + .await + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event() { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + let (mock_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let mock_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + app.lock().await.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + .await + ); + + mock_details_server.assert_async().await; + mock_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event_does_not_overwrite_tags_vec_if_tag_input_string_is_none() + { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tags: Some(vec![1, 2]), + priority: Some(0), + ..EditIndexerParams::default() + }; + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + let (mock_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let mock_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + .await + ); + + mock_details_server.assert_async().await; + mock_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event_does_not_add_seed_ratio_when_seed_ratio_field_is_none_in_details() + { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + ], + "tags": [1, 2], + "id": 1 + }); + + let (mock_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let mock_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + app.lock().await.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + .await + ); + + mock_details_server.assert_async().await; + mock_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event_populates_the_seed_ratio_value_when_seed_ratio_field_is_present_in_details() + { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + + let (mock_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let mock_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + app.lock().await.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + .await + ); + + mock_details_server.assert_async().await; + mock_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event_defaults_to_previous_values() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let edit_indexer_params = EditIndexerParams { + indexer_id: 1, + ..EditIndexerParams::default() + }; + let (mock_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json.clone()) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let mock_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(indexer_details_json)) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(edit_indexer_params)) + .await + ); + + mock_details_server.assert_async().await; + mock_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event_clears_tags_when_clear_tags_is_true() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1, 2], + "id": 1 + }); + let expected_edit_indexer_body = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [], + "id": 1 + }); + let edit_indexer_params = EditIndexerParams { + indexer_id: 1, + clear_tags: true, + ..EditIndexerParams::default() + }; + + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_edit_indexer_body)) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(edit_indexer_params)) + .await + ); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_get_lidarr_indexers_event() { + let indexers_response_json = json!([{ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "supportsRss": true, + "supportsSearch": true, + "protocol": "torrent", + "priority": 25, + "downloadClientId": 0, + "name": "Test Indexer", + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "implementationName": "Torznab", + "implementation": "Torznab", + "configContract": "TorznabSettings", + "tags": [1], + "id": 1 + }]); + let response: Vec = serde_json::from_value(indexers_response_json.clone()).unwrap(); + let (async_server, app, _server) = MockServarrApi::get() + .returns(indexers_response_json) + .build_for(LidarrEvent::GetIndexers) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Indexers(indexers) = network + .handle_lidarr_event(LidarrEvent::GetIndexers) + .await + .unwrap() + else { + panic!("Expected Indexers") + }; + + async_server.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.indexers.items, + vec![indexer()] + ); + assert_eq!(indexers, response); + } + + #[tokio::test] + async fn test_handle_test_lidarr_indexer_event_error() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let response_json = json!([ + { + "isWarning": false, + "propertyName": "", + "errorMessage": "test failure", + "severity": "error" + }]); + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json.clone()) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let async_test_server = server + .mock( + "POST", + format!("/api/v1{}", LidarrEvent::TestIndexer(1).resource()).as_str(), + ) + .with_status(400) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(indexer_details_json.clone())) + .with_body(response_json.to_string()) + .create_async() + .await; + app + .lock() + .await + .data + .lidarr_data + .indexers + .set_items(vec![indexer()]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Value(value) = network + .handle_lidarr_event(LidarrEvent::TestIndexer(1)) + .await + .unwrap() + else { + panic!("Expected Value") + }; + + async_details_server.assert_async().await; + async_test_server.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.indexer_test_errors, + Some("\"test failure\"".to_owned()) + ); + assert_eq!(value, response_json); + } + + #[tokio::test] + async fn test_handle_test_lidarr_indexer_event_success() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json.clone()) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let async_test_server = server + .mock( + "POST", + format!("/api/v1{}", LidarrEvent::TestIndexer(1).resource()).as_str(), + ) + .with_status(200) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(indexer_details_json.clone())) + .with_body("{}") + .create_async() + .await; + app + .lock() + .await + .data + .lidarr_data + .indexers + .set_items(vec![indexer()]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Value(value) = network + .handle_lidarr_event(LidarrEvent::TestIndexer(1)) + .await + .unwrap() + else { + panic!("Expected Value") + }; + async_details_server.assert_async().await; + async_test_server.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.indexer_test_errors, + Some(String::new()) + ); + assert_eq!(value, json!({})); + } + + #[tokio::test] + async fn test_handle_test_all_lidarr_indexers_event() { + let indexers = vec![ + Indexer { + id: 1, + name: Some("Test 1".to_owned()), + ..Indexer::default() + }, + Indexer { + id: 2, + name: Some("Test 2".to_owned()), + ..Indexer::default() + }, + ]; + let indexer_test_results_modal_items = vec![ + IndexerTestResultModalItem { + name: "Test 1".to_owned(), + is_valid: true, + validation_failures: HorizontallyScrollableText::default(), + }, + IndexerTestResultModalItem { + name: "Test 2".to_owned(), + is_valid: false, + validation_failures: "Failure for field 'test field 1': test error message, Failure for field 'test field 2': test error message 2".into(), + }, + ]; + let response_json = json!([ + { + "id": 1, + "isValid": true, + "validationFailures": [] + }, + { + "id": 2, + "isValid": false, + "validationFailures": [ + { + "propertyName": "test field 1", + "errorMessage": "test error message", + "severity": "error" + }, + { + "propertyName": "test field 2", + "errorMessage": "test error message 2", + "severity": "error" + }, + ] + }]); + let response: Vec = serde_json::from_value(response_json.clone()).unwrap(); + let (async_server, app, _server) = MockServarrApi::post() + .returns(response_json) + .status(400) + .build_for(LidarrEvent::TestAllIndexers) + .await; + app + .lock() + .await + .data + .lidarr_data + .indexers + .set_items(indexers); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::IndexerTestResults(results) = network + .handle_lidarr_event(LidarrEvent::TestAllIndexers) + .await + .unwrap() + else { + panic!("Expected IndexerTestResults") + }; + async_server.assert_async().await; + assert_some!(&app.lock().await.data.lidarr_data.indexer_test_all_results); + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .indexer_test_all_results + .as_ref() + .unwrap() + .items, + indexer_test_results_modal_items + ); + assert_eq!(results, response); + } + + #[tokio::test] + async fn test_handle_test_all_lidarr_indexers_event_sets_empty_table_on_api_error() { + let (async_server, app, _server) = MockServarrApi::post() + .status(500) + .build_for(LidarrEvent::TestAllIndexers) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::TestAllIndexers) + .await; + + async_server.assert_async().await; + assert_err!(result); + let app = app.lock().await; + assert_some!(&app.data.lidarr_data.indexer_test_all_results); + assert_is_empty!( + app + .data + .lidarr_data + .indexer_test_all_results + .as_ref() + .unwrap() + .items + ); + } +} diff --git a/src/network/lidarr_network/indexers/mod.rs b/src/network/lidarr_network/indexers/mod.rs new file mode 100644 index 0000000..b378924 --- /dev/null +++ b/src/network/lidarr_network/indexers/mod.rs @@ -0,0 +1,419 @@ +use crate::models::servarr_data::modals::IndexerTestResultModalItem; +use crate::models::servarr_models::{ + EditIndexerParams, Indexer, IndexerSettings, IndexerTestResult, +}; +use crate::models::stateful_table::StatefulTable; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::{Context, Result}; +use log::{debug, info}; +use serde_json::{Value, json}; + +#[cfg(test)] +#[path = "lidarr_indexers_network_tests.rs"] +mod lidarr_indexers_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn delete_lidarr_indexer( + &mut self, + indexer_id: i64, + ) -> Result<()> { + let event = LidarrEvent::DeleteIndexer(indexer_id); + info!("Deleting Lidarr indexer with id: {indexer_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{indexer_id}")), + None, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn edit_all_lidarr_indexer_settings( + &mut self, + params: IndexerSettings, + ) -> Result { + info!("Updating Lidarr indexer settings"); + let event = LidarrEvent::EditAllIndexerSettings(IndexerSettings::default()); + debug!("Indexer settings body: {params:?}"); + + let request_props = self + .request_props_from(event, RequestMethod::Put, Some(params), None, None) + .await; + + self + .handle_request::(request_props, |_, _| {}) + .await + } + + pub(in crate::network::lidarr_network) async fn get_all_lidarr_indexer_settings( + &mut self, + ) -> Result { + info!("Fetching Lidarr indexer settings"); + let event = LidarrEvent::GetAllIndexerSettings; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), IndexerSettings>(request_props, |indexer_settings, mut app| { + if app.data.lidarr_data.indexer_settings.is_none() { + app.data.lidarr_data.indexer_settings = Some(indexer_settings); + } else { + debug!("Indexer Settings are being modified. Ignoring update..."); + } + }) + .await + } + + pub(in crate::network::lidarr_network) async fn edit_lidarr_indexer( + &mut self, + mut edit_indexer_params: EditIndexerParams, + ) -> Result<()> { + if let Some(tag_input_str) = edit_indexer_params.tag_input_string.as_ref() { + let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await; + edit_indexer_params.tags = Some(tag_ids_vec); + } + let detail_event = LidarrEvent::GetIndexers; + let event = LidarrEvent::EditIndexer(EditIndexerParams::default()); + let id = edit_indexer_params.indexer_id; + info!("Updating Lidarr indexer with ID: {id}"); + info!("Fetching indexer details for indexer with ID: {id}"); + + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{id}")), + None, + ) + .await; + + let mut response = String::new(); + + self + .handle_request::<(), Value>(request_props, |detailed_indexer_body, _| { + response = detailed_indexer_body.to_string() + }) + .await?; + + info!("Constructing edit indexer body"); + + let mut detailed_indexer_body: Value = serde_json::from_str(&response)?; + + let ( + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + tags, + priority, + ) = { + let priority = detailed_indexer_body["priority"] + .as_i64() + .context("Failed to deserialize indexer 'priority' field")?; + let seed_ratio_field_option = detailed_indexer_body["fields"] + .as_array() + .context("Failed to get indexer 'fields' array")? + .iter() + .find(|field| field["name"] == "seedCriteria.seedRatio"); + let name = edit_indexer_params.name.unwrap_or( + detailed_indexer_body["name"] + .as_str() + .context("Failed to deserialize indexer 'name' field")? + .to_owned(), + ); + let enable_rss = edit_indexer_params.enable_rss.unwrap_or( + detailed_indexer_body["enableRss"] + .as_bool() + .context("Failed to deserialize indexer 'enableRss' field")?, + ); + let enable_automatic_search = edit_indexer_params.enable_automatic_search.unwrap_or( + detailed_indexer_body["enableAutomaticSearch"] + .as_bool() + .context("Failed to deserialize indexer 'enableAutomaticSearch' field")?, + ); + let enable_interactive_search = edit_indexer_params.enable_interactive_search.unwrap_or( + detailed_indexer_body["enableInteractiveSearch"] + .as_bool() + .context("Failed to deserialize indexer 'enableInteractiveSearch' field")?, + ); + let url = edit_indexer_params.url.unwrap_or( + detailed_indexer_body["fields"] + .as_array() + .context("Failed to get indexer 'fields' array for baseUrl")? + .iter() + .find(|field| field["name"] == "baseUrl") + .context("Field 'baseUrl' was not found in the indexer fields array")? + .get("value") + .unwrap_or(&json!("")) + .as_str() + .context("Failed to deserialize indexer 'baseUrl' value")? + .to_owned(), + ); + let api_key = edit_indexer_params.api_key.unwrap_or( + detailed_indexer_body["fields"] + .as_array() + .context("Failed to get indexer 'fields' array for apiKey")? + .iter() + .find(|field| field["name"] == "apiKey") + .context("Field 'apiKey' was not found in the indexer fields array")? + .get("value") + .unwrap_or(&json!("")) + .as_str() + .context("Failed to deserialize indexer 'apiKey' value")? + .to_owned(), + ); + let seed_ratio = edit_indexer_params.seed_ratio.unwrap_or_else(|| { + if let Some(seed_ratio_field) = seed_ratio_field_option { + return seed_ratio_field + .get("value") + .unwrap_or(&json!("")) + .as_str() + .unwrap_or("") + .to_owned(); + } + + String::new() + }); + let tags = if edit_indexer_params.clear_tags { + vec![] + } else { + edit_indexer_params.tags.unwrap_or( + detailed_indexer_body["tags"] + .as_array() + .context("Failed to get indexer 'tags' array")? + .iter() + .map(|item| { + item + .as_i64() + .context("Failed to deserialize indexer tag ID") + }) + .collect::>>()?, + ) + }; + let priority = edit_indexer_params.priority.unwrap_or(priority); + + ( + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + tags, + priority, + ) + }; + + *detailed_indexer_body + .get_mut("name") + .context("Failed to get mutable reference to indexer 'name' field")? = json!(name); + *detailed_indexer_body + .get_mut("priority") + .context("Failed to get mutable reference to indexer 'priority' field")? = json!(priority); + *detailed_indexer_body + .get_mut("enableRss") + .context("Failed to get mutable reference to indexer 'enableRss' field")? = json!(enable_rss); + *detailed_indexer_body + .get_mut("enableAutomaticSearch") + .context("Failed to get mutable reference to indexer 'enableAutomaticSearch' field")? = + json!(enable_automatic_search); + *detailed_indexer_body + .get_mut("enableInteractiveSearch") + .context("Failed to get mutable reference to indexer 'enableInteractiveSearch' field")? = + json!(enable_interactive_search); + *detailed_indexer_body + .get_mut("fields") + .and_then(|f| f.as_array_mut()) + .context("Failed to get mutable reference to indexer 'fields' array")? + .iter_mut() + .find(|field| field["name"] == "baseUrl") + .context("Failed to find 'baseUrl' field in indexer fields array")? + .get_mut("value") + .context("Failed to get mutable reference to 'baseUrl' value")? = json!(url); + *detailed_indexer_body + .get_mut("fields") + .and_then(|f| f.as_array_mut()) + .context("Failed to get mutable reference to indexer 'fields' array for apiKey")? + .iter_mut() + .find(|field| field["name"] == "apiKey") + .context("Failed to find 'apiKey' field in indexer fields array")? + .get_mut("value") + .context("Failed to get mutable reference to 'apiKey' value")? = json!(api_key); + *detailed_indexer_body + .get_mut("tags") + .context("Failed to get mutable reference to indexer 'tags' field")? = json!(tags); + let seed_ratio_field_option = detailed_indexer_body + .get_mut("fields") + .and_then(|f| f.as_array_mut()) + .context("Failed to get mutable reference to indexer 'fields' array for seed ratio")? + .iter_mut() + .find(|field| field["name"] == "seedCriteria.seedRatio"); + if let Some(seed_ratio_field) = seed_ratio_field_option { + seed_ratio_field + .as_object_mut() + .context("Failed to get mutable reference to 'seedCriteria.seedRatio' object")? + .insert("value".to_string(), json!(seed_ratio)); + } + + debug!("Edit indexer body: {detailed_indexer_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Put, + Some(detailed_indexer_body), + Some(format!("/{id}")), + Some("forceSave=true".to_owned()), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_indexers( + &mut self, + ) -> Result> { + info!("Fetching Lidarr indexers"); + let event = LidarrEvent::GetIndexers; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |indexers, mut app| { + app.data.lidarr_data.indexers.set_items(indexers); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn test_lidarr_indexer( + &mut self, + indexer_id: i64, + ) -> Result { + let detail_event = LidarrEvent::GetIndexers; + let event = LidarrEvent::TestIndexer(indexer_id); + info!("Testing Lidarr indexer with ID: {indexer_id}"); + + info!("Fetching indexer details for indexer with ID: {indexer_id}"); + + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{indexer_id}")), + None, + ) + .await; + + let mut test_body: Value = Value::default(); + + self + .handle_request::<(), Value>(request_props, |detailed_indexer_body, _| { + test_body = detailed_indexer_body; + }) + .await?; + + info!("Testing indexer"); + + let mut request_props = self + .request_props_from(event, RequestMethod::Post, Some(test_body), None, None) + .await; + request_props.ignore_status_code = true; + + self + .handle_request::(request_props, |test_results, mut app| { + if test_results.as_object().is_none() { + let error_message = test_results + .as_array() + .and_then(|arr| arr.first()) + .and_then(|item| item.get("errorMessage")) + .map(|msg| msg.to_string()) + .unwrap_or_else(|| "Unknown indexer test error".to_string()); + app.data.lidarr_data.indexer_test_errors = Some(error_message); + } else { + app.data.lidarr_data.indexer_test_errors = Some(String::new()); + }; + }) + .await + } + + pub(in crate::network::lidarr_network) async fn test_all_lidarr_indexers( + &mut self, + ) -> Result> { + info!("Testing all Lidarr indexers"); + let event = LidarrEvent::TestAllIndexers; + + let mut request_props = self + .request_props_from(event, RequestMethod::Post, None, None, None) + .await; + request_props.ignore_status_code = true; + + let result = self + .handle_request::<(), Vec>(request_props, |test_results, mut app| { + let mut test_all_indexer_results = StatefulTable::default(); + let indexers = app.data.lidarr_data.indexers.items.clone(); + let modal_test_results = test_results + .iter() + .map(|result| { + let name = indexers + .iter() + .filter(|&indexer| indexer.id == result.id) + .map(|indexer| indexer.name.clone()) + .nth(0) + .unwrap_or_default(); + let validation_failures = result + .validation_failures + .iter() + .map(|failure| { + format!( + "Failure for field '{}': {}", + failure.property_name, failure.error_message + ) + }) + .collect::>() + .join(", "); + + IndexerTestResultModalItem { + name: name.unwrap_or_default(), + is_valid: result.is_valid, + validation_failures: validation_failures.into(), + } + }) + .collect(); + test_all_indexer_results.set_items(modal_test_results); + app.data.lidarr_data.indexer_test_all_results = Some(test_all_indexer_results); + }) + .await; + + if result.is_err() { + self + .app + .lock() + .await + .data + .lidarr_data + .indexer_test_all_results = Some(StatefulTable::default()); + } + + result + } +} diff --git a/src/network/lidarr_network/lidarr_network_test_utils.rs b/src/network/lidarr_network/lidarr_network_test_utils.rs index 9c5cdd3..8963315 100644 --- a/src/network/lidarr_network/lidarr_network_test_utils.rs +++ b/src/network/lidarr_network/lidarr_network_test_utils.rs @@ -8,10 +8,13 @@ pub mod test_utils { LidarrHistoryEventType, LidarrHistoryItem, LidarrHistoryWrapper, Member, MetadataProfile, NewItemMonitorType, Ratings, SystemStatus, }; - use crate::models::servarr_models::{Quality, QualityProfile, QualityWrapper, RootFolder, Tag}; + use crate::models::servarr_models::IndexerSettings; + use crate::models::servarr_models::{ + Indexer, IndexerField, Quality, QualityProfile, QualityWrapper, RootFolder, Tag, + }; use bimap::BiMap; use chrono::DateTime; - use serde_json::Number; + use serde_json::{Number, json}; pub const ADD_ARTIST_SEARCH_RESULT_JSON: &str = r#"{ "foreignArtistId": "test-foreign-id", @@ -287,4 +290,47 @@ pub mod test_utils { ..LidarrHistoryData::default() } } + + pub fn indexer() -> Indexer { + Indexer { + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + supports_rss: true, + supports_search: true, + protocol: "torrent".to_owned(), + priority: 25, + download_client_id: 0, + name: Some("Test Indexer".to_owned()), + implementation_name: Some("Torznab".to_owned()), + implementation: Some("Torznab".to_owned()), + config_contract: Some("TorznabSettings".to_owned()), + tags: vec![Number::from(1)], + id: 1, + fields: Some(vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(json!("https://test.com")), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(json!("")), + }, + IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(json!("1.2")), + }, + ]), + } + } + + pub fn indexer_settings() -> IndexerSettings { + IndexerSettings { + id: 1, + minimum_age: 1, + retention: 1, + maximum_size: 12345, + rss_sync_interval: 60, + } + } } diff --git a/src/network/lidarr_network/lidarr_network_tests.rs b/src/network/lidarr_network/lidarr_network_tests.rs index d8c6ff6..380c533 100644 --- a/src/network/lidarr_network/lidarr_network_tests.rs +++ b/src/network/lidarr_network/lidarr_network_tests.rs @@ -5,7 +5,7 @@ mod tests { AddArtistBody, DeleteParams, EditArtistParams, LidarrSerdeable, MetadataProfile, }; use crate::models::servarr_data::lidarr::modals::EditArtistModal; - use crate::models::servarr_models::{QualityProfile, Tag}; + use crate::models::servarr_models::{EditIndexerParams, IndexerSettings, QualityProfile, Tag}; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::{NetworkEvent, NetworkResource, lidarr_network::LidarrEvent}; use bimap::BiMap; @@ -15,6 +15,17 @@ mod tests { use std::sync::Arc; use tokio::sync::Mutex; + #[rstest] + fn test_resource_all_indexer_settings( + #[values( + LidarrEvent::GetAllIndexerSettings, + LidarrEvent::EditAllIndexerSettings(IndexerSettings::default()) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/config/indexer"); + } + #[rstest] fn test_resource_artist( #[values( @@ -37,6 +48,18 @@ mod tests { assert_str_eq!(event.resource(), "/queue"); } + #[rstest] + fn test_resource_indexer( + #[values( + LidarrEvent::GetIndexers, + LidarrEvent::DeleteIndexer(0), + LidarrEvent::EditIndexer(EditIndexerParams::default()) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/indexer"); + } + #[rstest] fn test_resource_history(#[values(LidarrEvent::GetHistory(0))] event: LidarrEvent) { assert_str_eq!(event.resource(), "/history"); @@ -107,6 +130,8 @@ mod tests { #[case(LidarrEvent::GetTags, "/tag")] #[case(LidarrEvent::HealthCheck, "/health")] #[case(LidarrEvent::MarkHistoryItemAsFailed(0), "/history/failed")] + #[case(LidarrEvent::TestIndexer(0), "/indexer/test")] + #[case(LidarrEvent::TestAllIndexers, "/indexer/testall")] fn test_resource(#[case] event: LidarrEvent, #[case] expected_uri: &str) { assert_str_eq!(event.resource(), expected_uri); } diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs index c32befa..148f2a9 100644 --- a/src/network/lidarr_network/mod.rs +++ b/src/network/lidarr_network/mod.rs @@ -6,11 +6,12 @@ use crate::models::lidarr_models::{ AddArtistBody, AddLidarrRootFolderBody, DeleteParams, EditArtistParams, LidarrSerdeable, MetadataProfile, }; -use crate::models::servarr_models::{QualityProfile, Tag}; +use crate::models::servarr_models::{EditIndexerParams, IndexerSettings, QualityProfile, Tag}; use crate::network::{Network, RequestMethod}; mod downloads; mod history; +mod indexers; mod library; mod root_folders; mod system; @@ -31,16 +32,21 @@ pub enum LidarrEvent { DeleteAlbum(DeleteParams), DeleteArtist(DeleteParams), DeleteDownload(i64), + DeleteIndexer(i64), DeleteRootFolder(i64), DeleteTag(i64), EditArtist(EditArtistParams), + EditAllIndexerSettings(IndexerSettings), + EditIndexer(EditIndexerParams), GetAlbums(i64), GetAlbumDetails(i64), + GetAllIndexerSettings, GetArtistDetails(i64), GetDiskSpace, GetDownloads(u64), GetHistory(u64), GetHostConfig, + GetIndexers, MarkHistoryItemAsFailed(i64), GetMetadataProfiles, GetQualityProfiles, @@ -51,6 +57,8 @@ pub enum LidarrEvent { HealthCheck, ListArtists, SearchNewArtist(String), + TestIndexer(i64), + TestAllIndexers, ToggleAlbumMonitoring(i64), ToggleArtistMonitoring(i64), TriggerAutomaticArtistSearch(i64), @@ -63,6 +71,9 @@ impl NetworkResource for LidarrEvent { fn resource(&self) -> &'static str { match &self { LidarrEvent::AddTag(_) | LidarrEvent::DeleteTag(_) | LidarrEvent::GetTags => "/tag", + LidarrEvent::GetAllIndexerSettings | LidarrEvent::EditAllIndexerSettings(_) => { + "/config/indexer" + } LidarrEvent::DeleteArtist(_) | LidarrEvent::EditArtist(_) | LidarrEvent::GetArtistDetails(_) @@ -78,6 +89,9 @@ impl NetworkResource for LidarrEvent { LidarrEvent::GetHistory(_) => "/history", LidarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host", + LidarrEvent::GetIndexers | LidarrEvent::DeleteIndexer(_) | LidarrEvent::EditIndexer(_) => { + "/indexer" + } LidarrEvent::TriggerAutomaticArtistSearch(_) | LidarrEvent::UpdateAllArtists | LidarrEvent::UpdateAndScanArtist(_) @@ -87,6 +101,8 @@ impl NetworkResource for LidarrEvent { LidarrEvent::GetRootFolders | LidarrEvent::AddRootFolder(_) | LidarrEvent::DeleteRootFolder(_) => "/rootfolder", + LidarrEvent::TestIndexer(_) => "/indexer/test", + LidarrEvent::TestAllIndexers => "/indexer/testall", LidarrEvent::GetStatus => "/system/status", LidarrEvent::HealthCheck => "/health", LidarrEvent::SearchNewArtist(_) => "/artist/lookup", @@ -121,6 +137,18 @@ impl Network<'_, '_> { .delete_lidarr_download(download_id) .await .map(LidarrSerdeable::from), + LidarrEvent::EditAllIndexerSettings(params) => self + .edit_all_lidarr_indexer_settings(params) + .await + .map(LidarrSerdeable::from), + LidarrEvent::EditIndexer(params) => self + .edit_lidarr_indexer(params) + .await + .map(LidarrSerdeable::from), + LidarrEvent::DeleteIndexer(indexer_id) => self + .delete_lidarr_indexer(indexer_id) + .await + .map(LidarrSerdeable::from), LidarrEvent::DeleteRootFolder(root_folder_id) => self .delete_lidarr_root_folder(root_folder_id) .await @@ -132,6 +160,10 @@ impl Network<'_, '_> { LidarrEvent::GetAlbums(artist_id) => { self.get_albums(artist_id).await.map(LidarrSerdeable::from) } + LidarrEvent::GetAllIndexerSettings => self + .get_all_lidarr_indexer_settings() + .await + .map(LidarrSerdeable::from), LidarrEvent::GetArtistDetails(artist_id) => self .get_artist_details(artist_id) .await @@ -145,6 +177,7 @@ impl Network<'_, '_> { .get_lidarr_downloads(count) .await .map(LidarrSerdeable::from), + LidarrEvent::GetIndexers => self.get_lidarr_indexers().await.map(LidarrSerdeable::from), LidarrEvent::GetHistory(events) => self .get_lidarr_history(events) .await @@ -206,6 +239,14 @@ impl Network<'_, '_> { .update_lidarr_downloads() .await .map(LidarrSerdeable::from), + LidarrEvent::TestAllIndexers => self + .test_all_lidarr_indexers() + .await + .map(LidarrSerdeable::from), + LidarrEvent::TestIndexer(indexer_id) => self + .test_lidarr_indexer(indexer_id) + .await + .map(LidarrSerdeable::from), } } diff --git a/src/network/servarr_test_utils.rs b/src/network/servarr_test_utils.rs index 0f79be9..38ea73c 100644 --- a/src/network/servarr_test_utils.rs +++ b/src/network/servarr_test_utils.rs @@ -1,5 +1,5 @@ use crate::models::servarr_data::modals::IndexerTestResultModalItem; -use crate::models::servarr_models::{DiskSpace, QueueEvent}; +use crate::models::servarr_models::{DiskSpace, IndexerSettings, QueueEvent}; use chrono::DateTime; pub fn diskspace() -> DiskSpace { @@ -9,6 +9,16 @@ pub fn diskspace() -> DiskSpace { } } +pub fn indexer_settings() -> IndexerSettings { + IndexerSettings { + id: 1, + minimum_age: 1, + retention: 1, + maximum_size: 12345, + rss_sync_interval: 60, + } +} + pub fn indexer_test_result() -> IndexerTestResultModalItem { IndexerTestResultModalItem { name: "DrunkenSlug".to_owned(), diff --git a/src/network/sonarr_network/indexers/mod.rs b/src/network/sonarr_network/indexers/mod.rs index 4581604..a085fa6 100644 --- a/src/network/sonarr_network/indexers/mod.rs +++ b/src/network/sonarr_network/indexers/mod.rs @@ -1,6 +1,6 @@ use crate::models::servarr_data::modals::IndexerTestResultModalItem; +use crate::models::servarr_models::IndexerSettings; use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult}; -use crate::models::sonarr_models::IndexerSettings; use crate::models::stateful_table::StatefulTable; use crate::network::sonarr_network::SonarrEvent; use crate::network::{Network, RequestMethod}; diff --git a/src/network/sonarr_network/indexers/sonarr_indexers_network_tests.rs b/src/network/sonarr_network/indexers/sonarr_indexers_network_tests.rs index fcb0032..2eeaa8a 100644 --- a/src/network/sonarr_network/indexers/sonarr_indexers_network_tests.rs +++ b/src/network/sonarr_network/indexers/sonarr_indexers_network_tests.rs @@ -6,10 +6,9 @@ mod tests { use crate::models::sonarr_models::SonarrSerdeable; use crate::network::NetworkResource; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use crate::network::servarr_test_utils::indexer_settings; use crate::network::sonarr_network::SonarrEvent; - use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{ - indexer, indexer_settings, - }; + use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::indexer; use bimap::BiMap; use mockito::Matcher; use pretty_assertions::assert_eq; @@ -31,11 +30,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::DeleteIndexer(1)) .await - .is_ok() ); mock.assert_async().await; @@ -58,11 +56,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditAllIndexerSettings(indexer_settings())) .await - .is_ok() ); mock.assert_async().await; @@ -153,11 +150,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params)) .await - .is_ok() ); mock_details_server.assert_async().await; @@ -248,11 +244,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params)) .await - .is_ok() ); mock_details_server.assert_async().await; @@ -338,11 +333,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params)) .await - .is_ok() ); mock_details_server.assert_async().await; @@ -435,11 +429,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params)) .await - .is_ok() ); mock_details_server.assert_async().await; @@ -497,11 +490,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(edit_indexer_params)) .await - .is_ok() ); mock_details_server.assert_async().await; @@ -584,11 +576,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(edit_indexer_params)) .await - .is_ok() ); async_details_server.assert_async().await; @@ -642,6 +633,7 @@ mod tests { else { panic!("Expected Indexers") }; + async_server.assert_async().await; assert_eq!( app.lock().await.data.sonarr_data.indexers.items, @@ -714,6 +706,7 @@ mod tests { else { panic!("Expected Value") }; + async_details_server.assert_async().await; async_test_server.assert_async().await; assert_eq!( @@ -780,6 +773,7 @@ mod tests { else { panic!("Expected Value") }; + async_details_server.assert_async().await; async_test_server.assert_async().await; assert_eq!( @@ -860,16 +854,9 @@ mod tests { else { panic!("Expected IndexerTestResults") }; + async_server.assert_async().await; - assert!( - app - .lock() - .await - .data - .sonarr_data - .indexer_test_all_results - .is_some() - ); + assert_some!(&app.lock().await.data.sonarr_data.indexer_test_all_results); assert_eq!( app .lock() diff --git a/src/network/sonarr_network/mod.rs b/src/network/sonarr_network/mod.rs index b0f314c..a39f35c 100644 --- a/src/network/sonarr_network/mod.rs +++ b/src/network/sonarr_network/mod.rs @@ -5,10 +5,12 @@ use serde_json::{Value, json}; use super::{Network, NetworkEvent, NetworkResource}; use crate::{ models::{ - servarr_models::{AddRootFolderBody, EditIndexerParams, Language, QualityProfile, Tag}, + servarr_models::{ + AddRootFolderBody, EditIndexerParams, IndexerSettings, Language, QualityProfile, Tag, + }, sonarr_models::{ - AddSeriesBody, DeleteSeriesParams, EditSeriesParams, IndexerSettings, - SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTaskName, + AddSeriesBody, DeleteSeriesParams, EditSeriesParams, SonarrReleaseDownloadBody, + SonarrSerdeable, SonarrTaskName, }, }, network::RequestMethod, diff --git a/src/network/sonarr_network/sonarr_network_test_utils.rs b/src/network/sonarr_network/sonarr_network_test_utils.rs index f2d732e..d547997 100644 --- a/src/network/sonarr_network/sonarr_network_test_utils.rs +++ b/src/network/sonarr_network/sonarr_network_test_utils.rs @@ -5,10 +5,9 @@ pub mod test_utils { }; use crate::models::sonarr_models::{ AddSeriesSearchResult, AddSeriesSearchResultStatistics, BlocklistItem, DownloadRecord, - DownloadStatus, DownloadsResponse, Episode, EpisodeFile, IndexerSettings, MediaInfo, Rating, - Season, SeasonStatistics, Series, SeriesStatistics, SeriesStatus, SeriesType, - SonarrHistoryData, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrTask, - SonarrTaskName, + DownloadStatus, DownloadsResponse, Episode, EpisodeFile, MediaInfo, Rating, Season, + SeasonStatistics, Series, SeriesStatistics, SeriesStatus, SeriesType, SonarrHistoryData, + SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrTask, SonarrTaskName, }; use crate::models::{HorizontallyScrollableText, ScrollableText}; use bimap::BiMap; @@ -250,16 +249,6 @@ pub mod test_utils { } } - pub fn indexer_settings() -> IndexerSettings { - IndexerSettings { - id: 1, - minimum_age: 1, - retention: 1, - maximum_size: 12345, - rss_sync_interval: 60, - } - } - pub fn language() -> Language { Language { id: 1, diff --git a/src/network/sonarr_network/sonarr_network_tests.rs b/src/network/sonarr_network/sonarr_network_tests.rs index 87aa4ce..4d0bbe5 100644 --- a/src/network/sonarr_network/sonarr_network_tests.rs +++ b/src/network/sonarr_network/sonarr_network_tests.rs @@ -3,11 +3,9 @@ mod test { use crate::app::App; use crate::models::servarr_data::sonarr::modals::AddSeriesModal; use crate::models::servarr_models::{ - AddRootFolderBody, EditIndexerParams, Language, QualityProfile, Tag, - }; - use crate::models::sonarr_models::{ - AddSeriesBody, EditSeriesParams, IndexerSettings, SonarrTaskName, + AddRootFolderBody, EditIndexerParams, IndexerSettings, Language, QualityProfile, Tag, }; + use crate::models::sonarr_models::{AddSeriesBody, EditSeriesParams, SonarrTaskName}; use crate::models::sonarr_models::{DeleteSeriesParams, SonarrSerdeable}; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::tag; diff --git a/src/ui/lidarr_ui/indexers/edit_indexer_ui.rs b/src/ui/lidarr_ui/indexers/edit_indexer_ui.rs new file mode 100644 index 0000000..6327d35 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/edit_indexer_ui.rs @@ -0,0 +1,169 @@ +use std::sync::atomic::Ordering; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_INDEXER_BLOCKS}; +use crate::render_selectable_input_box; +use crate::ui::utils::title_block_centered; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::popup::Size; +use crate::ui::{DrawUi, draw_popup}; +use ratatui::Frame; +use ratatui::layout::{Constraint, Flex, Layout, Rect}; + +#[cfg(test)] +#[path = "edit_indexer_ui_tests.rs"] +mod edit_indexer_ui_tests; + +pub(super) struct EditIndexerUi; + +impl DrawUi for EditIndexerUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup(f, app, draw_edit_indexer_prompt, Size::WideLargePrompt); + } +} + +fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let block = title_block_centered("Edit Indexer"); + let yes_no_value = app.data.lidarr_data.prompt_confirm; + let selected_block = app.data.lidarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveLidarrBlock::EditIndexerConfirmPrompt; + let edit_indexer_modal_option = &app.data.lidarr_data.edit_indexer_modal; + let protocol = &app.data.lidarr_data.indexers.current_selection().protocol; + + if edit_indexer_modal_option.is_some() { + f.render_widget(block, area); + let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap(); + + let [settings_area, buttons_area] = + Layout::vertical([Constraint::Fill(1), Constraint::Length(3)]) + .margin(1) + .areas(area); + let [left_side_area, right_side_area] = + Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) + .margin(1) + .areas(settings_area); + let [ + name_area, + rss_area, + auto_search_area, + interactive_search_area, + priority_area, + ] = Layout::vertical([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + ]) + .areas(left_side_area); + let [url_area, api_key_area, seed_ratio_area, tags_area] = Layout::vertical([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + ]) + .areas(right_side_area); + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let priority = edit_indexer_modal.priority.to_string(); + let name_input_box = InputBox::new(&edit_indexer_modal.name.text) + .offset(edit_indexer_modal.name.offset.load(Ordering::SeqCst)) + .label("Name") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerNameInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerNameInput); + let url_input_box = InputBox::new(&edit_indexer_modal.url.text) + .offset(edit_indexer_modal.url.offset.load(Ordering::SeqCst)) + .label("URL") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerUrlInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerUrlInput); + let api_key_input_box = InputBox::new(&edit_indexer_modal.api_key.text) + .offset(edit_indexer_modal.api_key.offset.load(Ordering::SeqCst)) + .label("API Key") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerApiKeyInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerApiKeyInput); + let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text) + .offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerTagsInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerTagsInput); + let priority_input_box = InputBox::new(&priority) + .cursor_after_string(false) + .label("Indexer Priority ▴▾") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerPriorityInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerPriorityInput); + + render_selectable_input_box!(name_input_box, f, name_area); + render_selectable_input_box!(url_input_box, f, url_area); + render_selectable_input_box!(api_key_input_box, f, api_key_area); + + if protocol == "torrent" { + let seed_ratio_input_box = InputBox::new(&edit_indexer_modal.seed_ratio.text) + .offset(edit_indexer_modal.seed_ratio.offset.load(Ordering::SeqCst)) + .label("Seed Ratio") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerSeedRatioInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerSeedRatioInput); + let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text) + .offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerTagsInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerTagsInput); + + render_selectable_input_box!(seed_ratio_input_box, f, seed_ratio_area); + render_selectable_input_box!(tags_input_box, f, tags_area); + render_selectable_input_box!(priority_input_box, f, priority_area); + } else { + render_selectable_input_box!(tags_input_box, f, seed_ratio_area); + render_selectable_input_box!(priority_input_box, f, tags_area); + } + + let rss_checkbox = Checkbox::new("Enable RSS") + .checked(edit_indexer_modal.enable_rss.unwrap_or_default()) + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerToggleEnableRss); + let auto_search_checkbox = Checkbox::new("Enable Automatic Search") + .checked( + edit_indexer_modal + .enable_automatic_search + .unwrap_or_default(), + ) + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch); + let interactive_search_checkbox = Checkbox::new("Enable Interactive Search") + .checked( + edit_indexer_modal + .enable_interactive_search + .unwrap_or_default(), + ) + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch); + + let [save_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)]) + .flex(Flex::Center) + .areas(buttons_area); + + let save_button = Button::default() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::default() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(rss_checkbox, rss_area); + f.render_widget(auto_search_checkbox, auto_search_area); + f.render_widget(interactive_search_checkbox, interactive_search_area); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); + } + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} diff --git a/src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs b/src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs new file mode 100644 index 0000000..66ffa51 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs @@ -0,0 +1,81 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::models::servarr_models::Indexer; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::indexers::edit_indexer_ui::EditIndexerUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_edit_indexer_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block) { + assert!(EditIndexerUi::accepts(active_lidarr_block.into())); + } else { + assert!(!EditIndexerUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::models::servarr_data::lidarr::lidarr_data::EDIT_INDEXER_NZB_SELECTION_BLOCKS; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use crate::ui::ui_test_utils::test_utils::TerminalSize; + + use super::*; + + #[test] + fn test_edit_indexer_ui_renders_loading() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.data.lidarr_data.edit_indexer_modal = None; + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + EditIndexerUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_edit_indexer_ui_renders_edit_torrent_indexer() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + EditIndexerUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_edit_indexer_ui_renders_edit_usenet_indexer() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.indexers.set_items(vec![Indexer { + protocol: "usenet".into(), + ..indexer() + }]); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + EditIndexerUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/indexers/indexer_settings_ui.rs b/src/ui/lidarr_ui/indexers/indexer_settings_ui.rs new file mode 100644 index 0000000..684e2ef --- /dev/null +++ b/src/ui/lidarr_ui/indexers/indexer_settings_ui.rs @@ -0,0 +1,117 @@ +use ratatui::Frame; +use ratatui::layout::{Constraint, Flex, Layout, Rect}; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS, +}; +use crate::render_selectable_input_box; +use crate::ui::utils::title_block_centered; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::popup::Size; +use crate::ui::{DrawUi, draw_popup}; + +#[cfg(test)] +#[path = "indexer_settings_ui_tests.rs"] +mod indexer_settings_ui_tests; + +pub(super) struct IndexerSettingsUi; + +impl DrawUi for IndexerSettingsUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup(f, app, draw_edit_indexer_settings_prompt, Size::LargePrompt); + } +} + +fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let block = title_block_centered("Configure All Indexer Settings"); + let yes_no_value = app.data.lidarr_data.prompt_confirm; + let selected_block = app.data.lidarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveLidarrBlock::IndexerSettingsConfirmPrompt; + let indexer_settings_option = &app.data.lidarr_data.indexer_settings; + + if indexer_settings_option.is_some() { + f.render_widget(block, area); + let indexer_settings = indexer_settings_option.as_ref().unwrap(); + + let [ + _, + min_age_area, + retention_area, + max_size_area, + rss_sync_area, + _, + buttons_area, + ] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(3), + ]) + .margin(1) + .areas(area); + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let min_age = indexer_settings.minimum_age.to_string(); + let retention = indexer_settings.retention.to_string(); + let max_size = indexer_settings.maximum_size.to_string(); + let rss_sync_interval = indexer_settings.rss_sync_interval.to_string(); + + let min_age_text_box = InputBox::new(&min_age) + .cursor_after_string(false) + .label("Minimum Age (minutes) ▴▾") + .highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsMinimumAgeInput) + .selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsMinimumAgeInput); + let retention_input_box = InputBox::new(&retention) + .cursor_after_string(false) + .label("Retention (days) ▴▾") + .highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsRetentionInput) + .selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsRetentionInput); + let max_size_input_box = InputBox::new(&max_size) + .cursor_after_string(false) + .label("Maximum Size (MB) ▴▾") + .highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsMaximumSizeInput) + .selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsMaximumSizeInput); + let rss_sync_interval_input_box = InputBox::new(&rss_sync_interval) + .cursor_after_string(false) + .label("RSS Sync Interval (minutes) ▴▾") + .highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput) + .selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput); + + render_selectable_input_box!(min_age_text_box, f, min_age_area); + render_selectable_input_box!(retention_input_box, f, retention_area); + render_selectable_input_box!(max_size_input_box, f, max_size_area); + render_selectable_input_box!(rss_sync_interval_input_box, f, rss_sync_area); + } + + let [save_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)]) + .flex(Flex::Center) + .areas(buttons_area); + + let save_button = Button::default() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::default() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} diff --git a/src/ui/lidarr_ui/indexers/indexer_settings_ui_tests.rs b/src/ui/lidarr_ui/indexers/indexer_settings_ui_tests.rs new file mode 100644 index 0000000..6052463 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/indexer_settings_ui_tests.rs @@ -0,0 +1,44 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_indexer_settings_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block) { + assert!(IndexerSettingsUi::accepts(active_lidarr_block.into())); + } else { + assert!(!IndexerSettingsUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + + use super::*; + + #[test] + fn test_indexer_settings_ui_renders_indexer_settings() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::IndexerSettingsMinimumAgeInput.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexerSettingsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/indexers/indexers_ui_tests.rs b/src/ui/lidarr_ui/indexers/indexers_ui_tests.rs new file mode 100644 index 0000000..8019188 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/indexers_ui_tests.rs @@ -0,0 +1,156 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXERS_BLOCKS, + }; + use crate::models::servarr_models::Indexer; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::indexers::IndexersUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_indexers_ui_accepts() { + let mut indexers_blocks = Vec::new(); + indexers_blocks.extend(INDEXERS_BLOCKS); + indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS); + indexers_blocks.extend(EDIT_INDEXER_BLOCKS); + indexers_blocks.push(ActiveLidarrBlock::TestAllIndexers); + + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if indexers_blocks.contains(&active_lidarr_block) { + assert!(IndexersUi::accepts(active_lidarr_block.into())); + } else { + assert!(!IndexersUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[test] + fn test_indexers_ui_renders_loading() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_indexers_ui_renders_loading_test_results() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::TestIndexer.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_indexers_ui_renders_loading_test_results_when_indexer_test_errors_is_none() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::TestIndexer.into()); + app.data.lidarr_data.indexer_test_errors = None; + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_indexers_ui_renders_empty_indexers() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_indexers_ui_renders( + #[values( + ActiveLidarrBlock::DeleteIndexerPrompt, + ActiveLidarrBlock::Indexers, + ActiveLidarrBlock::TestIndexer + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("indexers_ui_{active_lidarr_block}"), output); + } + + #[test] + fn test_indexers_ui_renders_test_all_over_indexers() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_indexers_ui_renders_edit_usenet_indexer_over_indexers() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + app.data.lidarr_data.indexers.set_items(vec![Indexer { + protocol: "usenet".into(), + ..indexer() + }]); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_indexers_ui_renders_edit_torrent_indexer_over_indexers() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/indexers/mod.rs b/src/ui/lidarr_ui/indexers/mod.rs new file mode 100644 index 0000000..2968537 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/mod.rs @@ -0,0 +1,184 @@ +use crate::ui::styles::success_style; +use ratatui::Frame; +use ratatui::layout::{Constraint, Rect}; +use ratatui::text::Text; +use ratatui::widgets::{Cell, Row}; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, INDEXERS_BLOCKS}; +use crate::models::servarr_models::Indexer; +use crate::ui::DrawUi; +use crate::ui::lidarr_ui::indexers::edit_indexer_ui::EditIndexerUi; +use crate::ui::lidarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi; +use crate::ui::lidarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{layout_block_top_border, title_block}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; + +mod edit_indexer_ui; +mod indexer_settings_ui; +mod test_all_indexers_ui; + +#[cfg(test)] +#[path = "indexers_ui_tests.rs"] +mod indexers_ui_tests; + +pub(super) struct IndexersUi; + +impl DrawUi for IndexersUi { + fn accepts(route: Route) -> bool { + if let Route::Lidarr(active_lidarr_block, _) = route { + return EditIndexerUi::accepts(route) + || IndexerSettingsUi::accepts(route) + || TestAllIndexersUi::accepts(route) + || INDEXERS_BLOCKS.contains(&active_lidarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let route = app.get_current_route(); + draw_indexers(f, app, area); + + match route { + _ if EditIndexerUi::accepts(route) => EditIndexerUi::draw(f, app, area), + _ if IndexerSettingsUi::accepts(route) => IndexerSettingsUi::draw(f, app, area), + _ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, area), + Route::Lidarr(active_lidarr_block, _) => match active_lidarr_block { + ActiveLidarrBlock::TestIndexer => { + if app.is_loading || app.data.lidarr_data.indexer_test_errors.is_none() { + let loading_popup = Popup::new(LoadingBlock::new( + app.is_loading || app.data.lidarr_data.indexer_test_errors.is_none(), + title_block("Testing Indexer"), + )) + .size(Size::LargeMessage); + f.render_widget(loading_popup, f.area()); + } else { + let popup = { + let result = app + .data + .lidarr_data + .indexer_test_errors + .as_ref() + .expect("Test result is unpopulated"); + + if !result.is_empty() { + Popup::new(Message::new(result.clone())).size(Size::LargeMessage) + } else { + let message = Message::new("Indexer test succeeded!") + .title("Success") + .style(success_style().bold()); + Popup::new(message).size(Size::Message) + } + }; + + f.render_widget(popup, f.area()); + } + } + ActiveLidarrBlock::DeleteIndexerPrompt => { + let prompt = format!( + "Do you really want to delete this indexer: \n{}?", + app + .data + .lidarr_data + .indexers + .current_selection() + .name + .clone() + .unwrap_or_default() + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Indexer") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + _ => (), + }, + _ => (), + } + } +} + +fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let indexers_row_mapping = |indexer: &'_ Indexer| { + let Indexer { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + priority, + tags, + .. + } = indexer; + let bool_to_text = |flag: bool| { + if flag { + return Text::from("Enabled").success(); + } + + Text::from("Disabled").failure() + }; + + let rss = bool_to_text(*enable_rss); + let automatic_search = bool_to_text(*enable_automatic_search); + let interactive_search = bool_to_text(*enable_interactive_search); + let empty_tag = String::new(); + let tags: String = tags + .iter() + .map(|tag_id| { + app + .data + .lidarr_data + .tags_map + .get_by_left(&tag_id.as_i64().unwrap()) + .unwrap_or(&empty_tag) + .clone() + }) + .collect::>() + .join(", "); + + Row::new(vec![ + Cell::from(name.clone().unwrap_or_default()), + Cell::from(rss), + Cell::from(automatic_search), + Cell::from(interactive_search), + Cell::from(priority.to_string()), + Cell::from(tags), + ]) + .primary() + }; + let indexers_table = ManagarrTable::new( + Some(&mut app.data.lidarr_data.indexers), + indexers_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .headers([ + "Indexer", + "RSS", + "Automatic Search", + "Interactive Search", + "Priority", + "Tags", + ]) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(23), + ]); + + f.render_widget(indexers_table, area); +} diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_torrent_indexer.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_torrent_indexer.snap new file mode 100644 index 0000000..83a1c5a --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_torrent_indexer.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + ╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮ + │ │ + │ ╭─────────────────────────╮ ╭─────────────────────────╮ │ + │ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │ + │ ╰─────────────────────────╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable RSS: │ ✔ │ API Key: │someApiKey │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Automatic Search: │ ✔ │ Seed Ratio: │ratio │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Interactive Search: │ ✔ │ Tags: │25 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭─────────────────────────╮ │ + │ Indexer Priority ▴▾: │1 │ │ + │ ╰─────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────────────────────╮╭──────────────────────────╮ │ + │ │ Save ││ Cancel │ │ + │ ╰───────────────────────────╯╰──────────────────────────╯ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_usenet_indexer.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_usenet_indexer.snap new file mode 100644 index 0000000..cbb9e35 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_usenet_indexer.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + ╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮ + │ │ + │ ╭─────────────────────────╮ ╭─────────────────────────╮ │ + │ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │ + │ ╰─────────────────────────╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable RSS: │ ✔ │ API Key: │someApiKey │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Automatic Search: │ ✔ │ Tags: │25 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Interactive Search: │ ✔ │ Indexer Priority ▴▾: │1 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────────────────────╮╭──────────────────────────╮ │ + │ │ Save ││ Cancel │ │ + │ ╰───────────────────────────╯╰──────────────────────────╯ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_loading.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_loading.snap new file mode 100644 index 0000000..17a099f --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_loading.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + ╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexer_settings_ui__indexer_settings_ui_tests__tests__snapshot_tests__indexer_settings_ui_renders_indexer_settings.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexer_settings_ui__indexer_settings_ui_tests__tests__snapshot_tests__indexer_settings_ui_renders_indexer_settings.snap new file mode 100644 index 0000000..7f37593 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexer_settings_ui__indexer_settings_ui_tests__tests__snapshot_tests__indexer_settings_ui_renders_indexer_settings.snap @@ -0,0 +1,40 @@ +--- +source: src/ui/lidarr_ui/indexers/indexer_settings_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + + ╭─────────────────── Configure All Indexer Settings ───────────────────╮ + │ │ + │ │ + │ │ + │ ╭────────────────────────────────╮ │ + │ Minimum Age (minutes) ▴▾: │1 │ │ + │ ╰────────────────────────────────╯ │ + │ ╭────────────────────────────────╮ │ + │ Retention (days) ▴▾: │1 │ │ + │ ╰────────────────────────────────╯ │ + │ ╭────────────────────────────────╮ │ + │ Maximum Size (MB) ▴▾: │12345 │ │ + │ ╰────────────────────────────────╯ │ + │ ╭────────────────────────────────╮ │ + │ RSS Sync Interval (minutes) ▴▾: │60 │ │ + │ ╰────────────────────────────────╯ │ + │ │ + │ │ + │ ╭────────────────╮╭────────────────╮ │ + │ │ Save ││ Cancel │ │ + │ ╰────────────────╯╰────────────────╯ │ + ╰────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_DeleteIndexerPrompt.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_DeleteIndexerPrompt.snap new file mode 100644 index 0000000..608058e --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_DeleteIndexerPrompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + + + + + + + + + + + ╭──────────────────── Delete Indexer ─────────────────────╮ + │ Do you really want to delete this indexer: │ + │ Test Indexer? │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_Indexers.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_Indexers.snap new file mode 100644 index 0000000..4972808 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_Indexers.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexer.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexer.snap new file mode 100644 index 0000000..8b56ae6 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexer.snap @@ -0,0 +1,35 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │ error │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexerPrompt.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexerPrompt.snap new file mode 100644 index 0000000..8023961 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexerPrompt.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + + + + + + + + + + + + + + + + + + ╭────────────── Success ──────────────╮ + │ Indexer test succeeded! │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_torrent_indexer_over_indexers.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_torrent_indexer_over_indexers.snap new file mode 100644 index 0000000..f2ab6e8 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_torrent_indexer_over_indexers.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + + + + + + + + ╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮ + │ │ + │ ╭─────────────────────────╮ ╭─────────────────────────╮ │ + │ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │ + │ ╰─────────────────────────╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable RSS: │ ✔ │ API Key: │someApiKey │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Automatic Search: │ ✔ │ Seed Ratio: │ratio │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Interactive Search: │ ✔ │ Tags: │25 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭─────────────────────────╮ │ + │ Indexer Priority ▴▾: │1 │ │ + │ ╰─────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────────────────────╮╭──────────────────────────╮ │ + │ │ Save ││ Cancel │ │ + │ ╰───────────────────────────╯╰──────────────────────────╯ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_usenet_indexer_over_indexers.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_usenet_indexer_over_indexers.snap new file mode 100644 index 0000000..54ef1b1 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_usenet_indexer_over_indexers.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + + + + + + + + ╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮ + │ │ + │ ╭─────────────────────────╮ ╭─────────────────────────╮ │ + │ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │ + │ ╰─────────────────────────╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable RSS: │ ✔ │ API Key: │someApiKey │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Automatic Search: │ ✔ │ Tags: │25 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Interactive Search: │ ✔ │ Indexer Priority ▴▾: │1 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────────────────────╮╭──────────────────────────╮ │ + │ │ Save ││ Cancel │ │ + │ ╰───────────────────────────╯╰──────────────────────────╯ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_empty_indexers.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_empty_indexers.snap new file mode 100644 index 0000000..1cee723 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_empty_indexers.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading.snap new file mode 100644 index 0000000..c164738 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results.snap new file mode 100644 index 0000000..3cf1800 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results.snap @@ -0,0 +1,35 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... + + + + + + + + + + + + + + + + ╭ Testing Indexer ────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results_when_indexer_test_errors_is_none.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results_when_indexer_test_errors_is_none.snap new file mode 100644 index 0000000..bab738d --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results_when_indexer_test_errors_is_none.snap @@ -0,0 +1,35 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + + + + + + + + + + + + + + ╭ Testing Indexer ────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_test_all_over_indexers.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_test_all_over_indexers.snap new file mode 100644 index 0000000..63c5b17 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_test_all_over_indexers.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + ╭ Test All Indexers ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Indexer Pass/Fail Failure Messages │ + │=> DrunkenSlug x Some failure │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders.snap new file mode 100644 index 0000000..9b8832e --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/indexers/test_all_indexers_ui_tests.rs +expression: output +--- + + + + + + + ╭ Test All Indexers ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Indexer Pass/Fail Failure Messages │ + │=> DrunkenSlug x Some failure │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders_loading.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders_loading.snap new file mode 100644 index 0000000..418a025 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders_loading.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/indexers/test_all_indexers_ui_tests.rs +expression: output +--- + + + + + + + ╭ Test All Indexers ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/lidarr_ui/indexers/test_all_indexers_ui.rs new file mode 100644 index 0000000..6a3b30e --- /dev/null +++ b/src/ui/lidarr_ui/indexers/test_all_indexers_ui.rs @@ -0,0 +1,79 @@ +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; +use crate::models::servarr_data::modals::IndexerTestResultModalItem; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{get_width_from_percentage, title_block}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::Size; +use crate::ui::{DrawUi, draw_popup}; +use ratatui::Frame; +use ratatui::layout::{Constraint, Rect}; +use ratatui::widgets::{Cell, Row}; + +#[cfg(test)] +#[path = "test_all_indexers_ui_tests.rs"] +mod test_all_indexers_ui_tests; + +pub(super) struct TestAllIndexersUi; + +impl DrawUi for TestAllIndexersUi { + fn accepts(route: Route) -> bool { + if let Route::Lidarr(active_lidarr_block, _) = route { + return active_lidarr_block == ActiveLidarrBlock::TestAllIndexers; + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup(f, app, draw_test_all_indexers_test_results, Size::Large); + } +} + +fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let is_loading = app.is_loading || app.data.lidarr_data.indexer_test_all_results.is_none(); + let current_selection = if let Some(test_all_results) = + app.data.lidarr_data.indexer_test_all_results.as_ref() + && !test_all_results.is_empty() + { + test_all_results.current_selection().clone() + } else { + IndexerTestResultModalItem::default() + }; + f.render_widget(title_block("Test All Indexers"), area); + let test_results_row_mapping = |result: &IndexerTestResultModalItem| { + result.validation_failures.scroll_left_or_reset( + get_width_from_percentage(area, 86), + *result == current_selection, + app.ui_scroll_tick_count == 0, + ); + let pass_fail = if result.is_valid { "+" } else { "x" }; + let row = Row::new(vec![ + Cell::from(result.name.to_owned()), + Cell::from(pass_fail.to_owned()), + Cell::from(result.validation_failures.to_string()), + ]); + + if result.is_valid { + row.success() + } else { + row.failure() + } + }; + + let indexers_test_results_table = ManagarrTable::new( + app.data.lidarr_data.indexer_test_all_results.as_mut(), + test_results_row_mapping, + ) + .loading(is_loading) + .margin(1) + .headers(["Indexer", "Pass/Fail", "Failure Messages"]) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(10), + Constraint::Percentage(70), + ]); + + f.render_widget(indexers_test_results_table, area); +} diff --git a/src/ui/lidarr_ui/indexers/test_all_indexers_ui_tests.rs b/src/ui/lidarr_ui/indexers/test_all_indexers_ui_tests.rs new file mode 100644 index 0000000..9565f4d --- /dev/null +++ b/src/ui/lidarr_ui/indexers/test_all_indexers_ui_tests.rs @@ -0,0 +1,52 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_test_all_indexers_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if active_lidarr_block == ActiveLidarrBlock::TestAllIndexers { + assert!(TestAllIndexersUi::accepts(active_lidarr_block.into())); + } else { + assert!(!TestAllIndexersUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + + use super::*; + + #[test] + fn test_test_all_indexers_ui_renders_loading() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + TestAllIndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_test_all_indexers_ui_renders() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + TestAllIndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/lidarr_ui_tests.rs b/src/ui/lidarr_ui/lidarr_ui_tests.rs index a5c98d4..5265a47 100644 --- a/src/ui/lidarr_ui/lidarr_ui_tests.rs +++ b/src/ui/lidarr_ui/lidarr_ui_tests.rs @@ -25,6 +25,7 @@ mod tests { #[case(ActiveLidarrBlock::Downloads, 1)] #[case(ActiveLidarrBlock::History, 2)] #[case(ActiveLidarrBlock::RootFolders, 3)] + #[case(ActiveLidarrBlock::Indexers, 4)] fn test_lidarr_ui_renders_lidarr_tabs( #[case] active_lidarr_block: ActiveLidarrBlock, #[case] index: usize, diff --git a/src/ui/lidarr_ui/mod.rs b/src/ui/lidarr_ui/mod.rs index 243631a..85fc534 100644 --- a/src/ui/lidarr_ui/mod.rs +++ b/src/ui/lidarr_ui/mod.rs @@ -24,6 +24,7 @@ use super::{ widgets::loading_block::LoadingBlock, }; use crate::ui::lidarr_ui::downloads::DownloadsUi; +use crate::ui::lidarr_ui::indexers::IndexersUi; use crate::ui::lidarr_ui::root_folders::RootFoldersUi; use crate::{ app::App, @@ -39,6 +40,7 @@ use crate::{ mod downloads; mod history; +mod indexers; mod library; mod lidarr_ui_utils; @@ -63,6 +65,7 @@ impl DrawUi for LidarrUi { _ if DownloadsUi::accepts(route) => DownloadsUi::draw(f, app, content_area), _ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area), _ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area), + _ if IndexersUi::accepts(route) => IndexersUi::draw(f, app, content_area), _ => (), } } diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap index 5fde7c7..18499bc 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ Root Folders │ +│ Library │ Downloads │ History │ Root Folders │ Indexers │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags │ │=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex │ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap index 91fd750..7c6c9f9 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ Root Folders │ +│ Library │ Downloads │ History │ Root Folders │ Indexers │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Title Percent Complete Size Output Path Indexer Download Client │ │=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission │ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap index 6975d78..b3fced7 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ Root Folders │ +│ Library │ Downloads │ History │ Root Folders │ Indexers │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Source Title ▼ Event Type Quality Date │ │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Indexers.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Indexers.snap new file mode 100644 index 0000000..94edd2c --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Indexers.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ History │ Root Folders │ Indexers │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Indexer RSS Automatic Search Interactive Search Priority Tags │ +│=> Test Indexer Enabled Enabled Enabled 25 alex │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap index 4a8698b..08f841b 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ Root Folders │ +│ Library │ Downloads │ History │ Root Folders │ Indexers │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Path Free Space Unmapped Folders │ │=> /nfs 204800.00 GB 0 │