Compare commits
148
Commits
develop
...
87edf1fb9e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
|
||||
+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();
|
||||
|
||||
+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,
|
||||
};
|
||||
|
||||
@@ -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,7 +3,9 @@ mod property_tests {
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::*;
|
||||
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;
|
||||
@@ -16,11 +18,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 +42,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 +80,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,
|
||||
@@ -111,15 +115,16 @@ mod property_tests {
|
||||
|
||||
#[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,11 +142,12 @@ 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);
|
||||
|
||||
@@ -164,12 +170,13 @@ 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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
.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;
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
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 {
|
||||
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,14 +145,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_radarr_block {
|
||||
ActiveRadarrBlock::CollectionDetails => {
|
||||
if 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(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
@@ -387,8 +392,8 @@ 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(),
|
||||
overview: "New movie blah blah blah".to_owned(),
|
||||
@@ -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(),
|
||||
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(),
|
||||
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(),
|
||||
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,502 @@
|
||||
#[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, DownloadStatus};
|
||||
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, DOWNLOADS_BLOCKS};
|
||||
|
||||
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
@@ -0,0 +1,221 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::models::Route;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
ActiveReadarrBlock, INDEXER_SETTINGS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::IndexerSettings;
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::{handle_prompt_left_right_keys, matches_key};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "edit_indexer_settings_handler_tests.rs"]
|
||||
mod edit_indexer_settings_handler_tests;
|
||||
|
||||
pub(super) struct IndexerSettingsHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
_context: Option<ActiveReadarrBlock>,
|
||||
}
|
||||
|
||||
impl IndexerSettingsHandler<'_, '_> {
|
||||
fn build_edit_indexer_settings_params(&mut self) -> IndexerSettings {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexer_settings
|
||||
.take()
|
||||
.expect("IndexerSettings is None")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for IndexerSettingsHandler<'a, 'b> {
|
||||
fn accepts(active_block: ActiveReadarrBlock) -> bool {
|
||||
INDEXER_SETTINGS_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>,
|
||||
) -> IndexerSettingsHandler<'a, 'b> {
|
||||
IndexerSettingsHandler {
|
||||
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.indexer_settings.is_some()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {
|
||||
let indexer_settings = self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexer_settings
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt => {
|
||||
self.app.data.readarr_data.selected_block.up();
|
||||
}
|
||||
ActiveReadarrBlock::IndexerSettingsMinimumAgeInput => {
|
||||
indexer_settings.minimum_age += 1;
|
||||
}
|
||||
ActiveReadarrBlock::IndexerSettingsRetentionInput => {
|
||||
indexer_settings.retention += 1;
|
||||
}
|
||||
ActiveReadarrBlock::IndexerSettingsMaximumSizeInput => {
|
||||
indexer_settings.maximum_size += 1;
|
||||
}
|
||||
ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput => {
|
||||
indexer_settings.rss_sync_interval += 1;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_scroll_down(&mut self) {
|
||||
let indexer_settings = self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexer_settings
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt => {
|
||||
self.app.data.readarr_data.selected_block.down()
|
||||
}
|
||||
ActiveReadarrBlock::IndexerSettingsMinimumAgeInput => {
|
||||
if indexer_settings.minimum_age > 0 {
|
||||
indexer_settings.minimum_age -= 1;
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::IndexerSettingsRetentionInput => {
|
||||
if indexer_settings.retention > 0 {
|
||||
indexer_settings.retention -= 1;
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::IndexerSettingsMaximumSizeInput => {
|
||||
if indexer_settings.maximum_size > 0 {
|
||||
indexer_settings.maximum_size -= 1;
|
||||
}
|
||||
}
|
||||
ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput
|
||||
if indexer_settings.rss_sync_interval > 0 =>
|
||||
{
|
||||
indexer_settings.rss_sync_interval -= 1;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
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::AllIndexerSettingsPrompt {
|
||||
handle_prompt_left_right_keys!(
|
||||
self,
|
||||
ActiveReadarrBlock::IndexerSettingsConfirmPrompt,
|
||||
readarr_data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt => {
|
||||
match self.app.data.readarr_data.selected_block.get_active_block() {
|
||||
ActiveReadarrBlock::IndexerSettingsConfirmPrompt => {
|
||||
if self.app.data.readarr_data.prompt_confirm {
|
||||
self.app.data.readarr_data.prompt_confirm_action = Some(
|
||||
ReadarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_params()),
|
||||
);
|
||||
self.app.should_refresh = true;
|
||||
} else {
|
||||
self.app.data.readarr_data.indexer_settings = None;
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveReadarrBlock::IndexerSettingsMinimumAgeInput
|
||||
| ActiveReadarrBlock::IndexerSettingsRetentionInput
|
||||
| ActiveReadarrBlock::IndexerSettingsMaximumSizeInput
|
||||
| ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput => {
|
||||
self.app.push_navigation_stack(
|
||||
(
|
||||
self.app.data.readarr_data.selected_block.get_active_block(),
|
||||
None,
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
ActiveReadarrBlock::IndexerSettingsMinimumAgeInput
|
||||
| ActiveReadarrBlock::IndexerSettingsRetentionInput
|
||||
| ActiveReadarrBlock::IndexerSettingsMaximumSizeInput
|
||||
| ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput => self.app.pop_navigation_stack(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.readarr_data.prompt_confirm = false;
|
||||
self.app.data.readarr_data.indexer_settings = None;
|
||||
}
|
||||
_ => self.app.pop_navigation_stack(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
if self.active_readarr_block == ActiveReadarrBlock::AllIndexerSettingsPrompt
|
||||
&& self.app.data.readarr_data.selected_block.get_active_block()
|
||||
== ActiveReadarrBlock::IndexerSettingsConfirmPrompt
|
||||
&& matches_key!(confirm, self.key)
|
||||
{
|
||||
self.app.data.readarr_data.prompt_confirm = true;
|
||||
self.app.data.readarr_data.prompt_confirm_action = Some(
|
||||
ReadarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,610 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::assert_modal_absent;
|
||||
use crate::assert_navigation_pushed;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::readarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
ActiveReadarrBlock, INDEXER_SETTINGS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::IndexerSettings;
|
||||
use crate::network::readarr_network::readarr_network_test_utils::test_utils::indexer_settings;
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::models::BlockSelectionState;
|
||||
use crate::models::servarr_data::readarr::readarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS;
|
||||
use crate::models::servarr_models::IndexerSettings;
|
||||
|
||||
use super::*;
|
||||
|
||||
macro_rules! test_i64_counter_scroll_value {
|
||||
($block:expr, $key:expr, $data_ref:ident, $negatives:literal) => {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.data.readarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
|
||||
IndexerSettingsHandler::new($key, &mut app, $block, None).handle();
|
||||
|
||||
if $key == Key::Up {
|
||||
assert_eq!(
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexer_settings
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.$data_ref,
|
||||
1
|
||||
);
|
||||
} else {
|
||||
if $negatives {
|
||||
assert_eq!(
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexer_settings
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.$data_ref,
|
||||
-1
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexer_settings
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.$data_ref,
|
||||
0
|
||||
);
|
||||
|
||||
IndexerSettingsHandler::new(Key::Up, &mut app, $block, None).handle();
|
||||
|
||||
assert_eq!(
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexer_settings
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.$data_ref,
|
||||
1
|
||||
);
|
||||
|
||||
IndexerSettingsHandler::new($key, &mut app, $block, None).handle();
|
||||
assert_eq!(
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexer_settings
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.$data_ref,
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_settings_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.data.readarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
|
||||
app.data.readarr_data.selected_block.down();
|
||||
|
||||
IndexerSettingsHandler::new(
|
||||
key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
if key == Key::Up {
|
||||
assert_eq!(
|
||||
app.data.readarr_data.selected_block.get_active_block(),
|
||||
ActiveReadarrBlock::IndexerSettingsMinimumAgeInput
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
app.data.readarr_data.selected_block.get_active_block(),
|
||||
ActiveReadarrBlock::IndexerSettingsMaximumSizeInput
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_settings_prompt_scroll_no_op_when_not_ready(
|
||||
#[values(Key::Up, Key::Down)] key: Key,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.is_loading = true;
|
||||
app.data.readarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
|
||||
app.data.readarr_data.selected_block.down();
|
||||
|
||||
IndexerSettingsHandler::new(
|
||||
key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.readarr_data.selected_block.get_active_block(),
|
||||
ActiveReadarrBlock::IndexerSettingsRetentionInput
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_settings_minimum_age_scroll(#[values(Key::Up, Key::Down)] key: Key) {
|
||||
test_i64_counter_scroll_value!(
|
||||
ActiveReadarrBlock::IndexerSettingsMinimumAgeInput,
|
||||
key,
|
||||
minimum_age,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_settings_retention_scroll(#[values(Key::Up, Key::Down)] key: Key) {
|
||||
test_i64_counter_scroll_value!(
|
||||
ActiveReadarrBlock::IndexerSettingsRetentionInput,
|
||||
key,
|
||||
retention,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_settings_maximum_size_scroll(#[values(Key::Up, Key::Down)] key: Key) {
|
||||
test_i64_counter_scroll_value!(
|
||||
ActiveReadarrBlock::IndexerSettingsMaximumSizeInput,
|
||||
key,
|
||||
maximum_size,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_settings_rss_sync_interval_scroll(#[values(Key::Up, Key::Down)] key: Key) {
|
||||
test_i64_counter_scroll_value!(
|
||||
ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput,
|
||||
key,
|
||||
rss_sync_interval,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use crate::models::servarr_data::readarr::readarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS;
|
||||
|
||||
use crate::models::BlockSelectionState;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
|
||||
app.data.readarr_data.selected_block.y = INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1;
|
||||
|
||||
IndexerSettingsHandler::new(
|
||||
key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.readarr_data.prompt_confirm);
|
||||
|
||||
IndexerSettingsHandler::new(
|
||||
key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::{
|
||||
assert_navigation_popped,
|
||||
models::{
|
||||
BlockSelectionState,
|
||||
servarr_data::readarr::readarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS,
|
||||
servarr_models::IndexerSettings,
|
||||
},
|
||||
network::readarr_network::ReadarrEvent,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_settings_prompt_prompt_decline_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.selected_block
|
||||
.set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
|
||||
app.data.readarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
|
||||
IndexerSettingsHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into());
|
||||
assert_none!(app.data.readarr_data.prompt_confirm_action);
|
||||
assert!(!app.should_refresh);
|
||||
assert_none!(app.data.readarr_data.indexer_settings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_settings_prompt_prompt_confirmation_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.selected_block
|
||||
.set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
|
||||
app.data.readarr_data.indexer_settings = Some(indexer_settings());
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
|
||||
IndexerSettingsHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.readarr_data.prompt_confirm_action,
|
||||
&ReadarrEvent::EditAllIndexerSettings(indexer_settings())
|
||||
);
|
||||
assert_modal_absent!(app.data.readarr_data.indexer_settings);
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_settings_prompt_prompt_confirmation_submit_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
app.data.readarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
|
||||
IndexerSettingsHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt.into()
|
||||
);
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(ActiveReadarrBlock::IndexerSettingsMinimumAgeInput, 0)]
|
||||
#[case(ActiveReadarrBlock::IndexerSettingsRetentionInput, 1)]
|
||||
#[case(ActiveReadarrBlock::IndexerSettingsMaximumSizeInput, 2)]
|
||||
#[case(ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput, 3)]
|
||||
fn test_edit_indexer_settings_prompt_submit_selected_block(
|
||||
#[case] selected_block: ActiveReadarrBlock,
|
||||
#[case] y_index: usize,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.data.readarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
|
||||
app.data.readarr_data.selected_block.set_index(0, y_index);
|
||||
|
||||
IndexerSettingsHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, selected_block.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_settings_prompt_submit_selected_block_no_op_when_not_ready(
|
||||
#[values(0, 1, 2, 3, 4)] y_index: usize,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.is_loading = true;
|
||||
app.data.readarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
|
||||
app.data.readarr_data.selected_block.set_index(0, y_index);
|
||||
|
||||
IndexerSettingsHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_settings_selected_block_submit(
|
||||
#[values(
|
||||
ActiveReadarrBlock::IndexerSettingsMinimumAgeInput,
|
||||
ActiveReadarrBlock::IndexerSettingsRetentionInput,
|
||||
ActiveReadarrBlock::IndexerSettingsMaximumSizeInput,
|
||||
ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.data.readarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
|
||||
IndexerSettingsHandler::new(SUBMIT_KEY, &mut app, active_readarr_block, None).handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::models::servarr_models::IndexerSettings;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_settings_prompt_esc(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
app.data.readarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
|
||||
IndexerSettingsHandler::new(
|
||||
ESC_KEY,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_none!(app.data.readarr_data.indexer_settings);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_settings_selected_blocks_esc(
|
||||
#[values(
|
||||
ActiveReadarrBlock::IndexerSettingsMinimumAgeInput,
|
||||
ActiveReadarrBlock::IndexerSettingsRetentionInput,
|
||||
ActiveReadarrBlock::IndexerSettingsMaximumSizeInput,
|
||||
ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(active_readarr_block.into());
|
||||
app.data.readarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
|
||||
IndexerSettingsHandler::new(ESC_KEY, &mut app, active_readarr_block, None).handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.readarr_data.indexer_settings,
|
||||
&IndexerSettings::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use crate::{
|
||||
assert_navigation_popped,
|
||||
models::{
|
||||
BlockSelectionState, servarr_data::readarr::readarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS,
|
||||
},
|
||||
network::readarr_network::ReadarrEvent,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_settings_prompt_prompt_confirmation_confirm() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.selected_block
|
||||
.set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
|
||||
app.data.readarr_data.indexer_settings = Some(indexer_settings());
|
||||
|
||||
IndexerSettingsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.readarr_data.prompt_confirm_action,
|
||||
&ReadarrEvent::EditAllIndexerSettings(indexer_settings())
|
||||
);
|
||||
assert_modal_absent!(app.data.readarr_data.indexer_settings);
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexer_settings_handler_accepts() {
|
||||
ActiveReadarrBlock::iter().for_each(|active_readarr_block| {
|
||||
if INDEXER_SETTINGS_BLOCKS.contains(&active_readarr_block) {
|
||||
assert!(IndexerSettingsHandler::accepts(active_readarr_block));
|
||||
} else {
|
||||
assert!(!IndexerSettingsHandler::accepts(active_readarr_block));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_indexer_settings_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 = IndexerSettingsHandler::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_build_edit_indexer_settings_params() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.indexer_settings = Some(indexer_settings());
|
||||
|
||||
let actual_indexer_settings = IndexerSettingsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
)
|
||||
.build_edit_indexer_settings_params();
|
||||
|
||||
assert_eq!(actual_indexer_settings, indexer_settings());
|
||||
assert_modal_absent!(app.data.readarr_data.indexer_settings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_settings_handler_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = IndexerSettingsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_settings_handler_not_ready_when_indexer_settings_is_none() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = IndexerSettingsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_settings_handler_ready_when_not_loading_and_indexer_settings_is_some() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.is_loading = false;
|
||||
app.data.readarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
|
||||
let handler = IndexerSettingsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,750 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_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::indexers::IndexersHandler;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
ActiveReadarrBlock, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXERS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::Indexer;
|
||||
use crate::network::readarr_network::readarr_network_test_utils::test_utils::indexer;
|
||||
use crate::test_handler_delegation;
|
||||
|
||||
mod test_handle_delete {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_prompt() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
|
||||
IndexersHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::Indexers, None).handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::DeleteIndexerPrompt.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_prompt_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
|
||||
IndexersHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::Indexers, None).handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveReadarrBlock::Indexers.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn test_indexers_tab_left(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.readarr_data.main_tabs.set_index(5);
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.readarr_data.main_tabs.get_active_route(),
|
||||
ActiveReadarrBlock::RootFolders.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::RootFolders.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_indexers_tab_right(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.is_loading = is_ready;
|
||||
app.data.readarr_data.main_tabs.set_index(5);
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.data.readarr_data.main_tabs.get_active_route(),
|
||||
ActiveReadarrBlock::System.into()
|
||||
);
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::System.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_left_right_delete_indexer_prompt_toggle(
|
||||
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
|
||||
IndexersHandler::new(key, &mut app, ActiveReadarrBlock::DeleteIndexerPrompt, None).handle();
|
||||
|
||||
assert!(app.data.readarr_data.prompt_confirm);
|
||||
|
||||
IndexersHandler::new(key, &mut app, ActiveReadarrBlock::DeleteIndexerPrompt, None).handle();
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
use crate::models::servarr_data::modals::EditIndexerModal;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, ReadarrData,
|
||||
};
|
||||
use crate::models::servarr_models::{Indexer, IndexerField};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::network::readarr_network::readarr_network_test_utils::test_utils::indexer;
|
||||
use bimap::BiMap;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::{Number, Value};
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[rstest]
|
||||
fn test_edit_indexer_submit(#[values(true, false)] torrent_protocol: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
let protocol = if torrent_protocol {
|
||||
"torrent".to_owned()
|
||||
} else {
|
||||
"usenet".to_owned()
|
||||
};
|
||||
let decoy_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),
|
||||
enable_automatic_search: Some(false),
|
||||
enable_interactive_search: Some(true),
|
||||
url: "https://test.com".into(),
|
||||
api_key: "1234".into(),
|
||||
tags: "usenet, test".into(),
|
||||
..EditIndexerModal::default()
|
||||
};
|
||||
let mut readarr_data = ReadarrData {
|
||||
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
|
||||
..ReadarrData::default()
|
||||
};
|
||||
let mut fields = vec![
|
||||
IndexerField {
|
||||
name: Some("baseUrl".to_owned()),
|
||||
value: Some(Value::String("https://test.com".to_owned())),
|
||||
},
|
||||
IndexerField {
|
||||
name: Some("apiKey".to_owned()),
|
||||
value: Some(Value::String("1234".to_owned())),
|
||||
},
|
||||
];
|
||||
|
||||
if torrent_protocol {
|
||||
fields.push(IndexerField {
|
||||
name: Some("seedCriteria.seedRatio".to_owned()),
|
||||
value: Some(Value::from(1.2f64)),
|
||||
});
|
||||
expected_edit_indexer_modal.seed_ratio = "1.2".into();
|
||||
}
|
||||
|
||||
let indexer = Indexer {
|
||||
name: Some("Test".to_owned()),
|
||||
enable_rss: true,
|
||||
enable_automatic_search: false,
|
||||
enable_interactive_search: true,
|
||||
protocol,
|
||||
tags: vec![Number::from(1), Number::from(2)],
|
||||
fields: Some(fields),
|
||||
..Indexer::default()
|
||||
};
|
||||
let decoy_indexer = Indexer {
|
||||
name: Some("Decoy".to_owned()),
|
||||
enable_rss: false,
|
||||
enable_automatic_search: true,
|
||||
enable_interactive_search: false,
|
||||
protocol: decoy_protocol,
|
||||
tags: vec![Number::from(2)],
|
||||
fields: Some(vec![
|
||||
IndexerField {
|
||||
name: Some("baseUrl".to_owned()),
|
||||
value: Some(Value::String("https://decoy.com".to_owned())),
|
||||
},
|
||||
IndexerField {
|
||||
name: Some("apiKey".to_owned()),
|
||||
value: Some(Value::String("9999".to_owned())),
|
||||
},
|
||||
]),
|
||||
..Indexer::default()
|
||||
};
|
||||
readarr_data
|
||||
.indexers
|
||||
.set_items(vec![decoy_indexer, indexer]);
|
||||
readarr_data.indexers.select_index(Some(1));
|
||||
app.data.readarr_data = readarr_data;
|
||||
|
||||
IndexersHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::Indexers, None).handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::EditIndexerPrompt.into());
|
||||
assert_some_eq_x!(
|
||||
&app.data.readarr_data.edit_indexer_modal,
|
||||
&EditIndexerModal::from(&app.data.readarr_data)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
&app.data.readarr_data.edit_indexer_modal,
|
||||
&expected_edit_indexer_modal
|
||||
);
|
||||
if torrent_protocol {
|
||||
assert_eq!(
|
||||
app.data.readarr_data.selected_block.blocks,
|
||||
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
app.data.readarr_data.selected_block.blocks,
|
||||
EDIT_INDEXER_NZB_SELECTION_BLOCKS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_indexer_submit_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
|
||||
IndexersHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::Indexers, None).handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveReadarrBlock::Indexers.into());
|
||||
assert_none!(app.data.readarr_data.edit_indexer_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_prompt_confirm_submit() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.indexers.set_items(vec![indexer()]);
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::DeleteIndexerPrompt.into());
|
||||
|
||||
IndexersHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::DeleteIndexerPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.readarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.readarr_data.prompt_confirm_action,
|
||||
&ReadarrEvent::DeleteIndexer(8)
|
||||
);
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prompt_decline_submit() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::DeleteIndexerPrompt.into());
|
||||
|
||||
IndexersHandler::new(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::DeleteIndexerPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
assert_none!(app.data.readarr_data.prompt_confirm_action);
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
|
||||
use super::*;
|
||||
use crate::assert_navigation_popped;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[rstest]
|
||||
fn test_delete_indexer_prompt_block_esc(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::DeleteIndexerPrompt.into());
|
||||
app.data.readarr_data.prompt_confirm = true;
|
||||
|
||||
IndexersHandler::new(
|
||||
ESC_KEY,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::DeleteIndexerPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into());
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_test_indexer_esc(#[values(true, false)] is_ready: bool) {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = is_ready;
|
||||
app.data.readarr_data.indexer_test_errors = Some("test result".to_owned());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::TestIndexer.into());
|
||||
|
||||
IndexersHandler::new(ESC_KEY, &mut app, ActiveReadarrBlock::TestIndexer, None).handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into());
|
||||
assert_none!(app.data.readarr_data.indexer_test_errors);
|
||||
}
|
||||
|
||||
#[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::Indexers.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
|
||||
IndexersHandler::new(ESC_KEY, &mut app, ActiveReadarrBlock::Indexers, None).handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into());
|
||||
assert_is_empty!(app.error.text);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::network::readarr_network::readarr_network_test_utils::test_utils::indexer;
|
||||
use crate::{
|
||||
assert_navigation_popped,
|
||||
models::servarr_data::readarr::readarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS,
|
||||
network::readarr_network::ReadarrEvent,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_refresh_indexers_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::Indexers.into());
|
||||
assert!(app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_indexers_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveReadarrBlock::Indexers.into());
|
||||
assert!(!app.should_refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexer_settings_key() {
|
||||
let mut app = App::test_default();
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.settings.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
assert_eq!(
|
||||
app.data.readarr_data.selected_block.blocks,
|
||||
INDEXER_SETTINGS_SELECTION_BLOCKS
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexer_settings_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.settings.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveReadarrBlock::Indexers.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_key() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.test.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::TestIndexer.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.test.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveReadarrBlock::Indexers.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_all_key() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.test_all.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::TestAllIndexers.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_all_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.test_all.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), ActiveReadarrBlock::Indexers.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_indexer_prompt_confirm() {
|
||||
let mut app = App::test_default();
|
||||
app.data.readarr_data.indexers.set_items(vec![indexer()]);
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::DeleteIndexerPrompt.into());
|
||||
|
||||
IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::DeleteIndexerPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.readarr_data.prompt_confirm);
|
||||
assert_some_eq_x!(
|
||||
&app.data.readarr_data.prompt_confirm_action,
|
||||
&ReadarrEvent::DeleteIndexer(8)
|
||||
);
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::Indexers.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_edit_indexer_blocks_to_edit_indexer_handler(
|
||||
#[values(
|
||||
ActiveReadarrBlock::EditIndexerPrompt,
|
||||
ActiveReadarrBlock::EditIndexerConfirmPrompt,
|
||||
ActiveReadarrBlock::EditIndexerApiKeyInput,
|
||||
ActiveReadarrBlock::EditIndexerNameInput,
|
||||
ActiveReadarrBlock::EditIndexerSeedRatioInput,
|
||||
ActiveReadarrBlock::EditIndexerToggleEnableRss,
|
||||
ActiveReadarrBlock::EditIndexerToggleEnableAutomaticSearch,
|
||||
ActiveReadarrBlock::EditIndexerToggleEnableInteractiveSearch,
|
||||
ActiveReadarrBlock::EditIndexerUrlInput,
|
||||
ActiveReadarrBlock::EditIndexerTagsInput
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
test_handler_delegation!(
|
||||
IndexersHandler,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
active_readarr_block
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_indexer_settings_blocks_to_indexer_settings_handler(
|
||||
#[values(
|
||||
ActiveReadarrBlock::AllIndexerSettingsPrompt,
|
||||
ActiveReadarrBlock::IndexerSettingsConfirmPrompt,
|
||||
ActiveReadarrBlock::IndexerSettingsMaximumSizeInput,
|
||||
ActiveReadarrBlock::IndexerSettingsMinimumAgeInput,
|
||||
ActiveReadarrBlock::IndexerSettingsRetentionInput,
|
||||
ActiveReadarrBlock::IndexerSettingsRssSyncIntervalInput
|
||||
)]
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
) {
|
||||
test_handler_delegation!(
|
||||
IndexersHandler,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
active_readarr_block
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delegates_test_all_indexers_block_to_test_all_indexers_handler() {
|
||||
test_handler_delegation!(
|
||||
IndexersHandler,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
ActiveReadarrBlock::TestAllIndexers
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexers_handler_accepts() {
|
||||
let mut indexers_blocks = Vec::new();
|
||||
indexers_blocks.extend(INDEXERS_BLOCKS);
|
||||
indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS);
|
||||
indexers_blocks.extend(EDIT_INDEXER_BLOCKS);
|
||||
indexers_blocks.push(ActiveReadarrBlock::TestAllIndexers);
|
||||
|
||||
ActiveReadarrBlock::iter().for_each(|active_readarr_block| {
|
||||
if indexers_blocks.contains(&active_readarr_block) {
|
||||
assert!(IndexersHandler::accepts(active_readarr_block));
|
||||
} else {
|
||||
assert!(!IndexersHandler::accepts(active_readarr_block));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_indexers_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 = IndexersHandler::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_indexer_id() {
|
||||
let mut app = App::test_default();
|
||||
let decoy_indexer = Indexer { id: 3, ..indexer() };
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![decoy_indexer, indexer()]);
|
||||
app.data.readarr_data.indexers.select_index(Some(1));
|
||||
|
||||
let indexer_id = IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
)
|
||||
.extract_indexer_id();
|
||||
|
||||
assert_eq!(indexer_id, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexers_handler_not_ready_when_loading() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexers_handler_not_ready_when_indexers_is_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.is_loading = false;
|
||||
|
||||
let handler = IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexers_handler_ready_when_not_loading_and_indexers_is_not_empty() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveReadarrBlock::Indexers.into());
|
||||
app.is_loading = false;
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.set_items(vec![Indexer::default()]);
|
||||
|
||||
let handler = IndexersHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::Indexers,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::readarr_handlers::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::readarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
|
||||
use crate::handlers::readarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler;
|
||||
use crate::handlers::readarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler;
|
||||
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::servarr_data::readarr::readarr_data::{
|
||||
ActiveReadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
|
||||
INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
|
||||
};
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
|
||||
mod edit_indexer_handler;
|
||||
mod edit_indexer_settings_handler;
|
||||
mod test_all_indexers_handler;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "indexers_handler_tests.rs"]
|
||||
mod indexers_handler_tests;
|
||||
|
||||
pub(super) struct IndexersHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
context: Option<ActiveReadarrBlock>,
|
||||
}
|
||||
|
||||
impl IndexersHandler<'_, '_> {
|
||||
fn extract_indexer_id(&self) -> i64 {
|
||||
self.app.data.readarr_data.indexers.current_selection().id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for IndexersHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let indexers_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveReadarrBlock::Indexers.into());
|
||||
|
||||
if !handle_table(
|
||||
self,
|
||||
|app| &mut app.data.readarr_data.indexers,
|
||||
indexers_table_handling_config,
|
||||
) {
|
||||
match self.active_readarr_block {
|
||||
_ if EditIndexerHandler::accepts(self.active_readarr_block) => {
|
||||
EditIndexerHandler::new(self.key, self.app, self.active_readarr_block, self.context)
|
||||
.handle()
|
||||
}
|
||||
_ if IndexerSettingsHandler::accepts(self.active_readarr_block) => {
|
||||
IndexerSettingsHandler::new(self.key, self.app, self.active_readarr_block, self.context)
|
||||
.handle()
|
||||
}
|
||||
_ if TestAllIndexersHandler::accepts(self.active_readarr_block) => {
|
||||
TestAllIndexersHandler::new(self.key, self.app, self.active_readarr_block, self.context)
|
||||
.handle()
|
||||
}
|
||||
_ => self.handle_key_event(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveReadarrBlock) -> bool {
|
||||
EditIndexerHandler::accepts(active_block)
|
||||
|| IndexerSettingsHandler::accepts(active_block)
|
||||
|| TestAllIndexersHandler::accepts(active_block)
|
||||
|| INDEXERS_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>,
|
||||
) -> IndexersHandler<'a, 'b> {
|
||||
IndexersHandler {
|
||||
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.indexers.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::Indexers {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::DeleteIndexerPrompt.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::Indexers => handle_change_tab_left_right_keys(self.app, self.key),
|
||||
ActiveReadarrBlock::DeleteIndexerPrompt => handle_prompt_toggle(self.app, self.key),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::DeleteIndexerPrompt => {
|
||||
if self.app.data.readarr_data.prompt_confirm {
|
||||
self.app.data.readarr_data.prompt_confirm_action =
|
||||
Some(ReadarrEvent::DeleteIndexer(self.extract_indexer_id()));
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveReadarrBlock::Indexers => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::EditIndexerPrompt.into());
|
||||
self.app.data.readarr_data.edit_indexer_modal = Some((&self.app.data.readarr_data).into());
|
||||
let protocol = &self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.indexers
|
||||
.current_selection()
|
||||
.protocol;
|
||||
if protocol == "torrent" {
|
||||
self.app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS);
|
||||
} else {
|
||||
self.app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::DeleteIndexerPrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.readarr_data.prompt_confirm = false;
|
||||
}
|
||||
ActiveReadarrBlock::TestIndexer => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.readarr_data.indexer_test_errors = None;
|
||||
}
|
||||
_ => handle_clear_errors(self.app),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
match self.active_readarr_block {
|
||||
ActiveReadarrBlock::Indexers => match self.key {
|
||||
_ if matches_key!(refresh, key) => {
|
||||
self.app.should_refresh = true;
|
||||
}
|
||||
_ if matches_key!(test, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::TestIndexer.into());
|
||||
}
|
||||
_ if matches_key!(test_all, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::TestAllIndexers.into());
|
||||
}
|
||||
_ if matches_key!(settings, key) => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::AllIndexerSettingsPrompt.into());
|
||||
self.app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveReadarrBlock::DeleteIndexerPrompt => {
|
||||
if matches_key!(confirm, key) {
|
||||
self.app.data.readarr_data.prompt_confirm = true;
|
||||
self.app.data.readarr_data.prompt_confirm_action =
|
||||
Some(ReadarrEvent::DeleteIndexer(self.extract_indexer_id()));
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
Loaded 100 of 716 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user