Compare commits
164
Commits
develop
...
bdeaf6ae35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdeaf6ae35
|
||
|
|
35ed3f359d
|
||
|
|
040935178a
|
||
|
|
4f78a5940f
|
||
|
|
9822597b56
|
||
|
|
c42a46c7ca
|
||
|
|
1d936c5cb5
|
||
|
|
7b6d595397
|
||
|
|
25da032f02
|
||
|
|
e9c323c4e8
|
||
|
|
289abbd8eb
|
||
|
|
a68de41025
|
||
|
|
9d0973352f
|
||
|
|
76c490f45b
|
||
|
|
9b4b3003b5
|
||
|
|
d678f5777a
|
||
|
|
87edf1fb9e
|
||
|
|
d3a959d2dd
|
||
|
|
1857d5ba15
|
||
|
|
86b91ce6dd
|
||
|
|
9dbd985506
|
||
|
|
fbf711f428
|
||
|
|
db8e97a700
|
||
|
|
c069ed4a9b
|
||
|
|
3227495947
|
||
|
|
243b22a426
|
||
|
|
caafe1602e
|
||
|
|
65ba37c09a
|
||
|
|
c525fb9a4e
|
||
|
|
e79cc7a12d
|
||
|
|
12dbbf0b1b
|
||
|
|
1a5e5c3145
|
||
|
|
5fa521ba7f
|
||
|
|
f7c67ad58b
|
||
|
|
7e8072133c
|
||
|
|
e622be3288
|
||
|
|
08018e4457
|
||
|
|
78f5045844
|
||
|
|
e51b9d72b1
|
||
|
|
b9d96ce235
|
||
|
|
3e414f9f0c
|
||
|
|
f99d4f61f4
|
||
|
|
d08f7d4b4e
|
||
|
|
cef1ad13af
|
||
|
|
f141e53f30
|
||
|
|
18a50ab97c
|
||
|
|
4cbec79334
|
||
|
|
c9244b0459
|
||
|
|
b3ce15a564
|
||
|
|
ca04498717
|
||
|
|
27c458307f
|
||
|
|
ee6035c75d
|
||
|
|
3fd0a61737
|
||
|
|
720584ea17
|
||
|
|
59346973fc
|
||
|
|
a7178a481b
|
||
|
|
70441259a0
|
||
|
|
24c5dedd0c
|
||
|
|
58fe7d79ae
|
||
|
|
5e1f3bb7e3
|
||
|
|
a4a3c77586
|
||
|
|
ee07a0289f
|
||
|
|
91c2613e65
|
||
|
|
f73959b73f
|
||
|
|
bad43d1383
|
||
|
|
adf0896b7d
|
||
|
|
bd96c4ab23
|
||
|
|
e9d3851db8
|
||
|
|
773719dbd2
|
||
|
|
2b07ad96f1
|
||
|
|
7fa091cb44
|
||
|
|
7d1ff86c68
|
||
|
|
7500e2da24
|
||
|
|
6a7f9fe0da
|
||
|
|
dc4b11cf1b
|
||
|
|
ab9645dee9
|
||
|
|
da6c9e7b66
|
||
|
|
a311e2b895
|
||
|
|
61a18cb222
|
||
|
|
280e691a94
|
||
|
|
b4c6994362
|
||
|
|
d97c20d5ac
|
||
|
|
751ff6ecf5
|
||
|
|
14deed6b81
|
||
|
|
baf74d95e1
|
||
|
|
5480514b12
|
||
|
|
a8d13ae5c8
|
||
|
|
d1d5033710
|
||
|
|
fb465b06f8
|
||
|
|
06e2d7841e
|
||
|
|
d6deb6eea2
|
||
|
|
dbd84d929d
|
||
|
|
0867ae918e
|
||
|
|
cd5ef8e093
|
||
|
|
f4fd8516a6
|
||
|
|
aea7fd2607
|
||
|
|
daf991d1eb
|
||
|
|
be5826f2c4
|
||
|
|
c94b2a40d0
|
||
|
|
fb91c2edd6
|
||
|
|
5c593b3770
|
||
|
|
5a730f2f05
|
||
|
|
999a22a37f
|
||
|
|
666084a30f
|
||
|
|
1edceba056
|
||
|
|
40be5714e9
|
||
|
|
f5bcb7d83a
|
||
|
|
5c9fead725
|
||
|
|
6139b48d90
|
||
|
|
3569bcc036
|
||
|
|
fbfd64eb2a
|
||
|
|
e30d4dbd0c
|
||
|
|
f2787560d6
|
||
|
|
b48ce5db2e
|
||
|
|
634d1fc1b1
|
||
|
|
4c6a9ece4e
|
||
|
|
817ebeaf90
|
||
|
|
41830cc072
|
||
|
|
8752a85db5
|
||
|
|
9572b68f97
|
||
|
|
c82006862e
|
||
|
|
e9b9aa3a14
|
||
|
|
f896b65fd9
|
||
|
|
ac8a11233f
|
||
|
|
fcfc29894e
|
||
|
|
e514e9b6cc
|
||
|
|
9238c1b9d2
|
||
|
|
5042dbdb3a
|
||
|
|
817fe4d135
|
||
|
|
84eba9beac
|
||
|
|
e1be1df68a
|
||
|
|
3475660eb2
|
||
|
|
e8e3376327
|
||
|
|
b9fa3b36c0
|
||
|
|
29186ea03c
|
||
|
|
ce8c51b02f
|
||
|
|
0cc49089b6
|
||
|
|
1d8ccd7ff6
|
||
|
|
de41d6fef7
|
||
|
|
4a7cf922e8
|
||
|
|
44d147ce37
|
||
|
|
ab3992c372
|
||
|
|
bd0274411c
|
||
|
|
690a17e152
|
||
|
|
c6d729162a
|
||
|
|
e0e82e654e
|
||
|
|
97d67a6938
|
||
|
|
1072585903
|
||
|
|
520b422dcc
|
||
|
|
d8f171ec6f
|
||
|
|
cd341c3d71
|
||
|
|
29f043f5c7
|
||
|
|
4db9b92146
|
||
|
|
a362add8f0
|
||
|
|
e779f70187 | ||
|
|
a9054d9a98 | ||
|
|
645ca19d76 | ||
|
|
6f697f64f4 | ||
|
|
a72b2991fd | ||
|
|
6c41e0704e | ||
|
|
6e7707f6df | ||
|
|
a26444006b | ||
|
|
ccf3e28323 | ||
|
|
6eea6b92fb |
No files matched your search
@@ -2,3 +2,5 @@
|
||||
/.idea/
|
||||
/.scannerwork/
|
||||
/.act/
|
||||
/.sisyphus/
|
||||
/.coyote/
|
||||
@@ -5,6 +5,21 @@ 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.7.3 (2026-06-25)
|
||||
|
||||
### Feat
|
||||
|
||||
- Implemented log rolling so the log file doesn't just grow exponentially [#60]
|
||||
|
||||
### Fix
|
||||
|
||||
- addressed code review comments
|
||||
- tail-logs subcommand follows log rollovers and sleeps to minimize idle CPU loops
|
||||
|
||||
### Refactor
|
||||
|
||||
- Refactored several usages of sort_by_key and match guards to utilize newer Rust version APIs
|
||||
|
||||
## v0.7.2 (2026-04-20)
|
||||
|
||||
### Feat
|
||||
|
||||
Generated
+3
-3
@@ -93,9 +93,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
@@ -1844,7 +1844,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "managarr"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "managarr"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||
description = "A TUI and CLI to manage your Servarrs"
|
||||
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.89 AS builder
|
||||
FROM rust:1.95 AS builder
|
||||
WORKDIR /usr/src
|
||||
|
||||
# Download and compile Rust dependencies in an empty project and cache as a separate Docker layer
|
||||
|
||||
@@ -20,7 +20,7 @@ Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built
|
||||
- [x]  [Radarr](https://wiki.servarr.com/radarr)
|
||||
- [x]  [Sonarr](https://wiki.servarr.com/en/sonarr)
|
||||
- [x]  [Lidarr](https://wiki.servarr.com/en/lidarr)
|
||||
- [ ]  [Readarr](https://wiki.servarr.com/en/readarr)
|
||||
- [x]  [Readarr](https://wiki.servarr.com/en/readarr)
|
||||
- [ ]  [Prowlarr](https://wiki.servarr.com/en/prowlarr)
|
||||
- [ ]  [Whisparr](https://wiki.servarr.com/whisparr)
|
||||
- [ ]  [Bazarr](https://www.bazarr.media/)
|
||||
@@ -256,6 +256,7 @@ Commands:
|
||||
radarr Commands for manging your Radarr instance
|
||||
sonarr Commands for manging your Sonarr instance
|
||||
lidarr Commands for manging your Lidarr instance
|
||||
readarr Commands for managing your Readarr instance
|
||||
completions Generate shell completions for the Managarr CLI
|
||||
tail-logs Tail Managarr logs
|
||||
config-path Print the full path to the default configuration file.
|
||||
@@ -347,6 +348,7 @@ default port for that respective service. That is:
|
||||
| Radarr | `localhost` | 7878 |
|
||||
| Sonarr | `localhost` | 8989 |
|
||||
| Lidarr | `localhost` | 8686 |
|
||||
| Readarr | `localhost` | 8787 |
|
||||
|
||||
> [!TIP]
|
||||
> In general, all Servarrs store their API tokens under Settings -> General -> Security -> API Key in their web UIs.
|
||||
@@ -367,6 +369,10 @@ sonarr:
|
||||
lidarr:
|
||||
# Connect to lidarr running on localhost:8686
|
||||
- api_token: <your-lidarr-api-token-here>
|
||||
|
||||
readarr:
|
||||
# Connect to readarr running on localhost:8787
|
||||
- api_token: <your-readarr-api-token-here>
|
||||
```
|
||||
|
||||
## Configuration File Location
|
||||
|
||||
@@ -49,7 +49,7 @@ doctest:
|
||||
# Run all proptests
|
||||
[group: 'test']
|
||||
proptest:
|
||||
@cargo test proptest
|
||||
@cargo test -- property_tests proptest
|
||||
|
||||
alias test-snapshots := snapshot-tests
|
||||
# Run all snapshot tests
|
||||
|
||||
@@ -5,3 +5,4 @@
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 56330c025ad79db641d0eb9f429ab74e95822e1fb015b58f0e158ea674cd42a1 # shrinks to list_size = 1, page_ops = 1
|
||||
cc 29df5ff471eb388b0ffde573a959a539e4cf32acd9e1cea5187bba2268dd57f7 # shrinks to list_size = 1, filter_term = "𑛀"
|
||||
+32
-12
@@ -5,11 +5,13 @@ mod tests {
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
use serde_json::Value;
|
||||
use serial_test::serial;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, ReadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::{HorizontallyScrollableText, TabRoute};
|
||||
use crate::network::NetworkEvent;
|
||||
@@ -32,11 +34,18 @@ mod tests {
|
||||
..ServarrConfig::default()
|
||||
};
|
||||
let sonarr_config_2 = ServarrConfig::default();
|
||||
let readarr_config_1 = ServarrConfig {
|
||||
name: Some("Readarr Test".to_owned()),
|
||||
weight: Some(2),
|
||||
..ServarrConfig::default()
|
||||
};
|
||||
let readarr_config_2 = ServarrConfig::default();
|
||||
let config = AppConfig {
|
||||
theme: None,
|
||||
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
|
||||
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
|
||||
lidarr: None,
|
||||
readarr: Some(vec![readarr_config_1.clone(), readarr_config_2.clone()]),
|
||||
};
|
||||
let expected_tab_routes = vec![
|
||||
TabRoute {
|
||||
@@ -45,6 +54,12 @@ mod tests {
|
||||
contextual_help: None,
|
||||
config: Some(sonarr_config_1),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Readarr Test".to_owned(),
|
||||
route: ActiveReadarrBlock::default().into(),
|
||||
contextual_help: None,
|
||||
config: Some(readarr_config_1),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Radarr 1".to_owned(),
|
||||
route: ActiveRadarrBlock::default().into(),
|
||||
@@ -63,6 +78,12 @@ mod tests {
|
||||
contextual_help: None,
|
||||
config: Some(sonarr_config_2),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Readarr 1".to_owned(),
|
||||
route: ActiveReadarrBlock::default().into(),
|
||||
contextual_help: None,
|
||||
config: Some(readarr_config_2),
|
||||
},
|
||||
];
|
||||
|
||||
let app = App::new(
|
||||
@@ -80,9 +101,9 @@ mod tests {
|
||||
assert_eq!(app.server_tabs.index, 0);
|
||||
assert_eq!(app.server_tabs.tabs, expected_tab_routes);
|
||||
assert_eq!(app.tick_until_poll, 400);
|
||||
assert_eq!(app.ticks_until_scroll, 64);
|
||||
assert_eq!(app.scroll_interval, Duration::from_millis(100));
|
||||
assert_eq!(app.tick_count, 0);
|
||||
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||
assert!(!app.should_text_scroll);
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.is_routing);
|
||||
assert!(!app.should_refresh);
|
||||
@@ -101,7 +122,7 @@ mod tests {
|
||||
assert_eq!(app.error, HorizontallyScrollableText::default());
|
||||
assert_eq!(app.server_tabs.index, 0);
|
||||
assert_eq!(app.tick_until_poll, 400);
|
||||
assert_eq!(app.ticks_until_scroll, 64);
|
||||
assert_eq!(app.scroll_interval, Duration::from_millis(100));
|
||||
assert_eq!(app.tick_count, 0);
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.is_routing);
|
||||
@@ -188,6 +209,7 @@ mod tests {
|
||||
let data = Data {
|
||||
lidarr_data: LidarrData::default(),
|
||||
radarr_data,
|
||||
readarr_data: ReadarrData::default(),
|
||||
sonarr_data,
|
||||
};
|
||||
let mut app = App {
|
||||
@@ -247,22 +269,20 @@ mod tests {
|
||||
#[test]
|
||||
fn test_on_ui_scroll_tick() {
|
||||
let mut app = App {
|
||||
ticks_until_scroll: 1,
|
||||
scroll_interval: Duration::ZERO,
|
||||
..App::default()
|
||||
};
|
||||
|
||||
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
app.on_ui_scroll_tick();
|
||||
assert!(app.should_text_scroll);
|
||||
|
||||
app.on_ui_scroll_tick();
|
||||
assert!(app.should_text_scroll);
|
||||
|
||||
assert_eq!(app.ui_scroll_tick_count, 1);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
|
||||
app.scroll_interval = Duration::from_secs(60);
|
||||
app.last_scroll = Instant::now();
|
||||
app.on_ui_scroll_tick();
|
||||
|
||||
assert_eq!(app.ui_scroll_tick_count, 0);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
assert!(!app.should_text_scroll);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::app::App;
|
||||
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
|
||||
use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider;
|
||||
use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider;
|
||||
use crate::app::readarr::readarr_context_clues::ReadarrContextClueProvider;
|
||||
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
|
||||
use crate::models::Route;
|
||||
|
||||
@@ -23,6 +24,7 @@ impl ContextClueProvider for ServarrContextClueProvider {
|
||||
Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app),
|
||||
Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app),
|
||||
Route::Lidarr(_, _) => LidarrContextClueProvider::get_context_clues(app),
|
||||
Route::Readarr(_, _) => ReadarrContextClueProvider::get_context_clues(app),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ mod test {
|
||||
use crate::app::{App, key_binding::DEFAULT_KEYBINDINGS};
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
|
||||
#[test]
|
||||
@@ -303,6 +304,16 @@ mod test {
|
||||
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_servarr_context_clue_provider_delegates_to_readarr_provider() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::SystemTasks.into());
|
||||
|
||||
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_servarr_context_clue_provider_unsupported_route_returns_none() {
|
||||
let mut app = App::test_default();
|
||||
|
||||
@@ -27,6 +27,7 @@ generate_keybindings! {
|
||||
filter,
|
||||
sort,
|
||||
edit,
|
||||
view,
|
||||
logs,
|
||||
tasks,
|
||||
test,
|
||||
@@ -142,6 +143,11 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
|
||||
alt: None,
|
||||
desc: "edit",
|
||||
},
|
||||
view: KeyBinding {
|
||||
key: Key::Char('v'),
|
||||
alt: None,
|
||||
desc: "view",
|
||||
},
|
||||
events: KeyBinding {
|
||||
key: Key::Char('e'),
|
||||
alt: None,
|
||||
|
||||
@@ -26,6 +26,7 @@ mod test {
|
||||
#[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), None, "filter")]
|
||||
#[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), None, "sort")]
|
||||
#[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), None, "edit")]
|
||||
#[case(DEFAULT_KEYBINDINGS.view, Key::Char('v'), None, "view")]
|
||||
#[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), None, "events")]
|
||||
#[case(DEFAULT_KEYBINDINGS.logs, Key::Char('L'), None, "logs")]
|
||||
#[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), None, "tasks")]
|
||||
|
||||
@@ -7,8 +7,8 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS,
|
||||
ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS,
|
||||
TRACK_DETAILS_BLOCKS,
|
||||
ARTIST_OVERVIEW_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS, DELETE_ARTIST_BLOCKS,
|
||||
EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, TRACK_DETAILS_BLOCKS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -39,7 +39,7 @@ pub static ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.esc, "edit search"),
|
||||
];
|
||||
|
||||
pub static ARTIST_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
|
||||
pub static ARTIST_DETAILS_CONTEXT_CLUES: [ContextClue; 9] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
@@ -56,6 +56,15 @@ pub static ARTIST_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.view, "view overview"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static ARTIST_OVERVIEW_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
@@ -172,6 +181,9 @@ impl ContextClueProvider for LidarrContextClueProvider {
|
||||
.lidarr_data
|
||||
.artist_info_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
_ if ARTIST_OVERVIEW_BLOCKS.contains(&active_lidarr_block) => {
|
||||
Some(&ARTIST_OVERVIEW_CONTEXT_CLUES)
|
||||
}
|
||||
_ if ALBUM_DETAILS_BLOCKS.contains(&active_lidarr_block) => app
|
||||
.data
|
||||
.lidarr_data
|
||||
@@ -203,6 +215,11 @@ impl ContextClueProvider for LidarrContextClueProvider {
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
_ if DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block)
|
||||
|| DELETE_ALBUM_BLOCKS.contains(&active_lidarr_block) =>
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveLidarrBlock::AddArtistPrompt
|
||||
| ActiveLidarrBlock::AddArtistSelectMonitor
|
||||
| ActiveLidarrBlock::AddArtistSelectMonitorNewItems
|
||||
|
||||
@@ -9,8 +9,9 @@ mod tests {
|
||||
use crate::app::lidarr::lidarr_context_clues::{
|
||||
ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ALBUM_DETAILS_CONTEXT_CLUES,
|
||||
ALBUM_HISTORY_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES,
|
||||
ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES,
|
||||
MANUAL_ARTIST_SEARCH_CONTEXT_CLUES, TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
|
||||
ARTIST_OVERVIEW_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider,
|
||||
MANUAL_ALBUM_SEARCH_CONTEXT_CLUES, MANUAL_ARTIST_SEARCH_CONTEXT_CLUES,
|
||||
TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS,
|
||||
@@ -114,6 +115,10 @@ mod tests {
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.view, "view overview")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
@@ -121,6 +126,24 @@ mod tests {
|
||||
assert_none!(artist_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_context_clues() {
|
||||
let mut artist_overview_context_clues_iter = ARTIST_OVERVIEW_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
artist_overview_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artist_overview_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(artist_overview_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_artist_search_results_context_clues() {
|
||||
let mut add_artist_search_results_context_clues_iter =
|
||||
@@ -471,6 +494,16 @@ mod tests {
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_artist_overview_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistOverview.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTIST_OVERVIEW_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_artists_block() {
|
||||
let mut app = App::test_default();
|
||||
@@ -577,6 +610,42 @@ mod tests {
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_delete_artist_blocks(
|
||||
#[values(
|
||||
ActiveLidarrBlock::DeleteArtistPrompt,
|
||||
ActiveLidarrBlock::DeleteArtistConfirmPrompt,
|
||||
ActiveLidarrBlock::DeleteArtistToggleAddListExclusion,
|
||||
ActiveLidarrBlock::DeleteArtistToggleDeleteFile
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_delete_album_blocks(
|
||||
#[values(
|
||||
ActiveLidarrBlock::DeleteAlbumPrompt,
|
||||
ActiveLidarrBlock::DeleteAlbumConfirmPrompt,
|
||||
ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion,
|
||||
ActiveLidarrBlock::DeleteAlbumToggleDeleteFile
|
||||
)]
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_lidarr_block.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_system_tasks_clues() {
|
||||
let mut app = App::test_default();
|
||||
|
||||
@@ -498,7 +498,7 @@ mod tests {
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetTrackDetails(1).into()
|
||||
LidarrEvent::GetTrackDetails(5).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
@@ -518,7 +518,7 @@ mod tests {
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetTrackHistory(1, 1, 1).into()
|
||||
LidarrEvent::GetTrackHistory(1, 1, 5).into()
|
||||
);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
@@ -759,7 +759,7 @@ mod tests {
|
||||
album_details_modal.tracks.set_items(vec![track()]);
|
||||
app.data.lidarr_data.album_details_modal = Some(album_details_modal);
|
||||
|
||||
assert_eq!(app.extract_track_id().await, 1);
|
||||
assert_eq!(app.extract_track_id().await, 5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
+72
-8
@@ -7,6 +7,7 @@ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fs, process};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -16,6 +17,7 @@ use crate::cli::Command;
|
||||
use crate::models::servarr_data::Notification;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, ReadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
@@ -29,6 +31,7 @@ pub mod key_binding;
|
||||
mod key_binding_tests;
|
||||
pub mod lidarr;
|
||||
pub mod radarr;
|
||||
pub mod readarr;
|
||||
pub mod sonarr;
|
||||
|
||||
pub struct App<'a> {
|
||||
@@ -41,9 +44,10 @@ pub struct App<'a> {
|
||||
pub error: HorizontallyScrollableText,
|
||||
pub notification: Option<Notification>,
|
||||
pub tick_until_poll: u64,
|
||||
pub ticks_until_scroll: u64,
|
||||
pub scroll_interval: Duration,
|
||||
pub last_scroll: Instant,
|
||||
pub tick_count: u64,
|
||||
pub ui_scroll_tick_count: u64,
|
||||
pub should_text_scroll: bool,
|
||||
pub is_routing: bool,
|
||||
pub is_loading: bool,
|
||||
pub should_refresh: bool,
|
||||
@@ -120,6 +124,26 @@ impl App<'_> {
|
||||
server_tabs.extend(lidarr_tabs);
|
||||
}
|
||||
|
||||
if let Some(readarr_configs) = config.readarr {
|
||||
let mut unnamed_idx = 0;
|
||||
let readarr_tabs = readarr_configs.into_iter().map(|readarr_config| {
|
||||
let name = if let Some(name) = readarr_config.name.clone() {
|
||||
name
|
||||
} else {
|
||||
unnamed_idx += 1;
|
||||
format!("Readarr {unnamed_idx}")
|
||||
};
|
||||
|
||||
TabRoute {
|
||||
title: name,
|
||||
route: ActiveReadarrBlock::Authors.into(),
|
||||
contextual_help: None,
|
||||
config: Some(readarr_config),
|
||||
}
|
||||
});
|
||||
server_tabs.extend(readarr_tabs);
|
||||
}
|
||||
|
||||
let weight_sorted_tabs = server_tabs
|
||||
.into_iter()
|
||||
.sorted_by(|tab1, tab2| {
|
||||
@@ -171,10 +195,11 @@ impl App<'_> {
|
||||
}
|
||||
|
||||
pub fn on_ui_scroll_tick(&mut self) {
|
||||
if self.ui_scroll_tick_count == self.ticks_until_scroll {
|
||||
self.ui_scroll_tick_count = 0;
|
||||
if self.last_scroll.elapsed() >= self.scroll_interval {
|
||||
self.should_text_scroll = true;
|
||||
self.last_scroll = Instant::now();
|
||||
} else {
|
||||
self.ui_scroll_tick_count += 1;
|
||||
self.should_text_scroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +226,7 @@ impl App<'_> {
|
||||
Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await,
|
||||
Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await,
|
||||
Route::Lidarr(active_lidarr_block, _) => self.lidarr_on_tick(active_lidarr_block).await,
|
||||
Route::Readarr(active_readarr_block, _) => self.readarr_on_tick(active_readarr_block).await,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
@@ -260,9 +286,10 @@ impl Default for App<'_> {
|
||||
is_first_render: true,
|
||||
server_tabs: TabState::new(Vec::new()),
|
||||
tick_until_poll: 400,
|
||||
ticks_until_scroll: 64,
|
||||
scroll_interval: Duration::from_millis(100),
|
||||
last_scroll: Instant::now(),
|
||||
tick_count: 0,
|
||||
ui_scroll_tick_count: 0,
|
||||
should_text_scroll: false,
|
||||
is_loading: false,
|
||||
is_routing: false,
|
||||
should_refresh: false,
|
||||
@@ -296,6 +323,12 @@ impl App<'_> {
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Readarr".to_owned(),
|
||||
route: ActiveReadarrBlock::Authors.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
]),
|
||||
..App::default()
|
||||
}
|
||||
@@ -306,6 +339,7 @@ impl App<'_> {
|
||||
data: Data {
|
||||
lidarr_data: LidarrData::test_default_fully_populated(),
|
||||
radarr_data: RadarrData::test_default_fully_populated(),
|
||||
readarr_data: ReadarrData::test_default_fully_populated(),
|
||||
sonarr_data: SonarrData::test_default_fully_populated(),
|
||||
},
|
||||
server_tabs: TabState::new(vec![
|
||||
@@ -327,16 +361,27 @@ impl App<'_> {
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Readarr".to_owned(),
|
||||
route: ActiveReadarrBlock::Authors.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
]),
|
||||
..App::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_navigation_stack_depth(&self) -> usize {
|
||||
self.navigation_stack.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Data<'a> {
|
||||
pub lidarr_data: LidarrData<'a>,
|
||||
pub radarr_data: RadarrData<'a>,
|
||||
pub readarr_data: ReadarrData<'a>,
|
||||
pub sonarr_data: SonarrData<'a>,
|
||||
}
|
||||
|
||||
@@ -345,12 +390,17 @@ pub struct AppConfig {
|
||||
pub theme: Option<String>,
|
||||
pub lidarr: Option<Vec<ServarrConfig>>,
|
||||
pub radarr: Option<Vec<ServarrConfig>>,
|
||||
pub readarr: Option<Vec<ServarrConfig>>,
|
||||
pub sonarr: Option<Vec<ServarrConfig>>,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn validate(&self, config_path: &str) {
|
||||
if self.lidarr.is_none() && self.radarr.is_none() && self.sonarr.is_none() {
|
||||
if self.lidarr.is_none()
|
||||
&& self.radarr.is_none()
|
||||
&& self.sonarr.is_none()
|
||||
&& self.readarr.is_none()
|
||||
{
|
||||
log_and_print_error(format!(
|
||||
"No Servarrs are configured in the file: {config_path}"
|
||||
));
|
||||
@@ -368,6 +418,10 @@ impl AppConfig {
|
||||
if let Some(lidarr_configs) = &self.lidarr {
|
||||
lidarr_configs.iter().for_each(|config| config.validate());
|
||||
}
|
||||
|
||||
if let Some(readarr_configs) = &self.readarr {
|
||||
readarr_configs.iter().for_each(|config| config.validate());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_config_present_for_cli(&self, command: &Command) {
|
||||
@@ -389,6 +443,10 @@ impl AppConfig {
|
||||
msg("Lidarr");
|
||||
process::exit(1);
|
||||
}
|
||||
Command::Readarr(_) if self.readarr.is_none() => {
|
||||
msg("Readarr");
|
||||
process::exit(1);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -411,6 +469,12 @@ impl AppConfig {
|
||||
lidarr_config.post_process_initialization();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(readarr_configs) = self.readarr.as_mut() {
|
||||
for readarr_config in readarr_configs {
|
||||
readarr_config.post_process_initialization();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::app::context_clues::{
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ADD_MOVIE_BLOCKS, ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS, EDIT_INDEXER_BLOCKS,
|
||||
EDIT_MOVIE_BLOCKS, INDEXER_SETTINGS_BLOCKS, MOVIE_DETAILS_BLOCKS,
|
||||
ADD_MOVIE_BLOCKS, ActiveRadarrBlock, DELETE_MOVIE_BLOCKS, EDIT_COLLECTION_BLOCKS,
|
||||
EDIT_INDEXER_BLOCKS, EDIT_MOVIE_BLOCKS, INDEXER_SETTINGS_BLOCKS, MOVIE_DETAILS_BLOCKS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -120,6 +120,9 @@ impl ContextClueProvider for RadarrContextClueProvider {
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
_ if DELETE_MOVIE_BLOCKS.contains(&active_radarr_block) => {
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveRadarrBlock::AddMoviePrompt
|
||||
| ActiveRadarrBlock::AddMovieSelectMonitor
|
||||
| ActiveRadarrBlock::AddMovieSelectMinimumAvailability
|
||||
|
||||
@@ -415,6 +415,25 @@ mod tests {
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_radarr_context_clue_provider_confirmation_prompt_context_clues_delete_movie_blocks(
|
||||
#[values(
|
||||
ActiveRadarrBlock::DeleteMoviePrompt,
|
||||
ActiveRadarrBlock::DeleteMovieConfirmPrompt,
|
||||
ActiveRadarrBlock::DeleteMovieToggleAddListExclusion,
|
||||
ActiveRadarrBlock::DeleteMovieToggleDeleteFile
|
||||
)]
|
||||
active_radarr_block: ActiveRadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data = RadarrData::default();
|
||||
app.push_navigation_stack(active_radarr_block.into());
|
||||
|
||||
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_radarr_context_clue_provider_add_movie_search_results_context_clues(
|
||||
#[values(
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
use super::App;
|
||||
use crate::{
|
||||
models::servarr_data::readarr::readarr_data::ActiveReadarrBlock,
|
||||
network::readarr_network::ReadarrEvent,
|
||||
};
|
||||
|
||||
pub mod readarr_context_clues;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "readarr_tests.rs"]
|
||||
mod readarr_tests;
|
||||
|
||||
impl App<'_> {
|
||||
pub(super) async fn dispatch_by_readarr_block(
|
||||
&mut self,
|
||||
active_readarr_block: &ActiveReadarrBlock,
|
||||
) {
|
||||
match active_readarr_block {
|
||||
ActiveReadarrBlock::Authors => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetQualityProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetMetadataProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::ListAuthors.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::Blocklist => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetBlocklist.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::Downloads => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::AuthorDetails => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetBooks(self.extract_author_id().await).into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::AuthorHistory => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::GetAuthorHistory(self.extract_author_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::ManualAuthorSearch => {
|
||||
if self.data.readarr_data.author_releases.is_empty() {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::GetAuthorReleases(self.extract_author_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::BookDetails => {
|
||||
let book_id = self.extract_book_id().await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetBookEditions(book_id).into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetBookFiles(book_id).into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::BookHistory => {
|
||||
if !self.data.readarr_data.books.is_empty() {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::GetBookHistory(
|
||||
self.extract_author_id().await,
|
||||
self.extract_book_id().await,
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::ManualBookSearch => {
|
||||
match self.data.readarr_data.book_details_modal.as_ref() {
|
||||
Some(book_details_modal) if book_details_modal.book_releases.is_empty() => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::GetBookReleases(self.extract_book_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::AddAuthorSearchResults => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::SearchNewAuthor(self.extract_add_new_author_search_query().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::History => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetHistory(500).into())
|
||||
.await
|
||||
}
|
||||
ActiveReadarrBlock::RootFolders => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetRootFolders.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::Indexers => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetIndexers.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetAllIndexerSettings.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::TestIndexer => {
|
||||
self
|
||||
.dispatch_network_event(
|
||||
ReadarrEvent::TestIndexer(self.extract_readarr_indexer_id().await).into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::TestAllIndexers => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::TestAllIndexers.into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::System => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetTasks.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetQueuedEvents.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetLogs(500).into())
|
||||
.await;
|
||||
}
|
||||
ActiveReadarrBlock::SystemUpdates => {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetUpdates.into())
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.check_for_readarr_prompt_action().await;
|
||||
self.reset_tick_count();
|
||||
}
|
||||
|
||||
async fn extract_add_new_author_search_query(&self) -> String {
|
||||
self
|
||||
.data
|
||||
.readarr_data
|
||||
.add_author_search
|
||||
.as_ref()
|
||||
.expect("Add author search is empty")
|
||||
.text
|
||||
.clone()
|
||||
}
|
||||
|
||||
async fn extract_author_id(&self) -> i64 {
|
||||
self.data.readarr_data.authors.current_selection().id
|
||||
}
|
||||
|
||||
async fn extract_book_id(&self) -> i64 {
|
||||
self.data.readarr_data.books.current_selection().id
|
||||
}
|
||||
|
||||
async fn extract_readarr_indexer_id(&self) -> i64 {
|
||||
self.data.readarr_data.indexers.current_selection().id
|
||||
}
|
||||
|
||||
async fn check_for_readarr_prompt_action(&mut self) {
|
||||
if self.data.readarr_data.prompt_confirm {
|
||||
self.data.readarr_data.prompt_confirm = false;
|
||||
if let Some(readarr_event) = self.data.readarr_data.prompt_confirm_action.take() {
|
||||
self.dispatch_network_event(readarr_event.into()).await;
|
||||
self.should_refresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn readarr_on_tick(&mut self, active_readarr_block: ActiveReadarrBlock) {
|
||||
if self.is_first_render {
|
||||
self.refresh_readarr_metadata().await;
|
||||
self.dispatch_by_readarr_block(&active_readarr_block).await;
|
||||
self.is_first_render = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if self.should_refresh {
|
||||
self.dispatch_by_readarr_block(&active_readarr_block).await;
|
||||
self.refresh_readarr_metadata().await;
|
||||
}
|
||||
|
||||
if self.is_routing {
|
||||
if !self.should_refresh {
|
||||
self.cancellation_token.cancel();
|
||||
} else {
|
||||
self.dispatch_by_readarr_block(&active_readarr_block).await;
|
||||
}
|
||||
}
|
||||
|
||||
if self.tick_count.is_multiple_of(self.tick_until_poll) {
|
||||
self.refresh_readarr_metadata().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_readarr_metadata(&mut self) {
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetQualityProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetMetadataProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetRootFolders.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetDiskSpace.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(ReadarrEvent::GetStatus.into())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
|
||||
SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
ADD_AUTHOR_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, AUTHOR_DETAILS_BLOCKS, AUTHOR_OVERVIEW_BLOCKS,
|
||||
ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS,
|
||||
EDIT_AUTHOR_BLOCKS, EDIT_INDEXER_BLOCKS, EDITION_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "readarr_context_clues_tests.rs"]
|
||||
mod readarr_context_clues_tests;
|
||||
|
||||
pub static AUTHORS_CONTEXT_CLUES: [ContextClue; 10] = [
|
||||
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
|
||||
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.update, "update all"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
|
||||
];
|
||||
|
||||
pub static ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "edit search"),
|
||||
];
|
||||
|
||||
pub static AUTHOR_DETAILS_CONTEXT_CLUES: [ContextClue; 9] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.edit, "edit author"),
|
||||
(DEFAULT_KEYBINDINGS.delete, "delete book"),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
"toggle book monitoring",
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.view, "view overview"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static AUTHOR_OVERVIEW_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static AUTHOR_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.edit, "edit author"),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
|
||||
];
|
||||
|
||||
pub static MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.edit, "edit author"),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static BOOK_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
(DEFAULT_KEYBINDINGS.submit, "edition details"),
|
||||
];
|
||||
|
||||
pub static BOOK_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
|
||||
];
|
||||
|
||||
pub static BOOK_FILE_CONTEXT_CLUES: [ContextClue; 3] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.delete, "delete book file"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static MANUAL_BOOK_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static EDITION_DETAILS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub(in crate::app) struct ReadarrContextClueProvider;
|
||||
|
||||
impl ContextClueProvider for ReadarrContextClueProvider {
|
||||
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
|
||||
let Route::Readarr(active_readarr_block, _context_option) = app.get_current_route() else {
|
||||
panic!("ReadarrContextClueProvider::get_context_clues called with non-Readarr route");
|
||||
};
|
||||
|
||||
match active_readarr_block {
|
||||
_ if AUTHOR_DETAILS_BLOCKS.contains(&active_readarr_block) => app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_info_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
_ if AUTHOR_OVERVIEW_BLOCKS.contains(&active_readarr_block) => {
|
||||
Some(&AUTHOR_OVERVIEW_CONTEXT_CLUES)
|
||||
}
|
||||
_ if BOOK_DETAILS_BLOCKS.contains(&active_readarr_block) => app
|
||||
.data
|
||||
.readarr_data
|
||||
.book_details_modal
|
||||
.as_ref()
|
||||
.expect("book_details_modal is empty")
|
||||
.book_details_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
_ if EDITION_DETAILS_BLOCKS.contains(&active_readarr_block) => {
|
||||
Some(&EDITION_DETAILS_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveReadarrBlock::AddAuthorSearchInput
|
||||
| ActiveReadarrBlock::AddAuthorEmptySearchResults
|
||||
| ActiveReadarrBlock::TestAllIndexers
|
||||
| ActiveReadarrBlock::SystemLogs
|
||||
| ActiveReadarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES),
|
||||
_ if EDIT_AUTHOR_BLOCKS.contains(&active_readarr_block)
|
||||
|| EDIT_INDEXER_BLOCKS.contains(&active_readarr_block)
|
||||
|| INDEXER_SETTINGS_BLOCKS.contains(&active_readarr_block)
|
||||
|| ADD_ROOT_FOLDER_BLOCKS.contains(&active_readarr_block) =>
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
_ if DELETE_AUTHOR_BLOCKS.contains(&active_readarr_block)
|
||||
|| DELETE_BOOK_BLOCKS.contains(&active_readarr_block) =>
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveReadarrBlock::AddAuthorPrompt
|
||||
| ActiveReadarrBlock::AddAuthorSelectMonitor
|
||||
| ActiveReadarrBlock::AddAuthorSelectMonitorNewItems
|
||||
| ActiveReadarrBlock::AddAuthorSelectQualityProfile
|
||||
| ActiveReadarrBlock::AddAuthorSelectMetadataProfile
|
||||
| ActiveReadarrBlock::AddAuthorSelectRootFolder
|
||||
| ActiveReadarrBlock::AddAuthorTagsInput
|
||||
| ActiveReadarrBlock::AddAuthorAlreadyInLibrary => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
|
||||
_ if ADD_AUTHOR_BLOCKS.contains(&active_readarr_block) => {
|
||||
Some(&ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveReadarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES),
|
||||
_ => app
|
||||
.data
|
||||
.readarr_data
|
||||
.main_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
|
||||
SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::readarr::readarr_context_clues::{
|
||||
ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES, AUTHOR_DETAILS_CONTEXT_CLUES,
|
||||
AUTHOR_HISTORY_CONTEXT_CLUES, AUTHOR_OVERVIEW_CONTEXT_CLUES, AUTHORS_CONTEXT_CLUES,
|
||||
BOOK_DETAILS_CONTEXT_CLUES, BOOK_FILE_CONTEXT_CLUES, BOOK_HISTORY_CONTEXT_CLUES,
|
||||
EDITION_DETAILS_CONTEXT_CLUES, MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES,
|
||||
MANUAL_BOOK_SEARCH_CONTEXT_CLUES, ReadarrContextClueProvider,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::servarr_data::readarr::modals::BookDetailsModal;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
ADD_ROOT_FOLDER_BLOCKS, ActiveReadarrBlock, EDIT_AUTHOR_BLOCKS, EDIT_INDEXER_BLOCKS,
|
||||
INDEXER_SETTINGS_BLOCKS, ReadarrData,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn test_authors_context_clues() {
|
||||
let mut authors_context_clues_iter = AUTHORS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, "update all")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
authors_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
|
||||
);
|
||||
assert_none!(authors_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_context_clues() {
|
||||
let mut author_details_context_clues_iter = AUTHOR_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, "edit author")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, "delete book")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.toggle_monitoring,
|
||||
"toggle book monitoring",
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.view, "view overview")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(author_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_context_clues() {
|
||||
let mut author_overview_context_clues_iter = AUTHOR_OVERVIEW_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
author_overview_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_overview_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(author_overview_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_search_results_context_clues() {
|
||||
let mut add_author_search_results_context_clues_iter =
|
||||
ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
add_author_search_results_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
add_author_search_results_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "edit search")
|
||||
);
|
||||
assert_none!(add_author_search_results_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "ReadarrContextClueProvider::get_context_clues called with non-Readarr route"
|
||||
)]
|
||||
fn test_readarr_context_clue_provider_get_context_clues_non_readarr_route() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_history_context_clues() {
|
||||
let mut author_history_context_clues_iter = AUTHOR_HISTORY_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, "edit author")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
author_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
|
||||
);
|
||||
assert_none!(author_history_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_author_search_context_clues() {
|
||||
let mut manual_author_search_context_clues_iter = MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.edit, "edit author")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_author_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(manual_author_search_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_details_context_clues() {
|
||||
let mut book_details_context_clues_iter = BOOK_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
book_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "edition details")
|
||||
);
|
||||
assert_none!(book_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_history_context_clues() {
|
||||
let mut book_history_context_clues_iter = BOOK_HISTORY_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_history_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter/close")
|
||||
);
|
||||
assert_none!(book_history_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_file_context_clues() {
|
||||
let mut book_file_context_clues_iter = BOOK_FILE_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
book_file_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_file_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.delete, "delete book file")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
book_file_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(book_file_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_book_search_context_clues() {
|
||||
let mut manual_book_search_context_clues_iter = MANUAL_BOOK_SEARCH_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
manual_book_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_book_search_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_book_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_book_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.submit, "details")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
manual_book_search_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(manual_book_search_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edition_details_context_clues() {
|
||||
let mut edition_details_context_clues_iter = EDITION_DETAILS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
edition_details_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
edition_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(edition_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveReadarrBlock::AuthorDetails, &AUTHOR_DETAILS_CONTEXT_CLUES)]
|
||||
#[case(1, ActiveReadarrBlock::AuthorHistory, &AUTHOR_HISTORY_CONTEXT_CLUES)]
|
||||
#[case(2, ActiveReadarrBlock::ManualAuthorSearch, &MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES)]
|
||||
fn test_readarr_context_clue_provider_author_info_tabs(
|
||||
#[case] index: usize,
|
||||
#[case] active_readarr_block: ActiveReadarrBlock,
|
||||
#[case] expected_context_clues: &[ContextClue],
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data = ReadarrData::default();
|
||||
app.data.readarr_data.author_info_tabs.set_index(index);
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveReadarrBlock::BookDetails, &BOOK_DETAILS_CONTEXT_CLUES)]
|
||||
#[case(1, ActiveReadarrBlock::BookHistory, &BOOK_HISTORY_CONTEXT_CLUES)]
|
||||
#[case(2, ActiveReadarrBlock::BookFileInfo, &BOOK_FILE_CONTEXT_CLUES)]
|
||||
#[case(3, ActiveReadarrBlock::ManualBookSearch, &MANUAL_BOOK_SEARCH_CONTEXT_CLUES)]
|
||||
fn test_readarr_context_clue_provider_book_details_tabs(
|
||||
#[case] index: usize,
|
||||
#[case] active_readarr_block: ActiveReadarrBlock,
|
||||
#[case] expected_context_clues: &[ContextClue],
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
let mut book_details_modal = BookDetailsModal::default();
|
||||
book_details_modal.book_details_tabs.set_index(index);
|
||||
let readarr_data = ReadarrData {
|
||||
book_details_modal: Some(book_details_modal),
|
||||
..ReadarrData::default()
|
||||
};
|
||||
app.data.readarr_data = readarr_data;
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_book_edition_details_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::BookEditionDetails.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &EDITION_DETAILS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_author_overview_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &AUTHOR_OVERVIEW_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_authors_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Authors.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_authors_sort_prompt_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AuthorsSortPrompt.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_search_authors_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::SearchAuthors.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_filter_authors_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::FilterAuthors.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &AUTHORS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_readarr_context_clue_provider_bare_popup_context_clues(
|
||||
#[values(
|
||||
ActiveReadarrBlock::AddAuthorSearchInput,
|
||||
ActiveReadarrBlock::AddAuthorEmptySearchResults,
|
||||
ActiveReadarrBlock::TestAllIndexers,
|
||||
ActiveReadarrBlock::SystemLogs,
|
||||
ActiveReadarrBlock::SystemUpdates
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &BARE_POPUP_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks() {
|
||||
let mut blocks = EDIT_AUTHOR_BLOCKS.to_vec();
|
||||
blocks.extend(ADD_ROOT_FOLDER_BLOCKS);
|
||||
blocks.extend(INDEXER_SETTINGS_BLOCKS);
|
||||
blocks.extend(EDIT_INDEXER_BLOCKS);
|
||||
|
||||
for active_readarr_block in blocks {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_add_author_search_results_context_clues() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AddAuthorSearchResults.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_readarr_context_clue_provider_confirmation_prompt_context_clues_add_author_blocks(
|
||||
#[values(
|
||||
ActiveReadarrBlock::AddAuthorPrompt,
|
||||
ActiveReadarrBlock::AddAuthorSelectMonitor,
|
||||
ActiveReadarrBlock::AddAuthorSelectMonitorNewItems,
|
||||
ActiveReadarrBlock::AddAuthorSelectQualityProfile,
|
||||
ActiveReadarrBlock::AddAuthorSelectMetadataProfile,
|
||||
ActiveReadarrBlock::AddAuthorSelectRootFolder,
|
||||
ActiveReadarrBlock::AddAuthorTagsInput,
|
||||
ActiveReadarrBlock::AddAuthorAlreadyInLibrary
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_readarr_context_clue_provider_confirmation_prompt_context_clues_delete_author_blocks(
|
||||
#[values(
|
||||
ActiveReadarrBlock::DeleteAuthorPrompt,
|
||||
ActiveReadarrBlock::DeleteAuthorConfirmPrompt,
|
||||
ActiveReadarrBlock::DeleteAuthorToggleAddListExclusion,
|
||||
ActiveReadarrBlock::DeleteAuthorToggleDeleteFile
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_readarr_context_clue_provider_confirmation_prompt_context_clues_delete_book_blocks(
|
||||
#[values(
|
||||
ActiveReadarrBlock::DeleteBookPrompt,
|
||||
ActiveReadarrBlock::DeleteBookConfirmPrompt,
|
||||
ActiveReadarrBlock::DeleteBookToggleAddListExclusion,
|
||||
ActiveReadarrBlock::DeleteBookToggleDeleteFile
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readarr_context_clue_provider_system_tasks_clues() {
|
||||
let mut app = App::test_default();
|
||||
|
||||
app.push_navigation_stack(ActiveReadarrBlock::SystemTasks.into());
|
||||
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::models::readarr_models::{Author, Book, ReadarrRelease};
|
||||
use crate::models::servarr_data::readarr::modals::BookDetailsModal;
|
||||
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
|
||||
use crate::models::servarr_models::Indexer;
|
||||
use crate::network::NetworkEvent;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_readarr_block_authors() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Authors)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::ListAuthors.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_readarr_block_author_details() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::AuthorDetails)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetBooks(1).into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_blocklist_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Blocklist)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetBlocklist.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_author_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::AuthorHistory)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetAuthorHistory(1).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_author_search_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualAuthorSearch)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetAuthorReleases(1).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_author_search_block_author_releases_non_empty() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_releases
|
||||
.set_items(vec![ReadarrRelease::default()]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualAuthorSearch)
|
||||
.await;
|
||||
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_book_details_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
app.data.readarr_data.books.set_items(vec![Book {
|
||||
id: 2,
|
||||
..Book::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookDetails)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetBookEditions(2).into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetBookFiles(2).into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_book_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
app.data.readarr_data.books.set_items(vec![Book {
|
||||
id: 2,
|
||||
..Book::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookHistory)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetBookHistory(1, 2).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_book_history_block_no_op_when_books_table_is_empty() {
|
||||
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookHistory)
|
||||
.await;
|
||||
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_book_search_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
app.data.readarr_data.books.set_items(vec![Book {
|
||||
id: 2,
|
||||
..Book::default()
|
||||
}]);
|
||||
app.data.readarr_data.book_details_modal = Some(BookDetailsModal::default());
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualBookSearch)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetBookReleases(2).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_book_search_block_is_loading() {
|
||||
let mut app = App {
|
||||
is_loading: true,
|
||||
..App::test_default()
|
||||
};
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualBookSearch)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_manual_book_search_block_book_releases_non_empty() {
|
||||
let mut app = App::test_default();
|
||||
let mut book_details_modal = BookDetailsModal::default();
|
||||
book_details_modal
|
||||
.book_releases
|
||||
.set_items(vec![ReadarrRelease::default()]);
|
||||
app.data.readarr_data.book_details_modal = Some(book_details_modal);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::ManualBookSearch)
|
||||
.await;
|
||||
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_book_edition_details_block_does_not_dispatch() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.readarr_data.prompt_confirm = false;
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::BookEditionDetails)
|
||||
.await;
|
||||
|
||||
assert!(!app.is_loading);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_downloads_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Downloads)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_readarr_block_add_author_search_results() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.add_author_search = Some("test author".into());
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::AddAuthorSearchResults)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::SearchNewAuthor("test author".to_owned()).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_history_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::History)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetHistory(500).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_root_folders_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::RootFolders)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetRootFolders.into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_indexers_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Indexers)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetIndexers.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_all_indexer_settings_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::AllIndexerSettingsPrompt)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetAllIndexerSettings.into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_test_indexer_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.indexers.set_items(vec![Indexer {
|
||||
id: 3,
|
||||
..Indexer::default()
|
||||
}]);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::TestIndexer)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::TestIndexer(3).into()
|
||||
);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_test_all_indexers_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::TestAllIndexers)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::TestAllIndexers.into()
|
||||
);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_system_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::System)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTasks.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetQueuedEvents.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetLogs(500).into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_system_updates_block() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::SystemUpdates)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetUpdates.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_readarr_block_resets_the_tick_count() {
|
||||
let mut app = App {
|
||||
tick_count: 2,
|
||||
..App::test_default()
|
||||
};
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Authors)
|
||||
.await;
|
||||
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_readarr_block_clears_the_pending_prompt_confirmation() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
|
||||
app
|
||||
.dispatch_by_readarr_block(&ActiveReadarrBlock::Authors)
|
||||
.await;
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_for_readarr_prompt_action_no_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = false;
|
||||
|
||||
app.check_for_readarr_prompt_action().await;
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_for_readarr_prompt_action_with_no_prompt_confirm_action() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
|
||||
app.check_for_readarr_prompt_action().await;
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_none!(app.data.readarr_data.prompt_confirm_action);
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_for_readarr_prompt_action() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.data.readarr_data.prompt_confirm_action = Some(ReadarrEvent::GetStatus);
|
||||
|
||||
app.check_for_readarr_prompt_action().await;
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetStatus.into());
|
||||
assert!(app.should_refresh);
|
||||
assert_eq!(app.data.readarr_data.prompt_confirm_action, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_refresh_metadata() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_routing = true;
|
||||
|
||||
app.refresh_readarr_metadata().await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetRootFolders.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetDiskSpace.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetStatus.into());
|
||||
assert!(app.is_loading);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_first_render() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_first_render = true;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetRootFolders.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetDiskSpace.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetStatus.into());
|
||||
assert!(app.is_loading);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert!(!app.is_first_render);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_routing() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_routing = true;
|
||||
app.should_refresh = true;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
|
||||
let (tx, _) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_routing = true;
|
||||
app.should_refresh = false;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert!(app.cancellation_token.is_cancelled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_should_refresh() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.should_refresh = true;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(app.should_refresh);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_should_refresh_does_not_cancel_prompt_requests() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_loading = true;
|
||||
app.is_routing = true;
|
||||
app.should_refresh = true;
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 1;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
assert!(app.should_refresh);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert!(!app.cancellation_token.is_cancelled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readarr_on_tick_network_tick_frequency() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.network_tx = Some(tx);
|
||||
app.is_first_render = false;
|
||||
app.tick_count = 2;
|
||||
app.tick_until_poll = 2;
|
||||
|
||||
app.readarr_on_tick(ActiveReadarrBlock::Downloads).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), ReadarrEvent::GetTags.into());
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetRootFolders.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
ReadarrEvent::GetDownloads(500).into()
|
||||
);
|
||||
assert!(app.is_loading);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_add_new_author_search_query() {
|
||||
let app = App::test_default_fully_populated();
|
||||
|
||||
let query = app.extract_add_new_author_search_query().await;
|
||||
|
||||
assert_str_eq!(query, "Test Author");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[should_panic(expected = "Add author search is empty")]
|
||||
async fn test_extract_add_new_author_search_query_panics_when_the_query_is_not_set() {
|
||||
let app = App::test_default();
|
||||
|
||||
app.extract_add_new_author_search_query().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_author_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.authors.set_items(vec![Author {
|
||||
id: 1,
|
||||
..Author::default()
|
||||
}]);
|
||||
|
||||
assert_eq!(app.extract_author_id().await, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_book_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.books.set_items(vec![Book {
|
||||
id: 2,
|
||||
..Book::default()
|
||||
}]);
|
||||
|
||||
assert_eq!(app.extract_book_id().await, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_readarr_indexer_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.indexers.set_items(vec![Indexer {
|
||||
id: 3,
|
||||
..Indexer::default()
|
||||
}]);
|
||||
|
||||
assert_eq!(app.extract_readarr_indexer_id().await, 3);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,9 @@ use crate::app::context_clues::{
|
||||
use crate::app::{App, context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ADD_SERIES_BLOCKS, ActiveSonarrBlock, EDIT_INDEXER_BLOCKS, EDIT_SERIES_BLOCKS,
|
||||
EPISODE_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS,
|
||||
ADD_SERIES_BLOCKS, ActiveSonarrBlock, DELETE_SERIES_BLOCKS, EDIT_INDEXER_BLOCKS,
|
||||
EDIT_SERIES_BLOCKS, EPISODE_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS, SEASON_DETAILS_BLOCKS,
|
||||
SERIES_DETAILS_BLOCKS, SERIES_OVERVIEW_BLOCKS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -38,7 +39,7 @@ pub static SERIES_CONTEXT_CLUES: [ContextClue; 11] = [
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
|
||||
];
|
||||
|
||||
pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
|
||||
pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 9] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
@@ -55,6 +56,15 @@ pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
|
||||
DEFAULT_KEYBINDINGS.auto_search,
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.view, "view overview"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
pub static SERIES_OVERVIEW_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
];
|
||||
|
||||
@@ -177,6 +187,9 @@ impl ContextClueProvider for SonarrContextClueProvider {
|
||||
.sonarr_data
|
||||
.series_info_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
_ if SERIES_OVERVIEW_BLOCKS.contains(&active_sonarr_block) => {
|
||||
Some(&SERIES_OVERVIEW_CONTEXT_CLUES)
|
||||
}
|
||||
_ if SEASON_DETAILS_BLOCKS.contains(&active_sonarr_block) => app
|
||||
.data
|
||||
.sonarr_data
|
||||
@@ -207,6 +220,9 @@ impl ContextClueProvider for SonarrContextClueProvider {
|
||||
{
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
_ if DELETE_SERIES_BLOCKS.contains(&active_sonarr_block) => {
|
||||
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesPrompt
|
||||
| ActiveSonarrBlock::AddSeriesSelectMonitor
|
||||
| ActiveSonarrBlock::AddSeriesSelectSeriesType
|
||||
|
||||
@@ -16,7 +16,7 @@ mod tests {
|
||||
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES,
|
||||
MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES,
|
||||
SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES,
|
||||
SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES,
|
||||
SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SERIES_OVERVIEW_CONTEXT_CLUES,
|
||||
},
|
||||
};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
@@ -187,6 +187,10 @@ mod tests {
|
||||
DEFAULT_KEYBINDINGS.auto_search.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
series_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.view, "view overview")
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
series_details_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
@@ -194,6 +198,24 @@ mod tests {
|
||||
assert_none!(series_details_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_series_overview_context_clues() {
|
||||
let mut series_overview_context_clues_iter = SERIES_OVERVIEW_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
series_overview_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
series_overview_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
|
||||
);
|
||||
assert_none!(series_overview_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_details_context_clues() {
|
||||
let mut season_details_context_clues_iter = SEASON_DETAILS_CONTEXT_CLUES.iter();
|
||||
@@ -442,6 +464,16 @@ mod tests {
|
||||
assert_some_eq_x!(context_clues, expected_context_clues);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sonarr_context_clue_provider_series_overview_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeriesOverview.into());
|
||||
|
||||
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &SERIES_OVERVIEW_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveSonarrBlock::SeasonDetails, &SEASON_DETAILS_CONTEXT_CLUES)]
|
||||
#[case(1, ActiveSonarrBlock::SeasonHistory, &SEASON_HISTORY_CONTEXT_CLUES)]
|
||||
@@ -603,6 +635,24 @@ mod tests {
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_sonarr_context_clue_provider_confirmation_prompt_popup_clues_delete_series_blocks(
|
||||
#[values(
|
||||
ActiveSonarrBlock::DeleteSeriesPrompt,
|
||||
ActiveSonarrBlock::DeleteSeriesConfirmPrompt,
|
||||
ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion,
|
||||
ActiveSonarrBlock::DeleteSeriesToggleDeleteFile
|
||||
)]
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(active_sonarr_block.into());
|
||||
|
||||
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_sonarr_context_clue_provider_add_series_search_results_clues(
|
||||
#[values(
|
||||
|
||||
@@ -613,10 +613,10 @@ mod tests {
|
||||
async fn test_handle_edit_all_indexer_settings_command() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 1,
|
||||
minimum_age: 1,
|
||||
retention: 1,
|
||||
rss_sync_interval: 1,
|
||||
maximum_size: 12345,
|
||||
minimum_age: 11,
|
||||
retention: 29,
|
||||
rss_sync_interval: 60,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
@@ -628,11 +628,11 @@ mod tests {
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
id: 5,
|
||||
maximum_size: 22222,
|
||||
minimum_age: 34,
|
||||
retention: 48,
|
||||
rss_sync_interval: 90,
|
||||
},
|
||||
)))
|
||||
});
|
||||
@@ -649,10 +649,10 @@ mod tests {
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_all_indexer_settings_command = LidarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
maximum_size: Some(12345),
|
||||
minimum_age: Some(11),
|
||||
retention: Some(29),
|
||||
rss_sync_interval: Some(60),
|
||||
};
|
||||
|
||||
let result = LidarrEditCommandHandler::with(
|
||||
|
||||
@@ -284,10 +284,8 @@ mod tests {
|
||||
use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand;
|
||||
use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand;
|
||||
use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand;
|
||||
use crate::models::lidarr_models::{
|
||||
BlocklistItem, BlocklistResponse, LidarrReleaseDownloadBody, LidarrTaskName,
|
||||
};
|
||||
use crate::models::servarr_models::IndexerSettings;
|
||||
use crate::models::lidarr_models::{BlocklistItem, BlocklistResponse, LidarrTaskName};
|
||||
use crate::models::servarr_models::{IndexerSettings, ReleaseDownloadBody};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
@@ -583,7 +581,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_release_command() {
|
||||
let expected_release_download_body = LidarrReleaseDownloadBody {
|
||||
let expected_release_download_body = ReleaseDownloadBody {
|
||||
guid: "guid".to_owned(),
|
||||
indexer_id: 1,
|
||||
};
|
||||
|
||||
@@ -631,7 +631,7 @@ mod tests {
|
||||
async fn test_handle_list_track_history_command() {
|
||||
let expected_artist_id = 1;
|
||||
let expected_album_id = 1;
|
||||
let expected_track_id = 1;
|
||||
let expected_track_id = 5;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
|
||||
@@ -18,7 +18,8 @@ use super::{CliCommandHandler, Command};
|
||||
use crate::cli::lidarr::manual_search_command_handler::{
|
||||
LidarrManualSearchCommand, LidarrManualSearchCommandHandler,
|
||||
};
|
||||
use crate::models::lidarr_models::{LidarrReleaseDownloadBody, LidarrTaskName};
|
||||
use crate::models::lidarr_models::LidarrTaskName;
|
||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
@@ -231,7 +232,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, '
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
LidarrCommand::DownloadRelease { guid, indexer_id } => {
|
||||
let params = LidarrReleaseDownloadBody { guid, indexer_id };
|
||||
let params = ReleaseDownloadBody { guid, indexer_id };
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(LidarrEvent::DownloadRelease(params).into())
|
||||
|
||||
@@ -6,6 +6,7 @@ use clap_complete::Shell;
|
||||
use indoc::indoc;
|
||||
use lidarr::{LidarrCliHandler, LidarrCommand};
|
||||
use radarr::{RadarrCliHandler, RadarrCommand};
|
||||
use readarr::{ReadarrCliHandler, ReadarrCommand};
|
||||
use sonarr::{SonarrCliHandler, SonarrCommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -13,6 +14,7 @@ use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
pub mod lidarr;
|
||||
pub mod radarr;
|
||||
pub mod readarr;
|
||||
pub mod sonarr;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -30,6 +32,9 @@ pub enum Command {
|
||||
#[command(subcommand, about = "Commands for manging your Lidarr instance")]
|
||||
Lidarr(LidarrCommand),
|
||||
|
||||
#[command(subcommand, about = "Commands for managing your Readarr instance")]
|
||||
Readarr(ReadarrCommand),
|
||||
|
||||
#[command(
|
||||
arg_required_else_help = true,
|
||||
about = "Generate shell completions for the Managarr CLI"
|
||||
@@ -78,6 +83,11 @@ pub(crate) async fn handle_command(
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
Command::Readarr(readarr_command) => {
|
||||
ReadarrCliHandler::with(app, readarr_command, network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
|
||||
@@ -833,13 +833,13 @@ mod tests {
|
||||
async fn test_handle_edit_all_indexer_settings_command() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
allow_hardcoded_subs: true,
|
||||
availability_delay: 1,
|
||||
availability_delay: 19,
|
||||
id: 1,
|
||||
maximum_size: 1,
|
||||
minimum_age: 1,
|
||||
maximum_size: 18400,
|
||||
minimum_age: 27,
|
||||
prefer_indexer_flags: true,
|
||||
retention: 1,
|
||||
rss_sync_interval: 1,
|
||||
retention: 44,
|
||||
rss_sync_interval: 60,
|
||||
whitelisted_hardcoded_subs: "test".into(),
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
@@ -853,13 +853,13 @@ mod tests {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
allow_hardcoded_subs: false,
|
||||
availability_delay: 2,
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
availability_delay: 31,
|
||||
id: 8,
|
||||
maximum_size: 22750,
|
||||
minimum_age: 52,
|
||||
prefer_indexer_flags: false,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
retention: 66,
|
||||
rss_sync_interval: 90,
|
||||
whitelisted_hardcoded_subs: "testing".into(),
|
||||
},
|
||||
)))
|
||||
@@ -879,13 +879,13 @@ mod tests {
|
||||
let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings {
|
||||
allow_hardcoded_subs: true,
|
||||
disable_allow_hardcoded_subs: false,
|
||||
availability_delay: Some(1),
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
availability_delay: Some(19),
|
||||
maximum_size: Some(18400),
|
||||
minimum_age: Some(27),
|
||||
prefer_indexer_flags: true,
|
||||
disable_prefer_indexer_flags: false,
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
retention: Some(44),
|
||||
rss_sync_interval: Some(60),
|
||||
whitelisted_subtitle_tags: Some("test".to_owned()),
|
||||
};
|
||||
|
||||
@@ -904,13 +904,13 @@ mod tests {
|
||||
async fn test_handle_edit_all_indexer_settings_command_disable_flags_function_properly() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
allow_hardcoded_subs: false,
|
||||
availability_delay: 1,
|
||||
availability_delay: 19,
|
||||
id: 1,
|
||||
maximum_size: 1,
|
||||
minimum_age: 1,
|
||||
maximum_size: 18400,
|
||||
minimum_age: 27,
|
||||
prefer_indexer_flags: false,
|
||||
retention: 1,
|
||||
rss_sync_interval: 1,
|
||||
retention: 44,
|
||||
rss_sync_interval: 60,
|
||||
whitelisted_hardcoded_subs: "test".into(),
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
@@ -924,13 +924,13 @@ mod tests {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
allow_hardcoded_subs: true,
|
||||
availability_delay: 2,
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
availability_delay: 31,
|
||||
id: 8,
|
||||
maximum_size: 22750,
|
||||
minimum_age: 52,
|
||||
prefer_indexer_flags: true,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
retention: 66,
|
||||
rss_sync_interval: 90,
|
||||
whitelisted_hardcoded_subs: "testing".into(),
|
||||
},
|
||||
)))
|
||||
@@ -950,13 +950,13 @@ mod tests {
|
||||
let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings {
|
||||
allow_hardcoded_subs: false,
|
||||
disable_allow_hardcoded_subs: true,
|
||||
availability_delay: Some(1),
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
availability_delay: Some(19),
|
||||
maximum_size: Some(18400),
|
||||
minimum_age: Some(27),
|
||||
prefer_indexer_flags: false,
|
||||
disable_prefer_indexer_flags: true,
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
retention: Some(44),
|
||||
rss_sync_interval: Some(60),
|
||||
whitelisted_subtitle_tags: Some("test".to_owned()),
|
||||
};
|
||||
|
||||
@@ -976,13 +976,13 @@ mod tests {
|
||||
{
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
allow_hardcoded_subs: true,
|
||||
availability_delay: 2,
|
||||
availability_delay: 31,
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
prefer_indexer_flags: true,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
maximum_size: 22750,
|
||||
minimum_age: 52,
|
||||
prefer_indexer_flags: false,
|
||||
retention: 66,
|
||||
rss_sync_interval: 90,
|
||||
whitelisted_hardcoded_subs: "testing".into(),
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
@@ -996,13 +996,13 @@ mod tests {
|
||||
Ok(Serdeable::Radarr(RadarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
allow_hardcoded_subs: true,
|
||||
availability_delay: 2,
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
prefer_indexer_flags: true,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
availability_delay: 31,
|
||||
id: 8,
|
||||
maximum_size: 22750,
|
||||
minimum_age: 52,
|
||||
prefer_indexer_flags: false,
|
||||
retention: 66,
|
||||
rss_sync_interval: 90,
|
||||
whitelisted_hardcoded_subs: "testing".into(),
|
||||
},
|
||||
)))
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::ReadarrCommand;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::readarr_models::{
|
||||
AddAuthorBody, AddAuthorOptions, AddReadarrRootFolderBody, MonitorType, NewItemMonitorType,
|
||||
},
|
||||
network::{NetworkTrait, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "add_command_handler_tests.rs"]
|
||||
mod add_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrAddCommand {
|
||||
#[command(about = "Add a new author to your Readarr library")]
|
||||
Author {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Goodreads foreign author ID of the author you wish to add to your library",
|
||||
required = true
|
||||
)]
|
||||
foreign_author_id: String,
|
||||
#[arg(long, help = "The name of the author", required = true)]
|
||||
author_name: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The root folder path where all author data and metadata should live",
|
||||
required = true
|
||||
)]
|
||||
root_folder_path: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the quality profile to use for this author",
|
||||
required = true
|
||||
)]
|
||||
quality_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the metadata profile to use for this author",
|
||||
required = true
|
||||
)]
|
||||
metadata_profile_id: i64,
|
||||
#[arg(long, help = "Disable monitoring for this author")]
|
||||
disable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tag IDs to tag the author with",
|
||||
value_parser,
|
||||
action = ArgAction::Append
|
||||
)]
|
||||
tag: Vec<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "What Readarr should monitor for this author",
|
||||
value_enum,
|
||||
default_value_t = MonitorType::default()
|
||||
)]
|
||||
monitor: MonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "How Readarr should monitor new items for this author",
|
||||
value_enum,
|
||||
default_value_t = NewItemMonitorType::default()
|
||||
)]
|
||||
monitor_new_items: NewItemMonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tell Readarr to not start a search for missing books once the author is added to your library"
|
||||
)]
|
||||
no_search_for_missing_books: bool,
|
||||
},
|
||||
#[command(about = "Add a new root folder")]
|
||||
RootFolder {
|
||||
#[arg(long, help = "The name of the root folder", required = true)]
|
||||
name: String,
|
||||
#[arg(long, help = "The path of the new root folder", required = true)]
|
||||
root_folder_path: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the default quality profile for authors in this root folder",
|
||||
required = true
|
||||
)]
|
||||
quality_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the default metadata profile for authors in this root folder",
|
||||
required = true
|
||||
)]
|
||||
metadata_profile_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The default monitor option for authors in this root folder",
|
||||
value_enum,
|
||||
default_value_t = MonitorType::default()
|
||||
)]
|
||||
monitor: MonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The default monitor new items option for authors in this root folder",
|
||||
value_enum,
|
||||
default_value_t = NewItemMonitorType::default()
|
||||
)]
|
||||
monitor_new_items: NewItemMonitorType,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Default tag IDs for authors in this root folder",
|
||||
value_parser,
|
||||
action = ArgAction::Append
|
||||
)]
|
||||
tag: Vec<i64>,
|
||||
},
|
||||
#[command(about = "Add new tag")]
|
||||
Tag {
|
||||
#[arg(long, help = "The name of the tag to be added", required = true)]
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ReadarrAddCommand> for Command {
|
||||
fn from(value: ReadarrAddCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::Add(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrAddCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrAddCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrAddCommand> for ReadarrAddCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrAddCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrAddCommandHandler {
|
||||
_app: app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrAddCommand::Author {
|
||||
foreign_author_id,
|
||||
author_name,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
disable_monitoring,
|
||||
tag: tags,
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
no_search_for_missing_books,
|
||||
} => {
|
||||
let body = AddAuthorBody {
|
||||
foreign_author_id,
|
||||
author_name,
|
||||
monitored: !disable_monitoring,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
tags,
|
||||
tag_input_string: None,
|
||||
add_options: AddAuthorOptions {
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
search_for_missing_books: !no_search_for_missing_books,
|
||||
},
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::AddAuthor(body).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrAddCommand::RootFolder {
|
||||
name,
|
||||
root_folder_path,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
monitor,
|
||||
monitor_new_items,
|
||||
tag: tags,
|
||||
} => {
|
||||
let add_root_folder_body = AddReadarrRootFolderBody {
|
||||
name,
|
||||
path: root_folder_path,
|
||||
default_quality_profile_id: quality_profile_id,
|
||||
default_metadata_profile_id: metadata_profile_id,
|
||||
default_monitor_option: monitor,
|
||||
default_new_item_monitor_option: monitor_new_items,
|
||||
default_tags: tags,
|
||||
tag_input_string: None,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::AddRootFolder(add_root_folder_body).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrAddCommand::Tag { name } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::AddTag(name).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||
|
||||
use crate::{
|
||||
Cli,
|
||||
cli::{
|
||||
Command,
|
||||
readarr::{ReadarrCommand, add_command_handler::ReadarrAddCommand},
|
||||
},
|
||||
models::readarr_models::{MonitorType, NewItemMonitorType},
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_add_command_from() {
|
||||
let command = ReadarrAddCommand::Tag {
|
||||
name: String::new(),
|
||||
};
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Readarr(ReadarrCommand::Add(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "add", "author"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_foreign_author_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--author-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_author_name() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_root_folder_path() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_quality_profile_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_requires_metadata_profile_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_success_with_required_args_only() {
|
||||
let expected_args = ReadarrAddCommand::Author {
|
||||
foreign_author_id: "test-id".to_owned(),
|
||||
author_name: "Test Author".to_owned(),
|
||||
root_folder_path: "/nfs/books".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
disable_monitoring: false,
|
||||
tag: vec![],
|
||||
monitor: MonitorType::default(),
|
||||
monitor_new_items: NewItemMonitorType::default(),
|
||||
no_search_for_missing_books: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test Author",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_success_with_all_args() {
|
||||
let expected_args = ReadarrAddCommand::Author {
|
||||
foreign_author_id: "test-id".to_owned(),
|
||||
author_name: "Test Author".to_owned(),
|
||||
root_folder_path: "/nfs/books".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 2,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::Future,
|
||||
monitor_new_items: NewItemMonitorType::New,
|
||||
no_search_for_missing_books: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test Author",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--disable-monitoring",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
"--monitor",
|
||||
"future",
|
||||
"--monitor-new-items",
|
||||
"new",
|
||||
"--no-search-for-missing-books",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_monitor_type_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test Author",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--monitor",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_new_item_monitor_type_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test Author",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--monitor-new-items",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_author_tags_is_repeatable() {
|
||||
let expected_args = ReadarrAddCommand::Author {
|
||||
foreign_author_id: "test-id".to_owned(),
|
||||
author_name: "Test Author".to_owned(),
|
||||
root_folder_path: "/nfs/books".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 2,
|
||||
disable_monitoring: false,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::default(),
|
||||
monitor_new_items: NewItemMonitorType::default(),
|
||||
no_search_for_missing_books: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"author",
|
||||
"--foreign-author-id",
|
||||
"test-id",
|
||||
"--author-name",
|
||||
"Test Author",
|
||||
"--root-folder-path",
|
||||
"/nfs/books",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"2",
|
||||
"--tag",
|
||||
"1",
|
||||
"--tag",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type")
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_root_folder_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "add", "root-folder"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_root_folder_success() {
|
||||
let expected_args = ReadarrAddCommand::RootFolder {
|
||||
name: "Books".to_owned(),
|
||||
root_folder_path: "/nfs/test".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
tag: vec![],
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"add",
|
||||
"root-folder",
|
||||
"--name",
|
||||
"Books",
|
||||
"--root-folder-path",
|
||||
"/nfs/test",
|
||||
"--quality-profile-id",
|
||||
"1",
|
||||
"--metadata-profile-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "add", "tag"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tag_success() {
|
||||
let expected_args = ReadarrAddCommand::Tag {
|
||||
name: "test".to_owned(),
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "add", "tag", "--name", "test"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Add(add_command))) = result.unwrap().command else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(add_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::cli::readarr::add_command_handler::{ReadarrAddCommand, ReadarrAddCommandHandler};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::readarr_models::{
|
||||
AddAuthorBody, AddAuthorOptions, AddReadarrRootFolderBody, MonitorType, NewItemMonitorType,
|
||||
ReadarrSerdeable,
|
||||
};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_author_command() {
|
||||
let expected_body = AddAuthorBody {
|
||||
foreign_author_id: "test-id".to_owned(),
|
||||
author_name: "Test Author".to_owned(),
|
||||
monitored: false,
|
||||
root_folder_path: "/nfs/books".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
add_options: AddAuthorOptions {
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
search_for_missing_books: false,
|
||||
},
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::AddAuthor(expected_body).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_author_command = ReadarrAddCommand::Author {
|
||||
foreign_author_id: "test-id".to_owned(),
|
||||
author_name: "Test Author".to_owned(),
|
||||
root_folder_path: "/nfs/books".to_owned(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
disable_monitoring: true,
|
||||
tag: vec![1, 2],
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
no_search_for_missing_books: true,
|
||||
};
|
||||
|
||||
let result = ReadarrAddCommandHandler::with(&app_arc, add_author_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_root_folder_command() {
|
||||
let expected_root_folder_path = "/nfs/test".to_owned();
|
||||
let expected_add_root_folder_body = AddReadarrRootFolderBody {
|
||||
name: "Books".to_owned(),
|
||||
path: expected_root_folder_path.clone(),
|
||||
default_quality_profile_id: 1,
|
||||
default_metadata_profile_id: 1,
|
||||
default_monitor_option: MonitorType::All,
|
||||
default_new_item_monitor_option: NewItemMonitorType::All,
|
||||
default_tags: vec![1, 2],
|
||||
tag_input_string: None,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::AddRootFolder(expected_add_root_folder_body.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_root_folder_command = ReadarrAddCommand::RootFolder {
|
||||
name: "Books".to_owned(),
|
||||
root_folder_path: expected_root_folder_path,
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
tag: vec![1, 2],
|
||||
};
|
||||
|
||||
let result =
|
||||
ReadarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_tag_command() {
|
||||
let expected_tag_name = "test".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::AddTag(expected_tag_name.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let add_tag_command = ReadarrAddCommand::Tag {
|
||||
name: expected_tag_name,
|
||||
};
|
||||
|
||||
let result = ReadarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::ReadarrCommand;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
models::readarr_models::DeleteParams,
|
||||
network::{NetworkTrait, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "delete_command_handler_tests.rs"]
|
||||
mod delete_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrDeleteCommand {
|
||||
#[command(about = "Delete an author from your Readarr library")]
|
||||
Author {
|
||||
#[arg(long, help = "The ID of the author to delete", required = true)]
|
||||
author_id: i64,
|
||||
#[arg(long, help = "Delete the author files from disk as well")]
|
||||
delete_files_from_disk: bool,
|
||||
#[arg(long, help = "Add a list exclusion for this author")]
|
||||
add_list_exclusion: bool,
|
||||
},
|
||||
#[command(about = "Delete the specified item from the Readarr blocklist")]
|
||||
BlocklistItem {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the blocklist item to remove from the blocklist",
|
||||
required = true
|
||||
)]
|
||||
blocklist_item_id: i64,
|
||||
},
|
||||
#[command(about = "Delete a book from your Readarr library")]
|
||||
Book {
|
||||
#[arg(long, help = "The ID of the book to delete", required = true)]
|
||||
book_id: i64,
|
||||
#[arg(long, help = "Delete the book files from disk as well")]
|
||||
delete_files_from_disk: bool,
|
||||
#[arg(long, help = "Add a list exclusion for this book")]
|
||||
add_list_exclusion: bool,
|
||||
},
|
||||
#[command(about = "Delete the specified book file from disk")]
|
||||
BookFile {
|
||||
#[arg(long, help = "The ID of the book file to delete", required = true)]
|
||||
book_file_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the specified download")]
|
||||
Download {
|
||||
#[arg(long, help = "The ID of the download to delete", required = true)]
|
||||
download_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the indexer with the given ID")]
|
||||
Indexer {
|
||||
#[arg(long, help = "The ID of the indexer to delete", required = true)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(about = "Delete the root folder with the given ID")]
|
||||
RootFolder {
|
||||
#[arg(long, help = "The ID of the root folder to delete", required = true)]
|
||||
root_folder_id: i64,
|
||||
},
|
||||
#[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<ReadarrDeleteCommand> for Command {
|
||||
fn from(value: ReadarrDeleteCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::Delete(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrDeleteCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrDeleteCommand>
|
||||
for ReadarrDeleteCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrDeleteCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrDeleteCommandHandler {
|
||||
_app: app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrDeleteCommand::Author {
|
||||
author_id,
|
||||
delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
} => {
|
||||
let delete_author_params = DeleteParams {
|
||||
id: author_id,
|
||||
delete_files: delete_files_from_disk,
|
||||
add_import_list_exclusion: add_list_exclusion,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteAuthor(delete_author_params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteBlocklistItem(blocklist_item_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::Book {
|
||||
book_id,
|
||||
delete_files_from_disk,
|
||||
add_list_exclusion,
|
||||
} => {
|
||||
let delete_book_params = DeleteParams {
|
||||
id: book_id,
|
||||
delete_files: delete_files_from_disk,
|
||||
add_import_list_exclusion: add_list_exclusion,
|
||||
};
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteBook(delete_book_params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::BookFile { book_file_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteBookFile(book_file_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::Download { download_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteDownload(download_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::Indexer { indexer_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteIndexer(indexer_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::RootFolder { root_folder_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteRootFolder(root_folder_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrDeleteCommand::Tag { tag_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DeleteTag(tag_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{CommandFactory, Parser, error::ErrorKind};
|
||||
|
||||
use crate::{
|
||||
Cli,
|
||||
cli::{
|
||||
Command,
|
||||
readarr::{ReadarrCommand, delete_command_handler::ReadarrDeleteCommand},
|
||||
},
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_delete_command_from() {
|
||||
let command = ReadarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Readarr(ReadarrCommand::Delete(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_delete_author_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "author"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_author_author_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"author",
|
||||
"--author-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_author_defaults() {
|
||||
let expected_args = ReadarrDeleteCommand::Author {
|
||||
author_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"author",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_author_all_args_defined() {
|
||||
let expected_args = ReadarrDeleteCommand::Author {
|
||||
author_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"author",
|
||||
"--author-id",
|
||||
"1",
|
||||
"--delete-files-from-disk",
|
||||
"--add-list-exclusion",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "blocklist-item"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_blocklist_item_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"blocklist-item",
|
||||
"--blocklist-item-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_success() {
|
||||
let expected_args = ReadarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 7,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"blocklist-item",
|
||||
"--blocklist-item-id",
|
||||
"7",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "book"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_book_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"book",
|
||||
"--book-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_defaults() {
|
||||
let expected_args = ReadarrDeleteCommand::Book {
|
||||
book_id: 1,
|
||||
delete_files_from_disk: false,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "delete", "book", "--book-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_all_args_defined() {
|
||||
let expected_args = ReadarrDeleteCommand::Book {
|
||||
book_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"book",
|
||||
"--book-id",
|
||||
"1",
|
||||
"--delete-files-from-disk",
|
||||
"--add-list-exclusion",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_file_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "book-file"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_file_book_file_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"book-file",
|
||||
"--book-file-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_book_file_success() {
|
||||
let expected_args = ReadarrDeleteCommand::BookFile { book_file_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"book-file",
|
||||
"--book-file-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "download"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_download_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"download",
|
||||
"--download-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_success() {
|
||||
let expected_args = ReadarrDeleteCommand::Download { download_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"download",
|
||||
"--download-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "indexer"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_indexer_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_success() {
|
||||
let expected_args = ReadarrDeleteCommand::Indexer { indexer_id: 8 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"indexer",
|
||||
"--indexer-id",
|
||||
"8",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_root_folder_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "root-folder"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_root_folder_root_folder_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"root-folder",
|
||||
"--root-folder-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_root_folder_success() {
|
||||
let expected_args = ReadarrDeleteCommand::RootFolder { root_folder_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"root-folder",
|
||||
"--root-folder-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "delete", "tag"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tag_tag_id_requires_a_number() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"delete",
|
||||
"tag",
|
||||
"--tag-id",
|
||||
"not_a_number",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::ValueValidation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tag_success() {
|
||||
let expected_args = ReadarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "delete", "tag", "--tag-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::Delete(delete_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(delete_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::cli::readarr::delete_command_handler::{
|
||||
ReadarrDeleteCommand, ReadarrDeleteCommandHandler,
|
||||
};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::readarr_models::{DeleteParams, ReadarrSerdeable};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_author_command() {
|
||||
let expected_delete_author_params = DeleteParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::DeleteAuthor(expected_delete_author_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_author_command = ReadarrDeleteCommand::Author {
|
||||
author_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_author_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_book_command() {
|
||||
let expected_delete_book_params = DeleteParams {
|
||||
id: 1,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: false,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::DeleteBook(expected_delete_book_params).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_book_command = ReadarrDeleteCommand::Book {
|
||||
book_id: 1,
|
||||
delete_files_from_disk: true,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_book_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_blocklist_item_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::DeleteBlocklistItem(7).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_blocklist_item_command = ReadarrDeleteCommand::BlocklistItem {
|
||||
blocklist_item_id: 7,
|
||||
};
|
||||
|
||||
let result = ReadarrDeleteCommandHandler::with(
|
||||
&app_arc,
|
||||
delete_blocklist_item_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_book_file_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteBookFile(1).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_book_file_command = ReadarrDeleteCommand::BookFile { book_file_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_book_file_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_download_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteDownload(1).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_download_command = ReadarrDeleteCommand::Download { download_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_indexer_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteIndexer(8).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_indexer_command = ReadarrDeleteCommand::Indexer { indexer_id: 8 };
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_indexer_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_root_folder_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteRootFolder(1).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_root_folder_command = ReadarrDeleteCommand::RootFolder { root_folder_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_root_folder_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_tag_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::DeleteTag(1).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let delete_tag_command = ReadarrDeleteCommand::Tag { tag_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, ArgGroup, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::ReadarrCommand;
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::readarr_models::ReadarrSerdeable;
|
||||
use crate::models::servarr_models::{EditIndexerParams, IndexerSettings};
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command, mutex_flags_or_option},
|
||||
models::readarr_models::{EditAuthorParams, NewItemMonitorType},
|
||||
network::{NetworkTrait, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "edit_command_handler_tests.rs"]
|
||||
mod edit_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrEditCommand {
|
||||
#[command(
|
||||
about = "Edit the indexer settings that apply to all indexers",
|
||||
group(
|
||||
ArgGroup::new("edit_settings")
|
||||
.args([
|
||||
"maximum_size",
|
||||
"minimum_age",
|
||||
"retention",
|
||||
"rss_sync_interval",
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
AllIndexerSettings {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited"
|
||||
)]
|
||||
maximum_size: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
|
||||
)]
|
||||
minimum_age: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Usenet only: The retention time in days to retain releases. Set to zero to set for unlimited retention"
|
||||
)]
|
||||
retention: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The RSS sync interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
|
||||
)]
|
||||
rss_sync_interval: Option<i64>,
|
||||
},
|
||||
#[command(
|
||||
about = "Edit preferences for the specified author",
|
||||
group(
|
||||
ArgGroup::new("edit_author")
|
||||
.args([
|
||||
"enable_monitoring",
|
||||
"disable_monitoring",
|
||||
"monitor_new_items",
|
||||
"quality_profile_id",
|
||||
"metadata_profile_id",
|
||||
"root_folder_path",
|
||||
"tag",
|
||||
"clear_tags"
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
Author {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the author whose settings you want to edit",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Enable monitoring of this author in Readarr so Readarr will automatically download books from this author if they are available",
|
||||
conflicts_with = "disable_monitoring"
|
||||
)]
|
||||
enable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable monitoring of this author so Readarr does not automatically download books from this author if they are available",
|
||||
conflicts_with = "enable_monitoring"
|
||||
)]
|
||||
disable_monitoring: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "How Readarr should monitor new books from this author",
|
||||
value_enum
|
||||
)]
|
||||
monitor_new_items: Option<NewItemMonitorType>,
|
||||
#[arg(long, help = "The ID of the quality profile to use for this author")]
|
||||
quality_profile_id: Option<i64>,
|
||||
#[arg(long, help = "The ID of the metadata profile to use for this author")]
|
||||
metadata_profile_id: Option<i64>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The root folder path where all author data and metadata should live"
|
||||
)]
|
||||
root_folder_path: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Tag IDs to tag this author with",
|
||||
value_parser,
|
||||
action = ArgAction::Append,
|
||||
conflicts_with = "clear_tags"
|
||||
)]
|
||||
tag: Option<Vec<i64>>,
|
||||
#[arg(long, help = "Clear all tags on this author", conflicts_with = "tag")]
|
||||
clear_tags: bool,
|
||||
},
|
||||
#[command(
|
||||
about = "Edit preferences for the specified indexer",
|
||||
group(
|
||||
ArgGroup::new("edit_indexer")
|
||||
.args([
|
||||
"name",
|
||||
"enable_rss",
|
||||
"disable_rss",
|
||||
"enable_automatic_search",
|
||||
"disable_automatic_search",
|
||||
"enable_interactive_search",
|
||||
"disable_interactive_search",
|
||||
"url",
|
||||
"api_key",
|
||||
"seed_ratio",
|
||||
"tag",
|
||||
"priority",
|
||||
"clear_tags"
|
||||
]).required(true)
|
||||
.multiple(true))
|
||||
)]
|
||||
Indexer {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the indexer whose settings you wish to edit",
|
||||
required = true
|
||||
)]
|
||||
indexer_id: i64,
|
||||
#[arg(long, help = "The name of the indexer")]
|
||||
name: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Readarr that this indexer should be used when Readarr periodically looks for releases via RSS Sync",
|
||||
conflicts_with = "disable_rss"
|
||||
)]
|
||||
enable_rss: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer when Readarr periodically looks for releases via RSS Sync",
|
||||
conflicts_with = "enable_rss"
|
||||
)]
|
||||
disable_rss: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Readarr that this indexer should be used when automatic searches are performed via the UI or by Readarr",
|
||||
conflicts_with = "disable_automatic_search"
|
||||
)]
|
||||
enable_automatic_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer whenever automatic searches are performed via the UI or by Readarr",
|
||||
conflicts_with = "enable_automatic_search"
|
||||
)]
|
||||
disable_automatic_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indicate to Readarr that this indexer should be used when an interactive search is used",
|
||||
conflicts_with = "disable_interactive_search"
|
||||
)]
|
||||
enable_interactive_search: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Disable using this indexer whenever an interactive search is performed",
|
||||
conflicts_with = "enable_interactive_search"
|
||||
)]
|
||||
disable_interactive_search: bool,
|
||||
#[arg(long, help = "The URL of the indexer")]
|
||||
url: Option<String>,
|
||||
#[arg(long, help = "The API key used to access the indexer's API")]
|
||||
api_key: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ratio a torrent should reach before stopping; Empty uses the download client's default. Ratio should be at least 1.0 and follow the indexer's rules"
|
||||
)]
|
||||
seed_ratio: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Only use this indexer for authors with at least one matching tag ID. Leave blank to use with all authors.",
|
||||
value_parser,
|
||||
action = ArgAction::Append,
|
||||
conflicts_with = "clear_tags"
|
||||
)]
|
||||
tag: Option<Vec<i64>>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Readarr will still use all enabled indexers for RSS Sync and Searching"
|
||||
)]
|
||||
priority: Option<i64>,
|
||||
#[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")]
|
||||
clear_tags: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ReadarrEditCommand> for Command {
|
||||
fn from(value: ReadarrEditCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::Edit(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrEditCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrEditCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrEditCommand> for ReadarrEditCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrEditCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrEditCommandHandler {
|
||||
_app: app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrEditCommand::AllIndexerSettings {
|
||||
maximum_size,
|
||||
minimum_age,
|
||||
retention,
|
||||
rss_sync_interval,
|
||||
} => {
|
||||
if let Serdeable::Readarr(ReadarrSerdeable::IndexerSettings(previous_indexer_settings)) =
|
||||
self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetAllIndexerSettings.into())
|
||||
.await?
|
||||
{
|
||||
let params = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size),
|
||||
minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age),
|
||||
retention: retention.unwrap_or(previous_indexer_settings.retention),
|
||||
rss_sync_interval: rss_sync_interval
|
||||
.unwrap_or(previous_indexer_settings.rss_sync_interval),
|
||||
};
|
||||
self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::EditAllIndexerSettings(params).into())
|
||||
.await?;
|
||||
"All indexer settings updated".to_owned()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
ReadarrEditCommand::Author {
|
||||
author_id,
|
||||
enable_monitoring,
|
||||
disable_monitoring,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tag,
|
||||
clear_tags,
|
||||
} => {
|
||||
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
|
||||
let edit_author_params = EditAuthorParams {
|
||||
author_id,
|
||||
monitored: monitored_value,
|
||||
monitor_new_items,
|
||||
quality_profile_id,
|
||||
metadata_profile_id,
|
||||
root_folder_path,
|
||||
tags: tag,
|
||||
tag_input_string: None,
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::EditAuthor(edit_author_params).into())
|
||||
.await?;
|
||||
"Author updated".to_owned()
|
||||
}
|
||||
ReadarrEditCommand::Indexer {
|
||||
indexer_id,
|
||||
name,
|
||||
enable_rss,
|
||||
disable_rss,
|
||||
enable_automatic_search,
|
||||
disable_automatic_search,
|
||||
enable_interactive_search,
|
||||
disable_interactive_search,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tag,
|
||||
priority,
|
||||
clear_tags,
|
||||
} => {
|
||||
let rss_value = mutex_flags_or_option(enable_rss, disable_rss);
|
||||
let automatic_search_value =
|
||||
mutex_flags_or_option(enable_automatic_search, disable_automatic_search);
|
||||
let interactive_search_value =
|
||||
mutex_flags_or_option(enable_interactive_search, disable_interactive_search);
|
||||
let edit_indexer_params = EditIndexerParams {
|
||||
indexer_id,
|
||||
name,
|
||||
enable_rss: rss_value,
|
||||
enable_automatic_search: automatic_search_value,
|
||||
enable_interactive_search: interactive_search_value,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tags: tag,
|
||||
tag_input_string: None,
|
||||
priority,
|
||||
clear_tags,
|
||||
};
|
||||
|
||||
self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::EditIndexer(edit_indexer_params).into())
|
||||
.await?;
|
||||
"Indexer updated".to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,122 @@
|
||||
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 shared settings for all indexers")]
|
||||
AllIndexerSettings,
|
||||
#[command(about = "Get detailed information for the author with the given ID")]
|
||||
AuthorDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "Get detailed information for the book with the given ID")]
|
||||
BookDetails {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book whose details you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
#[command(about = "Fetch the host config for your Readarr instance")]
|
||||
HostConfig,
|
||||
#[command(about = "Fetch the security config for your Readarr instance")]
|
||||
SecurityConfig,
|
||||
#[command(about = "Get the system status")]
|
||||
SystemStatus,
|
||||
}
|
||||
|
||||
impl From<ReadarrGetCommand> for Command {
|
||||
fn from(value: ReadarrGetCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::Get(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrGetCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrGetCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrGetCommand> for ReadarrGetCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrGetCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrGetCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrGetCommand::AllIndexerSettings => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetAllIndexerSettings.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrGetCommand::AuthorDetails { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetAuthorDetails(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrGetCommand::BookDetails { book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBookDetails(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrGetCommand::HostConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetHostConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrGetCommand::SecurityConfig => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetSecurityConfig.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrGetCommand::SystemStatus => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetStatus.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
#[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 clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_all_indexer_settings_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "get", "all-indexer-settings"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_requires_author_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "get", "author-details"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"get",
|
||||
"author-details",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_details_requires_book_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "get", "book-details"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_details_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"get",
|
||||
"book-details",
|
||||
"--book-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_get_commands_have_no_arg_requirements(
|
||||
#[values("host-config", "security-config", "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_all_indexer_settings_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetAllIndexerSettings.into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_all_indexer_settings_command = ReadarrGetCommand::AllIndexerSettings;
|
||||
|
||||
let result = ReadarrGetCommandHandler::with(
|
||||
&app_arc,
|
||||
get_all_indexer_settings_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_author_details_command() {
|
||||
let expected_author_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetAuthorDetails(expected_author_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_author_details_command = ReadarrGetCommand::AuthorDetails { author_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrGetCommandHandler::with(&app_arc, get_author_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_book_details_command() {
|
||||
let expected_book_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBookDetails(expected_book_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_book_details_command = ReadarrGetCommand::BookDetails { book_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrGetCommandHandler::with(&app_arc, get_book_details_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_host_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetHostConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_host_config_command = ReadarrGetCommand::HostConfig;
|
||||
|
||||
let result =
|
||||
ReadarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_security_config_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetSecurityConfig.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let get_security_config_command = ReadarrGetCommand::SecurityConfig;
|
||||
|
||||
let result =
|
||||
ReadarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_system_status_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::ReadarrCommand;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{CliCommandHandler, Command},
|
||||
network::{NetworkTrait, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "list_command_handler_tests.rs"]
|
||||
mod list_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrListCommand {
|
||||
#[command(about = "Fetch all history events for the author with the given ID")]
|
||||
AuthorHistory {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author whose history you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "Trigger a manual search for releases of the author with the given ID")]
|
||||
AuthorReleases {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author whose releases you wish to search for",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "List all authors in your Readarr library")]
|
||||
Authors,
|
||||
#[command(about = "List all items in the Readarr blocklist")]
|
||||
Blocklist,
|
||||
#[command(about = "List all editions for the book with the given ID")]
|
||||
BookEditions {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book whose editions you want to list",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
#[command(about = "List all book files for the book with the given ID")]
|
||||
BookFiles {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book whose book files you want to list",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
#[command(about = "Fetch all history events for the book with the given ID")]
|
||||
BookHistory {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author who wrote the book whose history you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book whose history you wish to fetch",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
#[command(about = "List all books for the author with the given ID")]
|
||||
Books {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author whose books you want to list",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "List disk space details for all provisioned root folders in Readarr")]
|
||||
DiskSpace,
|
||||
#[command(about = "List all active downloads in Readarr")]
|
||||
Downloads {
|
||||
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
|
||||
count: u64,
|
||||
},
|
||||
#[command(about = "Fetch all Readarr history events")]
|
||||
History {
|
||||
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
|
||||
events: u64,
|
||||
},
|
||||
#[command(about = "List all Readarr indexers")]
|
||||
Indexers,
|
||||
#[command(about = "Fetch Readarr logs")]
|
||||
Logs {
|
||||
#[arg(long, help = "How many log events to fetch", default_value_t = 500)]
|
||||
events: u64,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Output the logs in the same format as they appear in the log files"
|
||||
)]
|
||||
output_in_log_format: bool,
|
||||
},
|
||||
#[command(about = "List all Readarr metadata profiles")]
|
||||
MetadataProfiles,
|
||||
#[command(about = "List all Readarr quality profiles")]
|
||||
QualityProfiles,
|
||||
#[command(about = "List all queued events")]
|
||||
QueuedEvents,
|
||||
#[command(about = "Trigger a manual search for releases of the book with the given ID")]
|
||||
Releases {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book whose releases you wish to search for",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
#[command(about = "List all root folders in Readarr")]
|
||||
RootFolders,
|
||||
#[command(about = "List all Readarr tags")]
|
||||
Tags,
|
||||
#[command(about = "List all Readarr tasks")]
|
||||
Tasks,
|
||||
#[command(about = "List all Readarr updates")]
|
||||
Updates,
|
||||
}
|
||||
|
||||
impl From<ReadarrListCommand> for Command {
|
||||
fn from(value: ReadarrListCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::List(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrListCommandHandler<'a, 'b> {
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrListCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrListCommand> for ReadarrListCommandHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrListCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrListCommandHandler {
|
||||
app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrListCommand::AuthorHistory { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetAuthorHistory(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::AuthorReleases { author_id } => {
|
||||
println!("Searching for author releases. This may take a minute...");
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetAuthorReleases(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Authors => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::ListAuthors.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Blocklist => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::BookEditions { book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBookEditions(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::BookFiles { book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBookFiles(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::BookHistory { author_id, book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBookHistory(author_id, book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Books { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBooks(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::DiskSpace => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetDiskSpace.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Downloads { count } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetDownloads(count).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::History { events } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetHistory(events).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Indexers => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetIndexers.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Logs {
|
||||
events,
|
||||
output_in_log_format,
|
||||
} => {
|
||||
let logs = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetLogs(events).into())
|
||||
.await?;
|
||||
|
||||
if output_in_log_format {
|
||||
let log_lines = &self.app.lock().await.data.readarr_data.logs.items;
|
||||
|
||||
serde_json::to_string_pretty(log_lines)?
|
||||
} else {
|
||||
serde_json::to_string_pretty(&logs)?
|
||||
}
|
||||
}
|
||||
ReadarrListCommand::MetadataProfiles => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetMetadataProfiles.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::QualityProfiles => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetQualityProfiles.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::QueuedEvents => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetQueuedEvents.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Releases { book_id } => {
|
||||
println!("Searching for book releases. This may take a minute...");
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBookReleases(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::RootFolders => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetRootFolders.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Tags => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetTags.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrListCommand::Tasks => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetTasks.into())
|
||||
.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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,958 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
readarr::{ReadarrCommand, list_command_handler::ReadarrListCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_list_command_from() {
|
||||
let command = ReadarrListCommand::DiskSpace;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Readarr(ReadarrCommand::List(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::{Parser, error::ErrorKind};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
fn test_list_commands_have_no_arg_requirements(
|
||||
#[values(
|
||||
"authors",
|
||||
"blocklist",
|
||||
"disk-space",
|
||||
"indexers",
|
||||
"metadata-profiles",
|
||||
"quality-profiles",
|
||||
"queued-events",
|
||||
"root-folders",
|
||||
"tags",
|
||||
"tasks",
|
||||
"updates"
|
||||
)]
|
||||
subcommand: &str,
|
||||
) {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", subcommand]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_logs_events_flag_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "logs", "--events"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_downloads_count_flag_requires_arguments() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"downloads",
|
||||
"--count",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_events_flag_requires_arguments() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "history", "--events"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_author_history_requires_author_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "author-history"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_author_history_success() {
|
||||
let expected_args = ReadarrListCommand::AuthorHistory { author_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"author-history",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(author_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(author_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_books_requires_author_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", "books"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_editions_requires_book_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "book-editions"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_editions_success() {
|
||||
let expected_args = ReadarrListCommand::BookEditions { book_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"book-editions",
|
||||
"--book-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(book_editions_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(book_editions_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_files_requires_book_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "book-files"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_files_success() {
|
||||
let expected_args = ReadarrListCommand::BookFiles { book_id: 1 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"book-files",
|
||||
"--book-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(book_files_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(book_files_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_history_requires_author_id_and_book_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "book-history"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_book_history_success() {
|
||||
let expected_args = ReadarrListCommand::BookHistory {
|
||||
author_id: 1,
|
||||
book_id: 2,
|
||||
};
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"book-history",
|
||||
"--author-id",
|
||||
"1",
|
||||
"--book-id",
|
||||
"2",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(book_history_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(book_history_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_books_success() {
|
||||
let expected_args = ReadarrListCommand::Books { author_id: 1 };
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "readarr", "list", "books", "--author-id", "1"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(books_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(books_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_releases_requires_book_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "list", "releases"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_author_releases_requires_author_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "list", "author-releases"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_author_releases_success() {
|
||||
let expected_args = ReadarrListCommand::AuthorReleases { author_id: 3 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"author-releases",
|
||||
"--author-id",
|
||||
"3",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(author_releases_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(author_releases_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_releases_success() {
|
||||
let expected_args = ReadarrListCommand::Releases { book_id: 7 };
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "readarr", "list", "releases", "--book-id", "7"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(releases_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(releases_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_logs_default_values() {
|
||||
let expected_args = ReadarrListCommand::Logs {
|
||||
events: 500,
|
||||
output_in_log_format: false,
|
||||
};
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "list", "logs"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(logs_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(logs_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_downloads_default_values() {
|
||||
let expected_args = ReadarrListCommand::Downloads { count: 500 };
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "list", "downloads"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(downloads_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(downloads_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_downloads_success() {
|
||||
let expected_args = ReadarrListCommand::Downloads { count: 1000 };
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"list",
|
||||
"downloads",
|
||||
"--count",
|
||||
"1000",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(downloads_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(downloads_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_default_values() {
|
||||
let expected_args = ReadarrListCommand::History { events: 500 };
|
||||
let result = Cli::try_parse_from(["managarr", "readarr", "list", "history"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(history_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(history_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_history_success() {
|
||||
let expected_args = ReadarrListCommand::History { events: 1000 };
|
||||
let result =
|
||||
Cli::try_parse_from(["managarr", "readarr", "list", "history", "--events", "1000"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
|
||||
let Some(Command::Readarr(ReadarrCommand::List(history_command))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(history_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
readarr::list_command_handler::{ReadarrListCommand, ReadarrListCommandHandler},
|
||||
},
|
||||
models::{HorizontallyScrollableText, Serdeable, readarr_models::ReadarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_authors_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::ListAuthors.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_authors_command = ReadarrListCommand::Authors;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_authors_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_blocklist_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_blocklist_command = ReadarrListCommand::Blocklist;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_blocklist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_author_history_command() {
|
||||
let expected_author_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetAuthorHistory(expected_author_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_author_history_command = ReadarrListCommand::AuthorHistory { author_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_author_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_books_command() {
|
||||
let expected_author_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBooks(expected_author_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_books_command = ReadarrListCommand::Books { author_id: 1 };
|
||||
|
||||
let result = ReadarrListCommandHandler::with(&app_arc, list_books_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_book_editions_command() {
|
||||
let expected_book_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBookEditions(expected_book_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_book_editions_command = ReadarrListCommand::BookEditions { book_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_book_editions_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_book_files_command() {
|
||||
let expected_book_id = 1;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBookFiles(expected_book_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_book_files_command = ReadarrListCommand::BookFiles { book_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_book_files_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_book_history_command() {
|
||||
let expected_author_id = 1;
|
||||
let expected_book_id = 2;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBookHistory(expected_author_id, expected_book_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_book_history_command = ReadarrListCommand::BookHistory {
|
||||
author_id: 1,
|
||||
book_id: 2,
|
||||
};
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_book_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_disk_space_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetDiskSpace.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_disk_space_command = ReadarrListCommand::DiskSpace;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_disk_space_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_downloads_command() {
|
||||
let expected_count = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetDownloads(expected_count).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_downloads_command = ReadarrListCommand::Downloads { count: 1000 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_history_command() {
|
||||
let expected_events = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetHistory(expected_events).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_history_command = ReadarrListCommand::History { events: 1000 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_indexers_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetIndexers.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_indexers_command = ReadarrListCommand::Indexers;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_indexers_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_logs_command() {
|
||||
let expected_events = 1000;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetLogs(expected_events).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_logs_command = ReadarrListCommand::Logs {
|
||||
events: 1000,
|
||||
output_in_log_format: false,
|
||||
};
|
||||
|
||||
let result = ReadarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_logs_command_output_in_log_format() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetLogs(1000).into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.readarr_data
|
||||
.logs
|
||||
.set_items(vec![HorizontallyScrollableText::from("readarr log line")]);
|
||||
let list_logs_command = ReadarrListCommand::Logs {
|
||||
events: 1000,
|
||||
output_in_log_format: true,
|
||||
};
|
||||
|
||||
let result = ReadarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
assert_str_eq!(result.unwrap(), "[\n \"readarr log line\"\n]");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_metadata_profiles_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetMetadataProfiles.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_metadata_profiles_command = ReadarrListCommand::MetadataProfiles;
|
||||
|
||||
let result = ReadarrListCommandHandler::with(
|
||||
&app_arc,
|
||||
list_metadata_profiles_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_quality_profiles_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetQualityProfiles.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_quality_profiles_command = ReadarrListCommand::QualityProfiles;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_quality_profiles_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_queued_events_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetQueuedEvents.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_queued_events_command = ReadarrListCommand::QueuedEvents;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_queued_events_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_releases_command() {
|
||||
let expected_book_id = 7;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetBookReleases(expected_book_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_releases_command = ReadarrListCommand::Releases { book_id: 7 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_releases_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_author_releases_command() {
|
||||
let expected_author_id = 3;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::GetAuthorReleases(expected_author_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_author_releases_command = ReadarrListCommand::AuthorReleases { author_id: 3 };
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_author_releases_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_root_folders_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetRootFolders.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_root_folders_command = ReadarrListCommand::RootFolders;
|
||||
|
||||
let result =
|
||||
ReadarrListCommandHandler::with(&app_arc, list_root_folders_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_tags_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetTags.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_tags_command = ReadarrListCommand::Tags;
|
||||
|
||||
let result = ReadarrListCommandHandler::with(&app_arc, list_tags_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_tasks_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetTasks.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let list_tasks_command = ReadarrListCommand::Tasks;
|
||||
|
||||
let result = ReadarrListCommandHandler::with(&app_arc, list_tasks_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use add_command_handler::{ReadarrAddCommand, ReadarrAddCommandHandler};
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use delete_command_handler::{ReadarrDeleteCommand, ReadarrDeleteCommandHandler};
|
||||
use edit_command_handler::{ReadarrEditCommand, ReadarrEditCommandHandler};
|
||||
use get_command_handler::{ReadarrGetCommand, ReadarrGetCommandHandler};
|
||||
use list_command_handler::{ReadarrListCommand, ReadarrListCommandHandler};
|
||||
use refresh_command_handler::{ReadarrRefreshCommand, ReadarrRefreshCommandHandler};
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
use trigger_automatic_search_command_handler::{
|
||||
ReadarrTriggerAutomaticSearchCommand, ReadarrTriggerAutomaticSearchCommandHandler,
|
||||
};
|
||||
|
||||
use super::{CliCommandHandler, Command};
|
||||
use crate::models::readarr_models::ReadarrTaskName;
|
||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::{app::App, network::NetworkTrait};
|
||||
|
||||
mod add_command_handler;
|
||||
mod delete_command_handler;
|
||||
mod edit_command_handler;
|
||||
mod get_command_handler;
|
||||
mod list_command_handler;
|
||||
mod refresh_command_handler;
|
||||
mod trigger_automatic_search_command_handler;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "readarr_command_tests.rs"]
|
||||
mod readarr_command_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrCommand {
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to add or create new resources within your Readarr instance"
|
||||
)]
|
||||
Add(ReadarrAddCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to delete resources from your Readarr instance"
|
||||
)]
|
||||
Delete(ReadarrDeleteCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to edit resources in your Readarr instance"
|
||||
)]
|
||||
Edit(ReadarrEditCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to fetch details of the resources in your Readarr instance"
|
||||
)]
|
||||
Get(ReadarrGetCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to list attributes from your Readarr instance"
|
||||
)]
|
||||
List(ReadarrListCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to refresh the data in your Readarr instance"
|
||||
)]
|
||||
Refresh(ReadarrRefreshCommand),
|
||||
#[command(
|
||||
subcommand,
|
||||
about = "Commands to trigger automatic searches for releases of different resources in your Readarr instance"
|
||||
)]
|
||||
TriggerAutomaticSearch(ReadarrTriggerAutomaticSearchCommand),
|
||||
#[command(about = "Clear the Readarr blocklist")]
|
||||
ClearBlocklist,
|
||||
#[command(about = "Manually download the given release")]
|
||||
DownloadRelease {
|
||||
#[arg(long, help = "The GUID of the release to download", required = true)]
|
||||
guid: String,
|
||||
#[arg(
|
||||
long,
|
||||
help = "The indexer ID to download the release from",
|
||||
required = true
|
||||
)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(about = "Mark the Readarr history item with the given ID as 'failed'")]
|
||||
MarkHistoryItemAsFailed {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the history item you wish to mark as 'failed'",
|
||||
required = true
|
||||
)]
|
||||
history_item_id: i64,
|
||||
},
|
||||
#[command(about = "Search for a new author to add to Readarr")]
|
||||
SearchNewAuthor {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The name of the author you want to search for",
|
||||
required = true
|
||||
)]
|
||||
query: String,
|
||||
},
|
||||
#[command(about = "Start the specified Readarr task")]
|
||||
StartTask {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The name of the task to trigger",
|
||||
value_enum,
|
||||
required = true
|
||||
)]
|
||||
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. '{}'"
|
||||
)]
|
||||
TestIndexer {
|
||||
#[arg(long, help = "The ID of the indexer to test", required = true)]
|
||||
indexer_id: i64,
|
||||
},
|
||||
#[command(
|
||||
about = "Toggle monitoring for the specified author corresponding to the given author ID"
|
||||
)]
|
||||
ToggleAuthorMonitoring {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the author to toggle monitoring on",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "Toggle monitoring for the specified book corresponding to the given book ID")]
|
||||
ToggleBookMonitoring {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The Readarr ID of the book to toggle monitoring on",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ReadarrCommand> for Command {
|
||||
fn from(readarr_command: ReadarrCommand) -> Command {
|
||||
Command::Readarr(readarr_command)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrCliHandler<'a, 'b> {
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrCommand> for ReadarrCliHandler<'a, 'b> {
|
||||
fn with(
|
||||
app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrCliHandler {
|
||||
app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrCommand::Add(add_command) => {
|
||||
ReadarrAddCommandHandler::with(self.app, add_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::Delete(delete_command) => {
|
||||
ReadarrDeleteCommandHandler::with(self.app, delete_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::Edit(edit_command) => {
|
||||
ReadarrEditCommandHandler::with(self.app, edit_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::Get(get_command) => {
|
||||
ReadarrGetCommandHandler::with(self.app, get_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::List(list_command) => {
|
||||
ReadarrListCommandHandler::with(self.app, list_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::Refresh(refresh_command) => {
|
||||
ReadarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
|
||||
ReadarrTriggerAutomaticSearchCommandHandler::with(
|
||||
self.app,
|
||||
trigger_automatic_search_command,
|
||||
self.network,
|
||||
)
|
||||
.handle()
|
||||
.await?
|
||||
}
|
||||
ReadarrCommand::ClearBlocklist => {
|
||||
self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::GetBlocklist.into())
|
||||
.await?;
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::ClearBlocklist.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::DownloadRelease { guid, indexer_id } => {
|
||||
let params = ReleaseDownloadBody { guid, indexer_id };
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::DownloadRelease(params).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
|
||||
let _ = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(
|
||||
&json!({"message": "Readarr history item marked as 'failed'"}),
|
||||
)?
|
||||
}
|
||||
ReadarrCommand::SearchNewAuthor { query } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::SearchNewAuthor(query).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::StartTask { task_name } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::StartTask(task_name).into())
|
||||
.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
|
||||
.handle_network_event(ReadarrEvent::TestIndexer(indexer_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::ToggleAuthorMonitoring { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::ToggleAuthorMonitoring(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrCommand::ToggleBookMonitoring { book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::ToggleBookMonitoring(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod cli {
|
||||
use clap::CommandFactory;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::Cli;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_command_requires_a_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr"]);
|
||||
|
||||
assert_err!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_history_item_as_failed_requires_history_item_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "mark-history-item-as-failed"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_release_requires_guid() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"download-release",
|
||||
"--indexer-id",
|
||||
"6",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_release_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"download-release",
|
||||
"--guid",
|
||||
"test-release-guid",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_release_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"download-release",
|
||||
"--guid",
|
||||
"test-release-guid",
|
||||
"--indexer-id",
|
||||
"6",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mark_history_item_as_failed_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"mark-history-item-as-failed",
|
||||
"--history-item-id",
|
||||
"1234",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_new_author_requires_query() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "search-new-author"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_new_author_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"search-new-author",
|
||||
"--query",
|
||||
"test query",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_requires_task_name() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "start-task"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_task_name_validation() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"start-task",
|
||||
"--task-name",
|
||||
"test",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_task_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"start-task",
|
||||
"--task-name",
|
||||
"check-health",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_indexer_requires_indexer_id() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "test-indexer"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_indexer_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"test-indexer",
|
||||
"--indexer-id",
|
||||
"8",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_author_monitoring_requires_author_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "toggle-author-monitoring"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_author_monitoring_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"toggle-author-monitoring",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_book_monitoring_requires_book_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "toggle-book-monitoring"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_book_monitoring_requirements_satisfied() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"toggle-book-monitoring",
|
||||
"--book-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::models::readarr_models::{
|
||||
BlocklistItem, BlocklistResponse, ReadarrSerdeable, ReadarrTaskName,
|
||||
};
|
||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
||||
use crate::{
|
||||
app::App,
|
||||
cli::{
|
||||
CliCommandHandler,
|
||||
readarr::{ReadarrCliHandler, ReadarrCommand},
|
||||
},
|
||||
models::Serdeable,
|
||||
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mark_history_item_as_failed_command() {
|
||||
let expected_history_item_id = 1234i64;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let mark_history_item_as_failed_command = ReadarrCommand::MarkHistoryItemAsFailed {
|
||||
history_item_id: expected_history_item_id,
|
||||
};
|
||||
|
||||
let result = ReadarrCliHandler::with(
|
||||
&app_arc,
|
||||
mark_history_item_as_failed_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_download_release_command() {
|
||||
let expected_release_download_body = ReleaseDownloadBody {
|
||||
guid: "test-release-guid".to_owned(),
|
||||
indexer_id: 6,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::DownloadRelease(expected_release_download_body).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let download_release_command = ReadarrCommand::DownloadRelease {
|
||||
guid: "test-release-guid".to_owned(),
|
||||
indexer_id: 6,
|
||||
};
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, download_release_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_new_author_command() {
|
||||
let expected_query = "test author".to_owned();
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::SearchNewAuthor(expected_query.clone()).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let search_new_author_command = ReadarrCommand::SearchNewAuthor {
|
||||
query: expected_query,
|
||||
};
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, search_new_author_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_start_task_command() {
|
||||
let expected_task_name = ReadarrTaskName::CheckHealth;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::StartTask(expected_task_name).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let start_task_command = ReadarrCommand::StartTask {
|
||||
task_name: ReadarrTaskName::CheckHealth,
|
||||
};
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_test_indexer_command() {
|
||||
let expected_indexer_id = 8;
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::TestIndexer(expected_indexer_id).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let test_indexer_command = ReadarrCommand::TestIndexer { indexer_id: 8 };
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
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::<NetworkEvent>(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();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::ToggleAuthorMonitoring(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let toggle_author_monitoring_command =
|
||||
ReadarrCommand::ToggleAuthorMonitoring { author_id: 1 };
|
||||
|
||||
let result = ReadarrCliHandler::with(
|
||||
&app_arc,
|
||||
toggle_author_monitoring_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_toggle_book_monitoring_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::ToggleBookMonitoring(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let toggle_book_monitoring_command = ReadarrCommand::ToggleBookMonitoring { book_id: 1 };
|
||||
|
||||
let result =
|
||||
ReadarrCliHandler::with(&app_arc, toggle_book_monitoring_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_clear_blocklist_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::GetBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::BlocklistResponse(
|
||||
BlocklistResponse {
|
||||
records: vec![BlocklistItem::default()],
|
||||
},
|
||||
)))
|
||||
});
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::ClearBlocklist.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let clear_blocklist_command = ReadarrCommand::ClearBlocklist;
|
||||
|
||||
let result = ReadarrCliHandler::with(&app_arc, clear_blocklist_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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 = "refresh_command_handler_tests.rs"]
|
||||
mod refresh_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrRefreshCommand {
|
||||
#[command(about = "Refresh all author data for all authors in your Readarr library")]
|
||||
AllAuthors,
|
||||
#[command(about = "Refresh author data and scan disk for the author with the given ID")]
|
||||
Author {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the author to refresh information on and to scan the disk for",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "Refresh all downloads in Readarr")]
|
||||
Downloads,
|
||||
}
|
||||
|
||||
impl From<ReadarrRefreshCommand> for Command {
|
||||
fn from(value: ReadarrRefreshCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::Refresh(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrRefreshCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrRefreshCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrRefreshCommand>
|
||||
for ReadarrRefreshCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrRefreshCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrRefreshCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrRefreshCommand::AllAuthors => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::UpdateAllAuthors.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrRefreshCommand::Author { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::UpdateAndScanAuthor(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrRefreshCommand::Downloads => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::UpdateDownloads.into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
readarr::{ReadarrCommand, refresh_command_handler::ReadarrRefreshCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_refresh_command_from() {
|
||||
let command = ReadarrRefreshCommand::Author { author_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Readarr(ReadarrCommand::Refresh(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_refresh_all_authors_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "refresh", "all-authors"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_downloads_has_no_arg_requirements() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "refresh", "downloads"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_author_requires_author_id() {
|
||||
let result =
|
||||
Cli::command().try_get_matches_from(["managarr", "readarr", "refresh", "author"]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_author_with_author_id() {
|
||||
let expected_args = ReadarrRefreshCommand::Author { author_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"refresh",
|
||||
"author",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
let Some(Command::Readarr(ReadarrCommand::Refresh(refresh_command))) =
|
||||
result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(refresh_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::readarr::refresh_command_handler::{
|
||||
ReadarrRefreshCommand, ReadarrRefreshCommandHandler,
|
||||
};
|
||||
use crate::{app::App, cli::CliCommandHandler};
|
||||
use crate::{
|
||||
models::{Serdeable, readarr_models::ReadarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_all_authors_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::UpdateAllAuthors.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_command = ReadarrRefreshCommand::AllAuthors;
|
||||
|
||||
let result = ReadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_author_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::UpdateAndScanAuthor(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_command = ReadarrRefreshCommand::Author { author_id: 1 };
|
||||
|
||||
let result = ReadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_refresh_downloads_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(ReadarrEvent::UpdateDownloads.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let refresh_command = ReadarrRefreshCommand::Downloads;
|
||||
|
||||
let result = ReadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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 = "trigger_automatic_search_command_handler_tests.rs"]
|
||||
mod trigger_automatic_search_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum ReadarrTriggerAutomaticSearchCommand {
|
||||
#[command(about = "Trigger an automatic search for the author with the specified ID")]
|
||||
Author {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the author you want to trigger an automatic search for",
|
||||
required = true
|
||||
)]
|
||||
author_id: i64,
|
||||
},
|
||||
#[command(about = "Trigger an automatic search for the book with the specified ID")]
|
||||
Book {
|
||||
#[arg(
|
||||
long,
|
||||
help = "The ID of the book you want to trigger an automatic search for",
|
||||
required = true
|
||||
)]
|
||||
book_id: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ReadarrTriggerAutomaticSearchCommand> for Command {
|
||||
fn from(value: ReadarrTriggerAutomaticSearchCommand) -> Self {
|
||||
Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ReadarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
}
|
||||
|
||||
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrTriggerAutomaticSearchCommand>
|
||||
for ReadarrTriggerAutomaticSearchCommandHandler<'a, 'b>
|
||||
{
|
||||
fn with(
|
||||
_app: &'a Arc<Mutex<App<'b>>>,
|
||||
command: ReadarrTriggerAutomaticSearchCommand,
|
||||
network: &'a mut dyn NetworkTrait,
|
||||
) -> Self {
|
||||
ReadarrTriggerAutomaticSearchCommandHandler {
|
||||
_app,
|
||||
command,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(self) -> Result<String> {
|
||||
let result = match self.command {
|
||||
ReadarrTriggerAutomaticSearchCommand::Author { author_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::TriggerAutomaticAuthorSearch(author_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
ReadarrTriggerAutomaticSearchCommand::Book { book_id } => {
|
||||
let resp = self
|
||||
.network
|
||||
.handle_network_event(ReadarrEvent::TriggerAutomaticBookSearch(book_id).into())
|
||||
.await?;
|
||||
serde_json::to_string_pretty(&resp)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
readarr::{
|
||||
ReadarrCommand,
|
||||
trigger_automatic_search_command_handler::ReadarrTriggerAutomaticSearchCommand,
|
||||
},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn test_readarr_trigger_automatic_search_command_from() {
|
||||
let command = ReadarrTriggerAutomaticSearchCommand::Author { author_id: 1 };
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(command))
|
||||
);
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
use clap::error::ErrorKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_author_search_requires_author_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"trigger-automatic-search",
|
||||
"author",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_author_search_with_author_id() {
|
||||
let expected_args = ReadarrTriggerAutomaticSearchCommand::Author { author_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"trigger-automatic-search",
|
||||
"author",
|
||||
"--author-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
let Some(Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(
|
||||
trigger_automatic_search_command,
|
||||
))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(trigger_automatic_search_command, expected_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_book_search_requires_book_id() {
|
||||
let result = Cli::command().try_get_matches_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"trigger-automatic-search",
|
||||
"book",
|
||||
]);
|
||||
|
||||
assert_err!(&result);
|
||||
assert_eq!(
|
||||
result.unwrap_err().kind(),
|
||||
ErrorKind::MissingRequiredArgument
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_automatic_book_search_with_book_id() {
|
||||
let expected_args = ReadarrTriggerAutomaticSearchCommand::Book { book_id: 1 };
|
||||
|
||||
let result = Cli::try_parse_from([
|
||||
"managarr",
|
||||
"readarr",
|
||||
"trigger-automatic-search",
|
||||
"book",
|
||||
"--book-id",
|
||||
"1",
|
||||
]);
|
||||
|
||||
assert_ok!(&result);
|
||||
let Some(Command::Readarr(ReadarrCommand::TriggerAutomaticSearch(
|
||||
trigger_automatic_search_command,
|
||||
))) = result.unwrap().command
|
||||
else {
|
||||
panic!("Unexpected command type");
|
||||
};
|
||||
assert_eq!(trigger_automatic_search_command, expected_args);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::readarr::trigger_automatic_search_command_handler::{
|
||||
ReadarrTriggerAutomaticSearchCommand, ReadarrTriggerAutomaticSearchCommandHandler,
|
||||
};
|
||||
use crate::{app::App, cli::CliCommandHandler};
|
||||
use crate::{
|
||||
models::{Serdeable, readarr_models::ReadarrSerdeable},
|
||||
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_trigger_automatic_author_search_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::TriggerAutomaticAuthorSearch(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let trigger_automatic_search_command =
|
||||
ReadarrTriggerAutomaticSearchCommand::Author { author_id: 1 };
|
||||
|
||||
let result = ReadarrTriggerAutomaticSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_trigger_automatic_book_search_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(
|
||||
ReadarrEvent::TriggerAutomaticBookSearch(1).into(),
|
||||
))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let trigger_automatic_search_command =
|
||||
ReadarrTriggerAutomaticSearchCommand::Book { book_id: 1 };
|
||||
|
||||
let result = ReadarrTriggerAutomaticSearchCommandHandler::with(
|
||||
&app_arc,
|
||||
trigger_automatic_search_command,
|
||||
&mut mock_network,
|
||||
)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -632,10 +632,10 @@ mod tests {
|
||||
async fn test_handle_edit_all_indexer_settings_command() {
|
||||
let expected_edit_all_indexer_settings = IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 1,
|
||||
minimum_age: 1,
|
||||
retention: 1,
|
||||
rss_sync_interval: 1,
|
||||
maximum_size: 12345,
|
||||
minimum_age: 13,
|
||||
retention: 21,
|
||||
rss_sync_interval: 60,
|
||||
};
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
@@ -647,11 +647,11 @@ mod tests {
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(
|
||||
IndexerSettings {
|
||||
id: 1,
|
||||
maximum_size: 2,
|
||||
minimum_age: 2,
|
||||
retention: 2,
|
||||
rss_sync_interval: 2,
|
||||
id: 9,
|
||||
maximum_size: 22222,
|
||||
minimum_age: 33,
|
||||
retention: 47,
|
||||
rss_sync_interval: 90,
|
||||
},
|
||||
)))
|
||||
});
|
||||
@@ -668,10 +668,10 @@ mod tests {
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let edit_all_indexer_settings_command = SonarrEditCommand::AllIndexerSettings {
|
||||
maximum_size: Some(1),
|
||||
minimum_age: Some(1),
|
||||
retention: Some(1),
|
||||
rss_sync_interval: Some(1),
|
||||
maximum_size: Some(12345),
|
||||
minimum_age: Some(13),
|
||||
retention: Some(21),
|
||||
rss_sync_interval: Some(60),
|
||||
};
|
||||
|
||||
let result = SonarrEditCommandHandler::with(
|
||||
|
||||
@@ -18,10 +18,12 @@ pub struct Events {
|
||||
rx: Receiver<InputEvent<Key>>,
|
||||
}
|
||||
|
||||
const DEFAULT_TICK_RATE_MS: u64 = 50;
|
||||
|
||||
impl Events {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let tick_rate: Duration = Duration::from_millis(50);
|
||||
let tick_rate: Duration = Duration::from_millis(DEFAULT_TICK_RATE_MS);
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut last_tick = Instant::now();
|
||||
@@ -51,9 +53,19 @@ impl Events {
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Result<Option<InputEvent<Key>>> {
|
||||
match self.rx.try_recv() {
|
||||
match self.rx.recv() {
|
||||
Ok(event) => Ok(Some(event)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn defaults_to_original_tick_rate() {
|
||||
assert_eq!(DEFAULT_TICK_RATE_MS, 50);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,16 @@ mod property_tests {
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::*;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::handle_events;
|
||||
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::{
|
||||
list_size, text_input_string,
|
||||
};
|
||||
use crate::models::radarr_models::Movie;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{Paginated, Scrollable};
|
||||
use crate::models::{Paginated, Scrollable, strip_non_search_characters};
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
@@ -16,11 +21,12 @@ mod property_tests {
|
||||
index in 0usize..1000
|
||||
) {
|
||||
let mut table = StatefulTable::<Movie>::default();
|
||||
let movies: Vec<Movie> = (0..list_size).map(|i| {
|
||||
let mut movie = Movie::default();
|
||||
movie.id = i as i64;
|
||||
movie
|
||||
}).collect();
|
||||
let movies: Vec<Movie> = (0..list_size)
|
||||
.map(|i| Movie {
|
||||
id: i as i64,
|
||||
..Movie::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
table.set_items(movies);
|
||||
|
||||
@@ -39,11 +45,12 @@ mod property_tests {
|
||||
scroll_amount in 0usize..20
|
||||
) {
|
||||
let mut table = StatefulTable::<Movie>::default();
|
||||
let movies: Vec<Movie> = (0..list_size).map(|i| {
|
||||
let mut movie = Movie::default();
|
||||
movie.id = i as i64;
|
||||
movie
|
||||
}).collect();
|
||||
let movies: Vec<Movie> = (0..list_size)
|
||||
.map(|i| Movie {
|
||||
id: i as i64,
|
||||
..Movie::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
table.set_items(movies);
|
||||
let initial_id = table.current_selection().id;
|
||||
@@ -76,7 +83,7 @@ mod property_tests {
|
||||
let mut app = App::test_default();
|
||||
let initial_route = app.get_current_route();
|
||||
|
||||
let routes = vec![
|
||||
let routes = [
|
||||
ActiveRadarrBlock::Movies,
|
||||
ActiveRadarrBlock::Collections,
|
||||
ActiveRadarrBlock::Downloads,
|
||||
@@ -100,26 +107,43 @@ mod property_tests {
|
||||
|
||||
#[test]
|
||||
fn test_string_input_safety(input in text_input_string()) {
|
||||
let _lowercase = input.to_lowercase();
|
||||
let _uppercase = input.to_uppercase();
|
||||
let _trimmed = input.trim();
|
||||
let _len = input.len();
|
||||
let _chars: Vec<char> = input.chars().collect();
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.movies.set_items(vec![Movie::default()]);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
handle_events(DEFAULT_KEYBINDINGS.filter.key, &mut app);
|
||||
|
||||
prop_assert!(true);
|
||||
for character in input.chars() {
|
||||
handle_events(Key::Char(character), &mut app);
|
||||
}
|
||||
|
||||
prop_assert_eq!(app.get_current_route(), ActiveRadarrBlock::FilterMovies.into());
|
||||
prop_assert_eq!(
|
||||
app.data.radarr_data.movies.filter.as_ref().unwrap().text.as_str(),
|
||||
input.as_str()
|
||||
);
|
||||
|
||||
for _ in 0..input.chars().count() {
|
||||
handle_events(DEFAULT_KEYBINDINGS.backspace.key, &mut app);
|
||||
}
|
||||
|
||||
prop_assert_eq!(
|
||||
app.data.radarr_data.movies.filter.as_ref().unwrap().text.as_str(),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_data_integrity(
|
||||
list_size in 1usize..100
|
||||
list_size in list_size()
|
||||
) {
|
||||
let mut table = StatefulTable::<Movie>::default();
|
||||
let movies: Vec<Movie> = (0..list_size).map(|i| {
|
||||
let mut movie = Movie::default();
|
||||
movie.id = i as i64;
|
||||
movie.title = format!("Movie {}", i).into();
|
||||
movie
|
||||
}).collect();
|
||||
let movies: Vec<Movie> = (0..list_size)
|
||||
.map(|i| Movie {
|
||||
id: i as i64,
|
||||
title: format!("Movie {i}").into(),
|
||||
..Movie::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
table.set_items(movies.clone());
|
||||
let original_count = table.items.len();
|
||||
@@ -137,19 +161,29 @@ mod property_tests {
|
||||
page_ops in 0usize..10
|
||||
) {
|
||||
let mut table = StatefulTable::<Movie>::default();
|
||||
let movies: Vec<Movie> = (0..list_size).map(|i| {
|
||||
let mut movie = Movie::default();
|
||||
movie.id = i as i64;
|
||||
movie
|
||||
}).collect();
|
||||
let movies: Vec<Movie> = (0..list_size)
|
||||
.map(|i| Movie {
|
||||
id: i as i64,
|
||||
..Movie::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
table.set_items(movies);
|
||||
|
||||
for i in 0..page_ops {
|
||||
let previous_index = table.state.selected().unwrap();
|
||||
|
||||
if i % 2 == 0 {
|
||||
table.page_down();
|
||||
|
||||
prop_assert_eq!(
|
||||
table.state.selected(),
|
||||
Some((previous_index + 20).min(list_size - 1))
|
||||
);
|
||||
} else {
|
||||
table.page_up();
|
||||
|
||||
prop_assert_eq!(table.state.selected(), Some(previous_index.saturating_sub(20)));
|
||||
}
|
||||
|
||||
let current = table.current_selection();
|
||||
@@ -164,28 +198,43 @@ mod property_tests {
|
||||
filter_term in text_input_string()
|
||||
) {
|
||||
let mut table = StatefulTable::<Movie>::default();
|
||||
let movies: Vec<Movie> = (0..list_size).map(|i| {
|
||||
let mut movie = Movie::default();
|
||||
movie.id = i as i64;
|
||||
movie.title = format!("Test Movie {}", i % 10).into();
|
||||
movie
|
||||
}).collect();
|
||||
|
||||
let movies: Vec<Movie> = (0..list_size)
|
||||
.map(|i| Movie {
|
||||
id: i as i64,
|
||||
title: format!("Test Movie {}", i % 10).into(),
|
||||
..Movie::default()
|
||||
})
|
||||
.collect();
|
||||
table.set_items(movies.clone());
|
||||
let original_size = table.items.len();
|
||||
table.filter = Some(filter_term.clone().into());
|
||||
let scrubbed_filter = strip_non_search_characters(&filter_term);
|
||||
let matches_filter = |movie: &Movie| {
|
||||
strip_non_search_characters(&movie.title.text).contains(&scrubbed_filter)
|
||||
};
|
||||
|
||||
if !filter_term.is_empty() {
|
||||
let filtered: Vec<Movie> = movies.into_iter()
|
||||
.filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase()))
|
||||
.collect();
|
||||
table.set_items(filtered);
|
||||
}
|
||||
let has_match = table.apply_filter(|movie| &movie.title.text);
|
||||
|
||||
prop_assert!(table.items.len() <= original_size);
|
||||
prop_assert_eq!(&table.items, &movies);
|
||||
prop_assert!(table.filter.is_none());
|
||||
|
||||
if !table.items.is_empty() {
|
||||
let current = table.current_selection();
|
||||
prop_assert!(current.id >= 0);
|
||||
if has_match {
|
||||
let filtered_items = table.filtered_items.as_ref().unwrap();
|
||||
|
||||
prop_assert!(!filtered_items.is_empty());
|
||||
prop_assert!(filtered_items.len() <= movies.len());
|
||||
|
||||
for movie in filtered_items {
|
||||
prop_assert!(matches_filter(movie));
|
||||
}
|
||||
|
||||
for movie in movies.iter().filter(|movie| !filtered_items.contains(movie)) {
|
||||
prop_assert!(!matches_filter(movie));
|
||||
}
|
||||
} else {
|
||||
prop_assert!(table.filtered_items.is_none());
|
||||
prop_assert!(
|
||||
filter_term.is_empty() || movies.iter().all(|movie| !matches_filter(movie))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
mod test_utils {
|
||||
pub mod test_utils {
|
||||
#[macro_export]
|
||||
macro_rules! simple_stateful_iterable_vec {
|
||||
($name:ident) => {
|
||||
@@ -333,10 +333,16 @@ mod test_utils {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack($base.into());
|
||||
app.push_navigation_stack($active_block.into());
|
||||
let navigation_stack_depth_before_delegation = app.get_navigation_stack_depth();
|
||||
|
||||
$handler::new(DEFAULT_KEYBINDINGS.esc.key, &mut app, $active_block, None).handle();
|
||||
|
||||
pretty_assertions::assert_eq!(app.get_current_route(), $base.into());
|
||||
pretty_assertions::assert_eq!(
|
||||
app.get_navigation_stack_depth(),
|
||||
navigation_stack_depth_before_delegation - 1,
|
||||
"Expected the delegated handler to pop exactly one route off the navigation stack"
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -405,4 +411,16 @@ mod test_utils {
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
pub mod proptest_helpers {
|
||||
use proptest::prelude::*;
|
||||
|
||||
pub fn list_size() -> impl Strategy<Value = usize> {
|
||||
1usize..100
|
||||
}
|
||||
|
||||
pub fn text_input_string() -> impl Strategy<Value = String> {
|
||||
"\\PC{0,50}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ mod tests {
|
||||
use crate::assert_modal_absent;
|
||||
use crate::assert_navigation_pushed;
|
||||
use crate::models::radarr_models::Movie;
|
||||
use crate::models::readarr_models::Author;
|
||||
use crate::models::sonarr_models::Series;
|
||||
use crate::{assert_modal_present, assert_navigation_popped};
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -22,6 +23,7 @@ mod tests {
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::servarr_data::{ActiveKeybindingBlock, Notification};
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
@@ -40,6 +42,7 @@ mod tests {
|
||||
#[rstest]
|
||||
#[case(ActiveRadarrBlock::Movies.into(), ActiveRadarrBlock::SearchMovie.into())]
|
||||
#[case(ActiveSonarrBlock::Series.into(), ActiveSonarrBlock::SearchSeries.into())]
|
||||
#[case(ActiveReadarrBlock::Authors.into(), ActiveReadarrBlock::SearchAuthors.into())]
|
||||
fn test_handle_events(#[case] base_block: Route, #[case] top_block: Route) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(base_block);
|
||||
@@ -54,6 +57,11 @@ mod tests {
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![Movie::default()]);
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.authors
|
||||
.set_items(vec![Author::default()]);
|
||||
|
||||
handle_events(DEFAULT_KEYBINDINGS.esc.key, &mut app);
|
||||
|
||||
@@ -61,9 +69,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveLidarrBlock::Artists, ActiveSonarrBlock::Series)]
|
||||
#[case(0, ActiveReadarrBlock::Authors, ActiveSonarrBlock::Series)]
|
||||
#[case(1, ActiveRadarrBlock::Movies, ActiveLidarrBlock::Artists)]
|
||||
#[case(2, ActiveSonarrBlock::Series, ActiveRadarrBlock::Movies)]
|
||||
#[case(2, ActiveSonarrBlock::Series, ActiveReadarrBlock::Authors)]
|
||||
#[case(3, ActiveLidarrBlock::Artists, ActiveRadarrBlock::Movies)]
|
||||
fn test_handle_change_tabs<T, U>(
|
||||
#[case] index: usize,
|
||||
#[case] left_block: T,
|
||||
@@ -226,6 +235,22 @@ mod tests {
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_handle_prompt_toggle_left_right_readarr(#[values(Key::Left, Key::Right)] key: Key) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Authors.into());
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
|
||||
handle_prompt_toggle(&mut app, key);
|
||||
|
||||
assert!(app.data.readarr_data.prompt_confirm);
|
||||
|
||||
handle_prompt_toggle(&mut app, key);
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_populate_keymapping_table_global_options() {
|
||||
let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
|
||||
|
||||
@@ -141,7 +141,7 @@ mod tests {
|
||||
#[case(
|
||||
ActiveLidarrBlock::Downloads,
|
||||
ActiveLidarrBlock::DeleteDownloadPrompt,
|
||||
LidarrEvent::DeleteDownload(1)
|
||||
LidarrEvent::DeleteDownload(9)
|
||||
)]
|
||||
#[case(
|
||||
ActiveLidarrBlock::Downloads,
|
||||
@@ -338,7 +338,7 @@ mod tests {
|
||||
#[case(
|
||||
ActiveLidarrBlock::Downloads,
|
||||
ActiveLidarrBlock::DeleteDownloadPrompt,
|
||||
LidarrEvent::DeleteDownload(1)
|
||||
LidarrEvent::DeleteDownload(9)
|
||||
)]
|
||||
#[case(
|
||||
ActiveLidarrBlock::Downloads,
|
||||
@@ -423,7 +423,7 @@ mod tests {
|
||||
)
|
||||
.extract_download_id();
|
||||
|
||||
assert_eq!(download_id, 1);
|
||||
assert_eq!(download_id, 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -196,6 +196,40 @@ mod tests {
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::History.into());
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_history_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.history.set_items(history_vec());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::History.into());
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.search.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::History,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::SearchHistory.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_history_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.history.set_items(history_vec());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::History.into());
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.filter.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::History,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::FilterHistory.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1076,10 +1076,16 @@ mod tests {
|
||||
.set_index(0, ADD_ARTIST_SELECTION_BLOCKS.len() - 1);
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
let mut add_searched_artists = StatefulTable::default();
|
||||
add_searched_artists.set_items(vec![add_artist_search_result()]);
|
||||
let mut decoy_search_result = add_artist_search_result();
|
||||
decoy_search_result.foreign_artist_id = "decoy-foreign-id".to_owned();
|
||||
decoy_search_result.artist_name = "Decoy Artist".into();
|
||||
add_searched_artists.set_items(vec![decoy_search_result, add_artist_search_result()]);
|
||||
add_searched_artists.select_index(Some(1));
|
||||
app.data.lidarr_data.add_searched_artists = Some(add_searched_artists);
|
||||
app.data.lidarr_data.quality_profile_map = BiMap::from_iter([(1, "Test".to_owned())]);
|
||||
app.data.lidarr_data.metadata_profile_map = BiMap::from_iter([(1, "Test".to_owned())]);
|
||||
app.data.lidarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "Lossless".to_owned())]);
|
||||
app.data.lidarr_data.metadata_profile_map =
|
||||
BiMap::from_iter([(3333, "None".to_owned()), (4444, "Standard".to_owned())]);
|
||||
app.data.lidarr_data.add_artist_modal = Some(AddArtistModal {
|
||||
tags: "usenet, testing".into(),
|
||||
..AddArtistModal::default()
|
||||
@@ -1092,6 +1098,15 @@ mod tests {
|
||||
.unwrap()
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(MonitorType::iter()));
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
@@ -1100,6 +1115,15 @@ mod tests {
|
||||
.unwrap()
|
||||
.monitor_new_items_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_new_items_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
@@ -1107,7 +1131,16 @@ mod tests {
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.quality_profile_list
|
||||
.set_items(vec!["Test".to_owned()]);
|
||||
.set_items(vec!["Any".to_owned(), "Lossless".to_owned()]);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.quality_profile_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
@@ -1115,7 +1148,16 @@ mod tests {
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.metadata_profile_list
|
||||
.set_items(vec!["Test".to_owned()]);
|
||||
.set_items(vec!["None".to_owned(), "Standard".to_owned()]);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.metadata_profile_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
@@ -1123,22 +1165,37 @@ mod tests {
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.root_folder_list
|
||||
.set_items(vec![RootFolder {
|
||||
path: "/music".to_owned(),
|
||||
..RootFolder::default()
|
||||
}]);
|
||||
.set_items(vec![
|
||||
RootFolder {
|
||||
path: "/decoy".to_owned(),
|
||||
..RootFolder::default()
|
||||
},
|
||||
RootFolder {
|
||||
path: "/music".to_owned(),
|
||||
..RootFolder::default()
|
||||
},
|
||||
]);
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.add_artist_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.root_folder_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
let expected_add_artist_body = AddArtistBody {
|
||||
foreign_artist_id: "test-foreign-id".to_string(),
|
||||
artist_name: "Test Artist".to_string(),
|
||||
monitored: true,
|
||||
root_folder_path: "/music".to_string(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
quality_profile_id: 2222,
|
||||
metadata_profile_id: 4444,
|
||||
tags: Vec::default(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
add_options: AddArtistOptions {
|
||||
monitor: MonitorType::All,
|
||||
monitor_new_items: NewItemMonitorType::All,
|
||||
monitor: MonitorType::Future,
|
||||
monitor_new_items: NewItemMonitorType::None,
|
||||
search_for_missing_albums: true,
|
||||
},
|
||||
};
|
||||
@@ -1468,10 +1525,16 @@ mod tests {
|
||||
.selected_block
|
||||
.set_index(0, ADD_ARTIST_SELECTION_BLOCKS.len() - 1);
|
||||
let mut add_searched_artists = StatefulTable::default();
|
||||
add_searched_artists.set_items(vec![add_artist_search_result()]);
|
||||
let mut decoy_search_result = add_artist_search_result();
|
||||
decoy_search_result.foreign_artist_id = "decoy-foreign-id".to_owned();
|
||||
decoy_search_result.artist_name = "Decoy Artist".into();
|
||||
add_searched_artists.set_items(vec![decoy_search_result, add_artist_search_result()]);
|
||||
add_searched_artists.select_index(Some(1));
|
||||
app.data.lidarr_data.add_searched_artists = Some(add_searched_artists);
|
||||
app.data.lidarr_data.quality_profile_map = BiMap::from_iter([(1, "Test".to_owned())]);
|
||||
app.data.lidarr_data.metadata_profile_map = BiMap::from_iter([(1, "Test".to_owned())]);
|
||||
app.data.lidarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "Lossless".to_owned())]);
|
||||
app.data.lidarr_data.metadata_profile_map =
|
||||
BiMap::from_iter([(3333, "None".to_owned()), (4444, "Standard".to_owned())]);
|
||||
let mut add_artist_modal = AddArtistModal {
|
||||
tags: "usenet, testing".into(),
|
||||
..AddArtistModal::default()
|
||||
@@ -1479,15 +1542,22 @@ mod tests {
|
||||
add_artist_modal
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(MonitorType::iter()));
|
||||
add_artist_modal.monitor_list.state.select(Some(1));
|
||||
add_artist_modal
|
||||
.monitor_new_items_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
add_artist_modal
|
||||
.monitor_new_items_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
add_artist_modal
|
||||
.quality_profile_list
|
||||
.set_items(vec!["Test".to_owned()]);
|
||||
.set_items(vec!["Any".to_owned(), "Lossless".to_owned()]);
|
||||
add_artist_modal.quality_profile_list.state.select(Some(1));
|
||||
add_artist_modal
|
||||
.metadata_profile_list
|
||||
.set_items(vec!["Test".to_owned()]);
|
||||
.set_items(vec!["None".to_owned(), "Standard".to_owned()]);
|
||||
add_artist_modal.metadata_profile_list.state.select(Some(1));
|
||||
add_artist_modal.root_folder_list.set_items(vec![
|
||||
RootFolder {
|
||||
id: 1,
|
||||
@@ -1511,13 +1581,13 @@ mod tests {
|
||||
artist_name: "Test Artist".to_string(),
|
||||
monitored: true,
|
||||
root_folder_path: "/nfs2".to_string(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
quality_profile_id: 2222,
|
||||
metadata_profile_id: 4444,
|
||||
tags: Vec::default(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
add_options: AddArtistOptions {
|
||||
monitor: Default::default(),
|
||||
monitor_new_items: Default::default(),
|
||||
monitor: MonitorType::Future,
|
||||
monitor_new_items: NewItemMonitorType::None,
|
||||
search_for_missing_albums: true,
|
||||
},
|
||||
};
|
||||
@@ -1641,34 +1711,47 @@ mod tests {
|
||||
add_artist_modal.root_folder_list.state.select(Some(1));
|
||||
add_artist_modal
|
||||
.quality_profile_list
|
||||
.set_items(vec!["Lossless".to_owned()]);
|
||||
.set_items(vec!["Any".to_owned(), "Lossless".to_owned()]);
|
||||
add_artist_modal.quality_profile_list.state.select(Some(1));
|
||||
add_artist_modal
|
||||
.metadata_profile_list
|
||||
.set_items(vec!["Standard".to_owned()]);
|
||||
.set_items(vec!["None".to_owned(), "Standard".to_owned()]);
|
||||
add_artist_modal.metadata_profile_list.state.select(Some(1));
|
||||
add_artist_modal
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(MonitorType::iter()));
|
||||
add_artist_modal.monitor_list.state.select(Some(1));
|
||||
add_artist_modal
|
||||
.monitor_new_items_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
add_artist_modal
|
||||
.monitor_new_items_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
app.data.lidarr_data.add_artist_modal = Some(add_artist_modal);
|
||||
app.data.lidarr_data.quality_profile_map = BiMap::from_iter([(1, "Lossless".to_owned())]);
|
||||
app.data.lidarr_data.metadata_profile_map = BiMap::from_iter([(1, "Standard".to_owned())]);
|
||||
app.data.lidarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "Lossless".to_owned())]);
|
||||
app.data.lidarr_data.metadata_profile_map =
|
||||
BiMap::from_iter([(3333, "None".to_owned()), (4444, "Standard".to_owned())]);
|
||||
let mut add_searched_artists = StatefulTable::default();
|
||||
add_searched_artists.set_items(vec![add_artist_search_result()]);
|
||||
let mut decoy_search_result = add_artist_search_result();
|
||||
decoy_search_result.foreign_artist_id = "decoy-foreign-id".to_owned();
|
||||
decoy_search_result.artist_name = "Decoy Artist".into();
|
||||
add_searched_artists.set_items(vec![decoy_search_result, add_artist_search_result()]);
|
||||
add_searched_artists.select_index(Some(1));
|
||||
app.data.lidarr_data.add_searched_artists = Some(add_searched_artists);
|
||||
let expected_add_artist_body = AddArtistBody {
|
||||
foreign_artist_id: "test-foreign-id".to_string(),
|
||||
artist_name: "Test Artist".into(),
|
||||
monitored: true,
|
||||
root_folder_path: "/nfs2".to_string(),
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
quality_profile_id: 2222,
|
||||
metadata_profile_id: 4444,
|
||||
tags: Vec::default(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
add_options: AddArtistOptions {
|
||||
monitor: Default::default(),
|
||||
monitor_new_items: Default::default(),
|
||||
monitor: MonitorType::Future,
|
||||
monitor_new_items: NewItemMonitorType::None,
|
||||
search_for_missing_albums: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,10 +5,9 @@ use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::lidarr_models::{
|
||||
LidarrHistoryItem, LidarrRelease, LidarrReleaseDownloadBody, Track,
|
||||
};
|
||||
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease, Track};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock};
|
||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use serde_json::Number;
|
||||
@@ -281,7 +280,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AlbumDetailsHandler<
|
||||
.unwrap()
|
||||
.album_releases
|
||||
.current_selection();
|
||||
let params = LidarrReleaseDownloadBody {
|
||||
let params = ReleaseDownloadBody {
|
||||
guid: guid.clone(),
|
||||
indexer_id: *indexer_id,
|
||||
};
|
||||
@@ -386,7 +385,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AlbumDetailsHandler<
|
||||
.unwrap()
|
||||
.album_releases
|
||||
.current_selection();
|
||||
let params = LidarrReleaseDownloadBody {
|
||||
let params = ReleaseDownloadBody {
|
||||
guid: guid.clone(),
|
||||
indexer_id: *indexer_id,
|
||||
};
|
||||
|
||||
@@ -9,10 +9,10 @@ mod tests {
|
||||
AlbumDetailsHandler, releases_sorting_options,
|
||||
};
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::lidarr_models::{LidarrRelease, LidarrReleaseDownloadBody};
|
||||
use crate::models::lidarr_models::LidarrRelease;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock};
|
||||
use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal;
|
||||
use crate::models::servarr_models::{Quality, QualityWrapper};
|
||||
use crate::models::servarr_models::{Quality, QualityWrapper, ReleaseDownloadBody};
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use rstest::rstest;
|
||||
use serde_json::Number;
|
||||
@@ -255,7 +255,7 @@ mod tests {
|
||||
)]
|
||||
#[case(
|
||||
ActiveLidarrBlock::DeleteTrackFilePrompt,
|
||||
LidarrEvent::DeleteTrackFile(1)
|
||||
LidarrEvent::DeleteTrackFile(7)
|
||||
)]
|
||||
fn test_album_details_prompt_confirm_submit(
|
||||
#[case] prompt_block: ActiveLidarrBlock,
|
||||
@@ -297,7 +297,7 @@ mod tests {
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ManualAlbumSearch.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody {
|
||||
&LidarrEvent::DownloadRelease(ReleaseDownloadBody {
|
||||
guid: "1234".to_owned(),
|
||||
indexer_id: 2,
|
||||
})
|
||||
@@ -606,7 +606,7 @@ mod tests {
|
||||
)]
|
||||
#[case(
|
||||
ActiveLidarrBlock::DeleteTrackFilePrompt,
|
||||
LidarrEvent::DeleteTrackFile(1)
|
||||
LidarrEvent::DeleteTrackFile(7)
|
||||
)]
|
||||
fn test_album_details_prompt_confirm_confirm_key(
|
||||
#[case] prompt_block: ActiveLidarrBlock,
|
||||
@@ -654,12 +654,60 @@ mod tests {
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ManualAlbumSearch.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody {
|
||||
&LidarrEvent::DownloadRelease(ReleaseDownloadBody {
|
||||
guid: "1234".to_owned(),
|
||||
indexer_id: 2,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_tracks_key() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into());
|
||||
|
||||
AlbumDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.search.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::AlbumDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::SearchTracks.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_album_history_key() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::AlbumHistory.into());
|
||||
|
||||
AlbumDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.search.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::AlbumHistory,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::SearchAlbumHistory.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_album_history_key() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::AlbumHistory.into());
|
||||
|
||||
AlbumDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.filter.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::AlbumHistory,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::FilterAlbumHistory.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -704,7 +752,7 @@ mod tests {
|
||||
)
|
||||
.extract_track_file_id();
|
||||
|
||||
assert_eq!(track_file_id, 1);
|
||||
assert_eq!(track_file_id, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use super::artist_overview_handler::ArtistOverviewHandler;
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::lidarr_handlers::history::history_sorting_options;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::lidarr_models::{
|
||||
Album, LidarrHistoryItem, LidarrRelease, LidarrReleaseDownloadBody,
|
||||
};
|
||||
use crate::models::lidarr_models::{Album, LidarrHistoryItem, LidarrRelease};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_SELECTION_BLOCKS,
|
||||
EDIT_ARTIST_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::modals::ArtistOverviewModal;
|
||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::models::{BlockSelectionState, Route, ScrollableText};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use serde_json::Number;
|
||||
|
||||
@@ -35,10 +36,36 @@ impl ArtistDetailsHandler<'_, '_> {
|
||||
fn extract_album_id(&self) -> i64 {
|
||||
self.app.data.lidarr_data.albums.current_selection().id
|
||||
}
|
||||
|
||||
fn build_artist_overview_modal(&mut self) {
|
||||
let overview = self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.current_selection()
|
||||
.overview
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
|
||||
self.app.data.lidarr_data.artist_overview_modal = Some(ArtistOverviewModal {
|
||||
overview: ScrollableText::with_string(overview),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
if ArtistOverviewHandler::accepts(self.active_lidarr_block) {
|
||||
return ArtistOverviewHandler::new(
|
||||
self.key,
|
||||
self.app,
|
||||
self.active_lidarr_block,
|
||||
self._context,
|
||||
)
|
||||
.handle();
|
||||
}
|
||||
|
||||
let albums_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveLidarrBlock::ArtistDetails.into())
|
||||
.searching_block(ActiveLidarrBlock::SearchAlbums.into())
|
||||
@@ -79,7 +106,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
ARTIST_DETAILS_BLOCKS.contains(&active_block)
|
||||
ArtistOverviewHandler::accepts(active_block) || ARTIST_DETAILS_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
@@ -202,7 +229,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
|
||||
.discography_releases
|
||||
.current_selection()
|
||||
.clone();
|
||||
let params = LidarrReleaseDownloadBody { guid, indexer_id };
|
||||
let params = ReleaseDownloadBody { guid, indexer_id };
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::DownloadRelease(params));
|
||||
}
|
||||
@@ -293,6 +320,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
|
||||
self.app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS);
|
||||
}
|
||||
_ if matches_key!(view, key) => {
|
||||
self.build_artist_overview_modal();
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveLidarrBlock::ArtistOverview.into());
|
||||
}
|
||||
_ if matches_key!(toggle_monitoring, key)
|
||||
&& !self.app.data.lidarr_data.albums.is_empty() =>
|
||||
{
|
||||
@@ -365,7 +398,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler
|
||||
.discography_releases
|
||||
.current_selection()
|
||||
.clone();
|
||||
let params = LidarrReleaseDownloadBody { guid, indexer_id };
|
||||
let params = ReleaseDownloadBody { guid, indexer_id };
|
||||
self.app.data.lidarr_data.prompt_confirm_action =
|
||||
Some(LidarrEvent::DownloadRelease(params));
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ mod tests {
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
|
||||
ARTIST_DETAILS_BLOCKS, ARTIST_OVERVIEW_BLOCKS, ActiveLidarrBlock,
|
||||
};
|
||||
use crate::models::servarr_models::{Quality, QualityWrapper};
|
||||
use crate::test_handler_delegation;
|
||||
|
||||
mod test_handle_delete {
|
||||
use super::*;
|
||||
@@ -137,8 +138,9 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
|
||||
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrReleaseDownloadBody};
|
||||
use crate::models::lidarr_models::LidarrHistoryItem;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||
artist, torrent_release,
|
||||
@@ -307,7 +309,7 @@ mod tests {
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ManualArtistSearch.into());
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.prompt_confirm_action,
|
||||
Some(LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody {
|
||||
Some(LidarrEvent::DownloadRelease(ReleaseDownloadBody {
|
||||
guid: release.guid,
|
||||
indexer_id: release.indexer_id,
|
||||
}))
|
||||
@@ -449,14 +451,16 @@ mod tests {
|
||||
use crate::assert_navigation_pushed;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler;
|
||||
use crate::models::lidarr_models::{Artist, LidarrReleaseDownloadBody};
|
||||
use crate::handlers::lidarr_handlers::library::artist_overview_handler::ArtistOverviewHandler;
|
||||
use crate::models::lidarr_models::{Album, Artist, LidarrHistoryItem};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, EDIT_ARTIST_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::torrent_release;
|
||||
use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped};
|
||||
use pretty_assertions::assert_eq;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
@@ -522,6 +526,161 @@ mod tests {
|
||||
assert_modal_absent!(app.data.lidarr_data.edit_artist_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_view_key_opens_the_artist_overview() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.lidarr_data.artist_overview_modal = None;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::ArtistOverview.into());
|
||||
assert_modal_present!(app.data.lidarr_data.artist_overview_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_view_key_builds_the_overview_from_the_selected_artist() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.lidarr_data.artist_overview_modal = None;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
let overview = &app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artist_overview_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.overview;
|
||||
assert_eq!(overview.offset, 0);
|
||||
assert_eq!(
|
||||
overview.get_text(),
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.current_selection()
|
||||
.overview
|
||||
.clone()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_view_key_for_an_artist_with_no_overview() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.lidarr_data.artist_overview_modal = None;
|
||||
let mut artist = app.data.lidarr_data.artists.current_selection().clone();
|
||||
artist.overview = None;
|
||||
app.data.lidarr_data.artists.set_items(vec![artist]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::ArtistOverview.into());
|
||||
assert_str_eq!(
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artist_overview_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.overview
|
||||
.get_text(),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_view_key_reopens_the_artist_overview_at_the_top() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.lidarr_data.artist_overview_modal = None;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::ArtistOverview.into());
|
||||
assert_eq!(
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artist_overview_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.overview
|
||||
.offset,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_view_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.lidarr_data.artist_overview_modal = None;
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveLidarrBlock::ArtistDetails.into()
|
||||
);
|
||||
assert_modal_absent!(app.data.lidarr_data.artist_overview_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_toggle_monitoring_key() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
@@ -801,18 +960,84 @@ mod tests {
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ManualArtistSearch.into());
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.prompt_confirm_action,
|
||||
Some(LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody {
|
||||
Some(LidarrEvent::DownloadRelease(ReleaseDownloadBody {
|
||||
guid: release.guid,
|
||||
indexer_id: release.indexer_id,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_albums_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.albums
|
||||
.set_items(vec![Album::default()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.search.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::SearchAlbums.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_artist_history_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artist_history
|
||||
.set_items(vec![LidarrHistoryItem::default()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistHistory.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.search.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistHistory,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::SearchArtistHistory.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_artist_history_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artist_history
|
||||
.set_items(vec![LidarrHistoryItem::default()]);
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistHistory.into());
|
||||
|
||||
ArtistDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.filter.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistHistory,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::FilterArtistHistory.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_details_handler_accepts() {
|
||||
let mut artist_details_blocks = ARTIST_DETAILS_BLOCKS.to_vec();
|
||||
artist_details_blocks.extend(ARTIST_OVERVIEW_BLOCKS);
|
||||
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) {
|
||||
if artist_details_blocks.contains(&active_lidarr_block) {
|
||||
assert!(ArtistDetailsHandler::accepts(active_lidarr_block));
|
||||
} else {
|
||||
assert!(!ArtistDetailsHandler::accepts(active_lidarr_block));
|
||||
@@ -820,6 +1045,15 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delegates_artist_overview_blocks_to_artist_overview_handler() {
|
||||
test_handler_delegation!(
|
||||
ArtistDetailsHandler,
|
||||
ActiveLidarrBlock::ArtistDetails,
|
||||
ActiveLidarrBlock::ArtistOverview
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_artist_id() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::matches_key;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ARTIST_OVERVIEW_BLOCKS, ActiveLidarrBlock};
|
||||
use crate::models::{Route, Scrollable, ScrollableText};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "artist_overview_handler_tests.rs"]
|
||||
mod artist_overview_handler_tests;
|
||||
|
||||
pub(super) struct ArtistOverviewHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl ArtistOverviewHandler<'_, '_> {
|
||||
fn overview(&mut self) -> &mut ScrollableText {
|
||||
&mut self
|
||||
.app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artist_overview_modal
|
||||
.as_mut()
|
||||
.expect("Artist overview modal is undefined")
|
||||
.overview
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistOverviewHandler<'a, 'b> {
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
ARTIST_OVERVIEW_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
) -> ArtistOverviewHandler<'a, 'b> {
|
||||
ArtistOverviewHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
_context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
if self.app.is_loading {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.app.data.lidarr_data.artist_overview_modal.is_some()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {
|
||||
self.overview().scroll_up();
|
||||
}
|
||||
|
||||
fn handle_scroll_down(&mut self) {
|
||||
self.overview().scroll_down();
|
||||
}
|
||||
|
||||
fn handle_home(&mut self) {
|
||||
self.overview().scroll_to_top();
|
||||
}
|
||||
|
||||
fn handle_end(&mut self) {
|
||||
self.overview().scroll_to_bottom();
|
||||
}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {}
|
||||
|
||||
fn handle_submit(&mut self) {}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.lidarr_data.artist_overview_modal = None;
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
if matches_key!(refresh, self.key) {
|
||||
self
|
||||
.app
|
||||
.pop_and_push_navigation_stack(self.active_lidarr_block.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::library::artist_overview_handler::ArtistOverviewHandler;
|
||||
use crate::models::ScrollableText;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ARTIST_OVERVIEW_BLOCKS, ActiveLidarrBlock,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::modals::ArtistOverviewModal;
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_scroll_up() {
|
||||
let mut app = app_with_artist_overview();
|
||||
set_offset(&mut app, 2);
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_scroll_up_no_op_when_not_ready() {
|
||||
let mut app = app_with_artist_overview();
|
||||
set_offset(&mut app, 2);
|
||||
app.is_loading = true;
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_scroll_down() {
|
||||
let mut app = app_with_artist_overview();
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_scroll_down_no_op_when_not_ready() {
|
||||
let mut app = app_with_artist_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 0);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_home_end {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_home() {
|
||||
let mut app = app_with_artist_overview();
|
||||
set_offset(&mut app, 3);
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.home.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_end() {
|
||||
let mut app = app_with_artist_overview();
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_home_no_op_when_not_ready() {
|
||||
let mut app = app_with_artist_overview();
|
||||
set_offset(&mut app, 3);
|
||||
app.is_loading = true;
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.home.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_end_no_op_when_not_ready() {
|
||||
let mut app = app_with_artist_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 0);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use super::*;
|
||||
use crate::{assert_modal_absent, assert_navigation_popped};
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_esc_tears_down_the_artist_overview_modal() {
|
||||
let mut app = app_with_artist_overview();
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert_modal_absent!(app.data.lidarr_data.artist_overview_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_esc_tears_down_the_modal_even_when_loading() {
|
||||
let mut app = app_with_artist_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into());
|
||||
assert_modal_absent!(app.data.lidarr_data.artist_overview_modal);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use super::*;
|
||||
use crate::assert_navigation_pushed;
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_refresh_key() {
|
||||
let mut app = app_with_artist_overview();
|
||||
app.is_routing = false;
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::ArtistOverview.into());
|
||||
assert!(app.is_routing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_refresh_key_no_op_when_not_ready() {
|
||||
let mut app = app_with_artist_overview();
|
||||
app.is_loading = true;
|
||||
app.is_routing = false;
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(!app.is_routing);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scrolling_the_overview_does_not_mutate_the_artist_record() {
|
||||
let mut app = app_with_artist_overview();
|
||||
let overview_before = app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.current_selection()
|
||||
.overview
|
||||
.clone();
|
||||
|
||||
ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 1);
|
||||
assert_eq!(
|
||||
app.data.lidarr_data.artists.current_selection().overview,
|
||||
overview_before
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_handler_accepts() {
|
||||
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
|
||||
if ARTIST_OVERVIEW_BLOCKS.contains(&active_lidarr_block) {
|
||||
assert!(
|
||||
ArtistOverviewHandler::accepts(active_lidarr_block),
|
||||
"{active_lidarr_block} is not accepted by the ArtistOverviewHandler"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
!ArtistOverviewHandler::accepts(active_lidarr_block),
|
||||
"{active_lidarr_block} is wrongly accepted by the ArtistOverviewHandler"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_handler_dispatches_no_network_events() {
|
||||
let mut app = app_with_artist_overview();
|
||||
|
||||
for key in [
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
DEFAULT_KEYBINDINGS.home.key,
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
DEFAULT_KEYBINDINGS.delete.key,
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
DEFAULT_KEYBINDINGS.submit.key,
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
] {
|
||||
ArtistOverviewHandler::new(key, &mut app, ActiveLidarrBlock::ArtistOverview, None).handle();
|
||||
|
||||
assert_none!(app.data.lidarr_data.prompt_confirm_action);
|
||||
assert!(!app.data.lidarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_handler_is_not_ready_when_loading() {
|
||||
let mut app = app_with_artist_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_handler_is_not_ready_when_the_modal_is_absent() {
|
||||
let mut app = app_with_artist_overview();
|
||||
app.data.lidarr_data.artist_overview_modal = None;
|
||||
|
||||
let handler = ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_handler_is_ready() {
|
||||
let mut app = app_with_artist_overview();
|
||||
|
||||
let handler = ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_overview_handler_ignores_special_keys_for_textbox_input() {
|
||||
let mut app = app_with_artist_overview();
|
||||
app.ignore_special_keys_for_textbox_input = true;
|
||||
|
||||
let handler = ArtistOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::ArtistOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.ignore_special_keys());
|
||||
}
|
||||
|
||||
fn app_with_artist_overview() -> App<'static> {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.lidarr_data.artist_overview_modal = Some(ArtistOverviewModal {
|
||||
overview: ScrollableText::with_string(
|
||||
"some interesting description of the artist\r\n\
|
||||
\r\n\
|
||||
She was born in Madison, Wisconsin: a city she has never really left.\r\n\
|
||||
\r\n\
|
||||
Her first album took her seven years to finish.\r\n"
|
||||
.to_owned(),
|
||||
),
|
||||
});
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistOverview.into());
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
fn overview_offset(app: &App<'_>) -> u16 {
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artist_overview_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.overview
|
||||
.offset
|
||||
}
|
||||
|
||||
fn set_offset(app: &mut App<'_>, offset: u16) {
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artist_overview_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.overview
|
||||
.offset = offset;
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ mod tests {
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.delete_files = false;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app
|
||||
.data
|
||||
@@ -136,7 +136,7 @@ mod tests {
|
||||
.set_items(vec![Album::default()]);
|
||||
let expected_delete_album_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
delete_files: false,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
|
||||
@@ -275,7 +275,7 @@ mod tests {
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into());
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = false;
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
@@ -284,7 +284,7 @@ mod tests {
|
||||
let expected_delete_album_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
add_import_list_exclusion: false,
|
||||
};
|
||||
app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS);
|
||||
app
|
||||
@@ -352,11 +352,11 @@ mod tests {
|
||||
.albums
|
||||
.set_items(vec![Album::default()]);
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = false;
|
||||
let expected_delete_album_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
add_import_list_exclusion: false,
|
||||
};
|
||||
|
||||
let delete_album_params = DeleteAlbumHandler::new(
|
||||
|
||||
@@ -130,7 +130,7 @@ mod tests {
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.prompt_confirm = true;
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.delete_files = false;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app
|
||||
.data
|
||||
@@ -139,7 +139,7 @@ mod tests {
|
||||
.set_items(vec![Artist::default()]);
|
||||
let expected_delete_artist_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
delete_files: false,
|
||||
add_import_list_exclusion: true,
|
||||
};
|
||||
app.data.lidarr_data.selected_block =
|
||||
@@ -280,7 +280,7 @@ mod tests {
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into());
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = false;
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
@@ -289,7 +289,7 @@ mod tests {
|
||||
let expected_delete_artist_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
add_import_list_exclusion: false,
|
||||
};
|
||||
app.data.lidarr_data.selected_block =
|
||||
BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS);
|
||||
@@ -358,11 +358,11 @@ mod tests {
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.data.lidarr_data.delete_files = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = true;
|
||||
app.data.lidarr_data.add_import_list_exclusion = false;
|
||||
let expected_delete_artist_params = DeleteParams {
|
||||
id: 0,
|
||||
delete_files: true,
|
||||
add_import_list_exclusion: true,
|
||||
add_import_list_exclusion: false,
|
||||
};
|
||||
|
||||
let delete_artist_params = DeleteArtistHandler::new(
|
||||
|
||||
@@ -782,29 +782,41 @@ mod tests {
|
||||
edit_artist
|
||||
.quality_profile_list
|
||||
.set_items(vec!["Lossless".to_owned(), "HD - 1080p".to_owned()]);
|
||||
edit_artist.quality_profile_list.state.select(Some(1));
|
||||
edit_artist
|
||||
.metadata_profile_list
|
||||
.set_items(vec!["Standard".to_owned(), "Full".to_owned()]);
|
||||
edit_artist.metadata_profile_list.state.select(Some(1));
|
||||
edit_artist
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
edit_artist.monitor_list.state.select(Some(1));
|
||||
app.data.lidarr_data.edit_artist_modal = Some(edit_artist);
|
||||
app.data.lidarr_data.artists.set_items(vec![Artist {
|
||||
monitored: false,
|
||||
..Artist::default()
|
||||
}]);
|
||||
app.data.lidarr_data.artists.set_items(vec![
|
||||
Artist {
|
||||
id: 999,
|
||||
monitored: false,
|
||||
..Artist::default()
|
||||
},
|
||||
Artist {
|
||||
id: 2,
|
||||
monitored: false,
|
||||
..Artist::default()
|
||||
},
|
||||
]);
|
||||
app.data.lidarr_data.artists.select_index(Some(1));
|
||||
app.data.lidarr_data.quality_profile_map = BiMap::from_iter([
|
||||
(1111, "Lossless".to_owned()),
|
||||
(2222, "HD - 1080p".to_owned()),
|
||||
]);
|
||||
app.data.lidarr_data.metadata_profile_map =
|
||||
BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]);
|
||||
BiMap::from_iter([(3333, "Standard".to_owned()), (4444, "Full".to_owned())]);
|
||||
let expected_edit_artist_params = EditArtistParams {
|
||||
artist_id: 0,
|
||||
artist_id: 2,
|
||||
monitored: Some(false),
|
||||
monitor_new_items: Some(NewItemMonitorType::All),
|
||||
quality_profile_id: Some(1111),
|
||||
metadata_profile_id: Some(1111),
|
||||
monitor_new_items: Some(NewItemMonitorType::None),
|
||||
quality_profile_id: Some(2222),
|
||||
metadata_profile_id: Some(4444),
|
||||
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
..EditArtistParams::default()
|
||||
@@ -1236,29 +1248,41 @@ mod tests {
|
||||
edit_artist
|
||||
.quality_profile_list
|
||||
.set_items(vec!["Lossless".to_owned(), "HD - 1080p".to_owned()]);
|
||||
edit_artist.quality_profile_list.state.select(Some(1));
|
||||
edit_artist
|
||||
.metadata_profile_list
|
||||
.set_items(vec!["Standard".to_owned(), "Full".to_owned()]);
|
||||
edit_artist.metadata_profile_list.state.select(Some(1));
|
||||
edit_artist
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
edit_artist.monitor_list.state.select(Some(1));
|
||||
app.data.lidarr_data.edit_artist_modal = Some(edit_artist);
|
||||
app.data.lidarr_data.artists.set_items(vec![Artist {
|
||||
monitored: false,
|
||||
..Artist::default()
|
||||
}]);
|
||||
app.data.lidarr_data.artists.set_items(vec![
|
||||
Artist {
|
||||
id: 999,
|
||||
monitored: false,
|
||||
..Artist::default()
|
||||
},
|
||||
Artist {
|
||||
id: 2,
|
||||
monitored: false,
|
||||
..Artist::default()
|
||||
},
|
||||
]);
|
||||
app.data.lidarr_data.artists.select_index(Some(1));
|
||||
app.data.lidarr_data.quality_profile_map = BiMap::from_iter([
|
||||
(1111, "Lossless".to_owned()),
|
||||
(2222, "HD - 1080p".to_owned()),
|
||||
]);
|
||||
app.data.lidarr_data.metadata_profile_map =
|
||||
BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]);
|
||||
BiMap::from_iter([(3333, "Standard".to_owned()), (4444, "Full".to_owned())]);
|
||||
let expected_edit_artist_params = EditArtistParams {
|
||||
artist_id: 0,
|
||||
artist_id: 2,
|
||||
monitored: Some(false),
|
||||
monitor_new_items: Some(NewItemMonitorType::All),
|
||||
quality_profile_id: Some(1111),
|
||||
metadata_profile_id: Some(1111),
|
||||
monitor_new_items: Some(NewItemMonitorType::None),
|
||||
quality_profile_id: Some(2222),
|
||||
metadata_profile_id: Some(4444),
|
||||
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
..EditArtistParams::default()
|
||||
@@ -1332,29 +1356,41 @@ mod tests {
|
||||
edit_artist
|
||||
.quality_profile_list
|
||||
.set_items(vec!["Lossless".to_owned(), "HD - 1080p".to_owned()]);
|
||||
edit_artist.quality_profile_list.state.select(Some(1));
|
||||
edit_artist
|
||||
.metadata_profile_list
|
||||
.set_items(vec!["Standard".to_owned(), "Full".to_owned()]);
|
||||
edit_artist.metadata_profile_list.state.select(Some(1));
|
||||
edit_artist
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
edit_artist.monitor_list.state.select(Some(1));
|
||||
app.data.lidarr_data.edit_artist_modal = Some(edit_artist);
|
||||
app.data.lidarr_data.artists.set_items(vec![Artist {
|
||||
monitored: false,
|
||||
..Artist::default()
|
||||
}]);
|
||||
app.data.lidarr_data.artists.set_items(vec![
|
||||
Artist {
|
||||
id: 999,
|
||||
monitored: false,
|
||||
..Artist::default()
|
||||
},
|
||||
Artist {
|
||||
id: 2,
|
||||
monitored: false,
|
||||
..Artist::default()
|
||||
},
|
||||
]);
|
||||
app.data.lidarr_data.artists.select_index(Some(1));
|
||||
app.data.lidarr_data.quality_profile_map = BiMap::from_iter([
|
||||
(1111, "Lossless".to_owned()),
|
||||
(2222, "HD - 1080p".to_owned()),
|
||||
]);
|
||||
app.data.lidarr_data.metadata_profile_map =
|
||||
BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]);
|
||||
BiMap::from_iter([(3333, "Standard".to_owned()), (4444, "Full".to_owned())]);
|
||||
let expected_edit_artist_params = EditArtistParams {
|
||||
artist_id: 0,
|
||||
artist_id: 2,
|
||||
monitored: Some(false),
|
||||
monitor_new_items: Some(NewItemMonitorType::All),
|
||||
quality_profile_id: Some(1111),
|
||||
metadata_profile_id: Some(1111),
|
||||
monitor_new_items: Some(NewItemMonitorType::None),
|
||||
quality_profile_id: Some(2222),
|
||||
metadata_profile_id: Some(4444),
|
||||
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
..EditArtistParams::default()
|
||||
|
||||
@@ -13,9 +13,9 @@ mod tests {
|
||||
use crate::handlers::lidarr_handlers::library::{LibraryHandler, artists_sorting_options};
|
||||
use crate::models::lidarr_models::{Album, Artist, ArtistStatistics, ArtistStatus};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ADD_ARTIST_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
|
||||
DELETE_ALBUM_BLOCKS, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS,
|
||||
LIBRARY_BLOCKS, TRACK_DETAILS_BLOCKS,
|
||||
ADD_ARTIST_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ARTIST_OVERVIEW_BLOCKS,
|
||||
ActiveLidarrBlock, DELETE_ALBUM_BLOCKS, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS,
|
||||
EDIT_ARTIST_SELECTION_BLOCKS, LIBRARY_BLOCKS, TRACK_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
@@ -29,6 +29,7 @@ mod tests {
|
||||
let mut library_handler_blocks = Vec::new();
|
||||
library_handler_blocks.extend(LIBRARY_BLOCKS);
|
||||
library_handler_blocks.extend(ARTIST_DETAILS_BLOCKS);
|
||||
library_handler_blocks.extend(ARTIST_OVERVIEW_BLOCKS);
|
||||
library_handler_blocks.extend(DELETE_ARTIST_BLOCKS);
|
||||
library_handler_blocks.extend(DELETE_ALBUM_BLOCKS);
|
||||
library_handler_blocks.extend(EDIT_ARTIST_BLOCKS);
|
||||
@@ -243,11 +244,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_toggle_monitoring_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.artists
|
||||
.set_items(vec![Artist::default()]);
|
||||
app.data.lidarr_data.artists.set_items(artists_vec());
|
||||
app.data.lidarr_data.artists.select_index(Some(1));
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
app.is_routing = false;
|
||||
|
||||
@@ -264,7 +262,7 @@ mod tests {
|
||||
assert!(app.is_routing);
|
||||
assert_some_eq_x!(
|
||||
&app.data.lidarr_data.prompt_confirm_action,
|
||||
&LidarrEvent::ToggleArtistMonitoring(0)
|
||||
&LidarrEvent::ToggleArtistMonitoring(2)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -769,4 +767,38 @@ mod tests {
|
||||
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_artists_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.artists.set_items(artists_vec());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.search.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Artists,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::SearchArtists.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_artists_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.lidarr_data.artists.set_items(artists_vec());
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.filter.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::Artists,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::FilterArtists.into());
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
mod add_artist_handler;
|
||||
mod album_details_handler;
|
||||
mod artist_details_handler;
|
||||
mod artist_overview_handler;
|
||||
mod delete_album_handler;
|
||||
mod delete_artist_handler;
|
||||
mod edit_artist_handler;
|
||||
|
||||
@@ -244,6 +244,38 @@ mod tests {
|
||||
assert_eq!(app.get_current_route(), active_lidarr_block.into());
|
||||
assert!(!app.is_routing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_track_history_key() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
|
||||
|
||||
TrackDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.search.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::TrackHistory,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::SearchTrackHistory.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_track_history_key() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into());
|
||||
|
||||
TrackDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.filter.key,
|
||||
&mut app,
|
||||
ActiveLidarrBlock::TrackHistory,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveLidarrBlock::FilterTrackHistory.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -160,7 +160,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_library_blocks_to_library_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::Artists,
|
||||
ActiveLidarrBlock::ArtistsSortPrompt,
|
||||
ActiveLidarrBlock::FilterArtists,
|
||||
ActiveLidarrBlock::FilterArtistsError,
|
||||
@@ -187,7 +186,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_downloads_blocks_to_downloads_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::Downloads,
|
||||
ActiveLidarrBlock::DeleteDownloadPrompt,
|
||||
ActiveLidarrBlock::UpdateDownloadsPrompt
|
||||
)]
|
||||
@@ -203,7 +201,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_blocklist_blocks_to_blocklist_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::Blocklist,
|
||||
ActiveLidarrBlock::BlocklistItemDetails,
|
||||
ActiveLidarrBlock::DeleteBlocklistItemPrompt,
|
||||
ActiveLidarrBlock::BlocklistClearAllItemsPrompt,
|
||||
@@ -221,7 +218,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_history_blocks_to_history_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::History,
|
||||
ActiveLidarrBlock::HistoryItemDetails,
|
||||
ActiveLidarrBlock::HistorySortPrompt,
|
||||
ActiveLidarrBlock::FilterHistory,
|
||||
@@ -241,7 +237,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_root_folders_blocks_to_root_folders_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::RootFolders,
|
||||
ActiveLidarrBlock::AddRootFolderPrompt,
|
||||
ActiveLidarrBlock::DeleteRootFolderPrompt
|
||||
)]
|
||||
@@ -258,7 +253,6 @@ mod tests {
|
||||
fn test_delegates_indexers_blocks_to_indexers_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::DeleteIndexerPrompt,
|
||||
ActiveLidarrBlock::Indexers,
|
||||
ActiveLidarrBlock::AllIndexerSettingsPrompt,
|
||||
ActiveLidarrBlock::IndexerSettingsConfirmPrompt,
|
||||
ActiveLidarrBlock::IndexerSettingsMaximumSizeInput,
|
||||
@@ -278,7 +272,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_system_blocks_to_system_handler(
|
||||
#[values(
|
||||
ActiveLidarrBlock::System,
|
||||
ActiveLidarrBlock::SystemLogs,
|
||||
ActiveLidarrBlock::SystemQueuedEvents,
|
||||
ActiveLidarrBlock::SystemTasks,
|
||||
|
||||
@@ -1052,27 +1052,31 @@ mod tests {
|
||||
add_root_folder
|
||||
.quality_profile_list
|
||||
.set_items(vec!["Lossless".to_owned(), "FLAC".to_owned()]);
|
||||
add_root_folder.quality_profile_list.state.select(Some(1));
|
||||
add_root_folder
|
||||
.metadata_profile_list
|
||||
.set_items(vec!["Standard".to_owned(), "Full".to_owned()]);
|
||||
add_root_folder.metadata_profile_list.state.select(Some(1));
|
||||
add_root_folder
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(MonitorType::iter()));
|
||||
add_root_folder.monitor_list.state.select(Some(1));
|
||||
add_root_folder
|
||||
.monitor_new_items_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
add_root_folder.monitor_new_items_list.state.select(Some(1));
|
||||
app.data.lidarr_data.add_root_folder_modal = Some(add_root_folder);
|
||||
app.data.lidarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Lossless".to_owned()), (2222, "FLAC".to_owned())]);
|
||||
app.data.lidarr_data.metadata_profile_map =
|
||||
BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]);
|
||||
BiMap::from_iter([(3333, "Standard".to_owned()), (4444, "Full".to_owned())]);
|
||||
let expected_add_root_folder_body = AddLidarrRootFolderBody {
|
||||
name: "Test Name".to_owned(),
|
||||
path: "/nfs/Test Path".to_owned(),
|
||||
default_quality_profile_id: 1111,
|
||||
default_metadata_profile_id: 1111,
|
||||
default_monitor_option: MonitorType::All,
|
||||
default_new_item_monitor_option: NewItemMonitorType::All,
|
||||
default_quality_profile_id: 2222,
|
||||
default_metadata_profile_id: 4444,
|
||||
default_monitor_option: MonitorType::Future,
|
||||
default_new_item_monitor_option: NewItemMonitorType::None,
|
||||
default_tags: Vec::new(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
};
|
||||
@@ -1525,27 +1529,31 @@ mod tests {
|
||||
add_root_folder
|
||||
.quality_profile_list
|
||||
.set_items(vec!["Lossless".to_owned(), "FLAC".to_owned()]);
|
||||
add_root_folder.quality_profile_list.state.select(Some(1));
|
||||
add_root_folder
|
||||
.metadata_profile_list
|
||||
.set_items(vec!["Standard".to_owned(), "Full".to_owned()]);
|
||||
add_root_folder.metadata_profile_list.state.select(Some(1));
|
||||
add_root_folder
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(MonitorType::iter()));
|
||||
add_root_folder.monitor_list.state.select(Some(1));
|
||||
add_root_folder
|
||||
.monitor_new_items_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
add_root_folder.monitor_new_items_list.state.select(Some(1));
|
||||
app.data.lidarr_data.add_root_folder_modal = Some(add_root_folder);
|
||||
app.data.lidarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Lossless".to_owned()), (2222, "FLAC".to_owned())]);
|
||||
app.data.lidarr_data.metadata_profile_map =
|
||||
BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]);
|
||||
BiMap::from_iter([(3333, "Standard".to_owned()), (4444, "Full".to_owned())]);
|
||||
let expected_add_root_folder_body = AddLidarrRootFolderBody {
|
||||
name: "Test Name".to_owned(),
|
||||
path: "/nfs/Test Path".to_owned(),
|
||||
default_quality_profile_id: 1111,
|
||||
default_metadata_profile_id: 1111,
|
||||
default_monitor_option: MonitorType::All,
|
||||
default_new_item_monitor_option: NewItemMonitorType::All,
|
||||
default_quality_profile_id: 2222,
|
||||
default_metadata_profile_id: 4444,
|
||||
default_monitor_option: MonitorType::Future,
|
||||
default_new_item_monitor_option: NewItemMonitorType::None,
|
||||
default_tags: Vec::new(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
};
|
||||
@@ -1619,27 +1627,31 @@ mod tests {
|
||||
add_root_folder
|
||||
.quality_profile_list
|
||||
.set_items(vec!["Lossless".to_owned(), "FLAC".to_owned()]);
|
||||
add_root_folder.quality_profile_list.state.select(Some(1));
|
||||
add_root_folder
|
||||
.metadata_profile_list
|
||||
.set_items(vec!["Standard".to_owned(), "Full".to_owned()]);
|
||||
add_root_folder.metadata_profile_list.state.select(Some(1));
|
||||
add_root_folder
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(MonitorType::iter()));
|
||||
add_root_folder.monitor_list.state.select(Some(1));
|
||||
add_root_folder
|
||||
.monitor_new_items_list
|
||||
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
|
||||
add_root_folder.monitor_new_items_list.state.select(Some(1));
|
||||
app.data.lidarr_data.add_root_folder_modal = Some(add_root_folder);
|
||||
app.data.lidarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Lossless".to_owned()), (2222, "FLAC".to_owned())]);
|
||||
app.data.lidarr_data.metadata_profile_map =
|
||||
BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]);
|
||||
BiMap::from_iter([(3333, "Standard".to_owned()), (4444, "Full".to_owned())]);
|
||||
let expected_add_root_folder_body = AddLidarrRootFolderBody {
|
||||
name: "Test Name".to_owned(),
|
||||
path: "/nfs/Test Path".to_owned(),
|
||||
default_quality_profile_id: 1111,
|
||||
default_metadata_profile_id: 1111,
|
||||
default_monitor_option: MonitorType::All,
|
||||
default_new_item_monitor_option: NewItemMonitorType::All,
|
||||
default_quality_profile_id: 2222,
|
||||
default_metadata_profile_id: 4444,
|
||||
default_monitor_option: MonitorType::Future,
|
||||
default_new_item_monitor_option: NewItemMonitorType::None,
|
||||
default_tags: Vec::new(),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
};
|
||||
|
||||
@@ -970,11 +970,17 @@ mod tests {
|
||||
#[test]
|
||||
fn test_extract_task_name() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.tasks
|
||||
.set_items(vec![LidarrTask::default()]);
|
||||
app.data.lidarr_data.tasks.set_items(vec![
|
||||
LidarrTask {
|
||||
task_name: LidarrTaskName::Backup,
|
||||
..LidarrTask::default()
|
||||
},
|
||||
LidarrTask {
|
||||
task_name: LidarrTaskName::RefreshArtist,
|
||||
..LidarrTask::default()
|
||||
},
|
||||
]);
|
||||
app.data.lidarr_data.tasks.select_index(Some(1));
|
||||
|
||||
let task_name = SystemDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
@@ -984,7 +990,7 @@ mod tests {
|
||||
)
|
||||
.extract_task_name();
|
||||
|
||||
assert_eq!(task_name, LidarrTaskName::default());
|
||||
assert_eq!(task_name, LidarrTaskName::RefreshArtist);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use lidarr_handlers::LidarrHandler;
|
||||
use radarr_handlers::RadarrHandler;
|
||||
use readarr_handlers::ReadarrHandler;
|
||||
use sonarr_handlers::SonarrHandler;
|
||||
|
||||
use crate::app::App;
|
||||
@@ -18,12 +19,16 @@ use crate::models::{HorizontallyScrollableText, Route};
|
||||
mod keybinding_handler;
|
||||
mod lidarr_handlers;
|
||||
mod radarr_handlers;
|
||||
mod readarr_handlers;
|
||||
mod sonarr_handlers;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "handlers_tests.rs"]
|
||||
mod handlers_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod handler_proptest;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "handler_test_utils.rs"]
|
||||
pub mod handler_test_utils;
|
||||
@@ -132,6 +137,9 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
|
||||
Route::Lidarr(active_lidarr_block, context) => {
|
||||
LidarrHandler::new(key, app, active_lidarr_block, context).handle()
|
||||
}
|
||||
Route::Readarr(active_readarr_block, context) => {
|
||||
ReadarrHandler::new(key, app, active_readarr_block, context).handle()
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -197,6 +205,9 @@ fn handle_prompt_toggle(app: &mut App<'_>, key: Key) {
|
||||
Route::Lidarr(_, _) => {
|
||||
app.data.lidarr_data.prompt_confirm = !app.data.lidarr_data.prompt_confirm
|
||||
}
|
||||
Route::Readarr(_, _) => {
|
||||
app.data.readarr_data.prompt_confirm = !app.data.readarr_data.prompt_confirm
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
|
||||
@@ -154,7 +154,7 @@ mod tests {
|
||||
#[case(
|
||||
ActiveRadarrBlock::Blocklist,
|
||||
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
|
||||
RadarrEvent::DeleteBlocklistItem(3)
|
||||
RadarrEvent::DeleteBlocklistItem(2)
|
||||
)]
|
||||
#[case(
|
||||
ActiveRadarrBlock::Blocklist,
|
||||
@@ -168,6 +168,7 @@ mod tests {
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.data.radarr_data.blocklist.select_index(Some(1));
|
||||
app.data.radarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
@@ -353,7 +354,7 @@ mod tests {
|
||||
#[case(
|
||||
ActiveRadarrBlock::Blocklist,
|
||||
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
|
||||
RadarrEvent::DeleteBlocklistItem(3)
|
||||
RadarrEvent::DeleteBlocklistItem(2)
|
||||
)]
|
||||
#[case(
|
||||
ActiveRadarrBlock::Blocklist,
|
||||
@@ -367,6 +368,7 @@ mod tests {
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.data.radarr_data.blocklist.select_index(Some(1));
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
@@ -556,6 +558,7 @@ mod tests {
|
||||
fn test_extract_blocklist_item_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.data.radarr_data.blocklist.select_index(Some(1));
|
||||
|
||||
let blocklist_item_id = BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
@@ -565,7 +568,7 @@ mod tests {
|
||||
)
|
||||
.extract_blocklist_item_id();
|
||||
|
||||
assert_eq!(blocklist_item_id, 3);
|
||||
assert_eq!(blocklist_item_id, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use super::movie_overview_handler::MovieOverviewHandler;
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::matches_key;
|
||||
use crate::models::servarr_data::radarr::modals::MovieOverviewModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
|
||||
EDIT_COLLECTION_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::models::{BlockSelectionState, Route, ScrollableText};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "collection_details_handler_tests.rs"]
|
||||
@@ -21,10 +23,35 @@ pub(super) struct CollectionDetailsHandler<'a, 'b> {
|
||||
_context: Option<ActiveRadarrBlock>,
|
||||
}
|
||||
|
||||
impl CollectionDetailsHandler<'_, '_> {}
|
||||
impl CollectionDetailsHandler<'_, '_> {
|
||||
fn build_movie_overview_modal(&mut self) {
|
||||
let overview = self
|
||||
.app
|
||||
.data
|
||||
.radarr_data
|
||||
.collection_movies
|
||||
.current_selection()
|
||||
.overview
|
||||
.clone();
|
||||
|
||||
self.app.data.radarr_data.movie_overview_modal = Some(MovieOverviewModal {
|
||||
overview: ScrollableText::with_string(overview),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
if MovieOverviewHandler::accepts(self.active_radarr_block) {
|
||||
return MovieOverviewHandler::new(
|
||||
self.key,
|
||||
self.app,
|
||||
self.active_radarr_block,
|
||||
self._context,
|
||||
)
|
||||
.handle();
|
||||
}
|
||||
|
||||
let collection_movies_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveRadarrBlock::CollectionDetails.into());
|
||||
|
||||
@@ -98,6 +125,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
|
||||
.iter()
|
||||
.any(|movie| movie.tmdb_id == tmdb_id)
|
||||
{
|
||||
self.build_movie_overview_modal();
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveRadarrBlock::ViewMovieOverview.into());
|
||||
@@ -117,13 +145,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_radarr_block {
|
||||
ActiveRadarrBlock::CollectionDetails => {
|
||||
self.app.data.radarr_data.collection_movies = StatefulTable::default();
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveRadarrBlock::ViewMovieOverview => self.app.pop_navigation_stack(),
|
||||
_ => (),
|
||||
if self.active_radarr_block == ActiveRadarrBlock::CollectionDetails {
|
||||
self.app.data.radarr_data.collection_movies = StatefulTable::default();
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,18 @@ mod tests {
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::radarr_handlers::collections::collection_details_handler::CollectionDetailsHandler;
|
||||
use crate::models::radarr_models::CollectionMovie;
|
||||
use crate::models::servarr_data::radarr::modals::MovieOverviewModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::test_handler_delegation;
|
||||
|
||||
mod test_handle_submit {
|
||||
use crate::assert_navigation_pushed;
|
||||
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::collection_movie;
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::radarr_models::Movie;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ADD_MOVIE_SELECTION_BLOCKS;
|
||||
use crate::{assert_modal_present, assert_navigation_pushed};
|
||||
use bimap::BiMap;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -139,16 +142,19 @@ mod tests {
|
||||
#[test]
|
||||
fn test_collection_details_submit_movie_already_in_library() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.collection_movies
|
||||
.set_items(vec![CollectionMovie::default()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![Movie::default()]);
|
||||
app.data.radarr_data.collection_movies.set_items(vec![
|
||||
CollectionMovie {
|
||||
tmdb_id: 5678,
|
||||
overview: "Some other overview".to_owned(),
|
||||
..CollectionMovie::default()
|
||||
},
|
||||
collection_movie(),
|
||||
]);
|
||||
app.data.radarr_data.collection_movies.select_index(Some(1));
|
||||
app.data.radarr_data.movies.set_items(vec![Movie {
|
||||
tmdb_id: collection_movie().tmdb_id,
|
||||
..Movie::default()
|
||||
}]);
|
||||
|
||||
CollectionDetailsHandler::new(
|
||||
SUBMIT_KEY,
|
||||
@@ -159,6 +165,63 @@ mod tests {
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveRadarrBlock::ViewMovieOverview.into());
|
||||
assert_modal_present!(app.data.radarr_data.movie_overview_modal);
|
||||
let movie_overview_modal = app.data.radarr_data.movie_overview_modal.as_ref().unwrap();
|
||||
assert_eq!(movie_overview_modal.overview.offset, 0);
|
||||
assert_str_eq!(
|
||||
movie_overview_modal.overview.get_text(),
|
||||
collection_movie().overview
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collection_details_submit_reopens_the_movie_overview_at_the_top() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into());
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.collection_movies
|
||||
.set_items(vec![collection_movie()]);
|
||||
app.data.radarr_data.movies.set_items(vec![Movie {
|
||||
tmdb_id: collection_movie().tmdb_id,
|
||||
..Movie::default()
|
||||
}]);
|
||||
|
||||
CollectionDetailsHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::CollectionDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
CollectionDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 5);
|
||||
|
||||
CollectionDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
CollectionDetailsHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::CollectionDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveRadarrBlock::ViewMovieOverview.into());
|
||||
assert_eq!(overview_offset(&app), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +261,7 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::ViewMovieOverview.into());
|
||||
app.data.radarr_data.movie_overview_modal = Some(MovieOverviewModal::default());
|
||||
|
||||
CollectionDetailsHandler::new(
|
||||
ESC_KEY,
|
||||
@@ -208,6 +272,7 @@ mod tests {
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::CollectionDetails.into());
|
||||
assert_modal_absent!(app.data.radarr_data.movie_overview_modal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +342,15 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delegates_view_movie_overview_to_movie_overview_handler() {
|
||||
test_handler_delegation!(
|
||||
CollectionDetailsHandler,
|
||||
ActiveRadarrBlock::CollectionDetails,
|
||||
ActiveRadarrBlock::ViewMovieOverview
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_collection_details_handler_ignore_special_keys(
|
||||
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
|
||||
@@ -345,4 +419,15 @@ mod tests {
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
fn overview_offset(app: &App<'_>) -> u16 {
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movie_overview_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.overview
|
||||
.offset
|
||||
}
|
||||
}
|
||||
@@ -381,6 +381,48 @@ mod tests {
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_collections_key() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.collections
|
||||
.set_items(vec![Collection::default()]);
|
||||
|
||||
CollectionsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.search.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::Collections,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveRadarrBlock::SearchCollection.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_collections_key() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.collections
|
||||
.set_items(vec![Collection::default()]);
|
||||
|
||||
CollectionsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.filter.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::Collections,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveRadarrBlock::FilterCollections.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_all_collections_prompt_confirm_confirm() {
|
||||
let mut app = App::test_default();
|
||||
|
||||
@@ -532,19 +532,34 @@ mod tests {
|
||||
edit_collection_modal
|
||||
.minimum_availability_list
|
||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||
edit_collection_modal
|
||||
.quality_profile_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
edit_collection_modal
|
||||
.minimum_availability_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
app.data.radarr_data.edit_collection_modal = Some(edit_collection_modal);
|
||||
app.data.radarr_data.collections.set_items(vec![Collection {
|
||||
monitored: false,
|
||||
search_on_add: false,
|
||||
..collection()
|
||||
}]);
|
||||
app.data.radarr_data.collections.set_items(vec![
|
||||
Collection {
|
||||
id: 999,
|
||||
..collection()
|
||||
},
|
||||
Collection {
|
||||
monitored: false,
|
||||
search_on_add: false,
|
||||
..collection()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.collections.select_index(Some(1));
|
||||
app.data.radarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
|
||||
let expected_edit_collection_params = EditCollectionParams {
|
||||
collection_id: 123,
|
||||
monitored: Some(false),
|
||||
minimum_availability: Some(MinimumAvailability::Announced),
|
||||
quality_profile_id: Some(1111),
|
||||
minimum_availability: Some(MinimumAvailability::InCinemas),
|
||||
quality_profile_id: Some(2222),
|
||||
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||
search_on_add: Some(false),
|
||||
};
|
||||
@@ -945,19 +960,34 @@ mod tests {
|
||||
edit_collection_modal
|
||||
.minimum_availability_list
|
||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||
edit_collection_modal
|
||||
.quality_profile_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
edit_collection_modal
|
||||
.minimum_availability_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
app.data.radarr_data.edit_collection_modal = Some(edit_collection_modal);
|
||||
app.data.radarr_data.collections.set_items(vec![Collection {
|
||||
monitored: false,
|
||||
search_on_add: false,
|
||||
..collection()
|
||||
}]);
|
||||
app.data.radarr_data.collections.set_items(vec![
|
||||
Collection {
|
||||
id: 999,
|
||||
..collection()
|
||||
},
|
||||
Collection {
|
||||
monitored: false,
|
||||
search_on_add: false,
|
||||
..collection()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.collections.select_index(Some(1));
|
||||
app.data.radarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
|
||||
let expected_edit_collection_params = EditCollectionParams {
|
||||
collection_id: 123,
|
||||
monitored: Some(false),
|
||||
minimum_availability: Some(MinimumAvailability::Announced),
|
||||
quality_profile_id: Some(1111),
|
||||
minimum_availability: Some(MinimumAvailability::InCinemas),
|
||||
quality_profile_id: Some(2222),
|
||||
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||
search_on_add: Some(false),
|
||||
};
|
||||
@@ -1034,19 +1064,34 @@ mod tests {
|
||||
edit_collection_modal
|
||||
.minimum_availability_list
|
||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||
edit_collection_modal
|
||||
.quality_profile_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
edit_collection_modal
|
||||
.minimum_availability_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
app.data.radarr_data.edit_collection_modal = Some(edit_collection_modal);
|
||||
app.data.radarr_data.collections.set_items(vec![Collection {
|
||||
monitored: false,
|
||||
search_on_add: false,
|
||||
..collection()
|
||||
}]);
|
||||
app.data.radarr_data.collections.set_items(vec![
|
||||
Collection {
|
||||
id: 999,
|
||||
..collection()
|
||||
},
|
||||
Collection {
|
||||
monitored: false,
|
||||
search_on_add: false,
|
||||
..collection()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.collections.select_index(Some(1));
|
||||
app.data.radarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
|
||||
let expected_edit_collection_params = EditCollectionParams {
|
||||
collection_id: 123,
|
||||
monitored: Some(false),
|
||||
minimum_availability: Some(MinimumAvailability::Announced),
|
||||
quality_profile_id: Some(1111),
|
||||
minimum_availability: Some(MinimumAvailability::InCinemas),
|
||||
quality_profile_id: Some(2222),
|
||||
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||
search_on_add: Some(false),
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::network::radarr_network::RadarrEvent;
|
||||
|
||||
mod collection_details_handler;
|
||||
mod edit_collection_handler;
|
||||
mod movie_overview_handler;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "collections_handler_tests.rs"]
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::{Route, Scrollable, ScrollableText};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "movie_overview_handler_tests.rs"]
|
||||
mod movie_overview_handler_tests;
|
||||
|
||||
pub(super) struct MovieOverviewHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
_active_radarr_block: ActiveRadarrBlock,
|
||||
_context: Option<ActiveRadarrBlock>,
|
||||
}
|
||||
|
||||
impl MovieOverviewHandler<'_, '_> {
|
||||
fn overview(&mut self) -> &mut ScrollableText {
|
||||
&mut self
|
||||
.app
|
||||
.data
|
||||
.radarr_data
|
||||
.movie_overview_modal
|
||||
.as_mut()
|
||||
.expect("Movie overview modal is undefined")
|
||||
.overview
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieOverviewHandler<'a, 'b> {
|
||||
fn accepts(active_block: ActiveRadarrBlock) -> bool {
|
||||
active_block == ActiveRadarrBlock::ViewMovieOverview
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveRadarrBlock,
|
||||
_context: Option<ActiveRadarrBlock>,
|
||||
) -> MovieOverviewHandler<'a, 'b> {
|
||||
MovieOverviewHandler {
|
||||
key,
|
||||
app,
|
||||
_active_radarr_block: active_block,
|
||||
_context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
if self.app.is_loading {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.app.data.radarr_data.movie_overview_modal.is_some()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {
|
||||
self.overview().scroll_up();
|
||||
}
|
||||
|
||||
fn handle_scroll_down(&mut self) {
|
||||
self.overview().scroll_down();
|
||||
}
|
||||
|
||||
fn handle_home(&mut self) {
|
||||
self.overview().scroll_to_top();
|
||||
}
|
||||
|
||||
fn handle_end(&mut self) {
|
||||
self.overview().scroll_to_bottom();
|
||||
}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {}
|
||||
|
||||
fn handle_submit(&mut self) {}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.radarr_data.movie_overview_modal = None;
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::radarr_handlers::collections::movie_overview_handler::MovieOverviewHandler;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_scroll_up() {
|
||||
let mut app = app_with_movie_overview();
|
||||
set_offset(&mut app, 2);
|
||||
|
||||
MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_scroll_up_no_op_when_not_ready() {
|
||||
let mut app = app_with_movie_overview();
|
||||
set_offset(&mut app, 2);
|
||||
app.is_loading = true;
|
||||
|
||||
MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_scroll_down() {
|
||||
let mut app = app_with_movie_overview();
|
||||
|
||||
MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_scroll_down_no_op_when_not_ready() {
|
||||
let mut app = app_with_movie_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 0);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_home_end {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_home() {
|
||||
let mut app = app_with_movie_overview();
|
||||
set_offset(&mut app, 3);
|
||||
|
||||
MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.home.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_end() {
|
||||
let mut app = app_with_movie_overview();
|
||||
|
||||
MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_home_no_op_when_not_ready() {
|
||||
let mut app = app_with_movie_overview();
|
||||
set_offset(&mut app, 3);
|
||||
app.is_loading = true;
|
||||
|
||||
MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.home.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_end_no_op_when_not_ready() {
|
||||
let mut app = app_with_movie_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 0);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use super::*;
|
||||
use crate::{assert_modal_absent, assert_navigation_popped};
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_esc_tears_down_the_movie_overview_modal() {
|
||||
let mut app = app_with_movie_overview();
|
||||
|
||||
MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::CollectionDetails.into());
|
||||
assert_modal_absent!(app.data.radarr_data.movie_overview_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_esc_tears_down_the_modal_even_when_loading() {
|
||||
let mut app = app_with_movie_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::CollectionDetails.into());
|
||||
assert_modal_absent!(app.data.radarr_data.movie_overview_modal);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scrolling_the_overview_does_not_mutate_the_collection_movie_record() {
|
||||
let mut app = app_with_movie_overview();
|
||||
let overview_before = app
|
||||
.data
|
||||
.radarr_data
|
||||
.collection_movies
|
||||
.current_selection()
|
||||
.overview
|
||||
.clone();
|
||||
|
||||
MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 1);
|
||||
assert_eq!(
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.collection_movies
|
||||
.current_selection()
|
||||
.overview,
|
||||
overview_before
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_handler_accepts() {
|
||||
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
|
||||
if active_radarr_block == ActiveRadarrBlock::ViewMovieOverview {
|
||||
assert!(
|
||||
MovieOverviewHandler::accepts(active_radarr_block),
|
||||
"{active_radarr_block} is not accepted by the MovieOverviewHandler"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
!MovieOverviewHandler::accepts(active_radarr_block),
|
||||
"{active_radarr_block} is wrongly accepted by the MovieOverviewHandler"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_handler_dispatches_no_network_events() {
|
||||
let mut app = app_with_movie_overview();
|
||||
|
||||
for key in [
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
DEFAULT_KEYBINDINGS.home.key,
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
DEFAULT_KEYBINDINGS.delete.key,
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
DEFAULT_KEYBINDINGS.submit.key,
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
] {
|
||||
MovieOverviewHandler::new(key, &mut app, ActiveRadarrBlock::ViewMovieOverview, None).handle();
|
||||
|
||||
assert_none!(app.data.radarr_data.prompt_confirm_action);
|
||||
assert!(!app.data.radarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_handler_is_not_ready_when_loading() {
|
||||
let mut app = app_with_movie_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_handler_is_not_ready_when_the_modal_is_absent() {
|
||||
let mut app = app_with_movie_overview();
|
||||
app.data.radarr_data.movie_overview_modal = None;
|
||||
|
||||
let handler = MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_handler_is_ready() {
|
||||
let mut app = app_with_movie_overview();
|
||||
|
||||
let handler = MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_movie_overview_handler_ignores_special_keys_for_textbox_input() {
|
||||
let mut app = app_with_movie_overview();
|
||||
app.ignore_special_keys_for_textbox_input = true;
|
||||
|
||||
let handler = MovieOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::ViewMovieOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.ignore_special_keys());
|
||||
}
|
||||
|
||||
fn app_with_movie_overview() -> App<'static> {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::ViewMovieOverview.into());
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
fn overview_offset(app: &App<'_>) -> u16 {
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movie_overview_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.overview
|
||||
.offset
|
||||
}
|
||||
|
||||
fn set_offset(app: &mut App<'_>, offset: u16) {
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movie_overview_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.overview
|
||||
.offset = offset;
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ mod tests {
|
||||
#[case(
|
||||
ActiveRadarrBlock::Downloads,
|
||||
ActiveRadarrBlock::DeleteDownloadPrompt,
|
||||
RadarrEvent::DeleteDownload(1)
|
||||
RadarrEvent::DeleteDownload(2)
|
||||
)]
|
||||
#[case(
|
||||
ActiveRadarrBlock::Downloads,
|
||||
@@ -149,11 +149,15 @@ mod tests {
|
||||
#[case] expected_action: RadarrEvent,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.downloads
|
||||
.set_items(vec![download_record()]);
|
||||
app.data.radarr_data.downloads.set_items(vec![
|
||||
download_record(),
|
||||
DownloadRecord {
|
||||
id: 2,
|
||||
title: "Test Download Title 2".to_owned(),
|
||||
..download_record()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.downloads.select_index(Some(1));
|
||||
app.data.radarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
@@ -332,7 +336,7 @@ mod tests {
|
||||
#[case(
|
||||
ActiveRadarrBlock::Downloads,
|
||||
ActiveRadarrBlock::DeleteDownloadPrompt,
|
||||
RadarrEvent::DeleteDownload(1)
|
||||
RadarrEvent::DeleteDownload(2)
|
||||
)]
|
||||
#[case(
|
||||
ActiveRadarrBlock::Downloads,
|
||||
@@ -345,11 +349,15 @@ mod tests {
|
||||
#[case] expected_action: RadarrEvent,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.downloads
|
||||
.set_items(vec![download_record()]);
|
||||
app.data.radarr_data.downloads.set_items(vec![
|
||||
download_record(),
|
||||
DownloadRecord {
|
||||
id: 2,
|
||||
title: "Test Download Title 2".to_owned(),
|
||||
..download_record()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.downloads.select_index(Some(1));
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
@@ -403,11 +411,15 @@ mod tests {
|
||||
#[test]
|
||||
fn test_extract_download_id() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.downloads
|
||||
.set_items(vec![download_record()]);
|
||||
app.data.radarr_data.downloads.set_items(vec![
|
||||
download_record(),
|
||||
DownloadRecord {
|
||||
id: 2,
|
||||
title: "Test Download Title 2".to_owned(),
|
||||
..download_record()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.downloads.select_index(Some(1));
|
||||
|
||||
let download_id = DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
@@ -417,7 +429,7 @@ mod tests {
|
||||
)
|
||||
.extract_download_id();
|
||||
|
||||
assert_eq!(download_id, 1);
|
||||
assert_eq!(download_id, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -199,6 +199,40 @@ mod tests {
|
||||
assert_eq!(app.get_current_route(), ActiveRadarrBlock::History.into());
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_history_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.history.set_items(history_vec());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::History.into());
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.search.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::History,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveRadarrBlock::SearchHistory.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_history_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.history.set_items(history_vec());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::History.into());
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.filter.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::History,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveRadarrBlock::FilterHistory.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -11,7 +11,7 @@ mod tests {
|
||||
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::indexer;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
|
||||
use crate::models::servarr_models::EditIndexerParams;
|
||||
use crate::models::servarr_models::{EditIndexerParams, Indexer};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use strum::IntoEnumIterator;
|
||||
@@ -916,9 +916,14 @@ mod tests {
|
||||
priority: 25,
|
||||
};
|
||||
app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal);
|
||||
app.data.radarr_data.indexers.set_items(vec![indexer()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.indexers
|
||||
.set_items(vec![indexer(), Indexer { id: 2, ..indexer() }]);
|
||||
app.data.radarr_data.indexers.select_index(Some(1));
|
||||
let expected_edit_indexer_params = EditIndexerParams {
|
||||
indexer_id: 1,
|
||||
indexer_id: 2,
|
||||
name: Some("Test Update".to_owned()),
|
||||
enable_rss: Some(false),
|
||||
enable_automatic_search: Some(false),
|
||||
@@ -1759,9 +1764,14 @@ mod tests {
|
||||
priority: 25,
|
||||
};
|
||||
app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal);
|
||||
app.data.radarr_data.indexers.set_items(vec![indexer()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.indexers
|
||||
.set_items(vec![indexer(), Indexer { id: 2, ..indexer() }]);
|
||||
app.data.radarr_data.indexers.select_index(Some(1));
|
||||
let expected_edit_indexer_params = EditIndexerParams {
|
||||
indexer_id: 1,
|
||||
indexer_id: 2,
|
||||
name: Some("Test Update".to_owned()),
|
||||
enable_rss: Some(false),
|
||||
enable_automatic_search: Some(false),
|
||||
@@ -1837,9 +1847,14 @@ mod tests {
|
||||
priority: 25,
|
||||
};
|
||||
app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal);
|
||||
app.data.radarr_data.indexers.set_items(vec![indexer()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.indexers
|
||||
.set_items(vec![indexer(), Indexer { id: 2, ..indexer() }]);
|
||||
app.data.radarr_data.indexers.select_index(Some(1));
|
||||
let expected_edit_indexer_params = EditIndexerParams {
|
||||
indexer_id: 1,
|
||||
indexer_id: 2,
|
||||
name: Some("Test Update".to_owned()),
|
||||
enable_rss: Some(false),
|
||||
enable_automatic_search: Some(false),
|
||||
|
||||
@@ -143,6 +143,11 @@ mod tests {
|
||||
} else {
|
||||
"usenet".to_owned()
|
||||
};
|
||||
let other_protocol = if torrent_protocol {
|
||||
"usenet".to_owned()
|
||||
} else {
|
||||
"torrent".to_owned()
|
||||
};
|
||||
let mut expected_edit_indexer_modal = EditIndexerModal {
|
||||
name: "Test".into(),
|
||||
enable_rss: Some(true),
|
||||
@@ -186,7 +191,14 @@ mod tests {
|
||||
fields: Some(fields),
|
||||
..Indexer::default()
|
||||
};
|
||||
radarr_data.indexers.set_items(vec![indexer]);
|
||||
radarr_data.indexers.set_items(vec![
|
||||
Indexer {
|
||||
protocol: other_protocol,
|
||||
..Indexer::default()
|
||||
},
|
||||
indexer,
|
||||
]);
|
||||
radarr_data.indexers.select_index(Some(1));
|
||||
app.data.radarr_data = radarr_data;
|
||||
|
||||
IndexersHandler::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Indexers, None).handle();
|
||||
@@ -233,7 +245,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_delete_indexer_prompt_confirm_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.indexers.set_items(vec![indexer()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.indexers
|
||||
.set_items(vec![indexer(), Indexer { id: 2, ..indexer() }]);
|
||||
app.data.radarr_data.indexers.select_index(Some(1));
|
||||
app.data.radarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into());
|
||||
@@ -249,7 +266,7 @@ mod tests {
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.radarr_data.prompt_confirm_action,
|
||||
&RadarrEvent::DeleteIndexer(1)
|
||||
&RadarrEvent::DeleteIndexer(2)
|
||||
);
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::Indexers.into());
|
||||
}
|
||||
@@ -523,7 +540,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_delete_indexer_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.indexers.set_items(vec![indexer()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.indexers
|
||||
.set_items(vec![indexer(), Indexer { id: 2, ..indexer() }]);
|
||||
app.data.radarr_data.indexers.select_index(Some(1));
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into());
|
||||
|
||||
@@ -538,7 +560,7 @@ mod tests {
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.radarr_data.prompt_confirm_action,
|
||||
&RadarrEvent::DeleteIndexer(1)
|
||||
&RadarrEvent::DeleteIndexer(2)
|
||||
);
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::Indexers.into());
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ mod tests {
|
||||
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::add_movie_body;
|
||||
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::collection_movie;
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::radarr_models::{AddMovieSearchResult, MinimumAvailability, MovieMonitor};
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieBody, AddMovieSearchResult, CollectionMovie, MinimumAvailability, MovieMonitor,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::modals::AddMovieModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ADD_MOVIE_BLOCKS, ActiveRadarrBlock};
|
||||
use crate::models::servarr_models::RootFolder;
|
||||
@@ -942,7 +944,14 @@ mod tests {
|
||||
fn test_add_movie_search_results_submit_movie_already_in_library() {
|
||||
let mut app = App::test_default();
|
||||
let mut add_searched_movies = StatefulTable::default();
|
||||
add_searched_movies.set_items(vec![AddMovieSearchResult::default()]);
|
||||
add_searched_movies.set_items(vec![
|
||||
AddMovieSearchResult {
|
||||
tmdb_id: 5678,
|
||||
..AddMovieSearchResult::default()
|
||||
},
|
||||
AddMovieSearchResult::default(),
|
||||
]);
|
||||
add_searched_movies.select_index(Some(1));
|
||||
app.data.radarr_data.add_searched_movies = Some(add_searched_movies);
|
||||
app
|
||||
.data
|
||||
@@ -1026,16 +1035,22 @@ mod tests {
|
||||
add_movie_modal.root_folder_list.state.select(Some(1));
|
||||
add_movie_modal
|
||||
.quality_profile_list
|
||||
.set_items(vec!["HD - 1080p".to_owned()]);
|
||||
.set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]);
|
||||
add_movie_modal
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(MovieMonitor::iter()));
|
||||
add_movie_modal
|
||||
.minimum_availability_list
|
||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||
add_movie_modal.quality_profile_list.state.select(Some(1));
|
||||
add_movie_modal.monitor_list.state.select(Some(1));
|
||||
add_movie_modal
|
||||
.minimum_availability_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
app.data.radarr_data.add_movie_modal = Some(add_movie_modal);
|
||||
app.data.radarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(2222, "HD - 1080p".to_owned())]);
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
|
||||
let context = if movie_details_context {
|
||||
app
|
||||
.data
|
||||
@@ -1061,7 +1076,7 @@ mod tests {
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::Movies.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.radarr_data.prompt_confirm_action,
|
||||
&RadarrEvent::AddMovie(add_movie_body())
|
||||
&RadarrEvent::AddMovie(expected_add_movie_body(movie_details_context))
|
||||
);
|
||||
assert_modal_absent!(app.data.radarr_data.add_movie_modal);
|
||||
}
|
||||
@@ -1461,16 +1476,22 @@ mod tests {
|
||||
add_movie_modal.root_folder_list.state.select(Some(1));
|
||||
add_movie_modal
|
||||
.quality_profile_list
|
||||
.set_items(vec!["HD - 1080p".to_owned()]);
|
||||
.set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]);
|
||||
add_movie_modal
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(MovieMonitor::iter()));
|
||||
add_movie_modal
|
||||
.minimum_availability_list
|
||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||
add_movie_modal.quality_profile_list.state.select(Some(1));
|
||||
add_movie_modal.monitor_list.state.select(Some(1));
|
||||
add_movie_modal
|
||||
.minimum_availability_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
app.data.radarr_data.add_movie_modal = Some(add_movie_modal);
|
||||
app.data.radarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(2222, "HD - 1080p".to_owned())]);
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
|
||||
let context = if movie_details_context {
|
||||
app
|
||||
.data
|
||||
@@ -1496,7 +1517,7 @@ mod tests {
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::Movies.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.radarr_data.prompt_confirm_action,
|
||||
&RadarrEvent::AddMovie(add_movie_body())
|
||||
&RadarrEvent::AddMovie(expected_add_movie_body(movie_details_context))
|
||||
);
|
||||
assert_modal_absent!(app.data.radarr_data.add_movie_modal);
|
||||
}
|
||||
@@ -1572,26 +1593,45 @@ mod tests {
|
||||
add_movie_modal.root_folder_list.state.select(Some(1));
|
||||
add_movie_modal
|
||||
.quality_profile_list
|
||||
.set_items(vec!["HD - 1080p".to_owned()]);
|
||||
.set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]);
|
||||
add_movie_modal
|
||||
.monitor_list
|
||||
.set_items(Vec::from_iter(MovieMonitor::iter()));
|
||||
add_movie_modal
|
||||
.minimum_availability_list
|
||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||
add_movie_modal.quality_profile_list.state.select(Some(1));
|
||||
add_movie_modal.monitor_list.state.select(Some(1));
|
||||
add_movie_modal
|
||||
.minimum_availability_list
|
||||
.state
|
||||
.select(Some(1));
|
||||
app.data.radarr_data.add_movie_modal = Some(add_movie_modal);
|
||||
app.data.radarr_data.quality_profile_map = BiMap::from_iter([(2222, "HD - 1080p".to_owned())]);
|
||||
app.data.radarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
|
||||
app.data.radarr_data.collection_movies.set_items(vec![
|
||||
CollectionMovie {
|
||||
tmdb_id: 5678,
|
||||
title: HorizontallyScrollableText::from("Some Other Movie"),
|
||||
..CollectionMovie::default()
|
||||
},
|
||||
collection_movie(),
|
||||
]);
|
||||
app.data.radarr_data.collection_movies.select_index(Some(1));
|
||||
let mut add_searched_movies = StatefulTable::default();
|
||||
add_searched_movies.set_items(vec![
|
||||
AddMovieSearchResult {
|
||||
tmdb_id: 5678,
|
||||
title: HorizontallyScrollableText::from("Some Other Movie"),
|
||||
..AddMovieSearchResult::default()
|
||||
},
|
||||
add_movie_search_result(),
|
||||
]);
|
||||
add_searched_movies.select_index(Some(1));
|
||||
app.data.radarr_data.add_searched_movies = Some(add_searched_movies);
|
||||
let context = if movie_details_context {
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.collection_movies
|
||||
.set_items(vec![collection_movie()]);
|
||||
Some(ActiveRadarrBlock::CollectionDetails)
|
||||
} else {
|
||||
let mut add_searched_movies = StatefulTable::default();
|
||||
add_searched_movies.set_items(vec![add_movie_search_result()]);
|
||||
app.data.radarr_data.add_searched_movies = Some(add_searched_movies);
|
||||
None
|
||||
};
|
||||
|
||||
@@ -1603,7 +1643,10 @@ mod tests {
|
||||
)
|
||||
.build_add_movie_body();
|
||||
|
||||
assert_eq!(actual_add_movie_body, add_movie_body());
|
||||
assert_eq!(
|
||||
actual_add_movie_body,
|
||||
expected_add_movie_body(movie_details_context)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1635,4 +1678,16 @@ mod tests {
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
fn expected_add_movie_body(movie_details_context: bool) -> AddMovieBody {
|
||||
if movie_details_context {
|
||||
add_movie_body()
|
||||
} else {
|
||||
AddMovieBody {
|
||||
tmdb_id: 4321,
|
||||
title: "Test Search Result".to_owned(),
|
||||
..add_movie_body()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ mod tests {
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::radarr_handlers::library::delete_movie_handler::DeleteMovieHandler;
|
||||
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::movie;
|
||||
use crate::models::radarr_models::DeleteMovieParams;
|
||||
use crate::models::radarr_models::{DeleteMovieParams, Movie};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS};
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
@@ -126,14 +126,19 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
let expected_delete_movie_params = DeleteMovieParams {
|
||||
id: 1,
|
||||
delete_movie_files: true,
|
||||
delete_movie_files: false,
|
||||
add_list_exclusion: true,
|
||||
};
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::DeleteMoviePrompt.into());
|
||||
app.data.radarr_data.movies.set_items(vec![movie()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![Movie { id: 999, ..movie() }, movie()]);
|
||||
app.data.radarr_data.movies.select_index(Some(1));
|
||||
app.data.radarr_data.prompt_confirm = true;
|
||||
app.data.radarr_data.delete_movie_files = true;
|
||||
app.data.radarr_data.delete_movie_files = false;
|
||||
app.data.radarr_data.add_list_exclusion = true;
|
||||
app.data.radarr_data.selected_block = BlockSelectionState::new(DELETE_MOVIE_SELECTION_BLOCKS);
|
||||
app
|
||||
@@ -271,13 +276,18 @@ mod tests {
|
||||
let expected_delete_movie_params = DeleteMovieParams {
|
||||
id: 1,
|
||||
delete_movie_files: true,
|
||||
add_list_exclusion: true,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::DeleteMoviePrompt.into());
|
||||
app.data.radarr_data.movies.set_items(vec![movie()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![Movie { id: 999, ..movie() }, movie()]);
|
||||
app.data.radarr_data.movies.select_index(Some(1));
|
||||
app.data.radarr_data.delete_movie_files = true;
|
||||
app.data.radarr_data.add_list_exclusion = true;
|
||||
app.data.radarr_data.add_list_exclusion = false;
|
||||
app.data.radarr_data.selected_block = BlockSelectionState::new(DELETE_MOVIE_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
@@ -338,13 +348,18 @@ mod tests {
|
||||
#[test]
|
||||
fn test_build_delete_movie_params() {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.movies.set_items(vec![movie()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![Movie { id: 999, ..movie() }, movie()]);
|
||||
app.data.radarr_data.movies.select_index(Some(1));
|
||||
app.data.radarr_data.delete_movie_files = true;
|
||||
app.data.radarr_data.add_list_exclusion = true;
|
||||
app.data.radarr_data.add_list_exclusion = false;
|
||||
let expected_delete_movie_params = DeleteMovieParams {
|
||||
id: 1,
|
||||
delete_movie_files: true,
|
||||
add_list_exclusion: true,
|
||||
add_list_exclusion: false,
|
||||
};
|
||||
|
||||
let delete_movie_params = DeleteMovieHandler::new(
|
||||
|
||||
@@ -656,18 +656,24 @@ mod tests {
|
||||
edit_movie
|
||||
.minimum_availability_list
|
||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||
edit_movie.quality_profile_list.state.select(Some(1));
|
||||
edit_movie.minimum_availability_list.state.select(Some(1));
|
||||
app.data.radarr_data.edit_movie_modal = Some(edit_movie);
|
||||
app.data.radarr_data.movies.set_items(vec![Movie {
|
||||
monitored: false,
|
||||
..movie()
|
||||
}]);
|
||||
app.data.radarr_data.movies.set_items(vec![
|
||||
Movie { id: 999, ..movie() },
|
||||
Movie {
|
||||
monitored: false,
|
||||
..movie()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.movies.select_index(Some(1));
|
||||
app.data.radarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
|
||||
let expected_edit_movie_params = EditMovieParams {
|
||||
movie_id: 1,
|
||||
monitored: Some(false),
|
||||
minimum_availability: Some(MinimumAvailability::Announced),
|
||||
quality_profile_id: Some(1111),
|
||||
minimum_availability: Some(MinimumAvailability::InCinemas),
|
||||
quality_profile_id: Some(2222),
|
||||
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||
tag_input_string: Some("usenet, testing".into()),
|
||||
..EditMovieParams::default()
|
||||
@@ -1089,18 +1095,24 @@ mod tests {
|
||||
edit_movie
|
||||
.minimum_availability_list
|
||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||
edit_movie.quality_profile_list.state.select(Some(1));
|
||||
edit_movie.minimum_availability_list.state.select(Some(1));
|
||||
app.data.radarr_data.edit_movie_modal = Some(edit_movie);
|
||||
app.data.radarr_data.movies.set_items(vec![Movie {
|
||||
monitored: false,
|
||||
..movie()
|
||||
}]);
|
||||
app.data.radarr_data.movies.set_items(vec![
|
||||
Movie { id: 999, ..movie() },
|
||||
Movie {
|
||||
monitored: false,
|
||||
..movie()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.movies.select_index(Some(1));
|
||||
app.data.radarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
|
||||
let expected_edit_movie_params = EditMovieParams {
|
||||
movie_id: 1,
|
||||
monitored: Some(false),
|
||||
minimum_availability: Some(MinimumAvailability::Announced),
|
||||
quality_profile_id: Some(1111),
|
||||
minimum_availability: Some(MinimumAvailability::InCinemas),
|
||||
quality_profile_id: Some(2222),
|
||||
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||
tag_input_string: Some("usenet, testing".into()),
|
||||
..EditMovieParams::default()
|
||||
@@ -1177,18 +1189,24 @@ mod tests {
|
||||
edit_movie
|
||||
.minimum_availability_list
|
||||
.set_items(Vec::from_iter(MinimumAvailability::iter()));
|
||||
edit_movie.quality_profile_list.state.select(Some(1));
|
||||
edit_movie.minimum_availability_list.state.select(Some(1));
|
||||
app.data.radarr_data.edit_movie_modal = Some(edit_movie);
|
||||
app.data.radarr_data.movies.set_items(vec![Movie {
|
||||
monitored: false,
|
||||
..movie()
|
||||
}]);
|
||||
app.data.radarr_data.movies.set_items(vec![
|
||||
Movie { id: 999, ..movie() },
|
||||
Movie {
|
||||
monitored: false,
|
||||
..movie()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.movies.select_index(Some(1));
|
||||
app.data.radarr_data.quality_profile_map =
|
||||
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
|
||||
let expected_edit_movie_params = EditMovieParams {
|
||||
movie_id: 1,
|
||||
monitored: Some(false),
|
||||
minimum_availability: Some(MinimumAvailability::Announced),
|
||||
quality_profile_id: Some(1111),
|
||||
minimum_availability: Some(MinimumAvailability::InCinemas),
|
||||
quality_profile_id: Some(2222),
|
||||
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||
tag_input_string: Some("usenet, testing".into()),
|
||||
..EditMovieParams::default()
|
||||
|
||||
@@ -391,6 +391,12 @@ mod tests {
|
||||
fn test_toggle_monitoring_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data = create_test_radarr_data();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![movie(), Movie { id: 2, ..movie() }]);
|
||||
app.data.radarr_data.movies.select_index(Some(1));
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
app.is_routing = false;
|
||||
|
||||
@@ -407,7 +413,7 @@ mod tests {
|
||||
assert!(app.is_routing);
|
||||
assert_some_eq_x!(
|
||||
&app.data.radarr_data.prompt_confirm_action,
|
||||
&RadarrEvent::ToggleMovieMonitoring(0)
|
||||
&RadarrEvent::ToggleMovieMonitoring(2)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -519,6 +525,48 @@ mod tests {
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_movies_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![Movie::default()]);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.search.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::Movies,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveRadarrBlock::SearchMovie.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_movies_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![Movie::default()]);
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
|
||||
LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.filter.key,
|
||||
&mut app,
|
||||
ActiveRadarrBlock::Movies,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveRadarrBlock::FilterMovies.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_all_movies_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
@@ -621,7 +669,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_extract_movie_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.movies.set_items(vec![movie()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![movie(), Movie { id: 2, ..movie() }]);
|
||||
app.data.radarr_data.movies.select_index(Some(1));
|
||||
|
||||
let movie_id = LibraryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
@@ -631,7 +684,7 @@ mod tests {
|
||||
)
|
||||
.extract_movie_id();
|
||||
|
||||
assert_eq!(movie_id, 1);
|
||||
assert_eq!(movie_id, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -17,7 +17,7 @@ mod tests {
|
||||
MovieDetailsHandler, releases_sorting_options,
|
||||
};
|
||||
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::{movie, release};
|
||||
use crate::models::radarr_models::{Credit, MovieHistoryItem};
|
||||
use crate::models::radarr_models::{Credit, Movie, MovieHistoryItem};
|
||||
use crate::models::radarr_models::{RadarrRelease, RadarrReleaseDownloadBody};
|
||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS};
|
||||
@@ -819,11 +819,22 @@ mod tests {
|
||||
fn test_build_radarr_release_download_body() {
|
||||
let mut app = App::test_default();
|
||||
let mut movie_details_modal = MovieDetailsModal::default();
|
||||
movie_details_modal
|
||||
.movie_releases
|
||||
.set_items(vec![release()]);
|
||||
movie_details_modal.movie_releases.set_items(vec![
|
||||
RadarrRelease {
|
||||
guid: "9999".to_owned(),
|
||||
indexer_id: 99,
|
||||
..release()
|
||||
},
|
||||
release(),
|
||||
]);
|
||||
movie_details_modal.movie_releases.select_index(Some(1));
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
app.data.radarr_data.movies.set_items(vec![movie()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![Movie { id: 999, ..movie() }, movie()]);
|
||||
app.data.radarr_data.movies.select_index(Some(1));
|
||||
let expected_body = RadarrReleaseDownloadBody {
|
||||
guid: "1234".to_owned(),
|
||||
indexer_id: 2,
|
||||
@@ -844,7 +855,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_extract_movie_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.movies.set_items(vec![movie()]);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movies
|
||||
.set_items(vec![Movie { id: 999, ..movie() }, movie()]);
|
||||
app.data.radarr_data.movies.select_index(Some(1));
|
||||
|
||||
let movie_id = MovieDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
|
||||
@@ -5,10 +5,10 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieBody, AddMovieOptions, AddMovieSearchResult, Collection, CollectionMovie,
|
||||
DownloadRecord, IndexerSettings, MediaInfo, MinimumAvailability, Movie, MovieCollection,
|
||||
MovieFile, MovieMonitor, RadarrRelease, Rating, RatingsList,
|
||||
MovieFile, MovieMonitor, MovieStatus, RadarrRelease, Rating, RatingsList,
|
||||
};
|
||||
use crate::models::servarr_models::{
|
||||
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
||||
DownloadStatus, Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
||||
};
|
||||
use chrono::DateTime;
|
||||
use serde_json::{Number, json};
|
||||
@@ -295,7 +295,12 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
||||
pub fn collection_movie() -> CollectionMovie {
|
||||
CollectionMovie {
|
||||
title: "Test".to_owned().into(),
|
||||
overview: "Collection blah blah blah".to_owned(),
|
||||
overview: "Collection movie blah blah blah\r\n\
|
||||
\r\n\
|
||||
It was shot in Madison, Wisconsin: a city the cast never really left.\r\n\
|
||||
\r\n\
|
||||
The sequel took seven years to finish.\r\n"
|
||||
.to_owned(),
|
||||
year: 2023,
|
||||
runtime: 120,
|
||||
tmdb_id: 1234,
|
||||
@@ -324,7 +329,7 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
||||
title: "Test".to_owned().into(),
|
||||
original_language: language(),
|
||||
size_on_disk: 3543348019,
|
||||
status: "Downloaded".to_owned(),
|
||||
status: MovieStatus::Released,
|
||||
overview: "Blah blah blah".to_owned(),
|
||||
path: "/nfs/movies".to_owned(),
|
||||
studio: Some("21st Century Alex".to_owned()),
|
||||
@@ -387,10 +392,10 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
||||
|
||||
pub fn add_movie_search_result() -> AddMovieSearchResult {
|
||||
AddMovieSearchResult {
|
||||
tmdb_id: 1234,
|
||||
title: HorizontallyScrollableText::from("Test"),
|
||||
tmdb_id: 4321,
|
||||
title: HorizontallyScrollableText::from("Test Search Result"),
|
||||
original_language: language(),
|
||||
status: "released".to_owned(),
|
||||
status: MovieStatus::Released,
|
||||
overview: "New movie blah blah blah".to_owned(),
|
||||
genres: genres(),
|
||||
year: 2023,
|
||||
@@ -402,7 +407,7 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
||||
pub fn download_record() -> DownloadRecord {
|
||||
DownloadRecord {
|
||||
title: "Test Download Title".to_owned(),
|
||||
status: "downloading".to_owned(),
|
||||
status: DownloadStatus::Downloading,
|
||||
id: 1,
|
||||
movie_id: 1,
|
||||
size: 3543348019,
|
||||
@@ -470,13 +475,13 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
||||
tmdb_id: 1234,
|
||||
title: "Test".to_owned(),
|
||||
root_folder_path: "/nfs2".to_owned(),
|
||||
minimum_availability: MinimumAvailability::Announced,
|
||||
minimum_availability: MinimumAvailability::InCinemas,
|
||||
monitored: true,
|
||||
quality_profile_id: 2222,
|
||||
tags: Vec::new(),
|
||||
tag_input_string: Some("usenet, testing".into()),
|
||||
add_options: AddMovieOptions {
|
||||
monitor: MovieMonitor::MovieOnly,
|
||||
monitor: MovieMonitor::MovieAndCollection,
|
||||
search_for_movie: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -123,7 +123,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_system_blocks_to_system_handler(
|
||||
#[values(
|
||||
ActiveRadarrBlock::System,
|
||||
ActiveRadarrBlock::SystemLogs,
|
||||
ActiveRadarrBlock::SystemQueuedEvents,
|
||||
ActiveRadarrBlock::SystemTasks,
|
||||
@@ -142,7 +141,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_library_blocks_to_library_handler(
|
||||
#[values(
|
||||
ActiveRadarrBlock::Movies,
|
||||
ActiveRadarrBlock::MoviesSortPrompt,
|
||||
ActiveRadarrBlock::SearchMovie,
|
||||
ActiveRadarrBlock::SearchMovieError,
|
||||
@@ -186,7 +184,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_collections_blocks_to_collections_handler(
|
||||
#[values(
|
||||
ActiveRadarrBlock::Collections,
|
||||
ActiveRadarrBlock::SearchCollection,
|
||||
ActiveRadarrBlock::CollectionsSortPrompt,
|
||||
ActiveRadarrBlock::SearchCollectionError,
|
||||
@@ -213,7 +210,6 @@ mod tests {
|
||||
fn test_delegates_indexers_blocks_to_indexers_handler(
|
||||
#[values(
|
||||
ActiveRadarrBlock::DeleteIndexerPrompt,
|
||||
ActiveRadarrBlock::Indexers,
|
||||
ActiveRadarrBlock::AllIndexerSettingsPrompt,
|
||||
ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
|
||||
ActiveRadarrBlock::IndexerSettingsConfirmPrompt,
|
||||
@@ -237,7 +233,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_downloads_blocks_to_downloads_handler(
|
||||
#[values(
|
||||
ActiveRadarrBlock::Downloads,
|
||||
ActiveRadarrBlock::DeleteDownloadPrompt,
|
||||
ActiveRadarrBlock::UpdateDownloadsPrompt
|
||||
)]
|
||||
@@ -253,7 +248,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_root_folders_blocks_to_root_folders_handler(
|
||||
#[values(
|
||||
ActiveRadarrBlock::RootFolders,
|
||||
ActiveRadarrBlock::AddRootFolderPrompt,
|
||||
ActiveRadarrBlock::DeleteRootFolderPrompt
|
||||
)]
|
||||
@@ -269,7 +263,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_blocklist_blocks_to_blocklist_handler(
|
||||
#[values(
|
||||
ActiveRadarrBlock::Blocklist,
|
||||
ActiveRadarrBlock::BlocklistItemDetails,
|
||||
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
|
||||
ActiveRadarrBlock::BlocklistClearAllItemsPrompt,
|
||||
@@ -287,7 +280,6 @@ mod tests {
|
||||
#[rstest]
|
||||
fn test_delegates_history_blocks_to_history_handler(
|
||||
#[values(
|
||||
ActiveRadarrBlock::History,
|
||||
ActiveRadarrBlock::HistoryItemDetails,
|
||||
ActiveRadarrBlock::HistorySortPrompt,
|
||||
ActiveRadarrBlock::FilterHistory,
|
||||
|
||||
@@ -314,11 +314,15 @@ mod tests {
|
||||
#[test]
|
||||
fn test_delete_root_folder_prompt_confirm_submit() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.root_folders
|
||||
.set_items(vec![root_folder()]);
|
||||
app.data.radarr_data.root_folders.set_items(vec![
|
||||
root_folder(),
|
||||
RootFolder {
|
||||
id: 2,
|
||||
path: "/nfs2".to_owned(),
|
||||
..root_folder()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.root_folders.select_index(Some(1));
|
||||
app.data.radarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into());
|
||||
@@ -334,7 +338,7 @@ mod tests {
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.radarr_data.prompt_confirm_action,
|
||||
&RadarrEvent::DeleteRootFolder(1)
|
||||
&RadarrEvent::DeleteRootFolder(2)
|
||||
);
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::RootFolders.into());
|
||||
}
|
||||
@@ -583,11 +587,15 @@ mod tests {
|
||||
#[test]
|
||||
fn test_delete_root_folder_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.root_folders
|
||||
.set_items(vec![root_folder()]);
|
||||
app.data.radarr_data.root_folders.set_items(vec![
|
||||
root_folder(),
|
||||
RootFolder {
|
||||
id: 2,
|
||||
path: "/nfs2".to_owned(),
|
||||
..root_folder()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.root_folders.select_index(Some(1));
|
||||
app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into());
|
||||
|
||||
@@ -602,7 +610,7 @@ mod tests {
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.radarr_data.prompt_confirm_action,
|
||||
&RadarrEvent::DeleteRootFolder(1)
|
||||
&RadarrEvent::DeleteRootFolder(2)
|
||||
);
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::RootFolders.into());
|
||||
}
|
||||
|
||||
@@ -677,10 +677,17 @@ mod tests {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.updates = ScrollableText::with_string("Test".to_owned());
|
||||
app.data.radarr_data.prompt_confirm = true;
|
||||
app.data.radarr_data.tasks.set_items(vec![RadarrTask {
|
||||
task_name: RadarrTaskName::default(),
|
||||
..RadarrTask::default()
|
||||
}]);
|
||||
app.data.radarr_data.tasks.set_items(vec![
|
||||
RadarrTask {
|
||||
task_name: RadarrTaskName::Backup,
|
||||
..RadarrTask::default()
|
||||
},
|
||||
RadarrTask {
|
||||
task_name: RadarrTaskName::RefreshMovie,
|
||||
..RadarrTask::default()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.tasks.select_index(Some(1));
|
||||
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into());
|
||||
|
||||
@@ -695,7 +702,7 @@ mod tests {
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.radarr_data.prompt_confirm_action,
|
||||
&RadarrEvent::StartTask(RadarrTaskName::default())
|
||||
&RadarrEvent::StartTask(RadarrTaskName::RefreshMovie)
|
||||
);
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::SystemTasks.into());
|
||||
}
|
||||
@@ -891,10 +898,17 @@ mod tests {
|
||||
fn test_system_tasks_start_task_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.updates = ScrollableText::with_string("Test".to_owned());
|
||||
app.data.radarr_data.tasks.set_items(vec![RadarrTask {
|
||||
task_name: RadarrTaskName::default(),
|
||||
..RadarrTask::default()
|
||||
}]);
|
||||
app.data.radarr_data.tasks.set_items(vec![
|
||||
RadarrTask {
|
||||
task_name: RadarrTaskName::Backup,
|
||||
..RadarrTask::default()
|
||||
},
|
||||
RadarrTask {
|
||||
task_name: RadarrTaskName::RefreshMovie,
|
||||
..RadarrTask::default()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.tasks.select_index(Some(1));
|
||||
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
|
||||
app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into());
|
||||
|
||||
@@ -909,7 +923,7 @@ mod tests {
|
||||
assert!(app.data.radarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.radarr_data.prompt_confirm_action,
|
||||
&RadarrEvent::StartTask(RadarrTaskName::default())
|
||||
&RadarrEvent::StartTask(RadarrTaskName::RefreshMovie)
|
||||
);
|
||||
assert_navigation_popped!(app, ActiveRadarrBlock::SystemTasks.into());
|
||||
}
|
||||
@@ -948,10 +962,17 @@ mod tests {
|
||||
#[test]
|
||||
fn test_extract_task_name() {
|
||||
let mut app = App::test_default();
|
||||
app.data.radarr_data.tasks.set_items(vec![RadarrTask {
|
||||
task_name: RadarrTaskName::default(),
|
||||
..RadarrTask::default()
|
||||
}]);
|
||||
app.data.radarr_data.tasks.set_items(vec![
|
||||
RadarrTask {
|
||||
task_name: RadarrTaskName::Backup,
|
||||
..RadarrTask::default()
|
||||
},
|
||||
RadarrTask {
|
||||
task_name: RadarrTaskName::RefreshMovie,
|
||||
..RadarrTask::default()
|
||||
},
|
||||
]);
|
||||
app.data.radarr_data.tasks.select_index(Some(1));
|
||||
|
||||
let task_name = SystemDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
@@ -961,7 +982,7 @@ mod tests {
|
||||
)
|
||||
.extract_task_name();
|
||||
|
||||
assert_eq!(task_name, RadarrTaskName::default());
|
||||
assert_eq!(task_name, RadarrTaskName::RefreshMovie);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,627 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use chrono::DateTime;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use rstest::rstest;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::assert_navigation_pushed;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::readarr_handlers::blocklist::{BlocklistHandler, blocklist_sorting_options};
|
||||
use crate::models::readarr_models::{Author, BlocklistItem};
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, BLOCKLIST_BLOCKS};
|
||||
use crate::models::servarr_models::{Quality, QualityWrapper};
|
||||
|
||||
mod test_handle_delete {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_prompt() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
|
||||
BlocklistHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::Blocklist, None).handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::DeleteBlocklistItemPrompt.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_blocklist_item_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
|
||||
BlocklistHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::Blocklist, None).handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveReadarrBlock::Blocklist.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_pushed;
|
||||
|
||||
#[rstest]
|
||||
fn test_blocklist_tab_left(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.readarr_data.main_tabs.set_index(2);
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.readarr_data.main_tabs.get_active_route(),
|
||||
ActiveReadarrBlock::Downloads.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::Downloads.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_blocklist_tab_right(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.readarr_data.main_tabs.set_index(2);
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.readarr_data.main_tabs.get_active_route(),
|
||||
ActiveReadarrBlock::History.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::History.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_blocklist_left_right_prompt_toggle(
|
||||
#[values(
|
||||
ActiveReadarrBlock::DeleteBlocklistItemPrompt,
|
||||
ActiveReadarrBlock::BlocklistClearAllItemsPrompt
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(key, &mut app, active_readarr_block, None).handle();
|
||||
|
||||
assert!(app.data.readarr_data.prompt_confirm);
|
||||
|
||||
BlocklistHandler::new(key, &mut app, active_readarr_block, None).handle();
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use crate::assert_navigation_popped;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::Blocklist, None).handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::BlocklistItemDetails.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_submit_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::Blocklist, None).handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveReadarrBlock::Blocklist.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
ActiveReadarrBlock::DeleteBlocklistItemPrompt,
|
||||
ReadarrEvent::DeleteBlocklistItem(3)
|
||||
)]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
ActiveReadarrBlock::BlocklistClearAllItemsPrompt,
|
||||
ReadarrEvent::ClearBlocklist
|
||||
)]
|
||||
fn test_blocklist_prompt_confirm_submit(
|
||||
#[case] base_route: ActiveReadarrBlock,
|
||||
#[case] prompt_block: ActiveReadarrBlock,
|
||||
#[case] expected_action: ReadarrEvent,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
BlocklistHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(app.data.readarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.readarr_data.prompt_confirm_action,
|
||||
&expected_action
|
||||
);
|
||||
assert_navigation_popped!(app, base_route.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_blocklist_prompt_decline_submit(
|
||||
#[values(
|
||||
ActiveReadarrBlock::DeleteBlocklistItemPrompt,
|
||||
ActiveReadarrBlock::BlocklistClearAllItemsPrompt
|
||||
)]
|
||||
prompt_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
BlocklistHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_none!(app.data.readarr_data.prompt_confirm_action);
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Blocklist.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
ActiveReadarrBlock::DeleteBlocklistItemPrompt
|
||||
)]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
ActiveReadarrBlock::BlocklistClearAllItemsPrompt
|
||||
)]
|
||||
fn test_blocklist_prompt_blocks_esc(
|
||||
#[case] base_block: ActiveReadarrBlock,
|
||||
#[case] prompt_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(base_block.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
|
||||
BlocklistHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert_navigation_popped!(app, base_block.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_esc_blocklist_item_details() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::BlocklistItemDetails.into());
|
||||
|
||||
BlocklistHandler::new(
|
||||
ESC_KEY,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::BlocklistItemDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Blocklist.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_default_esc(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.error = "test error".to_owned().into();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(ESC_KEY, &mut app, ActiveReadarrBlock::Blocklist, None).handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Blocklist.into());
|
||||
assert_is_empty!(app.error.text);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
|
||||
use super::*;
|
||||
use crate::{assert_navigation_popped, assert_navigation_pushed};
|
||||
|
||||
#[test]
|
||||
fn test_refresh_blocklist_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::Blocklist.into());
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_blocklist_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveReadarrBlock::Blocklist.into()
|
||||
);
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_blocklist_key() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.clear.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::BlocklistClearAllItemsPrompt.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_blocklist_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.clear.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveReadarrBlock::Blocklist.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
ActiveReadarrBlock::DeleteBlocklistItemPrompt,
|
||||
ReadarrEvent::DeleteBlocklistItem(3)
|
||||
)]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
ActiveReadarrBlock::BlocklistClearAllItemsPrompt,
|
||||
ReadarrEvent::ClearBlocklist
|
||||
)]
|
||||
fn test_blocklist_prompt_confirm(
|
||||
#[case] base_route: ActiveReadarrBlock,
|
||||
#[case] prompt_block: ActiveReadarrBlock,
|
||||
#[case] expected_action: ReadarrEvent,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
prompt_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.readarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.readarr_data.prompt_confirm_action,
|
||||
&expected_action
|
||||
);
|
||||
assert_navigation_popped!(app, base_route.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_sorting_options_author_name() {
|
||||
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| {
|
||||
a.author
|
||||
.author_name
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.author.author_name.text.to_lowercase())
|
||||
};
|
||||
let mut expected_blocklist_vec = blocklist_vec();
|
||||
expected_blocklist_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = blocklist_sorting_options()[0].clone();
|
||||
let mut sorted_blocklist_vec = blocklist_vec();
|
||||
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
|
||||
assert_str_eq!(sort_option.name, "Author Name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_sorting_options_source_title() {
|
||||
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| {
|
||||
a.source_title
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.to_lowercase())
|
||||
};
|
||||
let mut expected_blocklist_vec = blocklist_vec();
|
||||
expected_blocklist_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = blocklist_sorting_options()[1].clone();
|
||||
let mut sorted_blocklist_vec = blocklist_vec();
|
||||
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
|
||||
assert_str_eq!(sort_option.name, "Source Title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_sorting_options_quality() {
|
||||
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| {
|
||||
a.quality
|
||||
.quality
|
||||
.name
|
||||
.to_lowercase()
|
||||
.cmp(&b.quality.quality.name.to_lowercase())
|
||||
};
|
||||
let mut expected_blocklist_vec = blocklist_vec();
|
||||
expected_blocklist_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = blocklist_sorting_options()[2].clone();
|
||||
let mut sorted_blocklist_vec = blocklist_vec();
|
||||
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
|
||||
assert_str_eq!(sort_option.name, "Quality");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_sorting_options_date() {
|
||||
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering =
|
||||
|a, b| a.date.cmp(&b.date);
|
||||
let mut expected_blocklist_vec = blocklist_vec();
|
||||
expected_blocklist_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = blocklist_sorting_options()[3].clone();
|
||||
let mut sorted_blocklist_vec = blocklist_vec();
|
||||
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
|
||||
assert_str_eq!(sort_option.name, "Date");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_handler_accepts() {
|
||||
ActiveReadarrBlock::iter().for_each(|active_readarr_block| {
|
||||
if BLOCKLIST_BLOCKS.contains(&active_readarr_block) {
|
||||
assert!(BlocklistHandler::accepts(active_readarr_block));
|
||||
} else {
|
||||
assert!(!BlocklistHandler::accepts(active_readarr_block));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_blocklist_handler_ignore_special_keys(
|
||||
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
|
||||
let handler = BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handler.ignore_special_keys(),
|
||||
ignore_special_keys_for_textbox_input
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_blocklist_item_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.blocklist.set_items(blocklist_vec());
|
||||
app.data.readarr_data.blocklist.select_index(Some(1));
|
||||
|
||||
let blocklist_item_id = BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
None,
|
||||
)
|
||||
.extract_blocklist_item_id();
|
||||
|
||||
assert_eq!(blocklist_item_id, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_handler_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_handler_not_ready_when_blocklist_is_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocklist_handler_ready_when_not_loading_and_blocklist_is_not_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into());
|
||||
app.is_loading = false;
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.blocklist
|
||||
.set_items(vec![BlocklistItem::default()]);
|
||||
|
||||
let handler = BlocklistHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Blocklist,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
fn blocklist_vec() -> Vec<BlocklistItem> {
|
||||
vec![
|
||||
BlocklistItem {
|
||||
id: 3,
|
||||
source_title: "test 2".to_owned(),
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "AZW3".to_owned(),
|
||||
},
|
||||
},
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()),
|
||||
author: Author {
|
||||
author_name: "Zora Neale".into(),
|
||||
..Author::default()
|
||||
},
|
||||
..BlocklistItem::default()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 2,
|
||||
source_title: "test 3".to_owned(),
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "MOBI".to_owned(),
|
||||
},
|
||||
},
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()),
|
||||
author: Author {
|
||||
author_name: "Mary Shelley".into(),
|
||||
..Author::default()
|
||||
},
|
||||
..BlocklistItem::default()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 1,
|
||||
source_title: "test 1".to_owned(),
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "EPUB".to_owned(),
|
||||
},
|
||||
},
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()),
|
||||
author: Author {
|
||||
author_name: "".into(),
|
||||
..Author::default()
|
||||
},
|
||||
..BlocklistItem::default()
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::readarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::readarr_models::BlocklistItem;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, BLOCKLIST_BLOCKS};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "blocklist_handler_tests.rs"]
|
||||
mod blocklist_handler_tests;
|
||||
|
||||
pub(super) struct BlocklistHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
_context: Option<ActiveReadarrBlock>,
|
||||
}
|
||||
|
||||
impl BlocklistHandler<'_, '_> {
|
||||
fn extract_blocklist_item_id(&self) -> i64 {
|
||||
self.app.data.readarr_data.blocklist.current_selection().id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for BlocklistHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let blocklist_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveReadarrBlock::Blocklist.into())
|
||||
.sorting_block(ActiveReadarrBlock::BlocklistSortPrompt.into())
|
||||
.sort_options(blocklist_sorting_options());
|
||||
|
||||
if !handle_table(
|
||||
self,
|
||||
|app| &mut app.data.readarr_data.blocklist,
|
||||
blocklist_table_handling_config,
|
||||
) {
|
||||
self.handle_key_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveReadarrBlock) -> bool {
|
||||
BLOCKLIST_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveReadarrBlock,
|
||||
context: Option<ActiveReadarrBlock>,
|
||||
) -> Self {
|
||||
BlocklistHandler {
|
||||
key,
|
||||
app,
|
||||
active_readarr_block: active_block,
|
||||
_context: context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading && !self.app.data.readarr_data.blocklist.is_empty()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {
|
||||
if self.active_readarr_block == ActiveReadarrBlock::Blocklist {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::DeleteBlocklistItemPrompt.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::Blocklist => handle_change_tab_left_right_keys(self.app, self.key),
|
||||
ActiveReadarrBlock::DeleteBlocklistItemPrompt
|
||||
| ActiveReadarrBlock::BlocklistClearAllItemsPrompt => {
|
||||
handle_prompt_toggle(self.app, self.key)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::DeleteBlocklistItemPrompt => {
|
||||
if self.app.data.readarr_data.prompt_confirm {
|
||||
self.app.data.readarr_data.prompt_confirm_action = Some(
|
||||
ReadarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id()),
|
||||
);
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveReadarrBlock::BlocklistClearAllItemsPrompt => {
|
||||
if self.app.data.readarr_data.prompt_confirm {
|
||||
self.app.data.readarr_data.prompt_confirm_action = Some(ReadarrEvent::ClearBlocklist);
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveReadarrBlock::Blocklist => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::BlocklistItemDetails.into());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::DeleteBlocklistItemPrompt
|
||||
| ActiveReadarrBlock::BlocklistClearAllItemsPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.readarr_data.prompt_confirm = false;
|
||||
}
|
||||
ActiveReadarrBlock::BlocklistItemDetails | ActiveReadarrBlock::BlocklistSortPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
_ => handle_clear_errors(self.app),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::Blocklist => match self.key {
|
||||
_ if matches_key!(refresh, key) => {
|
||||
self.app.should_refresh = true;
|
||||
}
|
||||
_ if matches_key!(clear, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::BlocklistClearAllItemsPrompt.into());
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveReadarrBlock::DeleteBlocklistItemPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.readarr_data.prompt_confirm = true;
|
||||
self.app.data.readarr_data.prompt_confirm_action = Some(
|
||||
ReadarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id()),
|
||||
);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::BlocklistClearAllItemsPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.readarr_data.prompt_confirm = true;
|
||||
self.app.data.readarr_data.prompt_confirm_action = Some(ReadarrEvent::ClearBlocklist);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
fn blocklist_sorting_options() -> Vec<SortOption<BlocklistItem>> {
|
||||
vec![
|
||||
SortOption {
|
||||
name: "Author Name",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.author
|
||||
.author_name
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.author.author_name.text.to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Source Title",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.source_title
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Quality",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.quality
|
||||
.quality
|
||||
.name
|
||||
.to_lowercase()
|
||||
.cmp(&b.quality.quality.name.to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Date",
|
||||
cmp_fn: Some(|a, b| a.date.cmp(&b.date)),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::Number;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::assert_navigation_pushed;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::readarr_handlers::downloads::DownloadsHandler;
|
||||
use crate::models::readarr_models::DownloadRecord;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, DOWNLOADS_BLOCKS};
|
||||
use crate::models::servarr_models::DownloadStatus;
|
||||
|
||||
mod test_handle_delete {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_prompt() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
app.data.readarr_data.downloads.set_items(downloads_vec());
|
||||
|
||||
DownloadsHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::Downloads, None).handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::DeleteDownloadPrompt.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_download_prompt_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
app.data.readarr_data.downloads.set_items(downloads_vec());
|
||||
|
||||
DownloadsHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::Downloads, None).handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveReadarrBlock::Downloads.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_pushed;
|
||||
|
||||
#[rstest]
|
||||
fn test_downloads_tab_left(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.readarr_data.main_tabs.set_index(1);
|
||||
|
||||
DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Downloads,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.readarr_data.main_tabs.get_active_route(),
|
||||
ActiveReadarrBlock::Authors.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::Authors.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_downloads_tab_right(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.readarr_data.main_tabs.set_index(1);
|
||||
|
||||
DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Downloads,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.readarr_data.main_tabs.get_active_route(),
|
||||
ActiveReadarrBlock::Blocklist.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::Blocklist.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_downloads_left_right_prompt_toggle(
|
||||
#[values(
|
||||
ActiveReadarrBlock::DeleteDownloadPrompt,
|
||||
ActiveReadarrBlock::UpdateDownloadsPrompt
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
|
||||
DownloadsHandler::new(key, &mut app, active_readarr_block, None).handle();
|
||||
|
||||
assert!(app.data.readarr_data.prompt_confirm);
|
||||
|
||||
DownloadsHandler::new(key, &mut app, active_readarr_block, None).handle();
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Downloads,
|
||||
ActiveReadarrBlock::DeleteDownloadPrompt,
|
||||
ReadarrEvent::DeleteDownload(3)
|
||||
)]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Downloads,
|
||||
ActiveReadarrBlock::UpdateDownloadsPrompt,
|
||||
ReadarrEvent::UpdateDownloads
|
||||
)]
|
||||
fn test_downloads_prompt_confirm_submit(
|
||||
#[case] base_route: ActiveReadarrBlock,
|
||||
#[case] prompt_block: ActiveReadarrBlock,
|
||||
#[case] expected_action: ReadarrEvent,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.downloads.set_items(downloads_vec());
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(app.data.readarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.readarr_data.prompt_confirm_action,
|
||||
&expected_action
|
||||
);
|
||||
assert_navigation_popped!(app, base_route.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Downloads,
|
||||
ActiveReadarrBlock::DeleteDownloadPrompt
|
||||
)]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Downloads,
|
||||
ActiveReadarrBlock::UpdateDownloadsPrompt
|
||||
)]
|
||||
fn test_downloads_prompt_decline_submit(
|
||||
#[case] base_route: ActiveReadarrBlock,
|
||||
#[case] prompt_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.downloads.set_items(downloads_vec());
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_none!(app.data.readarr_data.prompt_confirm_action);
|
||||
assert_navigation_popped!(app, base_route.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Downloads,
|
||||
ActiveReadarrBlock::DeleteDownloadPrompt
|
||||
)]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Downloads,
|
||||
ActiveReadarrBlock::UpdateDownloadsPrompt
|
||||
)]
|
||||
fn test_downloads_prompt_blocks_esc(
|
||||
#[case] base_block: ActiveReadarrBlock,
|
||||
#[case] prompt_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(base_block.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
|
||||
DownloadsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert_navigation_popped!(app, base_block.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_default_esc(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.error = "test error".to_owned().into();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
|
||||
DownloadsHandler::new(ESC_KEY, &mut app, ActiveReadarrBlock::Downloads, None).handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Downloads.into());
|
||||
assert_is_empty!(app.error.text);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::{assert_navigation_popped, assert_navigation_pushed};
|
||||
|
||||
#[test]
|
||||
fn test_update_downloads_key() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
app.data.readarr_data.downloads.set_items(downloads_vec());
|
||||
|
||||
DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.update.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Downloads,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::UpdateDownloadsPrompt.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_downloads_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
app.data.readarr_data.downloads.set_items(downloads_vec());
|
||||
|
||||
DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.update.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Downloads,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveReadarrBlock::Downloads.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_downloads_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.downloads.set_items(downloads_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
|
||||
DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Downloads,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::Downloads.into());
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_downloads_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
app.data.readarr_data.downloads.set_items(downloads_vec());
|
||||
|
||||
DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Downloads,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveReadarrBlock::Downloads.into()
|
||||
);
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Downloads,
|
||||
ActiveReadarrBlock::DeleteDownloadPrompt,
|
||||
ReadarrEvent::DeleteDownload(3)
|
||||
)]
|
||||
#[case(
|
||||
ActiveReadarrBlock::Downloads,
|
||||
ActiveReadarrBlock::UpdateDownloadsPrompt,
|
||||
ReadarrEvent::UpdateDownloads
|
||||
)]
|
||||
fn test_downloads_prompt_confirm(
|
||||
#[case] base_route: ActiveReadarrBlock,
|
||||
#[case] prompt_block: ActiveReadarrBlock,
|
||||
#[case] expected_action: ReadarrEvent,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.downloads.set_items(downloads_vec());
|
||||
app.push_navigation_stack(base_route.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
prompt_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.readarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.readarr_data.prompt_confirm_action,
|
||||
&expected_action
|
||||
);
|
||||
assert_navigation_popped!(app, base_route.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_downloads_handler_accepts() {
|
||||
ActiveReadarrBlock::iter().for_each(|active_readarr_block| {
|
||||
if DOWNLOADS_BLOCKS.contains(&active_readarr_block) {
|
||||
assert!(DownloadsHandler::accepts(active_readarr_block));
|
||||
} else {
|
||||
assert!(!DownloadsHandler::accepts(active_readarr_block));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_downloads_handler_ignore_special_keys(
|
||||
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
|
||||
let handler = DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handler.ignore_special_keys(),
|
||||
ignore_special_keys_for_textbox_input
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_download_id() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.downloads.set_items(downloads_vec());
|
||||
app.data.readarr_data.downloads.select_index(Some(1));
|
||||
|
||||
let download_id = DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Downloads,
|
||||
None,
|
||||
)
|
||||
.extract_download_id();
|
||||
|
||||
assert_eq!(download_id, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_downloads_handler_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Downloads,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_downloads_handler_not_ready_when_downloads_is_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Downloads,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_downloads_handler_ready_when_not_loading_and_downloads_is_not_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Downloads.into());
|
||||
app.is_loading = false;
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.downloads
|
||||
.set_items(vec![DownloadRecord::default()]);
|
||||
|
||||
let handler = DownloadsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Downloads,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
fn downloads_vec() -> Vec<DownloadRecord> {
|
||||
vec![
|
||||
DownloadRecord {
|
||||
title: "Test Book Download Three".to_owned(),
|
||||
status: DownloadStatus::Downloading,
|
||||
id: 3,
|
||||
book_id: Some(Number::from(13)),
|
||||
author_id: Some(Number::from(23)),
|
||||
size: 3000.0,
|
||||
sizeleft: 750.0,
|
||||
output_path: Some("/nfs/nzbget/completed/books/Zora Neale - Test Book Three".into()),
|
||||
indexer: "test-indexer-three".to_owned(),
|
||||
download_client: Some("NZBGet".to_owned()),
|
||||
},
|
||||
DownloadRecord {
|
||||
title: "Test Book Download Two".to_owned(),
|
||||
status: DownloadStatus::Queued,
|
||||
id: 2,
|
||||
book_id: Some(Number::from(12)),
|
||||
author_id: Some(Number::from(22)),
|
||||
size: 2000.0,
|
||||
sizeleft: 500.0,
|
||||
output_path: Some("/nfs/nzbget/completed/books/Mary Shelley - Test Book Two".into()),
|
||||
indexer: "test-indexer-two".to_owned(),
|
||||
download_client: Some("Transmission".to_owned()),
|
||||
},
|
||||
DownloadRecord {
|
||||
title: "Test Book Download One".to_owned(),
|
||||
status: DownloadStatus::Paused,
|
||||
id: 1,
|
||||
book_id: Some(Number::from(11)),
|
||||
author_id: Some(Number::from(21)),
|
||||
size: 1000.0,
|
||||
sizeleft: 250.0,
|
||||
output_path: Some("/nfs/nzbget/completed/books/Ursula Le Guin - Test Book One".into()),
|
||||
indexer: "test-indexer-one".to_owned(),
|
||||
download_client: Some("SABnzbd".to_owned()),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::readarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, DOWNLOADS_BLOCKS};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "downloads_handler_tests.rs"]
|
||||
mod downloads_handler_tests;
|
||||
|
||||
pub(super) struct DownloadsHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
_context: Option<ActiveReadarrBlock>,
|
||||
}
|
||||
|
||||
impl DownloadsHandler<'_, '_> {
|
||||
fn extract_download_id(&self) -> i64 {
|
||||
self.app.data.readarr_data.downloads.current_selection().id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for DownloadsHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let downloads_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveReadarrBlock::Downloads.into());
|
||||
|
||||
if !handle_table(
|
||||
self,
|
||||
|app| &mut app.data.readarr_data.downloads,
|
||||
downloads_table_handling_config,
|
||||
) {
|
||||
self.handle_key_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveReadarrBlock) -> bool {
|
||||
DOWNLOADS_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveReadarrBlock,
|
||||
_context: Option<ActiveReadarrBlock>,
|
||||
) -> DownloadsHandler<'a, 'b> {
|
||||
DownloadsHandler {
|
||||
key,
|
||||
app,
|
||||
active_readarr_block: active_block,
|
||||
_context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading && !self.app.data.readarr_data.downloads.is_empty()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {
|
||||
if self.active_readarr_block == ActiveReadarrBlock::Downloads {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::DeleteDownloadPrompt.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::Downloads => handle_change_tab_left_right_keys(self.app, self.key),
|
||||
ActiveReadarrBlock::DeleteDownloadPrompt | ActiveReadarrBlock::UpdateDownloadsPrompt => {
|
||||
handle_prompt_toggle(self.app, self.key)
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::DeleteDownloadPrompt => {
|
||||
if self.app.data.readarr_data.prompt_confirm {
|
||||
self.app.data.readarr_data.prompt_confirm_action =
|
||||
Some(ReadarrEvent::DeleteDownload(self.extract_download_id()));
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveReadarrBlock::UpdateDownloadsPrompt => {
|
||||
if self.app.data.readarr_data.prompt_confirm {
|
||||
self.app.data.readarr_data.prompt_confirm_action = Some(ReadarrEvent::UpdateDownloads);
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::DeleteDownloadPrompt | ActiveReadarrBlock::UpdateDownloadsPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.readarr_data.prompt_confirm = false;
|
||||
}
|
||||
_ => handle_clear_errors(self.app),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::Downloads => match self.key {
|
||||
_ if matches_key!(update, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::UpdateDownloadsPrompt.into());
|
||||
}
|
||||
_ if matches_key!(refresh, key) => {
|
||||
self.app.should_refresh = true;
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveReadarrBlock::DeleteDownloadPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.readarr_data.prompt_confirm = true;
|
||||
self.app.data.readarr_data.prompt_confirm_action =
|
||||
Some(ReadarrEvent::DeleteDownload(self.extract_download_id()));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::UpdateDownloadsPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.readarr_data.prompt_confirm = true;
|
||||
self.app.data.readarr_data.prompt_confirm_action = Some(ReadarrEvent::UpdateDownloads);
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use chrono::DateTime;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use rstest::rstest;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::assert_navigation_pushed;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::readarr_handlers::history::{HistoryHandler, history_sorting_options};
|
||||
use crate::models::readarr_models::{ReadarrHistoryEventType, ReadarrHistoryItem};
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, HISTORY_BLOCKS};
|
||||
use crate::models::servarr_models::{Quality, QualityWrapper};
|
||||
|
||||
#[test]
|
||||
fn test_history_sorting_options_source_title() {
|
||||
let expected_cmp_fn: fn(&ReadarrHistoryItem, &ReadarrHistoryItem) -> Ordering = |a, b| {
|
||||
a.source_title
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.text.to_lowercase())
|
||||
};
|
||||
let mut expected_history_vec = history_vec();
|
||||
expected_history_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = history_sorting_options()[0].clone();
|
||||
let mut sorted_history_vec = history_vec();
|
||||
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_history_vec, expected_history_vec);
|
||||
assert_str_eq!(sort_option.name, "Source Title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_sorting_options_event_type() {
|
||||
let expected_cmp_fn: fn(&ReadarrHistoryItem, &ReadarrHistoryItem) -> Ordering = |a, b| {
|
||||
a.event_type
|
||||
.to_string()
|
||||
.to_lowercase()
|
||||
.cmp(&b.event_type.to_string().to_lowercase())
|
||||
};
|
||||
let mut expected_history_vec = history_vec();
|
||||
expected_history_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = history_sorting_options()[1].clone();
|
||||
let mut sorted_history_vec = history_vec();
|
||||
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_history_vec, expected_history_vec);
|
||||
assert_str_eq!(sort_option.name, "Event Type");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_sorting_options_quality() {
|
||||
let expected_cmp_fn: fn(&ReadarrHistoryItem, &ReadarrHistoryItem) -> Ordering = |a, b| {
|
||||
a.quality
|
||||
.quality
|
||||
.name
|
||||
.to_lowercase()
|
||||
.cmp(&b.quality.quality.name.to_lowercase())
|
||||
};
|
||||
let mut expected_history_vec = history_vec();
|
||||
expected_history_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = history_sorting_options()[2].clone();
|
||||
let mut sorted_history_vec = history_vec();
|
||||
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_history_vec, expected_history_vec);
|
||||
assert_str_eq!(sort_option.name, "Quality");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_sorting_options_date() {
|
||||
let expected_cmp_fn: fn(&ReadarrHistoryItem, &ReadarrHistoryItem) -> Ordering =
|
||||
|a, b| a.date.cmp(&b.date);
|
||||
let mut expected_history_vec = history_vec();
|
||||
expected_history_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = history_sorting_options()[3].clone();
|
||||
let mut sorted_history_vec = history_vec();
|
||||
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_history_vec, expected_history_vec);
|
||||
assert_str_eq!(sort_option.name, "Date");
|
||||
}
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_pushed;
|
||||
|
||||
#[rstest]
|
||||
fn test_history_tab_left(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.readarr_data.main_tabs.set_index(3);
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::History,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.readarr_data.main_tabs.get_active_route(),
|
||||
ActiveReadarrBlock::Blocklist.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::Blocklist.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_history_tab_right(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.readarr_data.main_tabs.set_index(3);
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::History,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.readarr_data.main_tabs.get_active_route(),
|
||||
ActiveReadarrBlock::RootFolders.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::RootFolders.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[test]
|
||||
fn test_history_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.history.set_items(history_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
|
||||
HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::History, None).handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::HistoryItemDetails.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_submit_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.data.readarr_data.history.set_items(history_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
|
||||
HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::History, None).handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveReadarrBlock::History.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[test]
|
||||
fn test_esc_history_item_details() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.history
|
||||
.set_items(vec![ReadarrHistoryItem::default()]);
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::HistoryItemDetails.into());
|
||||
|
||||
HistoryHandler::new(
|
||||
ESC_KEY,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::HistoryItemDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::History.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_default_esc(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.error = "test error".to_owned().into();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.history
|
||||
.set_items(vec![ReadarrHistoryItem::default()]);
|
||||
|
||||
HistoryHandler::new(ESC_KEY, &mut app, ActiveReadarrBlock::History, None).handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveReadarrBlock::History.into());
|
||||
assert_is_empty!(app.error.text);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_pushed;
|
||||
|
||||
#[test]
|
||||
fn test_refresh_history_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.history.set_items(history_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::History,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::History.into());
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_history_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.data.readarr_data.history.set_items(history_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::History,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveReadarrBlock::History.into());
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_history_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.history.set_items(history_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.search.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::History,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::SearchHistory.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_history_key() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.history.set_items(history_vec());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
|
||||
HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.filter.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::History,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::FilterHistory.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_handler_accepts() {
|
||||
ActiveReadarrBlock::iter().for_each(|active_readarr_block| {
|
||||
if HISTORY_BLOCKS.contains(&active_readarr_block) {
|
||||
assert!(HistoryHandler::accepts(active_readarr_block));
|
||||
} else {
|
||||
assert!(!HistoryHandler::accepts(active_readarr_block));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_history_handler_ignore_special_keys(
|
||||
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
|
||||
let handler = HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handler.ignore_special_keys(),
|
||||
ignore_special_keys_for_textbox_input
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_handler_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::History,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_handler_not_ready_when_history_is_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::History,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_handler_ready_when_not_loading_and_history_is_not_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::History.into());
|
||||
app.is_loading = false;
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.history
|
||||
.set_items(vec![ReadarrHistoryItem::default()]);
|
||||
|
||||
let handler = HistoryHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::History,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
fn history_vec() -> Vec<ReadarrHistoryItem> {
|
||||
vec![
|
||||
ReadarrHistoryItem {
|
||||
id: 3,
|
||||
source_title: "test 1".into(),
|
||||
event_type: ReadarrHistoryEventType::Grabbed,
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "EPUB".to_owned(),
|
||||
},
|
||||
},
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()),
|
||||
..ReadarrHistoryItem::default()
|
||||
},
|
||||
ReadarrHistoryItem {
|
||||
id: 2,
|
||||
source_title: "test 2".into(),
|
||||
event_type: ReadarrHistoryEventType::DownloadImported,
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "AZW3".to_owned(),
|
||||
},
|
||||
},
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()),
|
||||
..ReadarrHistoryItem::default()
|
||||
},
|
||||
ReadarrHistoryItem {
|
||||
id: 1,
|
||||
source_title: "test 3".into(),
|
||||
event_type: ReadarrHistoryEventType::BookFileDeleted,
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "MOBI".to_owned(),
|
||||
},
|
||||
},
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()),
|
||||
..ReadarrHistoryItem::default()
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::readarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_clear_errors};
|
||||
use crate::matches_key;
|
||||
use crate::models::Route;
|
||||
use crate::models::readarr_models::ReadarrHistoryItem;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, HISTORY_BLOCKS};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "history_handler_tests.rs"]
|
||||
mod history_handler_tests;
|
||||
|
||||
pub(super) struct HistoryHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
_context: Option<ActiveReadarrBlock>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for HistoryHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let history_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveReadarrBlock::History.into())
|
||||
.sorting_block(ActiveReadarrBlock::HistorySortPrompt.into())
|
||||
.sort_options(history_sorting_options())
|
||||
.searching_block(ActiveReadarrBlock::SearchHistory.into())
|
||||
.search_error_block(ActiveReadarrBlock::SearchHistoryError.into())
|
||||
.search_field_fn(|history| &history.source_title.text)
|
||||
.filtering_block(ActiveReadarrBlock::FilterHistory.into())
|
||||
.filter_error_block(ActiveReadarrBlock::FilterHistoryError.into())
|
||||
.filter_field_fn(|history| &history.source_title.text);
|
||||
|
||||
if !handle_table(
|
||||
self,
|
||||
|app| &mut app.data.readarr_data.history,
|
||||
history_table_handling_config,
|
||||
) {
|
||||
self.handle_key_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveReadarrBlock) -> bool {
|
||||
HISTORY_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveReadarrBlock,
|
||||
context: Option<ActiveReadarrBlock>,
|
||||
) -> Self {
|
||||
HistoryHandler {
|
||||
key,
|
||||
app,
|
||||
active_readarr_block: active_block,
|
||||
_context: context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading && !self.app.data.readarr_data.history.is_empty()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
if self.active_readarr_block == ActiveReadarrBlock::History {
|
||||
handle_change_tab_left_right_keys(self.app, self.key)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
if self.active_readarr_block == ActiveReadarrBlock::History {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::HistoryItemDetails.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
if self.active_readarr_block == ActiveReadarrBlock::HistoryItemDetails {
|
||||
self.app.pop_navigation_stack();
|
||||
} else {
|
||||
handle_clear_errors(self.app);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
if self.active_readarr_block == ActiveReadarrBlock::History {
|
||||
match self.key {
|
||||
_ if matches_key!(refresh, key) => {
|
||||
self.app.should_refresh = true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::handlers::readarr_handlers) fn history_sorting_options()
|
||||
-> Vec<SortOption<ReadarrHistoryItem>> {
|
||||
vec![
|
||||
SortOption {
|
||||
name: "Source Title",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.source_title
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.text.to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Event Type",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.event_type
|
||||
.to_string()
|
||||
.to_lowercase()
|
||||
.cmp(&b.event_type.to_string().to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Quality",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.quality
|
||||
.quality
|
||||
.name
|
||||
.to_lowercase()
|
||||
.cmp(&b.quality.quality.name.to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Date",
|
||||
cmp_fn: Some(|a, b| a.date.cmp(&b.date)),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, EDIT_INDEXER_BLOCKS};
|
||||
use crate::models::servarr_models::EditIndexerParams;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::{
|
||||
handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "edit_indexer_handler_tests.rs"]
|
||||
mod edit_indexer_handler_tests;
|
||||
|
||||
pub(super) struct EditIndexerHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
_context: Option<ActiveReadarrBlock>,
|
||||
}
|
||||
|
||||
impl EditIndexerHandler<'_, '_> {
|
||||
fn build_edit_indexer_params(&mut self) -> EditIndexerParams {
|
||||
let edit_indexer_modal = self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.take()
|
||||
.expect("EditIndexerModal is None");
|
||||
let indexer_id = self.app.data.readarr_data.indexers.current_selection().id;
|
||||
let tags = edit_indexer_modal.tags.text;
|
||||
let EditIndexerModal {
|
||||
name,
|
||||
enable_rss,
|
||||
enable_automatic_search,
|
||||
enable_interactive_search,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
priority,
|
||||
..
|
||||
} = edit_indexer_modal;
|
||||
|
||||
EditIndexerParams {
|
||||
indexer_id,
|
||||
name: Some(name.text),
|
||||
enable_rss,
|
||||
enable_automatic_search,
|
||||
enable_interactive_search,
|
||||
url: Some(url.text),
|
||||
api_key: Some(api_key.text),
|
||||
seed_ratio: Some(seed_ratio.text),
|
||||
tags: None,
|
||||
tag_input_string: Some(tags),
|
||||
priority: Some(priority),
|
||||
clear_tags: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for EditIndexerHandler<'a, 'b> {
|
||||
fn accepts(active_block: ActiveReadarrBlock) -> bool {
|
||||
EDIT_INDEXER_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveReadarrBlock,
|
||||
_context: Option<ActiveReadarrBlock>,
|
||||
) -> EditIndexerHandler<'a, 'b> {
|
||||
EditIndexerHandler {
|
||||
key,
|
||||
app,
|
||||
active_readarr_block: active_block,
|
||||
_context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading && self.app.data.readarr_data.edit_indexer_modal.is_some()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::EditIndexerPrompt => {
|
||||
self.app.data.readarr_data.selected_block.up();
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerPriorityInput => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.priority += 1;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_scroll_down(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::EditIndexerPrompt => {
|
||||
self.app.data.readarr_data.selected_block.down();
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerPriorityInput => {
|
||||
let edit_indexer_modal = self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
if edit_indexer_modal.priority > 1 {
|
||||
edit_indexer_modal.priority -= 1;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_home(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::EditIndexerNameInput => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.name
|
||||
.scroll_home();
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerUrlInput => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.url
|
||||
.scroll_home();
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerApiKeyInput => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.api_key
|
||||
.scroll_home();
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerSeedRatioInput => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.seed_ratio
|
||||
.scroll_home();
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerTagsInput => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
.scroll_home();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_end(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::EditIndexerNameInput => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.name
|
||||
.reset_offset();
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerUrlInput => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.url
|
||||
.reset_offset();
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerApiKeyInput => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.api_key
|
||||
.reset_offset();
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerSeedRatioInput => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.seed_ratio
|
||||
.reset_offset();
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerTagsInput => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
.reset_offset();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::EditIndexerPrompt => {
|
||||
handle_prompt_left_right_keys!(
|
||||
self,
|
||||
ActiveReadarrBlock::EditIndexerConfirmPrompt,
|
||||
readarr_data
|
||||
);
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerNameInput => {
|
||||
handle_text_box_left_right_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.name
|
||||
);
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerUrlInput => {
|
||||
handle_text_box_left_right_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.url
|
||||
);
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerApiKeyInput => {
|
||||
handle_text_box_left_right_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.api_key
|
||||
);
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerSeedRatioInput => {
|
||||
handle_text_box_left_right_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.seed_ratio
|
||||
);
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerTagsInput => {
|
||||
handle_text_box_left_right_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::EditIndexerPrompt => {
|
||||
let selected_block = self.app.data.readarr_data.selected_block.get_active_block();
|
||||
match selected_block {
|
||||
ActiveReadarrBlock::EditIndexerConfirmPrompt => {
|
||||
if self.app.data.readarr_data.prompt_confirm {
|
||||
self.app.data.readarr_data.prompt_confirm_action =
|
||||
Some(ReadarrEvent::EditIndexer(self.build_edit_indexer_params()));
|
||||
self.app.should_refresh = true;
|
||||
} else {
|
||||
self.app.data.readarr_data.edit_indexer_modal = None;
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerNameInput
|
||||
| ActiveReadarrBlock::EditIndexerUrlInput
|
||||
| ActiveReadarrBlock::EditIndexerApiKeyInput
|
||||
| ActiveReadarrBlock::EditIndexerSeedRatioInput
|
||||
| ActiveReadarrBlock::EditIndexerTagsInput => {
|
||||
self.app.push_navigation_stack(selected_block.into());
|
||||
self.app.ignore_special_keys_for_textbox_input = true;
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerPriorityInput => self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::EditIndexerPriorityInput.into()),
|
||||
ActiveReadarrBlock::EditIndexerToggleEnableRss => {
|
||||
let indexer = self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
indexer.enable_rss = Some(!indexer.enable_rss.unwrap_or_default());
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch => {
|
||||
let indexer = self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
indexer.enable_automatic_search =
|
||||
Some(!indexer.enable_automatic_search.unwrap_or_default());
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch => {
|
||||
let indexer = self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
indexer.enable_interactive_search =
|
||||
Some(!indexer.enable_interactive_search.unwrap_or_default());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerNameInput
|
||||
| ActiveReadarrBlock::EditIndexerUrlInput
|
||||
| ActiveReadarrBlock::EditIndexerApiKeyInput
|
||||
| ActiveReadarrBlock::EditIndexerSeedRatioInput
|
||||
| ActiveReadarrBlock::EditIndexerTagsInput => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.ignore_special_keys_for_textbox_input = false;
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::EditIndexerPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.readarr_data.prompt_confirm = false;
|
||||
self.app.data.readarr_data.edit_indexer_modal = None;
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerNameInput
|
||||
| ActiveReadarrBlock::EditIndexerUrlInput
|
||||
| ActiveReadarrBlock::EditIndexerApiKeyInput
|
||||
| ActiveReadarrBlock::EditIndexerSeedRatioInput
|
||||
| ActiveReadarrBlock::EditIndexerPriorityInput
|
||||
| ActiveReadarrBlock::EditIndexerTagsInput => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.ignore_special_keys_for_textbox_input = false;
|
||||
}
|
||||
_ => self.app.pop_navigation_stack(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::EditIndexerNameInput => {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.name
|
||||
);
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerUrlInput => {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.url
|
||||
);
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerApiKeyInput => {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.api_key
|
||||
);
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerSeedRatioInput => {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.seed_ratio
|
||||
);
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerTagsInput => {
|
||||
handle_text_box_keys!(
|
||||
self,
|
||||
self.key,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.edit_indexer_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.tags
|
||||
);
|
||||
}
|
||||
ActiveReadarrBlock::EditIndexerPrompt
|
||||
if self.app.data.readarr_data.selected_block.get_active_block()
|
||||
== ActiveReadarrBlock::EditIndexerConfirmPrompt
|
||||
&& matches_key!(confirm, self.key) =>
|
||||
{
|
||||
self.app.data.readarr_data.prompt_confirm = true;
|
||||
self.app.data.readarr_data.prompt_confirm_action =
|
||||
Some(ReadarrEvent::EditIndexer(self.build_edit_indexer_params()));
|
||||
self.app.should_refresh = true;
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 737 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user