Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6071f73b5
|
||
|
|
11524bb232
|
||
|
|
72ace0f2d5 | ||
|
|
9375e04ebf | ||
|
|
7a79e1c05e
|
||
|
|
06e504732d |
No files matched your search
@@ -88,3 +88,30 @@ jobs:
|
||||
|
||||
- name: cargo +1.95.0 check
|
||||
run: cargo check
|
||||
|
||||
dockerfile-msrv:
|
||||
# The Dockerfile's pinned Rust image is only built at release time, so a
|
||||
# rust-version bump that misses it fails silently until the next release.
|
||||
# Catch the drift on every push instead.
|
||||
name: dockerfile / msrv
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Check Dockerfile Rust image against Cargo.toml rust-version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
msrv=$(sed -nE 's/^rust-version = "([0-9.]+)"$/\1/p' Cargo.toml)
|
||||
image_tag=$(sed -nE 's/^FROM rust:([0-9.]+) AS builder$/\1/p' Dockerfile)
|
||||
: "${msrv:?failed to extract rust-version from Cargo.toml}"
|
||||
: "${image_tag:?failed to extract the rust image tag from the Dockerfile}"
|
||||
# The rust image tag pins major.minor, so compare on major.minor only
|
||||
msrv_mm=$(echo "$msrv" | cut -d. -f1,2)
|
||||
image_mm=$(echo "$image_tag" | cut -d. -f1,2)
|
||||
if [ "$(printf '%s\n' "$msrv_mm" "$image_mm" | sort -V | head -n1)" != "$msrv_mm" ]; then
|
||||
echo "Dockerfile builds with rust:$image_tag, but Cargo.toml declares rust-version = \"$msrv\"."
|
||||
echo "Cargo refuses to build under an older toolchain, so the release Docker publish step will fail."
|
||||
echo "Update the FROM rust:* line in the Dockerfile to at least $msrv_mm."
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: Dockerfile rust:$image_tag satisfies rust-version $msrv"
|
||||
+141
@@ -5,6 +5,147 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v0.8.0 (2026-09-30)
|
||||
|
||||
### Feat
|
||||
|
||||
- show sizes under a gigabyte in megabytes
|
||||
- **lidarr**: highlight tracks whose album is downloading or awaiting import
|
||||
- **readarr**: show the author overview in the add author prompt
|
||||
- **radarr**: render the movie overview popup at the large size
|
||||
- **radarr**: make the collection movie overview popup scrollable
|
||||
- **sonarr**: add a scrollable overview popup to series details
|
||||
- **lidarr**: add a scrollable overview popup to artist details
|
||||
- **readarr**: add a scrollable overview popup to author details
|
||||
- **readarr**: add the indexers UI
|
||||
- **readarr**: add the indexers handlers
|
||||
- **readarr**: add the system UI
|
||||
- **readarr**: add the system handlers
|
||||
- **readarr**: add the root folders UI
|
||||
- **readarr**: add the add root folder handler
|
||||
- **readarr**: add the root folders handler
|
||||
- **readarr**: add the history UI
|
||||
- **readarr**: add the downloads UI
|
||||
- **readarr**: add the blocklist UI
|
||||
- **readarr**: add the history handler
|
||||
- **readarr**: add the downloads handler
|
||||
- **readarr**: add the blocklist handler
|
||||
- **readarr**: add the delete book UI
|
||||
- **readarr**: add the delete author UI
|
||||
- **readarr**: add the edit author UI
|
||||
- **readarr**: add the add author UI
|
||||
- **readarr**: add the delete book handler
|
||||
- **readarr**: add the delete author handler
|
||||
- **readarr**: add the edit author handler
|
||||
- **readarr**: add the add author handler
|
||||
- **readarr**: render the edition details popup
|
||||
- **readarr**: render the book details popup
|
||||
- **readarr**: reach the book file info tab from the book details tabs
|
||||
- **readarr**: scroll and dismiss the edition details popup
|
||||
- **readarr**: handle the book details tabs and open the edition details popup
|
||||
- **readarr**: render the author details view
|
||||
- **readarr**: handle key events for the author details view
|
||||
- **readarr**: handle key events for the authors library table
|
||||
- **readarr**: fetch data on tick for the active block
|
||||
- **readarr**: edit the shared indexer settings and expose the command on the CLI
|
||||
- **readarr**: fetch the shared indexer settings and expose them on the CLI
|
||||
- **readarr**: test all indexers and expose the command on the CLI
|
||||
- **readarr**: test an indexer and expose the command on the CLI
|
||||
- **readarr**: delete an indexer and expose the command on the CLI
|
||||
- **readarr**: edit an indexer and expose the command on the CLI
|
||||
- **readarr**: fetch all indexers and expose them on the CLI
|
||||
- **readarr**: clear the blocklist and expose the command on the CLI
|
||||
- **readarr**: delete an item from the blocklist and expose the command on the CLI
|
||||
- **readarr**: fetch the blocklist and expose it on the CLI
|
||||
- **readarr**: manually download a release and expose the command on the CLI
|
||||
- **readarr**: fetch releases for an author and expose them on the CLI
|
||||
- **readarr**: fetch releases for a book and expose them on the CLI
|
||||
- **readarr**: mark a history item as failed and expose the command on the CLI
|
||||
- **readarr**: fetch Readarr history and expose it on the CLI
|
||||
- **readarr**: refresh Readarr downloads and expose the command on the CLI
|
||||
- **readarr**: delete a download and expose the command on the CLI
|
||||
- **readarr**: fetch Readarr downloads and expose them on the CLI
|
||||
- **readarr**: delete a book file and expose the command on the CLI
|
||||
- **readarr**: delete a book and expose the command on the CLI
|
||||
- **readarr**: toggle book monitoring and expose the command on the CLI
|
||||
- **readarr**: trigger an automatic book search and expose the command on the CLI
|
||||
- **readarr**: fetch Readarr book history and expose it on the CLI
|
||||
- **readarr**: fetch Readarr book files and expose them on the CLI
|
||||
- **readarr**: fetch Readarr book editions and expose them on the CLI
|
||||
- **readarr**: fetch Readarr book details and expose them on the CLI
|
||||
- **readarr**: fetch Readarr books and expose them on the CLI
|
||||
- **readarr**: fetch Readarr author history and expose it on the CLI
|
||||
- **readarr**: refresh all Readarr authors and expose the command on the CLI
|
||||
- **readarr**: refresh and rescan a Readarr author and expose the command on the CLI
|
||||
- **readarr**: trigger automatic author searches and expose the command on the CLI
|
||||
- **readarr**: toggle Readarr author monitoring and expose the command on the CLI
|
||||
- **readarr**: delete Readarr authors and expose the command on the CLI
|
||||
- **readarr**: edit Readarr authors and expose the command on the CLI
|
||||
- **readarr**: add authors to Readarr and expose the command on the CLI
|
||||
- **readarr**: search for new Readarr authors and expose the command on the CLI
|
||||
- **readarr**: fetch Readarr author details and expose them on the CLI
|
||||
- **readarr**: fetch Readarr authors and expose them on the CLI
|
||||
- **readarr**: delete Readarr root folders and expose the command on the CLI
|
||||
- **readarr**: add Readarr root folders and expose the command on the CLI
|
||||
- **readarr**: delete Readarr tags and expose the command on the CLI
|
||||
- **readarr**: add Readarr tags and expose the command on the CLI
|
||||
- **readarr**: fetch Readarr root folders and expose them on the CLI
|
||||
- **readarr**: fetch Readarr tags and expose them on the CLI
|
||||
- **readarr**: fetch Readarr metadata profiles and expose them on the CLI
|
||||
- **readarr**: fetch Readarr quality profiles and expose them on the CLI
|
||||
- **readarr**: fetch Readarr queued events and expose them on the CLI
|
||||
- **readarr**: trigger Readarr tasks and expose the command on the CLI
|
||||
- **readarr**: fetch Readarr tasks and expose them on the CLI
|
||||
- **readarr**: fetch Readarr logs and expose them on the CLI
|
||||
- **readarr**: fetch Readarr updates and expose them on the CLI
|
||||
- **readarr**: fetch the Readarr security config and expose it on the CLI
|
||||
- **readarr**: fetch the Readarr host config and expose it on the CLI
|
||||
- **readarr**: fetch Readarr disk space and expose it on the CLI
|
||||
- **readarr**: fetch the Readarr system status and expose it on the CLI
|
||||
- **readarr**: add the Readarr health check network call
|
||||
- scaffold Readarr support across the state, app, handler, UI, and CLI layers
|
||||
- updated gitignore to not include coyote memory or workspace config
|
||||
|
||||
### Fix
|
||||
|
||||
- **readarr**: send an empty images list when adding an author
|
||||
- **readarr**: accept release searches wrapped in a filter summary envelope
|
||||
- **readarr**: tolerate book payloads that omit the grabbed flag
|
||||
- **radarr**: render the movie overview one size down so the collection details stay visible behind it
|
||||
- **app/mod**: use correct api for tab when starting on non default tab
|
||||
- passes servarr-name global switch to both the tui and cli
|
||||
- **radarr**: model the movie status as an enum
|
||||
- page up and down stop at the table edges instead of wrapping
|
||||
- **radarr**: serialize the credit type under the same key it is read from
|
||||
- **radarr**: model the download status as a DownloadStatus enum
|
||||
- fall back to the default when a servarr returns an unknown enum value
|
||||
- show N/A instead of zeros when a season, album or book has no statistics
|
||||
- stop paging up or down on a single row table from crashing
|
||||
- keep history readable when an event type is unrecognised
|
||||
- **readarr**: attach editions when toggling book monitoring
|
||||
- **sonarr**: stop marking seasons fully downloaded when no episodes are known
|
||||
- keep a space between overview words that span a line break
|
||||
- parse the log level from the unscrolled log text
|
||||
- **radarr**: show a loading block while the updates popup refreshes
|
||||
- show confirmation prompt clues on delete prompts
|
||||
- drop redundant references in format arguments
|
||||
- **readarr**: stop a long author overview hiding the other fields
|
||||
- **ui**: render detail popup labels in bold
|
||||
- **readarr**: stop appending stray colons to the edition overview
|
||||
- **readarr**: move the book file delete to the File tab
|
||||
- **readarr**: render the update all authors confirmation prompt
|
||||
- **readarr**: drop the always-empty author type column from the authors table
|
||||
- tolerate update records that omit the changes list
|
||||
- address reviewer feedback — rename scroll field to bool, relocate static, drop env var
|
||||
- revert draw gating; replace frame-count scroll with wall-clock timer
|
||||
- address reviewer feedback — regex OnceLock and restore tick rate
|
||||
- reduce TUI idle CPU and improve server error banners
|
||||
|
||||
### Refactor
|
||||
|
||||
- share one DownloadStatus enum across the servarr models
|
||||
- move shared servarr types into servarr_models
|
||||
|
||||
## v0.7.3 (2026-06-25)
|
||||
|
||||
### Feat
|
||||
|
||||
Generated
+7
-7
@@ -1879,7 +1879,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "managarr"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
@@ -2568,9 +2568,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.18"
|
||||
version = "0.11.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
|
||||
checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"bytes",
|
||||
@@ -2591,9 +2591,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.15"
|
||||
version = "0.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
||||
checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -4538,9 +4538,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
|
||||
checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "managarr"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||
description = "A TUI and CLI to manage your Servarrs"
|
||||
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
|
||||
|
||||
+33
-3
@@ -1,4 +1,5 @@
|
||||
use std::fmt::Debug;
|
||||
use std::error::Error;
|
||||
use std::fmt::{self, Debug};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -156,8 +157,22 @@ impl<'a, 'b> Network<'a, 'b> {
|
||||
let error_body = format_error_body(&response_body);
|
||||
|
||||
error!("Request failed. Received {status} response code with body: {response_body}");
|
||||
self.app.lock().await.handle_error(anyhow!("Request failed. Received {status} response code with body: {error_body}"));
|
||||
Err(anyhow!("Request failed. Received {status} response code with body: {error_body}"))
|
||||
let message = format!("Request failed. Received {status} response code with body: {error_body}");
|
||||
let body = if response_body.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
serde_json::from_str::<Value>(&response_body)
|
||||
.unwrap_or(Value::String(response_body)),
|
||||
)
|
||||
};
|
||||
let network_error = NetworkError {
|
||||
message,
|
||||
status: status.as_u16(),
|
||||
body,
|
||||
};
|
||||
self.app.lock().await.handle_error(network_error.clone().into());
|
||||
Err(network_error.into())
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -304,6 +319,21 @@ fn format_error_body(response_body: &str) -> String {
|
||||
re.replace_all(&body, " ").to_string()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NetworkError {
|
||||
pub message: String,
|
||||
pub status: u16,
|
||||
pub body: Option<Value>,
|
||||
}
|
||||
|
||||
impl fmt::Display for NetworkError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for NetworkError {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq)]
|
||||
pub enum RequestMethod {
|
||||
Get,
|
||||
|
||||
@@ -10,7 +10,7 @@ mod tests {
|
||||
use reqwest::header::HeaderMap;
|
||||
use rstest::rstest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serde_json::{Value, json};
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
@@ -22,7 +22,9 @@ mod tests {
|
||||
use crate::network::network_tests::test_utils::test_network;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, NetworkEvent, NetworkTrait, RequestMethod, RequestProps};
|
||||
use crate::network::{
|
||||
Network, NetworkError, NetworkEvent, NetworkTrait, RequestMethod, RequestProps,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_network_event_radarr_event() {
|
||||
@@ -398,6 +400,104 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_request_non_success_code_downcasts_to_network_error_with_json_body() {
|
||||
let (async_server, app_arc, server) = mock_api(RequestMethod::Get, 404, true).await;
|
||||
let mut network = test_network(&app_arc);
|
||||
|
||||
let resp = network
|
||||
.handle_request::<(), Test>(
|
||||
RequestProps {
|
||||
uri: format!("{}/test", server.url()),
|
||||
method: RequestMethod::Get,
|
||||
body: None,
|
||||
api_token: "test1234".to_owned(),
|
||||
ignore_status_code: false,
|
||||
custom_headers: HeaderMap::new(),
|
||||
},
|
||||
|_, _| (),
|
||||
)
|
||||
.await;
|
||||
|
||||
async_server.assert_async().await;
|
||||
let network_error = resp.unwrap_err().downcast::<NetworkError>().unwrap();
|
||||
assert_str_eq!(
|
||||
network_error.message,
|
||||
r#"Request failed. Received 404 Not Found response code with body: { "value": "Test" }"#
|
||||
);
|
||||
assert_eq!(network_error.status, 404);
|
||||
assert_some_eq_x!(network_error.body.as_ref(), &json!({ "value": "Test" }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_request_non_success_code_downcasts_to_network_error_with_string_body() {
|
||||
let mut server = Server::new_async().await;
|
||||
let async_server = server
|
||||
.mock("GET", "/test")
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.with_status(503)
|
||||
.with_body("Service Unavailable")
|
||||
.create_async()
|
||||
.await;
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let mut network = test_network(&app_arc);
|
||||
|
||||
let resp = network
|
||||
.handle_request::<(), Test>(
|
||||
RequestProps {
|
||||
uri: format!("{}/test", server.url()),
|
||||
method: RequestMethod::Get,
|
||||
body: None,
|
||||
api_token: "test1234".to_owned(),
|
||||
ignore_status_code: false,
|
||||
custom_headers: HeaderMap::new(),
|
||||
},
|
||||
|_, _| (),
|
||||
)
|
||||
.await;
|
||||
|
||||
async_server.assert_async().await;
|
||||
let network_error = resp.unwrap_err().downcast::<NetworkError>().unwrap();
|
||||
assert_str_eq!(
|
||||
network_error.message,
|
||||
"Request failed. Received 503 Service Unavailable response code with body: Service Unavailable"
|
||||
);
|
||||
assert_eq!(network_error.status, 503);
|
||||
assert_some_eq_x!(
|
||||
network_error.body.as_ref(),
|
||||
&Value::String("Service Unavailable".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_request_non_success_code_downcasts_to_network_error_with_empty_body() {
|
||||
let (async_server, app_arc, server) = mock_api(RequestMethod::Post, 404, false).await;
|
||||
let mut network = test_network(&app_arc);
|
||||
|
||||
let resp = network
|
||||
.handle_request::<(), Test>(
|
||||
RequestProps {
|
||||
uri: format!("{}/test", server.url()),
|
||||
method: RequestMethod::Post,
|
||||
body: None,
|
||||
api_token: "test1234".to_owned(),
|
||||
ignore_status_code: false,
|
||||
custom_headers: HeaderMap::new(),
|
||||
},
|
||||
|_, _| (),
|
||||
)
|
||||
.await;
|
||||
|
||||
async_server.assert_async().await;
|
||||
let network_error = resp.unwrap_err().downcast::<NetworkError>().unwrap();
|
||||
assert_str_eq!(
|
||||
network_error.message,
|
||||
r#"Request failed. Received 404 Not Found response code with body: "#
|
||||
);
|
||||
assert_eq!(network_error.status, 404);
|
||||
assert_none!(network_error.body);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_call_api(
|
||||
|
||||
+41
-6
@@ -6,7 +6,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, anyhow};
|
||||
use anyhow::{Context, Error, anyhow};
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use log::{LevelFilter, error};
|
||||
@@ -18,13 +18,15 @@ use log4rs::config::{Appender, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use regex::Regex;
|
||||
use reqwest::{Certificate, Client};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::app::{App, AppConfig, log_and_print_error};
|
||||
use crate::cli::{self, Command};
|
||||
use crate::models::Route;
|
||||
use crate::network::Network;
|
||||
use crate::network::{Network, NetworkError};
|
||||
use crate::ui::theme::ThemeDefinitionsWrapper;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -282,6 +284,36 @@ pub(super) fn render_spinner() -> ProgressBar {
|
||||
pb
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CliError<'a> {
|
||||
error: CliErrorDetails<'a>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CliErrorDetails<'a> {
|
||||
message: String,
|
||||
status: Option<u16>,
|
||||
body: Option<&'a Value>,
|
||||
}
|
||||
|
||||
fn format_cli_error(error: &Error) -> String {
|
||||
let details = match error.downcast_ref::<NetworkError>() {
|
||||
Some(network_error) => CliErrorDetails {
|
||||
message: network_error.message.clone(),
|
||||
status: Some(network_error.status),
|
||||
body: network_error.body.as_ref(),
|
||||
},
|
||||
None => CliErrorDetails {
|
||||
message: error.to_string(),
|
||||
status: None,
|
||||
body: None,
|
||||
},
|
||||
};
|
||||
|
||||
serde_json::to_string_pretty(&CliError { error: details })
|
||||
.expect("CLI error details are infallibly serializable")
|
||||
}
|
||||
|
||||
pub(super) async fn start_cli_with_spinner(
|
||||
config: AppConfig,
|
||||
reqwest_client: Client,
|
||||
@@ -295,7 +327,8 @@ pub(super) async fn start_cli_with_spinner(
|
||||
let mut app = app.lock().await;
|
||||
app.cli_mode = true;
|
||||
select_cli_configuration(&mut app, &config, &command, servarr_name).unwrap_or_else(|error| {
|
||||
log_and_print_error(error.to_string());
|
||||
error!("{error}");
|
||||
eprintln!("{}", format_cli_error(&error));
|
||||
process::exit(1);
|
||||
});
|
||||
}
|
||||
@@ -309,7 +342,7 @@ pub(super) async fn start_cli_with_spinner(
|
||||
}
|
||||
Err(e) => {
|
||||
pb.finish();
|
||||
eprintln!("error: {}", e.to_string().red());
|
||||
eprintln!("{}", format_cli_error(&e));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -328,7 +361,8 @@ pub(super) async fn start_cli_no_spinner(
|
||||
let mut app = app.lock().await;
|
||||
app.cli_mode = true;
|
||||
select_cli_configuration(&mut app, &config, &command, servarr_name).unwrap_or_else(|error| {
|
||||
log_and_print_error(error.to_string());
|
||||
error!("{error}");
|
||||
eprintln!("{}", format_cli_error(&error));
|
||||
process::exit(1);
|
||||
});
|
||||
}
|
||||
@@ -339,7 +373,7 @@ pub(super) async fn start_cli_no_spinner(
|
||||
println!("{output}");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("error: {}", e.to_string().red());
|
||||
eprintln!("{}", format_cli_error(&e));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -360,6 +394,7 @@ pub fn select_cli_configuration(
|
||||
(Route::Radarr(..), Command::Radarr(_))
|
||||
| (Route::Sonarr(..), Command::Sonarr(_))
|
||||
| (Route::Lidarr(..), Command::Lidarr(_))
|
||||
| (Route::Readarr(..), Command::Readarr(_))
|
||||
)
|
||||
});
|
||||
app.server_tabs.index = selected_index.ok_or_else(|| {
|
||||
|
||||
+123
-1
@@ -1,6 +1,8 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::{Error, anyhow};
|
||||
use clap::Parser;
|
||||
use serde_json::json;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{BufRead, BufReader, Seek, SeekFrom, Write};
|
||||
|
||||
@@ -11,7 +13,10 @@ mod tests {
|
||||
use crate::{
|
||||
Cli,
|
||||
app::{App, AppConfig, ServarrConfig},
|
||||
utils::{convert_runtime, format_size, select_cli_configuration, was_log_rotated},
|
||||
network::NetworkError,
|
||||
utils::{
|
||||
convert_runtime, format_cli_error, format_size, select_cli_configuration, was_log_rotated,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -64,6 +69,123 @@ mod tests {
|
||||
assert_eq!(minutes, 34);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_cli_error_network_error_with_json_body() {
|
||||
let error = Error::new(NetworkError {
|
||||
message: "Request failed. Received 404 Not Found response code with body: Not Found"
|
||||
.to_owned(),
|
||||
status: 404,
|
||||
body: Some(json!({ "status": 404, "title": "Not Found" })),
|
||||
});
|
||||
|
||||
let output = format_cli_error(&error);
|
||||
|
||||
assert_str_eq!(
|
||||
output,
|
||||
r#"{
|
||||
"error": {
|
||||
"message": "Request failed. Received 404 Not Found response code with body: Not Found",
|
||||
"status": 404,
|
||||
"body": {
|
||||
"status": 404,
|
||||
"title": "Not Found"
|
||||
}
|
||||
}
|
||||
}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_cli_error_network_error_with_string_body() {
|
||||
let error = Error::new(NetworkError {
|
||||
message:
|
||||
"Request failed. Received 503 Service Unavailable response code with body: Service Unavailable"
|
||||
.to_owned(),
|
||||
status: 503,
|
||||
body: Some(json!("Service Unavailable")),
|
||||
});
|
||||
|
||||
let output = format_cli_error(&error);
|
||||
|
||||
assert_str_eq!(
|
||||
output,
|
||||
r#"{
|
||||
"error": {
|
||||
"message": "Request failed. Received 503 Service Unavailable response code with body: Service Unavailable",
|
||||
"status": 503,
|
||||
"body": "Service Unavailable"
|
||||
}
|
||||
}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_cli_error_network_error_with_null_body() {
|
||||
let error = Error::new(NetworkError {
|
||||
message: "Request failed. Received 404 Not Found response code with body: ".to_owned(),
|
||||
status: 404,
|
||||
body: None,
|
||||
});
|
||||
|
||||
let output = format_cli_error(&error);
|
||||
|
||||
assert_str_eq!(
|
||||
output,
|
||||
r#"{
|
||||
"error": {
|
||||
"message": "Request failed. Received 404 Not Found response code with body: ",
|
||||
"status": 404,
|
||||
"body": null
|
||||
}
|
||||
}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_cli_error_network_error_wrapped_in_context() {
|
||||
let error = Error::new(NetworkError {
|
||||
message: "Request failed. Received 404 Not Found response code with body: Not Found"
|
||||
.to_owned(),
|
||||
status: 404,
|
||||
body: Some(json!({ "status": 404, "title": "Not Found" })),
|
||||
})
|
||||
.context("failed to fetch system status");
|
||||
|
||||
let output = format_cli_error(&error);
|
||||
|
||||
assert_str_eq!(
|
||||
output,
|
||||
r#"{
|
||||
"error": {
|
||||
"message": "Request failed. Received 404 Not Found response code with body: Not Found",
|
||||
"status": 404,
|
||||
"body": {
|
||||
"status": 404,
|
||||
"title": "Not Found"
|
||||
}
|
||||
}
|
||||
}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_cli_error_plain_anyhow_error() {
|
||||
let error = anyhow!("Failed to send request. connection refused ");
|
||||
|
||||
let output = format_cli_error(&error);
|
||||
|
||||
assert_str_eq!(
|
||||
output,
|
||||
r#"{
|
||||
"error": {
|
||||
"message": "Failed to send request. connection refused ",
|
||||
"status": null,
|
||||
"body": null
|
||||
}
|
||||
}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_was_log_rotated_returns_false_when_file_has_not_rotated() {
|
||||
let path = std::env::temp_dir().join("managarr_test_no_rotation.log");
|
||||
|
||||
@@ -86,6 +86,57 @@ fn named_instance_of_different_servarr_type_rejects_command() {
|
||||
sonarr_mock.assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_readarr_instance_selected_for_readarr_command() {
|
||||
let mut radarr_server = Server::new();
|
||||
let radarr_mock = radarr_server
|
||||
.mock("GET", "/api/v3/system/status")
|
||||
.match_header("X-Api-Key", "test-token")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"{"version":"wrong-instance","startTime":"2024-01-01T00:00:00Z"}"#)
|
||||
.expect(0)
|
||||
.create();
|
||||
let mut readarr_server = Server::new();
|
||||
let readarr_mock = readarr_server
|
||||
.mock("GET", "/api/v1/system/status")
|
||||
.match_header("X-Api-Key", "test-token")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"{"version":"expected-instance","startTime":"2024-01-01T00:00:00Z"}"#)
|
||||
.expect(1)
|
||||
.create();
|
||||
let config = TemporaryConfig::new(&format!(
|
||||
"radarr:\n - name: Servarr\n uri: {}\n api_token: test-token\nreadarr:\n - name: Servarr\n uri: {}\n api_token: test-token\n",
|
||||
radarr_server.url(),
|
||||
readarr_server.url()
|
||||
));
|
||||
|
||||
let mut command = cargo_bin_cmd!("managarr");
|
||||
let assertion = command
|
||||
.arg("--config-file")
|
||||
.arg(config.path())
|
||||
.args([
|
||||
"--disable-spinner",
|
||||
"--servarr-name",
|
||||
"Servarr",
|
||||
"readarr",
|
||||
"get",
|
||||
"system-status",
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
let stdout = str::from_utf8(&assertion.get_output().stdout).unwrap();
|
||||
|
||||
assert!(
|
||||
stdout.contains("\"version\": \"expected-instance\""),
|
||||
"unexpected CLI output: {stdout}"
|
||||
);
|
||||
assert!(!stdout.contains("wrong-instance"));
|
||||
readarr_mock.assert();
|
||||
radarr_mock.assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_named_servarrs_select_command_matching_instance() {
|
||||
let mut radarr_server = Server::new();
|
||||
|
||||
Reference in new issue
Block a user