feat(readarr): fetch Readarr updates and expose them on the CLI
Adds ReadarrEvent::GetUpdates backed by get_readarr_updates, rendering the update history into the Readarr updates view, and surfaces it as 'readarr list updates'. Covers the case where an update record omits the changes key, which the live instance returns for its most recent release.
This commit is contained in:
6 files changed
+225
-3
No files matched your search
@@ -19,6 +19,8 @@ mod list_command_handler_tests;
|
||||
pub enum ReadarrListCommand {
|
||||
#[command(about = "List disk space details for all provisioned root folders in Readarr")]
|
||||
DiskSpace,
|
||||
#[command(about = "List all Readarr updates")]
|
||||
Updates,
|
||||
}
|
||||
|
||||
impl From<ReadarrListCommand> for Command {
|
||||
@@ -55,6 +57,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrListCommand> for ReadarrListComman
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Updates => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetUpdates.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
|
||||
@@ -23,7 +23,9 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn test_list_commands_have_no_arg_requirements(#[values("disk-space")] subcommand: &str) {
|
||||
fn test_list_commands_have_no_arg_requirements(
|
||||
#[values("disk-space", "updates")] subcommand: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", subcommand]);
|
||||
|
||||
assert_ok!(&result);
|
||||
@@ -69,5 +71,28 @@ mod tests {
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_updates_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetUpdates.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_updates_command = ReadarrListCommand::Updates;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_updates_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ pub enum ReadarrEvent {
|
||||
GetHostConfig,
|
||||
GetSecurityConfig,
|
||||
GetStatus,
|
||||
GetUpdates,
|
||||
HealthCheck,
|
||||
}
|
||||
|
||||
@@ -27,6 +28,7 @@ impl NetworkResource for ReadarrEvent {
|
||||
ReadarrEvent::GetDiskSpace => "/diskspace",
|
||||
ReadarrEvent::HealthCheck => "/health",
|
||||
ReadarrEvent::GetStatus => "/system/status",
|
||||
ReadarrEvent::GetUpdates => "/update",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +58,7 @@ impl Network<'_, '_> {
|
||||
.await
|
||||
.map(ReadarrSerdeable::from),
|
||||
ReadarrEvent::GetStatus => self.get_readarr_status().await.map(ReadarrSerdeable::from),
|
||||
ReadarrEvent::GetUpdates => self.get_readarr_updates().await.map(ReadarrSerdeable::from),
|
||||
ReadarrEvent::HealthCheck => self
|
||||
.get_readarr_healthcheck()
|
||||
.await
|
||||
|
||||
@@ -11,6 +11,7 @@ mod tests {
|
||||
#[case(ReadarrEvent::GetDiskSpace, "/diskspace")]
|
||||
#[case(ReadarrEvent::HealthCheck, "/health")]
|
||||
#[case(ReadarrEvent::GetStatus, "/system/status")]
|
||||
#[case(ReadarrEvent::GetUpdates, "/update")]
|
||||
fn test_resource(#[case] event: ReadarrEvent, #[case] expected_uri: &str) {
|
||||
assert_str_eq!(event.resource(), expected_uri);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::models::servarr_models::{DiskSpace, HostConfig, SecurityConfig, SystemStatus};
|
||||
use crate::models::ScrollableText;
|
||||
use crate::models::servarr_models::{DiskSpace, HostConfig, SecurityConfig, SystemStatus, Update};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use indoc::formatdoc;
|
||||
use log::info;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -73,4 +75,84 @@ impl Network<'_, '_> {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::readarr_network) async fn get_readarr_updates(
|
||||
&mut self,
|
||||
) -> Result<Vec<Update>> {
|
||||
info!("Fetching Readarr updates");
|
||||
let event = ReadarrEvent::GetUpdates;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Update>>(request_props, |updates_vec, mut app| {
|
||||
let latest_installed = if updates_vec
|
||||
.iter()
|
||||
.any(|update| update.latest && update.installed_on.is_some())
|
||||
{
|
||||
"already".to_owned()
|
||||
} else {
|
||||
"not".to_owned()
|
||||
};
|
||||
let updates = updates_vec
|
||||
.into_iter()
|
||||
.map(|update| {
|
||||
let install_status = if update.installed_on.is_some() {
|
||||
if update.installed {
|
||||
" (Currently Installed)".to_owned()
|
||||
} else {
|
||||
" (Previously Installed)".to_owned()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let vec_to_bullet_points = |vec: Vec<String>| {
|
||||
vec
|
||||
.iter()
|
||||
.map(|change| format!(" * {change}"))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
};
|
||||
|
||||
let mut update_info = formatdoc!(
|
||||
"{} - {}{install_status}
|
||||
{}",
|
||||
update.version,
|
||||
update.release_date,
|
||||
"-".repeat(200)
|
||||
);
|
||||
|
||||
if let Some(new_changes) = update.changes.new {
|
||||
let changes = vec_to_bullet_points(new_changes);
|
||||
update_info = formatdoc!(
|
||||
"{update_info}
|
||||
New:
|
||||
{changes}"
|
||||
)
|
||||
}
|
||||
|
||||
if let Some(fixes) = update.changes.fixed {
|
||||
let fixes = vec_to_bullet_points(fixes);
|
||||
update_info = formatdoc!(
|
||||
"{update_info}
|
||||
Fixed:
|
||||
{fixes}"
|
||||
);
|
||||
}
|
||||
|
||||
update_info
|
||||
})
|
||||
.reduce(|version_1, version_2| format!("{version_1}\n\n\n{version_2}"))
|
||||
.unwrap();
|
||||
|
||||
app.data.readarr_data.updates = ScrollableText::with_string(formatdoc!(
|
||||
"The latest version of Readarr is {latest_installed} installed
|
||||
|
||||
{updates}"
|
||||
));
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,12 @@ mod tests {
|
||||
use crate::models::readarr_models::ReadarrSerdeable;
|
||||
use crate::models::servarr_models::{
|
||||
AuthenticationMethod, AuthenticationRequired, CertificateValidation, DiskSpace, HostConfig,
|
||||
SecurityConfig, SystemStatus,
|
||||
SecurityConfig, SystemStatus, Update,
|
||||
};
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use chrono::DateTime;
|
||||
use indoc::formatdoc;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -249,4 +250,105 @@ mod tests {
|
||||
assert_err!(result);
|
||||
assert_is_empty!(app.lock().await.data.readarr_data.version);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_readarr_updates_event() {
|
||||
let updates_json = json!([{
|
||||
"version": "4.3.2.1",
|
||||
"releaseDate": "2023-04-15T02:02:53Z",
|
||||
"installed": true,
|
||||
"installedOn": "2023-04-15T02:02:53Z",
|
||||
"latest": true,
|
||||
"changes": { "new": ["Cool new thing"], "fixed": ["Some bugs killed"] },
|
||||
},
|
||||
{
|
||||
"version": "3.2.1.0",
|
||||
"releaseDate": "2023-04-15T02:02:53Z",
|
||||
"installed": false,
|
||||
"installedOn": "2023-04-15T02:02:53Z",
|
||||
"latest": false,
|
||||
"changes": { "new": ["Cool new thing (old)", "Other cool new thing (old)"] },
|
||||
},
|
||||
{
|
||||
"version": "2.1.0",
|
||||
"releaseDate": "2023-04-15T02:02:53Z",
|
||||
"installed": false,
|
||||
"latest": false,
|
||||
"changes": { "fixed": ["Killed bug 1", "Fixed bug 2"] },
|
||||
},
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"releaseDate": "2023-04-15T02:02:53Z",
|
||||
"installed": false,
|
||||
"latest": false,
|
||||
}]);
|
||||
let response: Vec<Update> = serde_json::from_value(updates_json.clone()).unwrap();
|
||||
let line_break = "-".repeat(200);
|
||||
let expected_text = formatdoc!(
|
||||
"The latest version of Readarr is already installed
|
||||
|
||||
4.3.2.1 - 2023-04-15 02:02:53 UTC (Currently Installed)
|
||||
{line_break}
|
||||
New:
|
||||
* Cool new thing
|
||||
Fixed:
|
||||
* Some bugs killed
|
||||
|
||||
|
||||
3.2.1.0 - 2023-04-15 02:02:53 UTC (Previously Installed)
|
||||
{line_break}
|
||||
New:
|
||||
* Cool new thing (old)
|
||||
* Other cool new thing (old)
|
||||
|
||||
|
||||
2.1.0 - 2023-04-15 02:02:53 UTC
|
||||
{line_break}
|
||||
Fixed:
|
||||
* Killed bug 1
|
||||
* Fixed bug 2
|
||||
|
||||
|
||||
1.0.0 - 2023-04-15 02:02:53 UTC
|
||||
{line_break}"
|
||||
);
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(updates_json)
|
||||
.build_for(ReadarrEvent::GetUpdates)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(3);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network.handle_readarr_event(ReadarrEvent::GetUpdates).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let ReadarrSerdeable::Updates(updates) = result.unwrap() else {
|
||||
panic!("Expected Updates")
|
||||
};
|
||||
|
||||
assert_eq!(updates, response);
|
||||
|
||||
assert_str_eq!(
|
||||
app.lock().await.data.readarr_data.updates.get_text(),
|
||||
expected_text
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_readarr_updates_event_failure() {
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(json!({}))
|
||||
.status(500)
|
||||
.build_for(ReadarrEvent::GetUpdates)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(3);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network.handle_readarr_event(ReadarrEvent::GetUpdates).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
assert_err!(result);
|
||||
assert_is_empty!(app.lock().await.data.readarr_data.updates.get_text());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user