diff --git a/src/cli/readarr/get_command_handler.rs b/src/cli/readarr/get_command_handler.rs new file mode 100644 index 00000000..4362793e --- /dev/null +++ b/src/cli/readarr/get_command_handler.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::Subcommand; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + network::{NetworkTrait, readarr_network::ReadarrEvent}, +}; + +use super::ReadarrCommand; + +#[cfg(test)] +#[path = "get_command_handler_tests.rs"] +mod get_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum ReadarrGetCommand { + #[command(about = "Get the system status")] + SystemStatus, +} + +impl From for Command { + fn from(value: ReadarrGetCommand) -> Self { + Command::Readarr(ReadarrCommand::Get(value)) + } +} + +pub(super) struct ReadarrGetCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: ReadarrGetCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrGetCommand> for ReadarrGetCommandHandler<'a, 'b> { + fn with( + _app: &'a Arc>>, + command: ReadarrGetCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + ReadarrGetCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + ReadarrGetCommand::SystemStatus => { + let resp = self + .network + .handle_network_event(ReadarrEvent::GetStatus.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/readarr/get_command_handler_tests.rs b/src/cli/readarr/get_command_handler_tests.rs new file mode 100644 index 00000000..822123bb --- /dev/null +++ b/src/cli/readarr/get_command_handler_tests.rs @@ -0,0 +1,72 @@ +#[cfg(test)] +mod tests { + use crate::Cli; + use crate::cli::{ + Command, + readarr::{ReadarrCommand, get_command_handler::ReadarrGetCommand}, + }; + use clap::CommandFactory; + use pretty_assertions::assert_eq; + + #[test] + fn test_readarr_get_command_from() { + let command = ReadarrGetCommand::SystemStatus; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Readarr(ReadarrCommand::Get(command))); + } + + mod cli { + use super::*; + use rstest::rstest; + + #[rstest] + fn test_get_commands_have_no_arg_requirements(#[values("system-status")] subcommand: &str) { + let result = Cli::command().try_get_matches_from(["managarr", "readarr", "get", subcommand]); + + assert_ok!(&result); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::{ + app::App, + cli::{ + CliCommandHandler, + readarr::get_command_handler::{ReadarrGetCommand, ReadarrGetCommandHandler}, + }, + models::{Serdeable, readarr_models::ReadarrSerdeable}, + network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent}, + }; + + #[tokio::test] + async fn test_handle_get_system_status_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(ReadarrEvent::GetStatus.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Readarr(ReadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let get_system_status_command = ReadarrGetCommand::SystemStatus; + + let result = + ReadarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + } +} diff --git a/src/cli/readarr/mod.rs b/src/cli/readarr/mod.rs index 7797af94..1bd27875 100644 --- a/src/cli/readarr/mod.rs +++ b/src/cli/readarr/mod.rs @@ -2,17 +2,26 @@ use std::sync::Arc; use anyhow::Result; use clap::Subcommand; +use get_command_handler::{ReadarrGetCommand, ReadarrGetCommandHandler}; use tokio::sync::Mutex; use super::{CliCommandHandler, Command}; use crate::{app::App, network::NetworkTrait}; +mod get_command_handler; + #[cfg(test)] #[path = "readarr_command_tests.rs"] mod readarr_command_tests; #[derive(Debug, Clone, PartialEq, Eq, Subcommand)] -pub enum ReadarrCommand {} +pub enum ReadarrCommand { + #[command( + subcommand, + about = "Commands to fetch details of the resources in your Readarr instance" + )] + Get(ReadarrGetCommand), +} impl From for Command { fn from(readarr_command: ReadarrCommand) -> Command { @@ -40,6 +49,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrCommand> for ReadarrCliHandler<'a, } async fn handle(self) -> Result { - match self.command {} + let result = match self.command { + ReadarrCommand::Get(get_command) => { + ReadarrGetCommandHandler::with(self.app, get_command, self.network) + .handle() + .await? + } + }; + + Ok(result) } } diff --git a/src/network/readarr_network/mod.rs b/src/network/readarr_network/mod.rs index b47a0032..7be4c816 100644 --- a/src/network/readarr_network/mod.rs +++ b/src/network/readarr_network/mod.rs @@ -5,12 +5,15 @@ use super::{NetworkEvent, NetworkResource}; use crate::models::readarr_models::ReadarrSerdeable; use crate::network::{Network, RequestMethod}; +mod system; + #[cfg(test)] #[path = "readarr_network_tests.rs"] mod readarr_network_tests; #[derive(Debug, Eq, PartialEq, Clone)] pub enum ReadarrEvent { + GetStatus, HealthCheck, } @@ -18,6 +21,7 @@ impl NetworkResource for ReadarrEvent { fn resource(&self) -> &'static str { match &self { ReadarrEvent::HealthCheck => "/health", + ReadarrEvent::GetStatus => "/system/status", } } } @@ -34,6 +38,7 @@ impl Network<'_, '_> { readarr_event: ReadarrEvent, ) -> Result { match readarr_event { + ReadarrEvent::GetStatus => self.get_readarr_status().await.map(ReadarrSerdeable::from), ReadarrEvent::HealthCheck => self .get_readarr_healthcheck() .await diff --git a/src/network/readarr_network/readarr_network_tests.rs b/src/network/readarr_network/readarr_network_tests.rs index 699ebd89..f20aa015 100644 --- a/src/network/readarr_network/readarr_network_tests.rs +++ b/src/network/readarr_network/readarr_network_tests.rs @@ -7,6 +7,7 @@ mod tests { #[rstest] #[case(ReadarrEvent::HealthCheck, "/health")] + #[case(ReadarrEvent::GetStatus, "/system/status")] fn test_resource(#[case] event: ReadarrEvent, #[case] expected_uri: &str) { assert_str_eq!(event.resource(), expected_uri); } diff --git a/src/network/readarr_network/system/mod.rs b/src/network/readarr_network/system/mod.rs new file mode 100644 index 00000000..1ac5a65b --- /dev/null +++ b/src/network/readarr_network/system/mod.rs @@ -0,0 +1,29 @@ +use crate::models::servarr_models::SystemStatus; +use crate::network::readarr_network::ReadarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::info; + +#[cfg(test)] +#[path = "readarr_system_network_tests.rs"] +mod readarr_system_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::readarr_network) async fn get_readarr_status( + &mut self, + ) -> Result { + info!("Fetching Readarr system status"); + let event = ReadarrEvent::GetStatus; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), SystemStatus>(request_props, |system_status, mut app| { + app.data.readarr_data.version = system_status.version; + app.data.readarr_data.start_time = system_status.start_time; + }) + .await + } +} diff --git a/src/network/readarr_network/system/readarr_system_network_tests.rs b/src/network/readarr_network/system/readarr_system_network_tests.rs new file mode 100644 index 00000000..dc9de9c6 --- /dev/null +++ b/src/network/readarr_network/system/readarr_system_network_tests.rs @@ -0,0 +1,59 @@ +#[cfg(test)] +mod tests { + use crate::models::readarr_models::ReadarrSerdeable; + use crate::models::servarr_models::SystemStatus; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use crate::network::readarr_network::ReadarrEvent; + use chrono::DateTime; + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::json; + + #[tokio::test] + async fn test_handle_get_readarr_status_event() { + let (mock, app, _server) = MockServarrApi::get() + .returns(json!({ + "version": "0.4.20.129", + "startTime": "2026-08-17T03:55:02Z" + })) + .build_for(ReadarrEvent::GetStatus) + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + let date_time = DateTime::from(DateTime::parse_from_rfc3339("2026-08-17T03:55:02Z").unwrap()); + + let result = network.handle_readarr_event(ReadarrEvent::GetStatus).await; + + mock.assert_async().await; + + let ReadarrSerdeable::SystemStatus(status) = result.unwrap() else { + panic!("Expected SystemStatus") + }; + + assert_eq!( + status, + SystemStatus { + version: "0.4.20.129".to_owned(), + start_time: date_time + } + ); + assert_str_eq!(app.lock().await.data.readarr_data.version, "0.4.20.129"); + assert_eq!(app.lock().await.data.readarr_data.start_time, date_time); + } + + #[tokio::test] + async fn test_handle_get_readarr_status_event_failure() { + let (mock, app, _server) = MockServarrApi::get() + .returns(json!({})) + .status(500) + .build_for(ReadarrEvent::GetStatus) + .await; + app.lock().await.server_tabs.set_index(3); + let mut network = test_network(&app); + + let result = network.handle_readarr_event(ReadarrEvent::GetStatus).await; + + mock.assert_async().await; + assert_err!(result); + assert_is_empty!(app.lock().await.data.readarr_data.version); + } +}