diff --git a/src/cli/readarr/mod.rs b/src/cli/readarr/mod.rs index 566a3197..76eb484f 100644 --- a/src/cli/readarr/mod.rs +++ b/src/cli/readarr/mod.rs @@ -110,6 +110,8 @@ pub enum ReadarrCommand { )] task_name: ReadarrTaskName, }, + #[command(about = "Test all Readarr indexers")] + TestAllIndexers, #[command( about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'" )] @@ -247,6 +249,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrCommand> for ReadarrCliHandler<'a, .await?; serde_json::to_string_pretty(&resp)? } + ReadarrCommand::TestAllIndexers => { + println!("Testing all Readarr indexers. This may take a minute..."); + let resp = self + .network + .handle_network_event(ReadarrEvent::TestAllIndexers.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } ReadarrCommand::TestIndexer { indexer_id } => { let resp = self .network diff --git a/src/cli/readarr/readarr_command_tests.rs b/src/cli/readarr/readarr_command_tests.rs index e7ad9cf7..21a25396 100644 --- a/src/cli/readarr/readarr_command_tests.rs +++ b/src/cli/readarr/readarr_command_tests.rs @@ -4,6 +4,7 @@ mod tests { use clap::CommandFactory; use clap::error::ErrorKind; use pretty_assertions::assert_eq; + use rstest::rstest; use crate::Cli; @@ -212,9 +213,11 @@ mod tests { ); } - #[test] - fn test_clear_blocklist_has_no_arg_requirements() { - let result = Cli::command().try_get_matches_from(["managarr", "readarr", "clear-blocklist"]); + #[rstest] + fn test_commands_that_have_no_arg_requirements( + #[values("clear-blocklist", "test-all-indexers")] subcommand: &str, + ) { + let result = Cli::command().try_get_matches_from(["managarr", "readarr", subcommand]); assert_ok!(&result); } @@ -395,6 +398,28 @@ mod tests { 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::(ReadarrEvent::TestAllIndexers.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Readarr(ReadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let test_all_indexers_command = ReadarrCommand::TestAllIndexers; + + let result = ReadarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_toggle_author_monitoring_command() { let mut mock_network = MockNetworkTrait::new(); diff --git a/src/network/readarr_network/indexers/mod.rs b/src/network/readarr_network/indexers/mod.rs index 9f6ff7e4..98afff73 100644 --- a/src/network/readarr_network/indexers/mod.rs +++ b/src/network/readarr_network/indexers/mod.rs @@ -1,4 +1,6 @@ -use crate::models::servarr_models::{EditIndexerParams, Indexer}; +use crate::models::servarr_data::modals::IndexerTestResultModalItem; +use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult}; +use crate::models::stateful_table::StatefulTable; use crate::network::readarr_network::ReadarrEvent; use crate::network::{Network, RequestMethod}; use anyhow::{Context, Result}; @@ -315,4 +317,65 @@ impl Network<'_, '_> { }) .await } + + pub(in crate::network::readarr_network) async fn test_all_readarr_indexers( + &mut self, + ) -> Result> { + info!("Testing all Readarr indexers"); + let event = ReadarrEvent::TestAllIndexers; + + let mut request_props = self + .request_props_from(event, RequestMethod::Post, None, None, None) + .await; + request_props.ignore_status_code = true; + + let result = self + .handle_request::<(), Vec>(request_props, |test_results, mut app| { + let mut test_all_indexer_results = StatefulTable::default(); + let indexers = app.data.readarr_data.indexers.items.clone(); + let modal_test_results = test_results + .iter() + .map(|result| { + let name = indexers + .iter() + .filter(|&indexer| indexer.id == result.id) + .map(|indexer| indexer.name.clone()) + .nth(0) + .unwrap_or_default(); + let validation_failures = result + .validation_failures + .iter() + .map(|failure| { + format!( + "Failure for field '{}': {}", + failure.property_name, failure.error_message + ) + }) + .collect::>() + .join(", "); + + IndexerTestResultModalItem { + name: name.unwrap_or_default(), + is_valid: result.is_valid, + validation_failures: validation_failures.into(), + } + }) + .collect(); + test_all_indexer_results.set_items(modal_test_results); + app.data.readarr_data.indexer_test_all_results = Some(test_all_indexer_results); + }) + .await; + + if result.is_err() { + self + .app + .lock() + .await + .data + .readarr_data + .indexer_test_all_results = Some(StatefulTable::default()); + } + + result + } } diff --git a/src/network/readarr_network/indexers/readarr_indexers_network_tests.rs b/src/network/readarr_network/indexers/readarr_indexers_network_tests.rs index 999da1d8..153c4ac0 100644 --- a/src/network/readarr_network/indexers/readarr_indexers_network_tests.rs +++ b/src/network/readarr_network/indexers/readarr_indexers_network_tests.rs @@ -1,12 +1,14 @@ #[cfg(test)] mod tests { use crate::models::readarr_models::ReadarrSerdeable; - use crate::models::servarr_models::{EditIndexerParams, Indexer}; + use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult}; + use crate::models::stateful_table::StatefulTable; use crate::network::NetworkResource; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::readarr_network::ReadarrEvent; use crate::network::readarr_network::readarr_network_test_utils::test_utils::{ - INDEXER_JSON, indexer, stale_indexer, + INDEXER_JSON, INDEXER_TEST_RESULTS_JSON, indexer, indexer_test_results, stale_indexer, + tested_indexers, }; use bimap::BiMap; use mockito::Matcher; @@ -570,4 +572,78 @@ mod tests { &"stale error".to_owned() ); } + + #[tokio::test] + async fn test_handle_test_all_readarr_indexers_event() { + let response_json: Value = serde_json::from_str(INDEXER_TEST_RESULTS_JSON).unwrap(); + let expected_results: Vec = + serde_json::from_str(INDEXER_TEST_RESULTS_JSON).unwrap(); + let (async_server, app, _server) = MockServarrApi::post() + .returns(response_json) + .status(400) + .build_for(ReadarrEvent::TestAllIndexers) + .await; + { + let mut app = app.lock().await; + app.server_tabs.set_index(3); + app.data.readarr_data.indexers.set_items(tested_indexers()); + } + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::TestAllIndexers) + .await; + + async_server.assert_async().await; + let ReadarrSerdeable::IndexerTestResults(results) = assert_ok!(result) else { + panic!("Expected IndexerTestResults") + }; + assert_eq!(results, expected_results); + let app = app.lock().await; + assert_eq!( + app + .data + .readarr_data + .indexer_test_all_results + .as_ref() + .unwrap() + .items, + indexer_test_results() + ); + } + + #[tokio::test] + async fn test_handle_test_all_readarr_indexers_event_failure() { + let (async_server, app, _server) = MockServarrApi::post() + .returns(json!({ "message": "Internal Server Error" })) + .status(500) + .build_for(ReadarrEvent::TestAllIndexers) + .await; + { + let mut stale_test_all_results = StatefulTable::default(); + stale_test_all_results.set_items(indexer_test_results()); + let mut app = app.lock().await; + app.server_tabs.set_index(3); + app.data.readarr_data.indexers.set_items(tested_indexers()); + app.data.readarr_data.indexer_test_all_results = Some(stale_test_all_results); + } + let mut network = test_network(&app); + + let result = network + .handle_readarr_event(ReadarrEvent::TestAllIndexers) + .await; + + async_server.assert_async().await; + assert_err!(result); + let app = app.lock().await; + assert_is_empty!( + app + .data + .readarr_data + .indexer_test_all_results + .as_ref() + .unwrap() + .items + ); + } } diff --git a/src/network/readarr_network/mod.rs b/src/network/readarr_network/mod.rs index 5e4ee3f5..a16ff907 100644 --- a/src/network/readarr_network/mod.rs +++ b/src/network/readarr_network/mod.rs @@ -75,6 +75,7 @@ pub enum ReadarrEvent { MarkHistoryItemAsFailed(i64), SearchNewAuthor(String), StartTask(ReadarrTaskName), + TestAllIndexers, TestIndexer(i64), ToggleAuthorMonitoring(i64), ToggleBookMonitoring(i64), @@ -121,6 +122,7 @@ impl NetworkResource for ReadarrEvent { "/indexer" } ReadarrEvent::TestIndexer(_) => "/indexer/test", + ReadarrEvent::TestAllIndexers => "/indexer/testall", ReadarrEvent::GetLogs(_) => "/log", ReadarrEvent::GetMetadataProfiles => "/metadataprofile", ReadarrEvent::GetQualityProfiles => "/qualityprofile", @@ -308,6 +310,10 @@ impl Network<'_, '_> { .start_readarr_task(task_name) .await .map(ReadarrSerdeable::from), + ReadarrEvent::TestAllIndexers => self + .test_all_readarr_indexers() + .await + .map(ReadarrSerdeable::from), ReadarrEvent::TestIndexer(indexer_id) => self .test_readarr_indexer(indexer_id) .await diff --git a/src/network/readarr_network/readarr_network_test_utils.rs b/src/network/readarr_network/readarr_network_test_utils.rs index 620d4abd..23537302 100644 --- a/src/network/readarr_network/readarr_network_test_utils.rs +++ b/src/network/readarr_network/readarr_network_test_utils.rs @@ -1,12 +1,14 @@ #[cfg(test)] #[allow(dead_code)] pub mod test_utils { + use crate::models::HorizontallyScrollableText; use crate::models::readarr_models::{ AddAuthorBody, AddAuthorOptions, AddAuthorSearchResult, Author, AuthorStatus, BlocklistItem, Book, BookFile, DownloadRecord, DownloadStatus, DownloadsResponse, Edition, MonitorType, NewItemMonitorType, ReadarrHistoryData, ReadarrHistoryEventType, ReadarrHistoryItem, ReadarrRelease, }; + use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_models::{Indexer, IndexerField, Quality, QualityWrapper, RootFolder}; use chrono::DateTime; use serde_json::{Number, json}; @@ -463,4 +465,60 @@ pub mod test_utils { tags: vec![Number::from(1)], } } + + pub const INDEXER_TEST_RESULTS_JSON: &str = r#"[ + { + "id": 8, + "isValid": true, + "validationFailures": [] + }, + { + "id": 14, + "isValid": false, + "validationFailures": [ + { + "propertyName": "baseUrl", + "errorMessage": "Unable to connect to indexer", + "severity": "error" + }, + { + "propertyName": "apiKey", + "errorMessage": "Invalid API key", + "severity": "error" + } + ] + } + ]"#; + + pub fn tested_indexers() -> Vec { + vec![ + Indexer { + id: 8, + name: Some("Test Indexer".to_owned()), + ..Indexer::default() + }, + Indexer { + id: 14, + name: Some("Other Indexer".to_owned()), + ..Indexer::default() + }, + ] + } + + pub fn indexer_test_results() -> Vec { + vec![ + IndexerTestResultModalItem { + name: "Test Indexer".to_owned(), + is_valid: true, + validation_failures: HorizontallyScrollableText::default(), + }, + IndexerTestResultModalItem { + name: "Other Indexer".to_owned(), + is_valid: false, + validation_failures: + "Failure for field 'baseUrl': Unable to connect to indexer, Failure for field 'apiKey': Invalid API key" + .into(), + }, + ] + } } diff --git a/src/network/readarr_network/readarr_network_tests.rs b/src/network/readarr_network/readarr_network_tests.rs index 3959166e..31db1641 100644 --- a/src/network/readarr_network/readarr_network_tests.rs +++ b/src/network/readarr_network/readarr_network_tests.rs @@ -53,6 +53,7 @@ mod tests { #[case(ReadarrEvent::EditIndexer(EditIndexerParams::default()), "/indexer")] #[case(ReadarrEvent::GetIndexers, "/indexer")] #[case(ReadarrEvent::TestIndexer(8), "/indexer/test")] + #[case(ReadarrEvent::TestAllIndexers, "/indexer/testall")] #[case(ReadarrEvent::GetLogs(500), "/log")] #[case(ReadarrEvent::GetMetadataProfiles, "/metadataprofile")] #[case(ReadarrEvent::GetQualityProfiles, "/qualityprofile")]