From a8609e08c590a9e6eec7a055738a6555d54970bd Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 7 Jan 2026 12:50:23 -0700 Subject: [PATCH] feat: CLI support for deleting a tag in Lidarr --- src/cli/lidarr/delete_command_handler.rs | 12 +++++ .../lidarr/delete_command_handler_tests.rs | 52 +++++++++++++++++++ src/cli/lidarr/list_command_handler_tests.rs | 22 +++----- .../lidarr_network/lidarr_network_tests.rs | 27 ++++++++++ src/network/lidarr_network/mod.rs | 26 +++++++++- 5 files changed, 124 insertions(+), 15 deletions(-) diff --git a/src/cli/lidarr/delete_command_handler.rs b/src/cli/lidarr/delete_command_handler.rs index 131b5aa..9a723cb 100644 --- a/src/cli/lidarr/delete_command_handler.rs +++ b/src/cli/lidarr/delete_command_handler.rs @@ -28,6 +28,11 @@ pub enum LidarrDeleteCommand { #[arg(long, help = "Add a list exclusion for this artist")] add_list_exclusion: bool, }, + #[command(about = "Delete the tag with the specified ID")] + Tag { + #[arg(long, help = "The ID of the tag to delete", required = true)] + tag_id: i64, + }, } impl From for Command { @@ -73,6 +78,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteComm .await?; serde_json::to_string_pretty(&resp)? } + LidarrDeleteCommand::Tag { tag_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteTag(tag_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } }; Ok(result) diff --git a/src/cli/lidarr/delete_command_handler_tests.rs b/src/cli/lidarr/delete_command_handler_tests.rs index dda5a61..20f4460 100644 --- a/src/cli/lidarr/delete_command_handler_tests.rs +++ b/src/cli/lidarr/delete_command_handler_tests.rs @@ -85,6 +85,32 @@ mod tests { }; assert_eq!(delete_command, expected_args); } + + #[test] + fn test_delete_tag_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "tag"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_tag_success() { + let expected_args = LidarrDeleteCommand::Tag { tag_id: 1 }; + + let result = Cli::try_parse_from(["managarr", "lidarr", "delete", "tag", "--tag-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); + } } mod handler { @@ -140,5 +166,31 @@ mod tests { assert_ok!(&result); } + + #[tokio::test] + async fn test_handle_delete_tag_command() { + let expected_tag_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteTag(expected_tag_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_tag_command = LidarrDeleteCommand::Tag { tag_id: 1 }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } } } diff --git a/src/cli/lidarr/list_command_handler_tests.rs b/src/cli/lidarr/list_command_handler_tests.rs index d01b7bd..9402480 100644 --- a/src/cli/lidarr/list_command_handler_tests.rs +++ b/src/cli/lidarr/list_command_handler_tests.rs @@ -18,16 +18,11 @@ mod tests { } mod cli { - use rstest::rstest; use super::*; + use rstest::rstest; #[rstest] - fn test_list_commands_have_no_arg_requirements( - #[values( - "artists", - "tags" - )] subcommand: &str - ) { + fn test_list_commands_have_no_arg_requirements(#[values("artists", "tags")] subcommand: &str) { let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", subcommand]); assert_ok!(&result); @@ -53,12 +48,12 @@ mod tests { }; #[rstest] - #[case(LidarrListCommand::Artists, LidarrEvent::ListArtists)] - #[case(LidarrListCommand::Tags, LidarrEvent::GetTags)] + #[case(LidarrListCommand::Artists, LidarrEvent::ListArtists)] + #[case(LidarrListCommand::Tags, LidarrEvent::GetTags)] #[tokio::test] async fn test_handle_list_command( #[case] list_command: LidarrListCommand, - #[case] expected_lidarr_event: LidarrEvent + #[case] expected_lidarr_event: LidarrEvent, ) { let mut mock_network = MockNetworkTrait::new(); mock_network @@ -72,10 +67,9 @@ mod tests { }); let app_arc = Arc::new(Mutex::new(App::test_default())); - let result = - LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network) - .handle() - .await; + let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network) + .handle() + .await; assert_ok!(&result); } diff --git a/src/network/lidarr_network/lidarr_network_tests.rs b/src/network/lidarr_network/lidarr_network_tests.rs index ee96299..3a2dee4 100644 --- a/src/network/lidarr_network/lidarr_network_tests.rs +++ b/src/network/lidarr_network/lidarr_network_tests.rs @@ -25,6 +25,18 @@ mod tests { assert_str_eq!(event.resource(), "/artist"); } + #[rstest] + fn test_resource_tag( + #[values( + LidarrEvent::AddTag(String::new()), + LidarrEvent::DeleteTag(0), + LidarrEvent::GetTags + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/tag"); + } + #[rstest] fn test_resource_config( #[values(LidarrEvent::GetHostConfig, LidarrEvent::GetSecurityConfig)] event: LidarrEvent, @@ -208,6 +220,21 @@ mod tests { ); } + #[tokio::test] + async fn test_handle_delete_lidarr_tag_event() { + let (mock, app, _server) = MockServarrApi::delete() + .path("/1") + .build_for(LidarrEvent::DeleteTag(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network.handle_lidarr_event(LidarrEvent::DeleteTag(1)).await; + + mock.assert_async().await; + assert!(result.is_ok()); + } + #[tokio::test] async fn test_extract_and_add_lidarr_tag_ids_vec() { let app_arc = Arc::new(Mutex::new(App::test_default())); diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs index 068a090..0ad2a62 100644 --- a/src/network/lidarr_network/mod.rs +++ b/src/network/lidarr_network/mod.rs @@ -25,6 +25,7 @@ pub mod lidarr_network_test_utils; pub enum LidarrEvent { AddTag(String), DeleteArtist(DeleteArtistParams), + DeleteTag(i64), EditArtist(EditArtistParams), GetArtistDetails(i64), GetDiskSpace, @@ -45,7 +46,7 @@ pub enum LidarrEvent { impl NetworkResource for LidarrEvent { fn resource(&self) -> &'static str { match &self { - LidarrEvent::AddTag(_) | LidarrEvent::GetTags => "/tag", + LidarrEvent::AddTag(_) | LidarrEvent::DeleteTag(_) | LidarrEvent::GetTags => "/tag", LidarrEvent::DeleteArtist(_) | LidarrEvent::EditArtist(_) | LidarrEvent::GetArtistDetails(_) @@ -80,6 +81,10 @@ impl Network<'_, '_> { LidarrEvent::DeleteArtist(params) => { self.delete_artist(params).await.map(LidarrSerdeable::from) } + LidarrEvent::DeleteTag(tag_id) => self + .delete_lidarr_tag(tag_id) + .await + .map(LidarrSerdeable::from), LidarrEvent::GetArtistDetails(artist_id) => self .get_artist_details(artist_id) .await @@ -213,6 +218,25 @@ impl Network<'_, '_> { .await } + async fn delete_lidarr_tag(&mut self, id: i64) -> Result<()> { + info!("Deleting Lidarr tag with ID: {id}"); + let event = LidarrEvent::DeleteTag(id); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{id}")), + None, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + pub(in crate::network::lidarr_network) async fn extract_and_add_lidarr_tag_ids_vec( &mut self, edit_tags: &str,