feat: Full CLI and TUI support for the Lidarr Indexers tab

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