From a7178a481bc6ad047fc8ce62874cfb625f56bb75 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 8 Sep 2026 10:10:25 -0600 Subject: [PATCH] feat(readarr): add the indexers handlers Add IndexersHandler and its three sub-handlers, EditIndexerHandler, IndexerSettingsHandler and TestAllIndexersHandler, and dispatch to them from ReadarrHandler. IndexersHandler::accepts ORs in all three children, so the parent and the children land together. Splitting would leave eighteen blocks accepted but inert, with Esc falling through to handle_clear_errors instead of popping. TestAllIndexersHandler::accepts compares the block for equality rather than consulting an array, since TestAllIndexers appears in no block array. --- .../indexers/edit_indexer_handler.rs | 532 +++++ .../indexers/edit_indexer_handler_tests.rs | 1925 +++++++++++++++++ .../indexers/edit_indexer_settings_handler.rs | 221 ++ .../edit_indexer_settings_handler_tests.rs | 610 ++++++ .../indexers/indexers_handler_tests.rs | 750 +++++++ src/handlers/readarr_handlers/indexers/mod.rs | 217 ++ .../indexers/test_all_indexers_handler.rs | 108 + .../test_all_indexers_handler_tests.rs | 133 ++ src/handlers/readarr_handlers/mod.rs | 5 + .../readarr_handlers/readarr_handler_tests.rs | 24 + 10 files changed, 4525 insertions(+) create mode 100644 src/handlers/readarr_handlers/indexers/edit_indexer_handler.rs create mode 100644 src/handlers/readarr_handlers/indexers/edit_indexer_handler_tests.rs create mode 100644 src/handlers/readarr_handlers/indexers/edit_indexer_settings_handler.rs create mode 100644 src/handlers/readarr_handlers/indexers/edit_indexer_settings_handler_tests.rs create mode 100644 src/handlers/readarr_handlers/indexers/indexers_handler_tests.rs create mode 100644 src/handlers/readarr_handlers/indexers/mod.rs create mode 100644 src/handlers/readarr_handlers/indexers/test_all_indexers_handler.rs create mode 100644 src/handlers/readarr_handlers/indexers/test_all_indexers_handler_tests.rs diff --git a/src/handlers/readarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/readarr_handlers/indexers/edit_indexer_handler.rs new file mode 100644 index 00000000..fef24458 --- /dev/null +++ b/src/handlers/readarr_handlers/indexers/edit_indexer_handler.rs @@ -0,0 +1,532 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::Route; +use crate::models::servarr_data::modals::EditIndexerModal; +use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, EDIT_INDEXER_BLOCKS}; +use crate::models::servarr_models::EditIndexerParams; +use crate::network::readarr_network::ReadarrEvent; +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_readarr_block: ActiveReadarrBlock, + _context: Option, +} + +impl EditIndexerHandler<'_, '_> { + fn build_edit_indexer_params(&mut self) -> EditIndexerParams { + let edit_indexer_modal = self + .app + .data + .readarr_data + .edit_indexer_modal + .take() + .expect("EditIndexerModal is None"); + let indexer_id = self.app.data.readarr_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, ActiveReadarrBlock> for EditIndexerHandler<'a, 'b> { + fn accepts(active_block: ActiveReadarrBlock) -> 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: ActiveReadarrBlock, + _context: Option, + ) -> EditIndexerHandler<'a, 'b> { + EditIndexerHandler { + key, + app, + active_readarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.readarr_data.edit_indexer_modal.is_some() + } + + fn handle_scroll_up(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::EditIndexerPrompt => { + self.app.data.readarr_data.selected_block.up(); + } + ActiveReadarrBlock::EditIndexerPriorityInput => { + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .priority += 1; + } + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::EditIndexerPrompt => { + self.app.data.readarr_data.selected_block.down(); + } + ActiveReadarrBlock::EditIndexerPriorityInput => { + let edit_indexer_modal = self + .app + .data + .readarr_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_readarr_block { + ActiveReadarrBlock::EditIndexerNameInput => { + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + .scroll_home(); + } + ActiveReadarrBlock::EditIndexerUrlInput => { + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + .scroll_home(); + } + ActiveReadarrBlock::EditIndexerApiKeyInput => { + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + .scroll_home(); + } + ActiveReadarrBlock::EditIndexerSeedRatioInput => { + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + .scroll_home(); + } + ActiveReadarrBlock::EditIndexerTagsInput => { + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + .scroll_home(); + } + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::EditIndexerNameInput => { + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + .reset_offset(); + } + ActiveReadarrBlock::EditIndexerUrlInput => { + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + .reset_offset(); + } + ActiveReadarrBlock::EditIndexerApiKeyInput => { + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + .reset_offset(); + } + ActiveReadarrBlock::EditIndexerSeedRatioInput => { + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + .reset_offset(); + } + ActiveReadarrBlock::EditIndexerTagsInput => { + self + .app + .data + .readarr_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_readarr_block { + ActiveReadarrBlock::EditIndexerPrompt => { + handle_prompt_left_right_keys!( + self, + ActiveReadarrBlock::EditIndexerConfirmPrompt, + readarr_data + ); + } + ActiveReadarrBlock::EditIndexerNameInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + ); + } + ActiveReadarrBlock::EditIndexerUrlInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + ); + } + ActiveReadarrBlock::EditIndexerApiKeyInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + ); + } + ActiveReadarrBlock::EditIndexerSeedRatioInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + ); + } + ActiveReadarrBlock::EditIndexerTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + ); + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::EditIndexerPrompt => { + let selected_block = self.app.data.readarr_data.selected_block.get_active_block(); + match selected_block { + ActiveReadarrBlock::EditIndexerConfirmPrompt => { + if self.app.data.readarr_data.prompt_confirm { + self.app.data.readarr_data.prompt_confirm_action = + Some(ReadarrEvent::EditIndexer(self.build_edit_indexer_params())); + self.app.should_refresh = true; + } else { + self.app.data.readarr_data.edit_indexer_modal = None; + } + + self.app.pop_navigation_stack(); + } + ActiveReadarrBlock::EditIndexerNameInput + | ActiveReadarrBlock::EditIndexerUrlInput + | ActiveReadarrBlock::EditIndexerApiKeyInput + | ActiveReadarrBlock::EditIndexerSeedRatioInput + | ActiveReadarrBlock::EditIndexerTagsInput => { + self.app.push_navigation_stack(selected_block.into()); + self.app.ignore_special_keys_for_textbox_input = true; + } + ActiveReadarrBlock::EditIndexerPriorityInput => self + .app + .push_navigation_stack(ActiveReadarrBlock::EditIndexerPriorityInput.into()), + ActiveReadarrBlock::EditIndexerToggleEnableRss => { + let indexer = self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_rss = Some(!indexer.enable_rss.unwrap_or_default()); + } + ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch => { + let indexer = self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_automatic_search = + Some(!indexer.enable_automatic_search.unwrap_or_default()); + } + ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch => { + let indexer = self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_interactive_search = + Some(!indexer.enable_interactive_search.unwrap_or_default()); + } + _ => (), + } + } + ActiveReadarrBlock::EditIndexerNameInput + | ActiveReadarrBlock::EditIndexerUrlInput + | ActiveReadarrBlock::EditIndexerApiKeyInput + | ActiveReadarrBlock::EditIndexerSeedRatioInput + | ActiveReadarrBlock::EditIndexerTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + ActiveReadarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::EditIndexerPrompt => { + self.app.pop_navigation_stack(); + self.app.data.readarr_data.prompt_confirm = false; + self.app.data.readarr_data.edit_indexer_modal = None; + } + ActiveReadarrBlock::EditIndexerNameInput + | ActiveReadarrBlock::EditIndexerUrlInput + | ActiveReadarrBlock::EditIndexerApiKeyInput + | ActiveReadarrBlock::EditIndexerSeedRatioInput + | ActiveReadarrBlock::EditIndexerPriorityInput + | ActiveReadarrBlock::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_readarr_block { + ActiveReadarrBlock::EditIndexerNameInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + ); + } + ActiveReadarrBlock::EditIndexerUrlInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + ); + } + ActiveReadarrBlock::EditIndexerApiKeyInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + ); + } + ActiveReadarrBlock::EditIndexerSeedRatioInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + ); + } + ActiveReadarrBlock::EditIndexerTagsInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + ); + } + ActiveReadarrBlock::EditIndexerPrompt + if self.app.data.readarr_data.selected_block.get_active_block() + == ActiveReadarrBlock::EditIndexerConfirmPrompt + && matches_key!(confirm, self.key) => + { + self.app.data.readarr_data.prompt_confirm = true; + self.app.data.readarr_data.prompt_confirm_action = + Some(ReadarrEvent::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/readarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/readarr_handlers/indexers/edit_indexer_handler_tests.rs new file mode 100644 index 00000000..2ba69b47 --- /dev/null +++ b/src/handlers/readarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -0,0 +1,1925 @@ +#[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::readarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_data::readarr::readarr_data::{ + ActiveReadarrBlock, EDIT_INDEXER_BLOCKS, + }; + use crate::models::servarr_models::{EditIndexerParams, Indexer}; + use crate::network::readarr_network::readarr_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::modals::EditIndexerModal; + use crate::models::servarr_data::readarr::readarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS; + + 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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + key, + &mut app, + ActiveReadarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + + if key == Key::Up { + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 2 + ); + } else { + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 1 + ); + + EditIndexerHandler::new( + Key::Up, + &mut app, + ActiveReadarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 2 + ); + + EditIndexerHandler::new( + key, + &mut app, + ActiveReadarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + assert_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.down(); + + EditIndexerHandler::new(key, &mut app, ActiveReadarrBlock::EditIndexerPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::EditIndexerNameInput + ); + } else { + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = true; + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.down(); + + EditIndexerHandler::new(key, &mut app, ActiveReadarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_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::modals::EditIndexerModal; + use crate::models::servarr_data::readarr::readarr_data::{ + EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + 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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.y = EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1; + + EditIndexerHandler::new(key, &mut app, ActiveReadarrBlock::EditIndexerPrompt, None).handle(); + + assert!(app.data.readarr_data.prompt_confirm); + + EditIndexerHandler::new(key, &mut app, ActiveReadarrBlock::EditIndexerPrompt, None).handle(); + + assert!(!app.data.readarr_data.prompt_confirm); + } + + #[rstest] + #[case( + 0, + ActiveReadarrBlock::EditIndexerNameInput, + ActiveReadarrBlock::EditIndexerUrlInput + )] + #[case( + 1, + ActiveReadarrBlock::EditIndexerToggleEnableRss, + ActiveReadarrBlock::EditIndexerApiKeyInput + )] + #[case( + 2, + ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveReadarrBlock::EditIndexerSeedRatioInput + )] + #[case( + 3, + ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveReadarrBlock::EditIndexerTagsInput + )] + fn test_left_right_block_toggle_torrents( + #[values(Key::Left, Key::Right)] key: Key, + #[case] starting_y_index: usize, + #[case] left_block: ActiveReadarrBlock, + #[case] right_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.y = starting_y_index; + + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + left_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveReadarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + right_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveReadarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + left_block + ); + } + + #[rstest] + #[case( + 0, + ActiveReadarrBlock::EditIndexerNameInput, + ActiveReadarrBlock::EditIndexerUrlInput + )] + #[case( + 1, + ActiveReadarrBlock::EditIndexerToggleEnableRss, + ActiveReadarrBlock::EditIndexerApiKeyInput + )] + #[case( + 2, + ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveReadarrBlock::EditIndexerTagsInput + )] + #[case( + 3, + ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveReadarrBlock::EditIndexerPriorityInput + )] + fn test_left_right_block_toggle_nzb( + #[values(Key::Left, Key::Right)] key: Key, + #[case] starting_y_index: usize, + #[case] left_block: ActiveReadarrBlock, + #[case] right_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.y = starting_y_index; + + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + left_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveReadarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + right_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveReadarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.y = 4; + app.data.readarr_data.prompt_confirm = false; + + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::EditIndexerPriorityInput + ); + + EditIndexerHandler::new(key, &mut app, ActiveReadarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::EditIndexerConfirmPrompt + ); + + EditIndexerHandler::new(key, &mut app, ActiveReadarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::EditIndexerConfirmPrompt + ); + assert!(app.data.readarr_data.prompt_confirm); + } + + #[test] + fn test_edit_indexer_name_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveReadarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveReadarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveReadarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveReadarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveReadarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveReadarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveReadarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveReadarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveReadarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveReadarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_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::readarr::readarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::network::readarr_network::ReadarrEvent; + use crate::network::readarr_network::readarr_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(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .readarr_data + .selected_block + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert_none!(app.data.readarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + assert_none!(app.data.readarr_data.edit_indexer_modal); + } + + #[test] + fn test_edit_indexer_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .readarr_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.readarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.readarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 8, + 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.readarr_data.prompt_confirm = true; + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert_modal_absent!(app.data.readarr_data.edit_indexer_modal); + assert!(app.should_refresh); + assert_eq!( + app.data.readarr_data.prompt_confirm_action, + Some(ReadarrEvent::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(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.readarr_data.prompt_confirm = true; + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::EditIndexerPrompt.into() + ); + assert_modal_present!(app.data.readarr_data.edit_indexer_modal); + assert!(!app.should_refresh); + assert_none!(app.data.readarr_data.prompt_confirm_action); + } + + #[rstest] + #[case(0, 0, ActiveReadarrBlock::EditIndexerNameInput)] + #[case(0, 1, ActiveReadarrBlock::EditIndexerUrlInput)] + #[case(1, 1, ActiveReadarrBlock::EditIndexerApiKeyInput)] + #[case(2, 1, ActiveReadarrBlock::EditIndexerSeedRatioInput)] + #[case(3, 1, ActiveReadarrBlock::EditIndexerTagsInput)] + fn test_edit_indexer_prompt_submit_input_fields( + #[case] starting_y: usize, + #[case] starting_x: usize, + #[case] block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .readarr_data + .selected_block + .set_index(starting_x, starting_y); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.set_index(0, 4); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.set_index(0, 1); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::EditIndexerPrompt.into() + ); + assert!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_rss + .unwrap() + ); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::EditIndexerPrompt.into() + ); + assert!( + !app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.set_index(0, 2); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::EditIndexerPrompt.into() + ); + assert!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_automatic_search + .unwrap() + ); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::EditIndexerPrompt.into() + ); + assert!( + !app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.set_index(0, 3); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::EditIndexerPrompt.into() + ); + assert!( + app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_interactive_search + .unwrap() + ); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::EditIndexerPrompt.into() + ); + assert!( + !app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerNameInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveReadarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_url_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerUrlInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveReadarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_api_key_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerApiKeyInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveReadarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerSeedRatioInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveReadarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_tags_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerTagsInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .readarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + ESC_KEY, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert!(!app.data.readarr_data.prompt_confirm); + assert_none!(app.data.readarr_data.edit_indexer_modal); + } + + #[rstest] + fn test_edit_indexer_input_fields_esc( + #[values( + ActiveReadarrBlock::EditIndexerNameInput, + ActiveReadarrBlock::EditIndexerUrlInput, + ActiveReadarrBlock::EditIndexerApiKeyInput, + ActiveReadarrBlock::EditIndexerSeedRatioInput, + ActiveReadarrBlock::EditIndexerTagsInput, + ActiveReadarrBlock::EditIndexerPriorityInput + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(active_readarr_block.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.ignore_special_keys_for_textbox_input = true; + + EditIndexerHandler::new(ESC_KEY, &mut app, active_readarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert!(!app.ignore_special_keys_for_textbox_input); + assert_some_eq_x!( + &app.data.readarr_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::modals::EditIndexerModal; + use crate::models::servarr_data::readarr::readarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS; + use crate::network::readarr_network::ReadarrEvent; + use crate::network::readarr_network::readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveReadarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveReadarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveReadarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveReadarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveReadarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveReadarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveReadarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveReadarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveReadarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveReadarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .readarr_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.readarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.readarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 8, + 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, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert_modal_absent!(app.data.readarr_data.edit_indexer_modal); + assert!(app.should_refresh); + assert_eq!( + app.data.readarr_data.prompt_confirm_action, + Some(ReadarrEvent::EditIndexer(expected_edit_indexer_params)) + ); + } + } + + #[test] + fn test_edit_indexer_handler_accepts() { + ActiveReadarrBlock::iter().for_each(|active_readarr_block| { + if EDIT_INDEXER_BLOCKS.contains(&active_readarr_block) { + assert!(EditIndexerHandler::accepts(active_readarr_block)); + } else { + assert!(!EditIndexerHandler::accepts(active_readarr_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, + ActiveReadarrBlock::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(true), + 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.readarr_data.edit_indexer_modal = Some(edit_indexer_modal); + let decoy_indexer = Indexer { id: 3, ..indexer() }; + app + .data + .readarr_data + .indexers + .set_items(vec![decoy_indexer, indexer()]); + app.data.readarr_data.indexers.select_index(Some(1)); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 8, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(true), + 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, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ) + .build_edit_indexer_params(); + + assert_eq!(params, expected_edit_indexer_params); + assert_modal_absent!(app.data.readarr_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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = false; + app.data.readarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + let handler = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::EditIndexerPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/readarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/readarr_handlers/indexers/edit_indexer_settings_handler.rs new file mode 100644 index 00000000..524daa11 --- /dev/null +++ b/src/handlers/readarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -0,0 +1,221 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::Route; +use crate::models::servarr_data::readarr::readarr_data::{ + ActiveReadarrBlock, INDEXER_SETTINGS_BLOCKS, +}; +use crate::models::servarr_models::IndexerSettings; +use crate::network::readarr_network::ReadarrEvent; +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_readarr_block: ActiveReadarrBlock, + _context: Option, +} + +impl IndexerSettingsHandler<'_, '_> { + fn build_edit_indexer_settings_params(&mut self) -> IndexerSettings { + self + .app + .data + .readarr_data + .indexer_settings + .take() + .expect("IndexerSettings is None") + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for IndexerSettingsHandler<'a, 'b> { + fn accepts(active_block: ActiveReadarrBlock) -> 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: ActiveReadarrBlock, + _context: Option, + ) -> IndexerSettingsHandler<'a, 'b> { + IndexerSettingsHandler { + key, + app, + active_readarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.readarr_data.indexer_settings.is_some() + } + + fn handle_scroll_up(&mut self) { + let indexer_settings = self + .app + .data + .readarr_data + .indexer_settings + .as_mut() + .unwrap(); + match self.active_readarr_block { + ActiveReadarrBlock::AllIndexerSettingsPrompt => { + self.app.data.readarr_data.selected_block.up(); + } + ActiveReadarrBlock::IndexerSettingsMinimumAgeInput => { + indexer_settings.minimum_age += 1; + } + ActiveReadarrBlock::IndexerSettingsRetentionInput => { + indexer_settings.retention += 1; + } + ActiveReadarrBlock::IndexerSettingsMaximumSizeInput => { + indexer_settings.maximum_size += 1; + } + ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput => { + indexer_settings.rss_sync_interval += 1; + } + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + let indexer_settings = self + .app + .data + .readarr_data + .indexer_settings + .as_mut() + .unwrap(); + match self.active_readarr_block { + ActiveReadarrBlock::AllIndexerSettingsPrompt => { + self.app.data.readarr_data.selected_block.down() + } + ActiveReadarrBlock::IndexerSettingsMinimumAgeInput => { + if indexer_settings.minimum_age > 0 { + indexer_settings.minimum_age -= 1; + } + } + ActiveReadarrBlock::IndexerSettingsRetentionInput => { + if indexer_settings.retention > 0 { + indexer_settings.retention -= 1; + } + } + ActiveReadarrBlock::IndexerSettingsMaximumSizeInput => { + if indexer_settings.maximum_size > 0 { + indexer_settings.maximum_size -= 1; + } + } + ActiveReadarrBlock::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_readarr_block == ActiveReadarrBlock::AllIndexerSettingsPrompt { + handle_prompt_left_right_keys!( + self, + ActiveReadarrBlock::IndexerSettingsConfirmPrompt, + readarr_data + ); + } + } + + fn handle_submit(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::AllIndexerSettingsPrompt => { + match self.app.data.readarr_data.selected_block.get_active_block() { + ActiveReadarrBlock::IndexerSettingsConfirmPrompt => { + if self.app.data.readarr_data.prompt_confirm { + self.app.data.readarr_data.prompt_confirm_action = Some( + ReadarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_params()), + ); + self.app.should_refresh = true; + } else { + self.app.data.readarr_data.indexer_settings = None; + } + + self.app.pop_navigation_stack(); + } + ActiveReadarrBlock::IndexerSettingsMinimumAgeInput + | ActiveReadarrBlock::IndexerSettingsRetentionInput + | ActiveReadarrBlock::IndexerSettingsMaximumSizeInput + | ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput => { + self.app.push_navigation_stack( + ( + self.app.data.readarr_data.selected_block.get_active_block(), + None, + ) + .into(), + ) + } + + _ => (), + } + } + + ActiveReadarrBlock::IndexerSettingsMinimumAgeInput + | ActiveReadarrBlock::IndexerSettingsRetentionInput + | ActiveReadarrBlock::IndexerSettingsMaximumSizeInput + | ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::AllIndexerSettingsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.readarr_data.prompt_confirm = false; + self.app.data.readarr_data.indexer_settings = None; + } + _ => self.app.pop_navigation_stack(), + } + } + + fn handle_char_key_event(&mut self) { + if self.active_readarr_block == ActiveReadarrBlock::AllIndexerSettingsPrompt + && self.app.data.readarr_data.selected_block.get_active_block() + == ActiveReadarrBlock::IndexerSettingsConfirmPrompt + && matches_key!(confirm, self.key) + { + self.app.data.readarr_data.prompt_confirm = true; + self.app.data.readarr_data.prompt_confirm_action = Some( + ReadarrEvent::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/readarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/readarr_handlers/indexers/edit_indexer_settings_handler_tests.rs new file mode 100644 index 00000000..2e6b3fb3 --- /dev/null +++ b/src/handlers/readarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -0,0 +1,610 @@ +#[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::readarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; + use crate::models::servarr_data::readarr::readarr_data::{ + ActiveReadarrBlock, INDEXER_SETTINGS_BLOCKS, + }; + use crate::models::servarr_models::IndexerSettings; + use crate::network::readarr_network::readarr_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::readarr::readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new($key, &mut app, $block, None).handle(); + + if $key == Key::Up { + assert_eq!( + app + .data + .readarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 1 + ); + } else { + if $negatives { + assert_eq!( + app + .data + .readarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + -1 + ); + } else { + assert_eq!( + app + .data + .readarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 0 + ); + + IndexerSettingsHandler::new(Key::Up, &mut app, $block, None).handle(); + + assert_eq!( + app + .data + .readarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 1 + ); + + IndexerSettingsHandler::new($key, &mut app, $block, None).handle(); + assert_eq!( + app + .data + .readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.down(); + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + if key == Key::Up { + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::IndexerSettingsMinimumAgeInput + ); + } else { + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = true; + app.data.readarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.down(); + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::IndexerSettingsRetentionInput + ); + } + + #[rstest] + fn test_edit_indexer_settings_minimum_age_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveReadarrBlock::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!( + ActiveReadarrBlock::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!( + ActiveReadarrBlock::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!( + ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput, + key, + rss_sync_interval, + false + ); + } + } + + mod test_handle_left_right_action { + use crate::models::servarr_data::readarr::readarr_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(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.y = INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1; + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert!(app.data.readarr_data.prompt_confirm); + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert!(!app.data.readarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::{ + assert_navigation_popped, + models::{ + BlockSelectionState, + servarr_data::readarr::readarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + servarr_models::IndexerSettings, + }, + network::readarr_network::ReadarrEvent, + }; + + 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(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .readarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.readarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert_none!(app.data.readarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + assert_none!(app.data.readarr_data.indexer_settings); + } + + #[test] + fn test_edit_indexer_settings_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .readarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.readarr_data.indexer_settings = Some(indexer_settings()); + app.data.readarr_data.prompt_confirm = true; + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert_some_eq_x!( + &app.data.readarr_data.prompt_confirm_action, + &ReadarrEvent::EditAllIndexerSettings(indexer_settings()) + ); + assert_modal_absent!(app.data.readarr_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(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into()); + app.data.readarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.readarr_data.prompt_confirm = true; + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::AllIndexerSettingsPrompt.into() + ); + assert!(!app.should_refresh); + } + + #[rstest] + #[case(ActiveReadarrBlock::IndexerSettingsMinimumAgeInput, 0)] + #[case(ActiveReadarrBlock::IndexerSettingsRetentionInput, 1)] + #[case(ActiveReadarrBlock::IndexerSettingsMaximumSizeInput, 2)] + #[case(ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput, 3)] + fn test_edit_indexer_settings_prompt_submit_selected_block( + #[case] selected_block: ActiveReadarrBlock, + #[case] y_index: usize, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.set_index(0, y_index); + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = true; + app.data.readarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.set_index(0, y_index); + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::AllIndexerSettingsPrompt.into() + ); + } + + #[rstest] + fn test_edit_indexer_settings_selected_block_submit( + #[values( + ActiveReadarrBlock::IndexerSettingsMinimumAgeInput, + ActiveReadarrBlock::IndexerSettingsRetentionInput, + ActiveReadarrBlock::IndexerSettingsMaximumSizeInput, + ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.data.readarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into()); + app.push_navigation_stack(active_readarr_block.into()); + + IndexerSettingsHandler::new(SUBMIT_KEY, &mut app, active_readarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into()); + app.data.readarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new( + ESC_KEY, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert!(!app.data.readarr_data.prompt_confirm); + assert_none!(app.data.readarr_data.indexer_settings); + } + + #[rstest] + fn test_edit_indexer_settings_selected_blocks_esc( + #[values( + ActiveReadarrBlock::IndexerSettingsMinimumAgeInput, + ActiveReadarrBlock::IndexerSettingsRetentionInput, + ActiveReadarrBlock::IndexerSettingsMaximumSizeInput, + ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(active_readarr_block.into()); + app.data.readarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new(ESC_KEY, &mut app, active_readarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert_some_eq_x!( + &app.data.readarr_data.indexer_settings, + &IndexerSettings::default() + ); + } + } + + mod test_handle_key_char { + use crate::{ + assert_navigation_popped, + models::{ + BlockSelectionState, servarr_data::readarr::readarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + }, + network::readarr_network::ReadarrEvent, + }; + + use super::*; + + #[test] + fn test_edit_indexer_settings_prompt_prompt_confirmation_confirm() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .readarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.readarr_data.indexer_settings = Some(indexer_settings()); + + IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert_some_eq_x!( + &app.data.readarr_data.prompt_confirm_action, + &ReadarrEvent::EditAllIndexerSettings(indexer_settings()) + ); + assert_modal_absent!(app.data.readarr_data.indexer_settings); + assert!(app.should_refresh); + } + } + + #[test] + fn test_indexer_settings_handler_accepts() { + ActiveReadarrBlock::iter().for_each(|active_readarr_block| { + if INDEXER_SETTINGS_BLOCKS.contains(&active_readarr_block) { + assert!(IndexerSettingsHandler::accepts(active_readarr_block)); + } else { + assert!(!IndexerSettingsHandler::accepts(active_readarr_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, + ActiveReadarrBlock::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.readarr_data.indexer_settings = Some(indexer_settings()); + + let actual_indexer_settings = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ) + .build_edit_indexer_settings_params(); + + assert_eq!(actual_indexer_settings, indexer_settings()); + assert_modal_absent!(app.data.readarr_data.indexer_settings); + } + + #[test] + fn test_edit_indexer_settings_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = false; + app.data.readarr_data.indexer_settings = Some(IndexerSettings::default()); + + let handler = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/readarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/readarr_handlers/indexers/indexers_handler_tests.rs new file mode 100644 index 00000000..9ad8265b --- /dev/null +++ b/src/handlers/readarr_handlers/indexers/indexers_handler_tests.rs @@ -0,0 +1,750 @@ +#[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::readarr_handlers::indexers::IndexersHandler; + use crate::models::servarr_data::readarr::readarr_data::{ + ActiveReadarrBlock, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXERS_BLOCKS, + }; + use crate::models::servarr_models::Indexer; + use crate::network::readarr_network::readarr_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(ActiveReadarrBlock::Indexers.into()); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::Indexers, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::Indexers, None).handle(); + + assert_eq!(app.get_current_route(), ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = is_ready; + app.data.readarr_data.main_tabs.set_index(5); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!( + app.data.readarr_data.main_tabs.get_active_route(), + ActiveReadarrBlock::RootFolders.into() + ); + assert_navigation_pushed!(app, ActiveReadarrBlock::RootFolders.into()); + } + + #[rstest] + fn test_indexers_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.is_loading = is_ready; + app.data.readarr_data.main_tabs.set_index(5); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!( + app.data.readarr_data.main_tabs.get_active_route(), + ActiveReadarrBlock::System.into() + ); + assert_navigation_pushed!(app, ActiveReadarrBlock::System.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(ActiveReadarrBlock::Indexers.into()); + + IndexersHandler::new(key, &mut app, ActiveReadarrBlock::DeleteIndexerPrompt, None).handle(); + + assert!(app.data.readarr_data.prompt_confirm); + + IndexersHandler::new(key, &mut app, ActiveReadarrBlock::DeleteIndexerPrompt, None).handle(); + + assert!(!app.data.readarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use super::*; + use crate::assert_navigation_popped; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_data::readarr::readarr_data::{ + EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, ReadarrData, + }; + use crate::models::servarr_models::{Indexer, IndexerField}; + use crate::network::readarr_network::ReadarrEvent; + use crate::network::readarr_network::readarr_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(ActiveReadarrBlock::Indexers.into()); + let protocol = if torrent_protocol { + "torrent".to_owned() + } else { + "usenet".to_owned() + }; + let decoy_protocol = if torrent_protocol { + "usenet".to_owned() + } else { + "torrent".to_owned() + }; + let mut expected_edit_indexer_modal = EditIndexerModal { + name: "Test".into(), + enable_rss: Some(true), + enable_automatic_search: Some(false), + enable_interactive_search: Some(true), + url: "https://test.com".into(), + api_key: "1234".into(), + tags: "usenet, test".into(), + ..EditIndexerModal::default() + }; + let mut readarr_data = ReadarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..ReadarrData::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: false, + enable_interactive_search: true, + protocol, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + ..Indexer::default() + }; + let decoy_indexer = Indexer { + name: Some("Decoy".to_owned()), + enable_rss: false, + enable_automatic_search: true, + enable_interactive_search: false, + protocol: decoy_protocol, + tags: vec![Number::from(2)], + fields: Some(vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://decoy.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("9999".to_owned())), + }, + ]), + ..Indexer::default() + }; + readarr_data + .indexers + .set_items(vec![decoy_indexer, indexer]); + readarr_data.indexers.select_index(Some(1)); + app.data.readarr_data = readarr_data; + + IndexersHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::Indexers, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::EditIndexerPrompt.into()); + assert_some_eq_x!( + &app.data.readarr_data.edit_indexer_modal, + &EditIndexerModal::from(&app.data.readarr_data) + ); + assert_some_eq_x!( + &app.data.readarr_data.edit_indexer_modal, + &expected_edit_indexer_modal + ); + if torrent_protocol { + assert_eq!( + app.data.readarr_data.selected_block.blocks, + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS + ); + } else { + assert_eq!( + app.data.readarr_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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = true; + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::Indexers, None).handle(); + + assert_eq!(app.get_current_route(), ActiveReadarrBlock::Indexers.into()); + assert_none!(app.data.readarr_data.edit_indexer_modal); + } + + #[test] + fn test_delete_indexer_prompt_confirm_submit() { + let mut app = App::test_default(); + app.data.readarr_data.indexers.set_items(vec![indexer()]); + app.data.readarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(app.data.readarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.readarr_data.prompt_confirm_action, + &ReadarrEvent::DeleteIndexer(8) + ); + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + } + + #[test] + fn test_prompt_decline_submit() { + let mut app = App::test_default(); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(!app.data.readarr_data.prompt_confirm); + assert_none!(app.data.readarr_data.prompt_confirm_action); + assert_navigation_popped!(app, ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::DeleteIndexerPrompt.into()); + app.data.readarr_data.prompt_confirm = true; + + IndexersHandler::new( + ESC_KEY, + &mut app, + ActiveReadarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert!(!app.data.readarr_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.readarr_data.indexer_test_errors = Some("test result".to_owned()); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::TestIndexer.into()); + + IndexersHandler::new(ESC_KEY, &mut app, ActiveReadarrBlock::TestIndexer, None).handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert_none!(app.data.readarr_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(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + + IndexersHandler::new(ESC_KEY, &mut app, ActiveReadarrBlock::Indexers, None).handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + + use super::*; + use crate::network::readarr_network::readarr_network_test_utils::test_utils::indexer; + use crate::{ + assert_navigation_popped, + models::servarr_data::readarr::readarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + network::readarr_network::ReadarrEvent, + }; + + #[test] + fn test_refresh_indexers_key() { + let mut app = App::test_default(); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::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 + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveReadarrBlock::Indexers.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_indexer_settings_key() { + let mut app = App::test_default(); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.settings.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::AllIndexerSettingsPrompt.into()); + assert_eq!( + app.data.readarr_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(ActiveReadarrBlock::Indexers.into()); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.settings.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveReadarrBlock::Indexers.into()); + } + + #[test] + fn test_test_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveReadarrBlock::Indexers.into()); + } + + #[test] + fn test_test_all_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test_all.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test_all.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveReadarrBlock::Indexers.into()); + } + + #[test] + fn test_delete_indexer_prompt_confirm() { + let mut app = App::test_default(); + app.data.readarr_data.indexers.set_items(vec![indexer()]); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveReadarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(app.data.readarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.readarr_data.prompt_confirm_action, + &ReadarrEvent::DeleteIndexer(8) + ); + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + } + } + + #[rstest] + fn test_delegates_edit_indexer_blocks_to_edit_indexer_handler( + #[values( + ActiveReadarrBlock::EditIndexerPrompt, + ActiveReadarrBlock::EditIndexerConfirmPrompt, + ActiveReadarrBlock::EditIndexerApiKeyInput, + ActiveReadarrBlock::EditIndexerNameInput, + ActiveReadarrBlock::EditIndexerSeedRatioInput, + ActiveReadarrBlock::EditIndexerToggleEnableRss, + ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveReadarrBlock::EditIndexerUrlInput, + ActiveReadarrBlock::EditIndexerTagsInput + )] + active_readarr_block: ActiveReadarrBlock, + ) { + test_handler_delegation!( + IndexersHandler, + ActiveReadarrBlock::Indexers, + active_readarr_block + ); + } + + #[rstest] + fn test_delegates_indexer_settings_blocks_to_indexer_settings_handler( + #[values( + ActiveReadarrBlock::AllIndexerSettingsPrompt, + ActiveReadarrBlock::IndexerSettingsConfirmPrompt, + ActiveReadarrBlock::IndexerSettingsMaximumSizeInput, + ActiveReadarrBlock::IndexerSettingsMinimumAgeInput, + ActiveReadarrBlock::IndexerSettingsRetentionInput, + ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_readarr_block: ActiveReadarrBlock, + ) { + test_handler_delegation!( + IndexersHandler, + ActiveReadarrBlock::Indexers, + active_readarr_block + ); + } + + #[test] + fn test_delegates_test_all_indexers_block_to_test_all_indexers_handler() { + test_handler_delegation!( + IndexersHandler, + ActiveReadarrBlock::Indexers, + ActiveReadarrBlock::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(ActiveReadarrBlock::TestAllIndexers); + + ActiveReadarrBlock::iter().for_each(|active_readarr_block| { + if indexers_blocks.contains(&active_readarr_block) { + assert!(IndexersHandler::accepts(active_readarr_block)); + } else { + assert!(!IndexersHandler::accepts(active_readarr_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, + ActiveReadarrBlock::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(); + let decoy_indexer = Indexer { id: 3, ..indexer() }; + app + .data + .readarr_data + .indexers + .set_items(vec![decoy_indexer, indexer()]); + app.data.readarr_data.indexers.select_index(Some(1)); + + let indexer_id = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ) + .extract_indexer_id(); + + assert_eq!(indexer_id, 8); + } + + #[test] + fn test_indexers_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::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(ActiveReadarrBlock::Indexers.into()); + app.is_loading = false; + app + .data + .readarr_data + .indexers + .set_items(vec![Indexer::default()]); + + let handler = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::Indexers, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/readarr_handlers/indexers/mod.rs b/src/handlers/readarr_handlers/indexers/mod.rs new file mode 100644 index 00000000..52b8c431 --- /dev/null +++ b/src/handlers/readarr_handlers/indexers/mod.rs @@ -0,0 +1,217 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::readarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::readarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; +use crate::handlers::readarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; +use crate::handlers::readarr_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::readarr::readarr_data::{ + ActiveReadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, +}; +use crate::models::{BlockSelectionState, Route}; +use crate::network::readarr_network::ReadarrEvent; + +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_readarr_block: ActiveReadarrBlock, + context: Option, +} + +impl IndexersHandler<'_, '_> { + fn extract_indexer_id(&self) -> i64 { + self.app.data.readarr_data.indexers.current_selection().id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for IndexersHandler<'a, 'b> { + fn handle(&mut self) { + let indexers_table_handling_config = + TableHandlingConfig::new(ActiveReadarrBlock::Indexers.into()); + + if !handle_table( + self, + |app| &mut app.data.readarr_data.indexers, + indexers_table_handling_config, + ) { + match self.active_readarr_block { + _ if EditIndexerHandler::accepts(self.active_readarr_block) => { + EditIndexerHandler::new(self.key, self.app, self.active_readarr_block, self.context) + .handle() + } + _ if IndexerSettingsHandler::accepts(self.active_readarr_block) => { + IndexerSettingsHandler::new(self.key, self.app, self.active_readarr_block, self.context) + .handle() + } + _ if TestAllIndexersHandler::accepts(self.active_readarr_block) => { + TestAllIndexersHandler::new(self.key, self.app, self.active_readarr_block, self.context) + .handle() + } + _ => self.handle_key_event(), + } + } + } + + fn accepts(active_block: ActiveReadarrBlock) -> 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: ActiveReadarrBlock, + context: Option, + ) -> IndexersHandler<'a, 'b> { + IndexersHandler { + key, + app, + active_readarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.readarr_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_readarr_block == ActiveReadarrBlock::Indexers { + self + .app + .push_navigation_stack(ActiveReadarrBlock::DeleteIndexerPrompt.into()); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::Indexers => handle_change_tab_left_right_keys(self.app, self.key), + ActiveReadarrBlock::DeleteIndexerPrompt => handle_prompt_toggle(self.app, self.key), + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::DeleteIndexerPrompt => { + if self.app.data.readarr_data.prompt_confirm { + self.app.data.readarr_data.prompt_confirm_action = + Some(ReadarrEvent::DeleteIndexer(self.extract_indexer_id())); + } + + self.app.pop_navigation_stack(); + } + ActiveReadarrBlock::Indexers => { + self + .app + .push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into()); + self.app.data.readarr_data.edit_indexer_modal = Some((&self.app.data.readarr_data).into()); + let protocol = &self + .app + .data + .readarr_data + .indexers + .current_selection() + .protocol; + if protocol == "torrent" { + self.app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + } else { + self.app.data.readarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + } + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::DeleteIndexerPrompt => { + self.app.pop_navigation_stack(); + self.app.data.readarr_data.prompt_confirm = false; + } + ActiveReadarrBlock::TestIndexer => { + self.app.pop_navigation_stack(); + self.app.data.readarr_data.indexer_test_errors = None; + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_readarr_block { + ActiveReadarrBlock::Indexers => match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ if matches_key!(test, key) => { + self + .app + .push_navigation_stack(ActiveReadarrBlock::TestIndexer.into()); + } + _ if matches_key!(test_all, key) => { + self + .app + .push_navigation_stack(ActiveReadarrBlock::TestAllIndexers.into()); + } + _ if matches_key!(settings, key) => { + self + .app + .push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into()); + self.app.data.readarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + } + _ => (), + }, + ActiveReadarrBlock::DeleteIndexerPrompt => { + if matches_key!(confirm, key) { + self.app.data.readarr_data.prompt_confirm = true; + self.app.data.readarr_data.prompt_confirm_action = + Some(ReadarrEvent::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/readarr_handlers/indexers/test_all_indexers_handler.rs b/src/handlers/readarr_handlers/indexers/test_all_indexers_handler.rs new file mode 100644 index 00000000..178620c4 --- /dev/null +++ b/src/handlers/readarr_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::readarr::readarr_data::ActiveReadarrBlock; + +#[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_readarr_block: ActiveReadarrBlock, + _context: Option, +} + +impl TestAllIndexersHandler<'_, '_> {} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for TestAllIndexersHandler<'a, 'b> { + fn handle(&mut self) { + let indexer_test_all_results_table_handling_config = + TableHandlingConfig::new(ActiveReadarrBlock::TestAllIndexers.into()); + + if !handle_table( + self, + |app| { + app + .data + .readarr_data + .indexer_test_all_results + .as_mut() + .unwrap() + }, + indexer_test_all_results_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveReadarrBlock) -> bool { + active_block == ActiveReadarrBlock::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: ActiveReadarrBlock, + _context: Option, + ) -> TestAllIndexersHandler<'a, 'b> { + TestAllIndexersHandler { + key, + app, + active_readarr_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.readarr_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_readarr_block == ActiveReadarrBlock::TestAllIndexers { + self.app.pop_navigation_stack(); + self.app.data.readarr_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/readarr_handlers/indexers/test_all_indexers_handler_tests.rs b/src/handlers/readarr_handlers/indexers/test_all_indexers_handler_tests.rs new file mode 100644 index 00000000..ba912014 --- /dev/null +++ b/src/handlers/readarr_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::readarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler; + use crate::models::servarr_data::modals::IndexerTestResultModalItem; + use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; + 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(ActiveReadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveReadarrBlock::TestAllIndexers.into()); + app.data.readarr_data.indexer_test_all_results = Some(StatefulTable::default()); + + TestAllIndexersHandler::new(ESC_KEY, &mut app, ActiveReadarrBlock::TestAllIndexers, None) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into()); + assert_none!(app.data.readarr_data.indexer_test_all_results); + } + } + + #[test] + fn test_test_all_indexers_handler_accepts() { + ActiveReadarrBlock::iter().for_each(|active_readarr_block| { + if active_readarr_block == ActiveReadarrBlock::TestAllIndexers { + assert!(TestAllIndexersHandler::accepts(active_readarr_block)); + } else { + assert!(!TestAllIndexersHandler::accepts(active_readarr_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, + ActiveReadarrBlock::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(ActiveReadarrBlock::TestAllIndexers.into()); + app.is_loading = true; + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::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(ActiveReadarrBlock::TestAllIndexers.into()); + app.is_loading = false; + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::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(ActiveReadarrBlock::TestAllIndexers.into()); + app.is_loading = false; + app.data.readarr_data.indexer_test_all_results = Some(StatefulTable::default()); + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::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(ActiveReadarrBlock::TestAllIndexers.into()); + app.is_loading = false; + let mut results = StatefulTable::default(); + results.set_items(vec![IndexerTestResultModalItem::default()]); + app.data.readarr_data.indexer_test_all_results = Some(results); + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::TestAllIndexers, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/readarr_handlers/mod.rs b/src/handlers/readarr_handlers/mod.rs index 759f172d..c2961253 100644 --- a/src/handlers/readarr_handlers/mod.rs +++ b/src/handlers/readarr_handlers/mod.rs @@ -2,6 +2,7 @@ use super::KeyEventHandler; use crate::handlers::readarr_handlers::blocklist::BlocklistHandler; use crate::handlers::readarr_handlers::downloads::DownloadsHandler; use crate::handlers::readarr_handlers::history::HistoryHandler; +use crate::handlers::readarr_handlers::indexers::IndexersHandler; use crate::handlers::readarr_handlers::library::LibraryHandler; use crate::handlers::readarr_handlers::root_folders::RootFoldersHandler; use crate::handlers::readarr_handlers::system::SystemHandler; @@ -14,6 +15,7 @@ use crate::{ mod blocklist; mod downloads; mod history; +mod indexers; mod library; mod root_folders; mod system; @@ -48,6 +50,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for ReadarrHandler<'a, RootFoldersHandler::new(self.key, self.app, self.active_readarr_block, self.context) .handle(); } + _ if IndexersHandler::accepts(self.active_readarr_block) => { + IndexersHandler::new(self.key, self.app, self.active_readarr_block, self.context).handle(); + } _ if SystemHandler::accepts(self.active_readarr_block) => { SystemHandler::new(self.key, self.app, self.active_readarr_block, self.context).handle(); } diff --git a/src/handlers/readarr_handlers/readarr_handler_tests.rs b/src/handlers/readarr_handlers/readarr_handler_tests.rs index 00504374..445aab40 100644 --- a/src/handlers/readarr_handlers/readarr_handler_tests.rs +++ b/src/handlers/readarr_handlers/readarr_handler_tests.rs @@ -247,6 +247,30 @@ mod tests { ); } + #[rstest] + fn test_delegates_indexers_blocks_to_indexers_handler( + #[values( + ActiveReadarrBlock::Indexers, + ActiveReadarrBlock::DeleteIndexerPrompt, + ActiveReadarrBlock::TestIndexer, + ActiveReadarrBlock::TestAllIndexers, + ActiveReadarrBlock::EditIndexerPrompt, + ActiveReadarrBlock::AllIndexerSettingsPrompt, + ActiveReadarrBlock::IndexerSettingsConfirmPrompt, + ActiveReadarrBlock::IndexerSettingsMaximumSizeInput, + ActiveReadarrBlock::IndexerSettingsMinimumAgeInput, + ActiveReadarrBlock::IndexerSettingsRetentionInput, + ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_readarr_block: ActiveReadarrBlock, + ) { + test_handler_delegation!( + ReadarrHandler, + ActiveReadarrBlock::Indexers, + active_readarr_block + ); + } + #[rstest] fn test_delegates_system_blocks_to_system_handler( #[values(