Compare commits

...

130 Commits

Author SHA1 Message Date
c8a06f3601 refactored managarr table initializer so a mutable app reference can be passed 2026-01-05 09:49:03 -07:00
50d4ddfa28 Merge branch 'refs/heads/main' into tachyonfx
# Conflicts:
#	src/app/mod.rs
#	src/main.rs
2025-12-19 13:45:35 -07:00
368f7505ff feat: Improved UI speed and responsiveness
Check / stable / fmt (push) Has been cancelled
Check / beta / clippy (push) Has been cancelled
Check / stable / clippy (push) Has been cancelled
Check / nightly / doc (push) Has been cancelled
Check / 1.89.0 / check (push) Has been cancelled
Test Suite / ubuntu / beta (push) Has been cancelled
Test Suite / ubuntu / stable (push) Has been cancelled
Test Suite / macos-latest / stable (push) Has been cancelled
Test Suite / windows-latest / stable (push) Has been cancelled
Test Suite / ubuntu / stable / coverage (push) Has been cancelled
2025-12-19 13:41:14 -07:00
ea73f1d3d4 testing 2025-12-19 13:09:26 -07:00
d69056e5d6 feat: Further improved responsiveness and speed of the TUI 2025-12-19 12:28:33 -07:00
051f30b097 feat: Improved overall UI responsiveness 2025-12-18 16:19:03 -07:00
6a9fd0999c build: Added additional compile-time optimizations for speed improvements and a smaller binary 2025-12-17 16:45:51 -07:00
Alex Clarke
d8ac94d067 Merge pull request #51 from Dark-Alex-17/test-refactoring
Refactored all tests and wrote snapshot tests to finally cover the UI module
2025-12-16 16:41:04 -07:00
0532d59746 test: Wrote snapshot tests for all Sonarr UI 2025-12-16 14:12:10 -07:00
e0fcbc71e1 ci: updated Cargo Husky hooks to not use the Makefile 2025-12-15 20:21:14 -07:00
c072c57bbb style: Applied uniform formatting across all new UI test files 2025-12-15 20:18:28 -07:00
aadd6c8abf fix: Fixed an issue with the Managarr table that would incorrectly try to display things before is_loading was ready 2025-12-15 20:17:28 -07:00
316ed64315 test: Created snapshot tests for all Radarr UI modules 2025-12-15 20:17:09 -07:00
7084ca1be2 ci: Added more aliases and commands to the Justfile for snapshot tests 2025-12-15 20:16:10 -07:00
317daddb8e fix: Fixed a bug where the edit collection popup would not display when opening it from collection details 2025-12-15 20:15:52 -07:00
8ef291efd8 ci: Fixed some typos in the justfile 2025-12-15 13:22:00 -07:00
92be9c50bf docs: Updated contributing docs to mention how to use Just 2025-12-15 13:18:54 -07:00
f0e5ecd5de ci: Migrated to using Just instead of using Makefiles and included a helix config 2025-12-15 13:16:16 -07:00
e2c44583e8 style: Applied formatting to recently merged in files 2025-12-15 08:37:54 -07:00
5da741f3a9 ci: Updated Cargo.lock after main merge 2025-12-15 08:37:28 -07:00
35c5eb65cb Merge remote-tracking branch 'origin/main' into test-refactoring
# Conflicts:
#	Cargo.lock
#	src/cli/mod.rs
#	src/cli/radarr/add_command_handler.rs
#	src/cli/radarr/get_command_handler.rs
#	src/cli/radarr/list_command_handler.rs
2025-12-15 07:55:14 -07:00
7e53a26e5f Merge remote-tracking branch 'origin/main' into test-refactoring 2025-12-15 07:52:47 -07:00
github-actions[bot]
436b3f85d0 bump: version 0.6.2 → 0.6.3 [skip ci] 2025-12-13 20:23:15 +00:00
9c1a9cc3c5 fix: Wrapped all Sonarr use of Language with Option to fix the 'null' array issue in the new Sonarr API 2025-12-13 13:06:33 -07:00
82f30f126d test: Implemented UI snapshot tests 2025-12-12 15:44:11 -07:00
github-actions[bot]
9599ac28ca chore: bump Cargo.toml to 0.6.2 2025-12-12 17:02:43 +00:00
github-actions[bot]
e71a699ed8 bump: version 0.6.1 → 0.6.2 [skip ci] 2025-12-12 17:02:30 +00:00
ff4eb8ca98 ci: Specify commitizen version [skip ci] 2025-12-12 10:00:11 -07:00
b69973b9af ci: Fix the commitizen version issue [skip ci] 2025-12-12 09:54:15 -07:00
3e133fa147 refactor: Replaced all modulo usages of tick_until_poll with is_multiple_of 2025-12-12 09:20:05 -07:00
ae506789ab fix: Fixed breaking Sonarr Episode file API calls after recent Sonarr API update 2025-12-12 09:17:36 -07:00
c3fa689617 refactor: Use is_multiple_of for the tick counter in the UI module 2025-12-09 14:30:17 -07:00
b51e42b4b2 refactor: Updated all model tests to use purpose-built assertions to improve readability and maintainability 2025-12-09 14:29:35 -07:00
d4bea91186 refactor: Updated all handler tests to use purpose built assertions to improve readability and maintainability 2025-12-09 14:28:47 -07:00
d47dadeb88 refactor: Used is_multiple_of to make life easier and cleaner in the app module 2025-12-09 14:28:09 -07:00
b807904c6c refactor: Refactored all cli tests to use purpose-built assertions 2025-12-08 17:07:31 -07:00
ee1bee22eb refactor: Improved test assertions in the app module 2025-12-08 16:47:40 -07:00
f6c4c1623f style: Removed unused imports after massive handler testing refactor 2025-12-04 23:03:48 -07:00
49fd086b92 refactor: Created dedicated proptests and assertions to clean up the handler unit tests 2025-12-04 22:57:06 -07:00
35dce0bf01 refactor: Migrated the handle_table_events macro into a trait for better IDE support, created a TableEventAdapter wrapper for the KeyEventHandlers to make it so that the trait can be used properly and a simple function to replace the previous call to the handle_table_events macro 2025-12-04 16:03:58 -07:00
71240373c0 refactor: Simplified both the table_handler macro and the stateful_table implementation 2025-12-04 11:34:45 -07:00
659023d561 refactor: Improved error handling for the tail-logs subcommand to propagate errors up the stack instead of exiting there. 2025-12-04 10:10:19 -07:00
a0073b65ad refactor: Added accessor methods to servarr_data structs, replaced for loops with functional iterator chains, eliminated mutable state tracking, and updated network module to use get_or_insert_default() for modal options 2025-12-04 10:02:32 -07:00
cba53e0841 refactor: Improved error handling project-wide and cleaned up some regexes with unnecessary escapes (tail_logs and interpolate_env_vars) 2025-12-04 09:03:58 -07:00
e50fb88bfc refactor: Refactored to use more idiomatic let-else statements where applicable 2025-12-03 15:23:07 -07:00
ad58912baf test: Updated Rust edition to 2024 and refactored network module tests to be more idiomatic 2025-12-03 14:49:27 -07:00
c4e8d64710 ci: Fixed changelog generation for releases 2025-09-14 18:46:28 -06:00
ca4319001c docs: Updated the Homebrew tap description [skip-ci] 2025-09-12 11:25:20 -06:00
ebc58b831d ci: Modified the CI/CD pipeline to bump the procedural macros as well 2025-09-02 17:27:12 -06:00
github-actions[bot]
b1572c903c chore: bump Cargo.toml to 0.6.1 2025-09-02 22:58:28 +00:00
github-actions[bot]
5b73924e2a bump: version 0.6.0 → 0.6.1 [skip ci] 2025-09-02 22:58:25 +00:00
97ce258a8d fix: Fixed UI bugs introduced as part of the hotkey refactor 2025-09-02 15:49:36 -06:00
fedec79a95 refactor: Updated crate to publish properly with the procedural macros 2025-08-29 17:31:14 -06:00
8e74709b9c chore(release): Add metadata to enum_display_style_derive 2025-08-29 17:22:15 -06:00
github-actions[bot]
be09e1d932 bump: version 0.5.1 → 0.6.0 [skip ci] 2025-08-29 22:33:29 +00:00
Alex Clarke
d95417d075 Merge pull request #49 from Dark-Alex-17/develop
chore: Pre-release merge develop into main
2025-08-29 16:14:22 -06:00
2e3e511e3b ci: Improved the release workflow to be more compatible with Act local testing 2025-08-29 15:41:39 -06:00
8555052cc4 build: Modified the Dockerfile to be able to build the procedural macros as well 2025-08-29 15:09:22 -06:00
80bc6793c7 ci: Updated the release flow to also update the Cargo.toml version automatically 2025-08-29 13:45:14 -06:00
049a0c5d49 docs: Updated CONTRIBUTING.md to explain how to use act 2025-08-29 13:27:36 -06:00
ae9cb77e6d ci: Added step to release flow to update the Cargo.toml version 2025-08-29 11:13:32 -06:00
126ed5ed72 build: Updated dependencies 2025-08-29 10:59:29 -06:00
63ae64cebd ci: Modified CI/CD workflows so they can be tested locally with Act 2025-08-29 10:58:39 -06:00
0b29351366 feat: Support for custom headers to be added to every request to each server to support alternative authentication mechanisms [#47] 2025-08-29 10:23:17 -06:00
4ea39f74fe Updated the Rust version to 1.89 in the dockerfile to match the crate definition 2025-08-26 16:43:15 -06:00
76fcf5e67f docs: Fixed a typo in the README 2025-08-15 14:39:47 -06:00
11457736e6 refactor: Network module is now broken out into similar directory structures for each servarr to mimic the rest of the project to make it easier to develop and maintain 2025-08-14 13:14:23 -06:00
e2a6af1cbd refactor: Refactored the IndexerTestResut model into the general Servarr models 2025-08-12 17:05:17 -06:00
20ea15009d build: Updated dependencies and upgraded to Rust 1.89.0 2025-08-12 16:56:45 -06:00
00ab0f27f7 feat: Refactor all keybinding tips into a dynamically changing menu that can be invoked via '?' [#32] 2025-08-12 16:27:34 -06:00
1f4870d082 feat: Display total disk usage for series in the Library view to mirror Radarr functionality [#44] 2025-08-11 10:24:00 -06:00
e96af7410e feat: Pagination support for jumping 20 items at a time in all table views [#45] 2025-08-08 17:04:28 -06:00
345bb8ce03 refactor: Renamed 'ctrl-*' keyboard shortcuts to 'C-*' to simplify and shrink the on-screen help 2025-08-08 15:09:39 -06:00
dbcfc77ad4 feat: Support toggling Movie monitoring directly from the library view [#43] 2025-08-08 14:54:28 -06:00
e653532212 feat: Support toggling Movie monitoring from the CLI 2025-08-08 14:49:15 -06:00
8e7e31f64d feat: Support toggling Series monitoring directly from the Sonarr library view [#43] 2025-08-08 14:48:27 -06:00
bff3795cc6 feat: Support toggling Series monitoring from the CLI 2025-08-08 14:46:35 -06:00
8782f1353d docs: Formatted and cleaned up README a bit 2025-08-08 14:10:33 -06:00
f08f255a46 fix: Marked Radarr studios as nullable to prevent crashes 2025-08-07 20:05:05 -06:00
02870043ec style: Addressed updated linter complaints 2025-08-07 19:49:11 -06:00
154e491922 fix: Fixed a bug where the Sonarr API was returning empty values for seeders when searching for season releases 2025-08-07 16:00:10 -06:00
ef5e702255 feat: Fixed the Radarr downloads tab to display more than 10 downloads at a time and added a new --count flag to the CLI for specifying the number of downloads to return 2025-07-13 15:04:39 -06:00
cb4cd93bcd feat: Fetch more than 10 downloads when listing Sonarr downloads, and add a --count flag to the CLI to specify how many downloads to fetch 2025-07-13 14:48:15 -06:00
Alex Clarke
ee034c9caf Update README.md to remove the cheeky "try before you buy" heading since some users reported it as misleading 2025-05-29 23:48:16 -06:00
c133a4ecd2 fix: Improve fault tolerance for tag associations in Radarr and Sonarr 2025-05-18 13:49:52 -06:00
f09a2efa5e Updated README to have labels for the theme examples for better readability 2025-04-08 17:31:29 -06:00
49983f4173 fix: Upgraded to the most recent version of Tokio to mitigate CWE-664 2025-04-08 11:05:32 -06:00
0f9894e1be fix: Updated all dependencies and updated openssl to the most recent version to mitigate CWE-416 2025-04-07 15:22:56 -06:00
Alex Clarke
8c0dffec31 Merge pull request #41 from cwelsys/fix-key-event-handling
fix: Ensure key events are only processed on key press to avoid dupli…
2025-04-07 12:01:14 -06:00
Alex Clarke
0085d944ea Merge branch 'develop' into fix-key-event-handling 2025-04-07 11:58:06 -06:00
Connor Welsh
89cf0e66a6 fix: Ensure key events are only processed on key press to avoid duplicates 2025-04-06 17:29:06 -04:00
Alex Clarke
baef436f78 Merge pull request #39 from Dark-Alex-17/alternative-keymappings
Alternative keymappings
2025-04-01 10:11:22 -06:00
cf00d7992e fix: Updated the name of the should_ignore_quit_key to ignore_special_keys_for_textbox_input to give a better idea of what the flag is used for; also added alt keybinding for backspace 2025-03-27 15:21:44 -06:00
f25829f3c1 Merge remote-tracking branch 'refs/remotes/origin/develop' 2025-03-21 11:56:12 -06:00
8c783bc405 fix: Marked videoCodecs as Option to resolve #38 2025-03-21 11:55:54 -06:00
0048d71b74 feat: Support alternative keymappings for all keys, featuring hjkl movements 2025-03-17 22:02:15 -06:00
c633347ecc Merge remote-tracking branch 'refs/remotes/origin/develop' 2025-03-17 20:49:52 -06:00
Alex Clarke
ecd6a0ec32 Merge pull request #37 from Dark-Alex-17/custom-themes
Support Themes
2025-03-17 14:23:18 -06:00
30507d9d01 docs: Updated the README to include the new flags 2025-03-17 13:26:46 -06:00
6245a794d5 docs: Update all screenshots to not have any auto-generated usernames in the tags columns 2025-03-10 16:22:24 -06:00
5c822e4890 Merge branch 'develop' 2025-03-10 16:13:48 -06:00
cab06fe43f fix: Marked the Season.statistics field as Option so that a panic does not happen for outdated Sonarr data. This resolves #35 2025-03-10 16:13:04 -06:00
b4ff5f3351 feat: Added the Eldritch theme and updated documentation 2025-03-10 15:49:40 -06:00
0834802481 fix: When adding a film from the Collection Details modal, the render order was wrong: Radarr Library -> Collection Table -> Add Movie Prompt (missing the Collection details prompt too). Correct order is: Collection Table -> Collection Details Modal -> Add Movie Modal 2025-03-10 15:08:02 -06:00
3afd74dcbf fix: Fixed a bug that was rendering encompassing blocks after other widgets were rendered, thus overwriting the custom styles on each previously rendered widget 2025-03-10 15:01:58 -06:00
b1a0bdfbb6 Merge branch 'develop' 2025-03-07 12:02:47 -07:00
6d38bc5e1d Merge branch 'main' 2025-03-07 12:02:19 -07:00
5ba1ba15c9 ci: Update to the most recent Rust version 2025-03-07 11:55:32 -07:00
db05d2abfb Merge branch 'develop' into custom-themes 2025-03-07 10:37:48 -07:00
1840c4e39a Merge branch 'main' into develop
# Conflicts:
#	proc_macros/enum_display_style_derive/src/lib.rs
2025-03-07 10:37:23 -07:00
c5a3f424d6 refactor: Reformatted code to make the format checks pass 2025-03-07 10:36:40 -07:00
04aa6b81b5 Merge branch 'develop' into custom-themes 2025-03-07 10:35:07 -07:00
5ff3b9b996 Merge branch 'main' into develop 2025-03-07 10:34:16 -07:00
228e4a61a4 fix: Updated ring dependency to mitigate CWE-770 2025-03-07 10:33:57 -07:00
df38ea5413 feat: Write built in themes to the themes file on first run so users can define custom themes 2025-03-06 17:44:52 -07:00
709f6ca6ca test: Added integration tests for the ValidateTheme macro 2025-03-06 16:00:50 -07:00
b012fc29e4 Merge branch 'develop' into custom-themes
# Conflicts:
#	Cargo.toml
2025-03-06 15:35:05 -07:00
bdad723aef refactor: Formatted files using rustfmt 2025-03-06 15:32:59 -07:00
f97d46cec3 refactor: Created a derive macro for defining the display style of Enum models and removed the use of the EnumDisplayStyle trait 2025-03-06 15:29:30 -07:00
7381eaef57 refactor: Expanded the serde_enum_from macro to further reduce code duplication 2025-03-05 15:09:51 -07:00
72c922b311 feat: Created a theme validation macro to verify theme configurations before allowing the TUI to start 2025-03-05 14:37:34 -07:00
Alex Clarke
fd14a8152c fix: change the name of the theme configuration file to 'themes' 2025-03-04 18:29:21 -07:00
5cb60c317d feat: Initial support for custom user-defined themes 2025-03-04 18:09:09 -07:00
847de75713 fix: Modified the Sonarr DownloadRecord so that the episode_id is optional to prevent crashes for weird downloads 2025-03-01 14:50:20 -07:00
58723cf3e8 ci: Ensure the docker release is fully up-to-date 2025-02-28 21:45:05 -07:00
c613168bfb docs: Updated the CHANGELOG accordingly 2025-02-28 21:26:13 -07:00
github-actions[bot]
6f83de77f2 chore: Bump the version in Cargo.lock 2025-03-01 03:30:05 +00:00
github-actions[bot]
3f6ef3beb4 bump: version 0.5.0 → 0.5.1 [skip ci] 2025-03-01 03:30:04 +00:00
14e50c1465 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-02-28 20:29:29 -07:00
0aa9fdca14 ci: Overwrite previous artifact uploads for proper releases 2025-02-28 20:29:15 -07:00
689 changed files with 47614 additions and 22863 deletions
+4
View File
@@ -0,0 +1,4 @@
--artifact-server-path=./.act/artifacts
--cache-server-path=./.act/cache
--container-options --privileged
--env ACT=true
+2 -5
View File
@@ -7,8 +7,5 @@ echo "Running pre-push hook:"
echo "Executing: cargo fmt"
cargo fmt
echo "Executing: make lint"
make lint
echo "Executing: cargo test"
cargo test
echo "Executing: cargo clippy --all"
cargo clippy --all
+4 -4
View File
@@ -7,8 +7,8 @@ echo "Running pre-push hook:"
echo "Executing: cargo fmt --check"
cargo fmt --check
echo "Executing: make lint"
make lint
echo "Executing: cargo clippy --all"
cargo clippy --all
echo "Executing: cargo test"
cargo test
echo "Executing: cargo test --all"
cargo test --all
+4 -4
View File
@@ -76,15 +76,15 @@ jobs:
RUSTDOCFLAGS: --cfg docsrs
msrv:
# check that we can build using the minimal rust version that is specified by this crate
name: 1.82.0 / check
name: 1.89.0 / check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install 1.82.0
- name: Install 1.89.0
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.82.0
toolchain: 1.89.0
- name: cargo +1.82.0 check
- name: cargo +1.89.0 check
run: cargo check
+210 -21
View File
@@ -8,9 +8,9 @@ on:
workflow_dispatch:
inputs:
bump_type:
description: "Specify the type of version bump"
description: 'Specify the type of version bump'
required: true
default: "patch"
default: 'patch'
type: choice
options:
- patch
@@ -23,6 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Configure SSH for Git
if: env.ACT != 'true'
run: |
mkdir -p ~/.ssh
echo "${{ secrets.RELEASE_BOT_SSH_KEY }}" > ~/.ssh/id_ed25519
@@ -30,20 +31,27 @@ jobs:
ssh-keyscan -H github.com >> ~/.ssh/known_hosts
- name: Checkout repository
if: env.ACT != 'true'
uses: actions/checkout@v3
with:
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
fetch-depth: 0
- name: Checkout repository
if: env.ACT == 'true'
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: '3.10'
- name: Install Commitizen
run: |
python -m pip install --upgrade pip
pip install commitizen
python3 -m pip install --upgrade pip
pip3 install commitizen==4.8.3
npm install -g conventional-changelog-cli
- name: Configure Git user
@@ -62,12 +70,6 @@ jobs:
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Update the Cargo.lock
run: |
cargo update
git add Cargo.lock
git commit -m "chore: Bump the version in Cargo.lock"
- name: Get the new version tag
id: version
run: |
@@ -84,14 +86,133 @@ jobs:
echo "Previous tag: $PREV_TAG"
echo "prev_version=$PREV_TAG" >> $GITHUB_ENV
- name: Bump Cargo.toml version
shell: bash
working-directory: ${{ github.workspace }}
env:
VERSION: ${{ env.version }}
run: |
set -euo pipefail
: "${VERSION:?env.version is empty}"
# Ignore Act's local artifact dir noise
echo artifacts/ >> .git/info/exclude || true
# Edit the version line right after name="managarr"
sed -E -i '
/^[[:space:]]*name[[:space:]]*=[[:space:]]*"managarr"[[:space:]]*$/ {
n
s|^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"|version = "'"$VERSION"'"|
}
' Cargo.toml
cargo update || true
# Git config that helps in containers (Act)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
# Debug: show what changed
git status --porcelain
git diff --name-only -- Cargo.toml Cargo.lock || true
# Only commit if one of these files actually changed
if ! git diff --quiet -- Cargo.toml Cargo.lock; then
# Stage only modifications of already tracked files (won't pick up artifacts/)
git add -u -- Cargo.toml Cargo.lock
git commit -m "chore: bump Cargo.toml to $VERSION"
else
echo "No changes to commit (already at $VERSION)"
fi
- name: Bump validate_theme_derive/Cargo.toml version
shell: bash
working-directory: ${{ github.workspace }}/proc_macros/validate_theme_derive
env:
VERSION: ${{ env.version }}
run: |
set -euo pipefail
: "${VERSION:?env.version is empty}"
# Ignore Act's local artifact dir noise
echo artifacts/ >> .git/info/exclude || true
# Edit the version line right after name="managarr"
sed -E -i '
/^[[:space:]]*name[[:space:]]*=[[:space:]]*"managarr"[[:space:]]*$/ {
n
s|^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"|version = "'"$VERSION"'"|
}
' Cargo.toml
cargo update || true
# Git config that helps in containers (Act)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
# Debug: show what changed
git status --porcelain
git diff --name-only -- Cargo.toml || true
# Only commit if one of these files actually changed
if ! git diff --quiet -- Cargo.toml; then
# Stage only modifications of already tracked files (won't pick up artifacts/)
git add -u -- Cargo.toml
git commit -m "chore: bump validate_theme_derive Cargo.toml to $VERSION"
else
echo "No changes to commit (already at $VERSION)"
fi
- name: Bump enum_display_style_derive/Cargo.toml version
shell: bash
working-directory: ${{ github.workspace }}/proc_macros/enum_display_style_derive
env:
VERSION: ${{ env.version }}
run: |
set -euo pipefail
: "${VERSION:?env.version is empty}"
# Ignore Act's local artifact dir noise
echo artifacts/ >> .git/info/exclude || true
# Edit the version line right after name="managarr"
sed -E -i '
/^[[:space:]]*name[[:space:]]*=[[:space:]]*"managarr"[[:space:]]*$/ {
n
s|^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"|version = "'"$VERSION"'"|
}
' Cargo.toml
cargo update || true
# Git config that helps in containers (Act)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
# Debug: show what changed
git status --porcelain
git diff --name-only -- Cargo.toml || true
# Only commit if one of these files actually changed
if ! git diff --quiet -- Cargo.toml; then
# Stage only modifications of already tracked files (won't pick up artifacts/)
git add -u -- Cargo.toml
git commit -m "chore: bump enum_display_style_derive Cargo.toml to $VERSION"
else
echo "No changes to commit (already at $VERSION)"
fi
- name: Generate changelog for the version bump
id: changelog
run: |
changelog=$(conventional-changelog -p angular -i CHANGELOG.md -s --from ${{ env.prev_version }} --to ${{ env.version }})
echo "$changelog" > artifacts/changelog.md
echo "changelog_body=$(cat artifacts/changelog.md)" >> $GITHUB_ENV
conventional-changelog -p angular -i CHANGELOG.md --from ${{ env.prev_version }} --to v${{ env.version }} > artifacts/changelog.md
- name: Push changes
if: env.ACT != 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
@@ -102,6 +223,17 @@ jobs:
with:
path: artifacts
- name: Upload the changed Cargo files (Act)
if: env.ACT == 'true'
uses: actions/upload-artifact@v4
with:
name: bumped-cargo-files
path: |
Cargo.toml
Cargo.lock
proc_macros/validate_theme_derive/Cargo.toml
proc_macros/enum_display_style_derive/Cargo.toml
build-release-artifacts:
name: build-release
needs: [bump-version]
@@ -129,7 +261,7 @@ jobs:
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }}
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
@@ -140,10 +272,18 @@ jobs:
fetch-depth: 1
- name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: |
git fetch --all
git pull
- name: Get bumped Cargo files (Act)
if: env.ACT == 'true'
uses: actions/download-artifact@v4
with:
name: bumped-cargo-files
path: ${{ github.workspace }}
- uses: actions/cache@v3
name: Cache Cargo registry
with:
@@ -246,6 +386,12 @@ jobs:
needs: [build-release-artifacts]
runs-on: ubuntu-latest
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout repository
uses: actions/checkout@v3
with:
@@ -258,6 +404,7 @@ jobs:
merge-multiple: true
- name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: |
git fetch --all
git pull
@@ -266,10 +413,14 @@ jobs:
run: |
release_version="$(cat ./artifacts/release-version)"
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
changelog_body="$(cat ./artifacts/changelog.md)"
echo "changelog_body=$(cat artifacts/changelog.md)" >> $GITHUB_ENV
- name: Validate release environment variables
run: |
echo "Release version: ${{ env.RELEASE_VERSION }}"
echo "Changelog body: $(cat artifacts/changelog.md)"
- name: Create a GitHub Release
if: env.ACT != 'true'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -300,8 +451,8 @@ jobs:
artifacts/managarr-armv7-musl.tar.gz
artifacts/managarr-armv7-musl.sha256
tag_name: v${{ env.RELEASE_VERSION }}
name: "v${{ env.RELEASE_VERSION }}"
body: ${{ env.changelog_body }}
name: 'v${{ env.RELEASE_VERSION }}'
body_path: artifacts/changelog.md
draft: false
prerelease: false
@@ -309,12 +460,19 @@ jobs:
uses: actions/upload-artifact@v4
with:
path: artifacts
overwrite: true
publish-chocolatey-package:
needs: [publish-github-release]
name: Publish Chocolatey Package
runs-on: windows-latest
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -343,6 +501,7 @@ jobs:
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Package and Publish package to Chocolatey
if: env.ACT != 'true'
run: |
mkdir ./deployment/chocolatey/tools
# Run packaging script
@@ -362,6 +521,12 @@ jobs:
name: Update Homebrew formulas
runs-on: ubuntu-latest
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -394,11 +559,13 @@ jobs:
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Execute Homebrew packaging script
if: env.ACT != 'true'
run: |
# run packaging script
python "./deployment/homebrew/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/homebrew/managarr.rb.template" "./managarr.rb" ${{ env.MACOS_SHA }} ${{ env.MACOS_SHA_ARM }} ${{ env.LINUX_SHA }}
- name: Push changes to Homebrew tap
if: env.ACT != 'true'
env:
TOKEN: ${{ secrets.MANAGARR_GITHUB_TOKEN }}
run: |
@@ -418,6 +585,12 @@ jobs:
name: Publishing Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -429,6 +602,12 @@ jobs:
path: artifacts
merge-multiple: true
- name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: |
git fetch --all
git pull
- name: Set version variable
run: |
version="$(cat artifacts/release-version)"
@@ -445,6 +624,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: env.ACT != 'true'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
@@ -456,7 +636,7 @@ jobs:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
push: ${{ env.ACT != 'true' }}
tags: darkalex17/managarr:latest, darkalex17/managarr:${{ env.version }}
publish-crate:
@@ -465,7 +645,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }}
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
@@ -475,7 +655,15 @@ jobs:
with:
fetch-depth: 0
- name: Get bumped Cargo files (Act)
if: env.ACT == 'true'
uses: actions/download-artifact@v4
with:
name: bumped-cargo-files
path: ${{ github.workspace }}
- name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: |
git fetch --all
git pull
@@ -495,5 +683,6 @@ jobs:
uses: dtolnay/rust-toolchain@stable
- uses: katyo/publish-crates@v2
if: env.ACT != 'true'
with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+1
View File
@@ -175,6 +175,7 @@ jobs:
uses: Swatinem/rust-cache@v2
- name: Upload to codecov.io
if: env.ACT != 'true'
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
+1
View File
@@ -1,3 +1,4 @@
/target
/.idea/
/.scannerwork/
/.act/
+5
View File
@@ -0,0 +1,5 @@
[keys.normal.backspace]
b = ":sh zellij run -x '4%%' --width '92%%' -f -n Build -- just build"
r = ":sh zellij run -x '3%%' -y '8%%' --width '95%%' --height '90%%' -fc -n 'Run' -- just run"
t = ":sh zellij run -x '4%%' --width '92%%' -f -n Tests -- just test"
l = ":sh zellij run -x '4%%' --width '92%%' -f -n Lint -- just lint"
+74 -1
View File
@@ -5,7 +5,80 @@ 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.5.0 (2025-03-01)
## v0.6.3 (2025-12-13)
### Fix
- Wrapped all Sonarr use of Language with Option to fix the 'null' array issue in the new Sonarr API
## v0.6.2 (2025-12-12)
### Fix
- Fixed breaking Sonarr Episode file API calls after recent Sonarr API update
### Refactor
- Replaced all modulo usages of tick_until_poll with is_multiple_of
## v0.6.1 (2025-09-02)
### Fix
- Fixed UI bugs introduced as part of the hotkey refactor
### Refactor
- Updated crate to publish properly with the procedural macros
## v0.6.0 (2025-08-29)
### Feat
- Support for custom headers to be added to every request to each server to support alternative authentication mechanisms [#47]
- Refactor all keybinding tips into a dynamically changing menu that can be invoked via '?' [#32]
- Display total disk usage for series in the Library view to mirror Radarr functionality [#44]
- Pagination support for jumping 20 items at a time in all table views [#45]
- Support toggling Movie monitoring directly from the library view [#43]
- Support toggling Movie monitoring from the CLI
- Support toggling Series monitoring directly from the Sonarr library view [#43]
- Support toggling Series monitoring from the CLI
- Fixed the Radarr downloads tab to display more than 10 downloads at a time and added a new --count flag to the CLI for specifying the number of downloads to return
- Fetch more than 10 downloads when listing Sonarr downloads, and add a --count flag to the CLI to specify how many downloads to fetch
- Support alternative keymappings for all keys, featuring hjkl movements
- Added the Eldritch theme and updated documentation
- Write built in themes to the themes file on first run so users can define custom themes
- Created a theme validation macro to verify theme configurations before allowing the TUI to start
- Initial support for custom user-defined themes
### Fix
- Marked Radarr studios as nullable to prevent crashes
- Fixed a bug where the Sonarr API was returning empty values for seeders when searching for season releases
- Improve fault tolerance for tag associations in Radarr and Sonarr
- Upgraded to the most recent version of Tokio to mitigate CWE-664
- Updated all dependencies and updated openssl to the most recent version to mitigate CWE-416
- Updated the name of the should_ignore_quit_key to ignore_special_keys_for_textbox_input to give a better idea of what the flag is used for; also added alt keybinding for backspace
- Marked videoCodecs as Option to resolve #38
- Marked the `Season.statistics` field as `Option` so that a panic does not happen for outdated Sonarr data. This resolves #35
- When adding a film from the Collection Details modal, the render order was wrong: Radarr Library -> Collection Table -> Add Movie Prompt (missing the Collection details prompt too). Correct order is: Collection Table -> Collection Details Modal -> Add Movie Modal
- Fixed a bug that was rendering encompassing blocks after other widgets were rendered, thus overwriting the custom styles on each previously rendered widget
- change the name of the theme configuration file to 'themes'
- Ensure key events are only processed on key press to avoid duplicates
- Updated ring dependency to mitigate CWE-770
- Modified the Sonarr DownloadRecord so that the episode_id is optional to prevent crashes for weird downloads
### Refactor
- Network module is now broken out into similar directory structures for each servarr to mimic the rest of the project to make it easier to develop and maintain
- Refactored the IndexerTestResut model into the general Servarr models
- Renamed 'ctrl-*' keyboard shortcuts to 'C-*' to simplify and shrink the on-screen help
- Formatted files using rustfmt
- Reformatted code to make the format checks pass
- Created a derive macro for defining the display style of Enum models and removed the use of the EnumDisplayStyle trait
- Expanded the serde_enum_from macro to further reduce code duplication
## v0.5.1 (2025-03-01)
### Feat
+23 -2
View File
@@ -63,12 +63,33 @@ cz commit
## Setup workspace
1. Clone this repo
2. Run `cargo test` to setup hooks
2. Run `cargo test` to set up hooks
3. Make changes
4. Run the application using `make run` or `cargo run`
4. Run the application using `just run` or `just run`
- Install `just` (`cargo install just`) if you haven't already to use the [justfile](./justfile) in this project.
5. Commit changes. This will trigger pre-commit hooks that will run format, test and lint. If there are errors or warnings from Clippy, please fix them.
6. Push your code to a new branch named after the feature/bug/etc. you're adding. This will trigger pre-push hooks that will run lint and test.
7. Create a PR
### CI/CD Testing with Act
If you also are planning on testing out your changes before pushing them with [Act](https://github.com/nektos/act), you will need to set up `act`,
`docker`, and configure your local system to run different architectures:
1. Install `docker` by following the instructions on the [official Docker installation page](https://docs.docker.com/get-docker/).
2. Install `act` by following the instructions on the [official Act installation page](https://nektosact.com/installation/index.html).
3. Install `binfmt` on your system once so that `act` can run the correct architecture for the CI/CD workflows.
You can do this by running:
```shell
sudo docker run --rm --privileged tonistiigi/binfmt --install all
```
Then, you can run workflows locally without having to commit and see if the GitHub action passes or fails.
**For example**: To test the [release.yml](.github/workflows/release.yml) workflow locally, you can run:
```shell
act -W .github/workflows/release.yml --input_type bump=minor
```
## Questions? Reach out to me!
If you encounter any questions while developing Managarr, please don't hesitate to reach out to me at alex.j.tusa@gmail.com. I'm happy to help contributors, new and experienced in any way I can!
Generated
+1193 -577
View File
File diff suppressed because it is too large Load Diff
+28 -10
View File
@@ -1,6 +1,6 @@
[package]
name = "managarr"
version = "0.5.0"
version = "0.6.3"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI and CLI to manage your Servarrs"
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
@@ -8,11 +8,17 @@ documentation = "https://github.com/Dark-Alex-17/managarr"
repository = "https://github.com/Dark-Alex-17/managarr"
homepage = "https://github.com/Dark-Alex-17/managarr"
readme = "README.md"
edition = "2021"
edition = "2024"
license = "MIT"
rust-version = "1.82.0"
rust-version = "1.89.0"
exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
[workspace]
members = [
"proc_macros/enum_display_style_derive",
"proc_macros/validate_theme_derive",
]
[dependencies]
anyhow = "1.0.68"
backtrace = "0.3.74"
@@ -34,35 +40,45 @@ serde_json = "1.0.91"
serde = { version = "1.0.214", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
strum_macros = "0.26.4"
tokio = { version = "1.36.0", features = ["full"] }
tokio = { version = "1.44.2", features = ["full"] }
tokio-util = "0.7.8"
ratatui = { version = "0.29.0", features = [
"all-widgets",
"unstable-widget-ref",
] }
urlencoding = "2.1.2"
clap = { version = "4.5.20", features = ["derive", "cargo", "env", "wrap_help"] }
clap = { version = "4.5.20", features = [
"derive",
"cargo",
"env",
"wrap_help",
] }
clap_complete = "4.5.33"
itertools = "0.13.0"
itertools = "0.14.0"
ctrlc = "3.4.5"
colored = "2.1.0"
colored = "3.0.0"
async-trait = "0.1.83"
dirs-next = "2.0.0"
managarr-tree-widget = "0.24.0"
indicatif = "0.17.9"
derive_setters = "0.1.6"
deunicode = "1.6.0"
paste = "1.0.15"
openssl = { version = "0.10.70", features = ["vendored"] }
veil = "0.2.0"
validate_theme_derive = "0.1.0"
enum_display_style_derive = "0.1.0"
tachyonfx = { version = "0.21.0", features = ["sendable"] }
[dev-dependencies]
assert_cmd = "2.0.16"
mockall = "0.13.0"
mockito = "1.0.0"
pretty_assertions = "1.3.0"
rstest = "0.23.0"
proptest = "1.6.0"
rstest = "0.25.0"
serial_test = "3.2.0"
assertables = "9.8.2"
insta = "1.41.1"
[dev-dependencies.cargo-husky]
version = "1"
@@ -76,4 +92,6 @@ name = "managarr"
[profile.release]
lto = true
codegen-units = 1
codegen-units = 3
opt-level = "s"
strip = true
+2 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.85 AS builder
FROM rust:1.89 AS builder
WORKDIR /usr/src
# Download and compile Rust dependencies in an empty project and cache as a separate Docker layer
@@ -6,6 +6,7 @@ RUN USER=root cargo new --bin managarr-temp
WORKDIR /usr/src/managarr-temp
COPY Cargo.* .
COPY proc_macros ./proc_macros
RUN cargo build --release
# remove src from empty project
RUN rm -r src
-47
View File
@@ -1,47 +0,0 @@
#!make
VERSION := latest
IMG_NAME := darkalex17/managarr
IMAGE := ${IMG_NAME}:${VERSION}
default: run
.PHONY: test test-cov build run lint lint-fix fmt analyze sonar release delete-tag
test:
@cargo test
## Run all tests with coverage - `cargo install cargo-tarpaulin`
test-cov:
@cargo tarpaulin
build: test
@cargo build --release
docker:
@DOCKER_BUILDKIT=1 docker build --rm -t ${IMAGE} .
run:
@CARGO_INCREMENTAL=1 cargo fmt && make lint && cargo run
lint:
@find . | grep '\.\/src\/.*\.rs$$' | xargs touch && CARGO_INCREMENTAL=0 cargo clippy --all-targets --workspace
lint-fix:
@cargo fix
fmt:
@cargo fmt
minimal-versions:
@cargo +nightly update -Zdirect-minimal-versions
## Analyze for unsafe usage - `cargo install cargo-geiger`
analyze:
@cargo geiger
release:
@git tag -a ${V} -m "Release ${V}" && git push origin ${V}
delete-tag:
@git tag -d ${V} && git push --delete origin ${V}
+40 -20
View File
@@ -28,7 +28,7 @@ Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built
- [ ] ![bazarr_logo](logos/bazarr.png) [Bazarr](https://www.bazarr.media/)
- [ ] ![tautulli_logo](logos/tautulli.png) [Tautulli](https://tautulli.com/)
## Try Before You Buy
## Try Out the Demo
To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository.
Simply run the following command to start a demo:
@@ -63,7 +63,7 @@ Please note that you will need to create and popular your configuration file fir
**Note:** If you run into errors using relative file paths when mounting the volume with the configuration file, try using an absolute path.
### Homebrew (Mac and Linux)
To install Managarr from Homebrew, install the Managarr tap and then you'll be able to install Managarr:
To install Managarr from Homebrew, install the Managarr tap. Then you'll be able to install Managarr:
```shell
brew tap Dark-Alex-17/managarr
@@ -119,16 +119,16 @@ Binaries are available on the [releases](https://github.com/Dark-Alex-17/managar
#### Windows Instructions
To use a binary from the releases page on Windows, do the following:
1. Download the latest binary [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS.
1. Download the latest [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS.
2. Use 7-Zip or TarTool to unpack the Tar file.
3. Run the executable `managarr.exe`!
#### Linux/MacOS Instructions
To use a binary from the releases page on Linux/MacOS, do the following:
1. Download the latest binary [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS.
1. Download the latest [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS.
2. `cd` to the directory where you downloaded the binary.
3. Extract the binary with `tar -C /usr/local/bin -xzf managarr-<arch>.tar.gz` (NB: This may require `sudo`)
3. Extract the binary with `tar -C /usr/local/bin -xzf managarr-<arch>.tar.gz` (Note: This may require `sudo`)
4. Now you can run `managarr`!
## Features
@@ -166,21 +166,21 @@ Key:
| TUI | CLI | Feature |
|-----|-----|--------------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, blocklist, episodes |
| ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
| ✅ | ✅ | View your library, downloads, blocklist, episodes |
| ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library |
| ✅ | ✅ | Add series to your library |
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
| ✅ | ✅ | Manually search for series, seasons, or episodes |
| ✅ | ✅ | Edit your series and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
| ✅ | ✅ | Search your library |
| ✅ | ✅ | Add series to your library |
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
| ✅ | ✅ | Manually search for series, seasons, or episodes |
| ✅ | ✅ | Edit your series and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
### Readarr
@@ -206,6 +206,19 @@ Key:
- [ ] Support for Tautulli
### Themes
Managarr ships with a few themes out of the box. Here's a few examples:
#### Default
![default](themes/default/manual_episode_search.png)
#### Dracula
![dracula](themes/dracula/manual_episode_search.png)
#### Watermelon Dark
![watermelon-dark](themes/watermelon-dark/manual_episode_search.png)
You can also create your own custom themes as well. To learn more about what themes are built-in to Managarr and how
to create your own custom themes, check out the [Themes README](themes/README.md).
### The Managarr CLI
Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs.
@@ -218,7 +231,7 @@ To see all available commands, simply run `managarr --help`:
```shell
$ managarr --help
managarr 0.5.0
managarr 0.5.1
Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs
@@ -235,6 +248,8 @@ Commands:
Options:
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
--config-file <CONFIG_FILE> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
--themes-file <THEMES_FILE> The Managarr themes file to use [env: MANAGARR_THEMES_FILE=]
--theme <THEME> The name of the Managarr theme to use [env: MANAGARR_THEME=]
--servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
This is useful when you have multiple instances of the same Servarr defined in your config file.
By default, if left empty, the first configured Servarr instance listed in the config file will be used.
@@ -315,6 +330,7 @@ managarr --config-file /path/to/config.yml
### Example Configuration:
```yaml
theme: default
radarr:
- host: 192.168.0.78
port: 7878
@@ -341,6 +357,9 @@ whisparr:
port: 6969
api_token: someApiToken1234567890
ssl_cert_path: /path/to/whisparr.crt
custom_headers: # Example of adding custom headers to all requests to the Servarr instance
traefik-auth-bypass-key: someBypassKey1234567890
SOME-OTHER-CUSTOM-HEADER: ${MY_CUSTOM_HEADER_VALUE}
bazarr:
- host: 192.168.0.67
port: 6767
@@ -357,6 +376,7 @@ tautulli:
### Example Multi-Instance Configuration:
```yaml
theme: default
radarr:
- host: 192.168.0.78 # No name specified, so this instance's name will default to 'Radarr 1'
port: 7878
-1
View File
@@ -12,4 +12,3 @@ coverage:
ignore:
- "**/*_tests.rs"
- "src/ui"
+1 -1
View File
@@ -1,7 +1,7 @@
# Documentation: https://docs.brew.sh/Formula-Cookbook
# https://rubydoc.brew.sh/Formula
class Managarr < Formula
desc "A fast and simple dashboard for Kubernetes written in Rust"
desc "Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC)"
homepage "https://github.com/Dark-Alex-17/managarr"
if OS.mac? and Hardware::CPU.arm?
url "https://github.com/Dark-Alex-17/managarr/releases/download/v$version/managarr-macos-arm64.tar.gz"
+91
View File
@@ -0,0 +1,91 @@
VERSION := "latest"
IMG_NAME := "darkalex17/managarr"
IMAGE := "{{IMG_NAME}}:{{VERSION}}"
# List all recipes
default:
@just --list
# Format all files
[group: 'style']
fmt:
@cargo fmt --all
alias clippy := lint
# Run Clippy to inspect all files
[group: 'style']
lint:
@cargo clippy --all
alias clippy-fix := lint-fix
# Automatically fix clippy issues where possible
[group: 'style']
lint-fix:
@cargo fix
# Analyze the project for unsafe usage
[group: 'style']
@analyze:
#!/usr/bin/env bash
cargo geiger -h > /dev/null 2>&1 | cargo install cargo-geiger
cargo geiger
# Run all tests
[group: 'test']
test:
@cargo test --all
# Run all tests with coverage
[group:'test']
@test-cov:
#!/usr/bin/env bash
cargo tarpaulin -h > /dev/null 2>&1 || cargo install cargo-tarpaulin
cargo tarpaulin
# Run all doc tests
[group: 'test']
doctest:
@cargo test --all --doc
# Run all proptests
[group: 'test']
proptest:
@cargo test proptest
alias test-snapshots := snapshot-tests
# Run all snapshot tests
[group: 'test']
snapshot-tests:
@cargo test snapshot
alias review := snapshot-review
# Review snapshot test changes
[group: 'test']
@snapshot-review:
#!/usr/bin/env bash
cargo insta -h > /dev/null 2>&1 || cargo install cargo-insta
cargo insta review
alias clean-orphaned-snapshots := snapshot-delete-unreferenced
# Delete any unreferenced snapshots
[group: 'test']
@snapshot-delete-unreferenced:
#!/usr/bin/env bash
cargo insta -h > /dev/null 2>&1 || cargo install cargo-insta
cargo insta test --unreferenced=delete
# Build and run the binary for the current system
run:
@cargo run
# Build the project for the current system architecture
[group: 'build']
[arg('build_type', pattern="debug|release")]
build build_type='debug':
@cargo build {{ if build_type == "release" { "--release" } else { "" } }}
# Build the docker image
[group: 'build']
build-docker:
@DOCKER_BUILDKIT=1 docker build --rm -t {{IMAGE}}
@@ -0,0 +1,18 @@
[package]
name = "enum_display_style_derive"
version = "0.6.1"
edition = "2024"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A proc-macro to derive a `Display` and `FromStr` implementation for enums with a `style` attribute."
license = "MIT"
documentation = "https://github.com/Dark-Alex-17/managarr"
repository = "https://github.com/Dark-Alex-17/managarr"
homepage = "https://github.com/Dark-Alex-17/managarr"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.39"
syn = "2.0.99"
darling = "0.20.10"
@@ -0,0 +1,76 @@
mod macro_models;
use crate::macro_models::DisplayStyleArgs;
use darling::FromVariant;
use quote::quote;
use syn::{Data, DeriveInput, parse_macro_input};
/// Derive macro for generating a `to_display_str` method for an enum.
///
/// # Example
///
/// Using default values for the display style:
///
/// ```
/// use enum_display_style_derive::EnumDisplayStyle;
///
/// #[derive(EnumDisplayStyle)]
/// enum Weekend {
/// Saturday,
/// Sunday,
/// }
///
/// assert_eq!(Weekend::Saturday.to_display_str(), "Saturday");
/// assert_eq!(Weekend::Sunday.to_display_str(), "Sunday");
/// ```
///
/// Using custom values for the display style:
///
/// ```
/// use enum_display_style_derive::EnumDisplayStyle;
///
/// #[derive(EnumDisplayStyle)]
/// enum MonitorStatus {
/// #[display_style(name = "Monitor Transactions")]
/// Active,
/// #[display_style(name = "Don't Monitor Transactions")]
/// None,
/// }
///
/// assert_eq!(MonitorStatus::Active.to_display_str(), "Monitor Transactions");
/// assert_eq!(MonitorStatus::None.to_display_str(), "Don't Monitor Transactions");
/// ```
#[proc_macro_derive(EnumDisplayStyle, attributes(display_style))]
pub fn enum_display_style_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let enum_name = &input.ident;
let mut match_arms = Vec::new();
if let Data::Enum(data_enum) = &input.data {
let variants = &data_enum.variants;
for variant in variants {
let variant_ident = &variant.ident;
let variant_display_name = DisplayStyleArgs::from_variant(variant)
.unwrap()
.name
.unwrap_or_else(|| variant_ident.to_string());
match_arms.push(quote! {
#enum_name::#variant_ident => #variant_display_name,
});
}
}
quote! {
impl<'a> #enum_name {
pub fn to_display_str(self) -> &'a str {
match self {
#(#match_arms)*
}
}
}
}
.into()
}
@@ -0,0 +1,7 @@
use darling::FromVariant;
#[derive(Debug, FromVariant)]
#[darling(attributes(display_style))]
pub struct DisplayStyleArgs {
pub name: Option<String>,
}
@@ -0,0 +1,20 @@
[package]
name = "validate_theme_derive"
version = "0.6.1"
edition = "2024"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A proc-macro to validate a theme."
license = "MIT"
documentation = "https://github.com/Dark-Alex-17/managarr"
repository = "https://github.com/Dark-Alex-17/managarr"
homepage = "https://github.com/Dark-Alex-17/managarr"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.39"
syn = "2.0.99"
[dev-dependencies]
log = "0.4.17"
@@ -0,0 +1,106 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, parse_macro_input};
/// Derive macro for generating a `validate` method for a Theme struct.
/// The `validate` method ensures that all values with the `validate` attribute are not `None`.
/// Otherwise, an error message it output to both the log file and stdout and the program exits.
///
/// # Example
///
/// Valid themes pass through the program transitively without any messages being output.
///
/// ```
/// use validate_theme_derive::ValidateTheme;
///
/// #[derive(ValidateTheme, Default)]
/// struct Theme {
/// pub name: String,
/// #[validate]
/// pub good: Option<Style>,
/// #[validate]
/// pub bad: Option<Style>,
/// pub ugly: Option<Style>,
/// }
///
/// struct Style {
/// color: String,
/// }
///
/// let theme = Theme {
/// good: Some(Style { color: "Green".to_owned() }),
/// bad: Some(Style { color: "Red".to_owned() }),
/// ..Theme::default()
/// };
///
/// // Since only `good` and `bad` have the `validate` attribute, the `validate` method will only check those fields.
/// theme.validate();
/// // Since both `good` and `bad` have values, the program will not exit and no message is output.
/// ```
///
/// Invalid themes will output an error message to both the log file and stdout and the program will exit.
///
/// ```should_panic
/// use validate_theme_derive::ValidateTheme;
///
/// #[derive(ValidateTheme, Default)]
/// struct Theme {
/// pub name: String,
/// #[validate]
/// pub good: Option<Style>,
/// #[validate]
/// pub bad: Option<Style>,
/// pub ugly: Option<Style>,
/// }
///
/// struct Style {
/// color: String,
/// }
///
/// let theme = Theme {
/// bad: Some(Style { color: "Red".to_owned() }),
/// ..Theme::default()
/// };
///
/// // Since `good` has the `validate` attribute and since `good` is `None`, the `validate` method will output an error message and exit the program.
/// theme.validate();
/// ```
#[proc_macro_derive(ValidateTheme, attributes(validate))]
pub fn derive_validate_theme(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let mut validation_checks = Vec::new();
if let Data::Struct(data_struct) = &input.data
&& let Fields::Named(fields) = &data_struct.fields
{
for field in &fields.named {
let field_name = &field.ident;
let has_validate_attr = field
.attrs
.iter()
.any(|attr| attr.path().is_ident("validate"));
if has_validate_attr {
validation_checks.push(quote! {
if self.#field_name.is_none() {
log::error!("{} is missing a color value.", stringify!(#field_name));
eprintln!("{} is missing a color value.", stringify!(#field_name));
std::process::exit(1);
}
})
}
}
}
quote! {
impl #struct_name {
pub fn validate(&self) {
#(#validation_checks)*
}
}
}
.into()
}
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 56330c025ad79db641d0eb9f429ab74e95822e1fb015b58f0e158ea674cd42a1 # shrinks to list_size = 1, page_ops = 1
@@ -0,0 +1,9 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc fb4b58aa3015a125fc33a78dfaf27981db4191247151b327a351fc445e07c231 # shrinks to input = "j"
cc d6ec17d4d3f635f0a095ade650a316d26abc1f9fe2b6d9cf67bf2f8b4ebedb60 # shrinks to backspace_count = 0
cc cd46ee46e18cf86c940fb89c7206f0b482909880b8f2eabe3dd20682b9912c8a # shrinks to input = "h"
@@ -0,0 +1,9 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 24ae243412a324cb46c36cb4f629ddd4c9326b1479d1186d9b5545ac5e86dbba # shrinks to num_scroll_attempts = 0
cc c06a1cc1e4740b2498c50d7be64715bf09ef3ac4cf3bb3642f960578a3e06c74 # shrinks to is_loading = false, num_items = 1
cc 930207899afea2d389c7fa3974e31c2eb1803e71bcbd8179246c795903905ec7 # shrinks to parent_width = 20, parent_height = 12, percent_x = 1, percent_y = 1
Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 220 KiB

+155 -46
View File
@@ -2,16 +2,17 @@
mod tests {
use anyhow::anyhow;
use pretty_assertions::{assert_eq, assert_str_eq};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde_json::Value;
use serial_test::serial;
use tokio::sync::mpsc;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
use crate::app::{interpolate_env_vars, App, AppConfig, Data, ServarrConfig};
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::{HorizontallyScrollableText, TabRoute};
use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkEvent;
use crate::network::radarr_network::RadarrEvent;
use tokio_util::sync::CancellationToken;
#[test]
@@ -31,6 +32,7 @@ mod tests {
};
let sonarr_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()]),
};
@@ -38,40 +40,24 @@ mod tests {
TabRoute {
title: "Sonarr Test".to_owned(),
route: ActiveSonarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(sonarr_config_1),
},
TabRoute {
title: "Radarr 1".to_owned(),
route: ActiveRadarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(radarr_config_2),
},
TabRoute {
title: "Radarr Test".to_owned(),
route: ActiveRadarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(radarr_config_1),
},
TabRoute {
title: "Sonarr 1".to_owned(),
route: ActiveSonarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(sonarr_config_2),
},
@@ -83,9 +69,9 @@ mod tests {
CancellationToken::new(),
);
assert!(app.navigation_stack.is_empty());
assert_is_empty!(app.navigation_stack);
assert_eq!(app.get_current_route(), ActiveSonarrBlock::default().into());
assert!(app.network_tx.is_some());
assert_some!(app.network_tx);
assert!(!app.cancellation_token.is_cancelled());
assert!(app.is_first_render);
assert_eq!(app.error, HorizontallyScrollableText::default());
@@ -94,10 +80,11 @@ mod tests {
assert_eq!(app.tick_until_poll, 400);
assert_eq!(app.ticks_until_scroll, 4);
assert_eq!(app.tick_count, 0);
assert_eq!(app.ui_scroll_tick_count, 0);
assert!(!app.is_loading);
assert!(!app.is_routing);
assert!(!app.should_refresh);
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app.cli_mode);
}
@@ -105,8 +92,8 @@ mod tests {
fn test_app_default() {
let app = App::default();
assert!(app.navigation_stack.is_empty());
assert!(app.network_tx.is_none());
assert_is_empty!(app.navigation_stack);
assert_none!(app.network_tx);
assert!(!app.cancellation_token.is_cancelled());
assert!(app.is_first_render);
assert_eq!(app.error, HorizontallyScrollableText::default());
@@ -117,7 +104,7 @@ mod tests {
assert!(!app.is_loading);
assert!(!app.is_routing);
assert!(!app.should_refresh);
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app.cli_mode);
}
@@ -254,6 +241,27 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[test]
fn test_on_ui_scroll_tick() {
let mut app = App {
ticks_until_scroll: 1,
..App::default()
};
assert_eq!(app.ui_scroll_tick_count, 0);
assert_eq!(app.tick_count, 0);
app.on_ui_scroll_tick();
assert_eq!(app.ui_scroll_tick_count, 1);
assert_eq!(app.tick_count, 0);
app.on_ui_scroll_tick();
assert_eq!(app.ui_scroll_tick_count, 0);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_on_tick_first_render() {
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
@@ -283,7 +291,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -340,22 +348,59 @@ mod tests {
fn test_app_config_default() {
let app_config = AppConfig::default();
assert!(app_config.radarr.is_none());
assert!(app_config.sonarr.is_none());
assert_none!(app_config.radarr);
assert_none!(app_config.sonarr);
}
#[test]
fn test_servarr_config_default() {
let servarr_config = ServarrConfig::default();
assert_eq!(servarr_config.name, None);
assert_eq!(servarr_config.host, Some("localhost".to_string()));
assert_eq!(servarr_config.port, None);
assert_eq!(servarr_config.uri, None);
assert_eq!(servarr_config.weight, None);
assert_eq!(servarr_config.api_token, Some(String::new()));
assert_eq!(servarr_config.api_token_file, None);
assert_eq!(servarr_config.ssl_cert_path, None);
assert_none!(servarr_config.name);
assert_some_eq_x!(&servarr_config.host, "localhost");
assert_none!(servarr_config.port);
assert_none!(servarr_config.uri);
assert_none!(servarr_config.weight);
assert_some_eq_x!(&servarr_config.api_token, "");
assert_none!(servarr_config.api_token_file);
assert_none!(servarr_config.ssl_cert_path);
assert_none!(servarr_config.custom_headers);
}
#[test]
fn serialize_header_map_basic() {
let mut header_map = HeaderMap::new();
header_map.insert(
HeaderName::from_static("x-api-key"),
HeaderValue::from_static("abc123"),
);
header_map.insert(
HeaderName::from_static("header-1"),
HeaderValue::from_static("test"),
);
let config = ServarrConfig {
custom_headers: Some(header_map),
..ServarrConfig::default()
};
let v: Value = serde_json::to_value(&config).expect("serialize ok");
let custom = v.get("custom_headers").unwrap();
assert!(custom.is_object());
let obj = custom.as_object().unwrap();
assert_some_eq_x!(obj.get("x-api-key"), "abc123");
assert_some_eq_x!(obj.get("header-1"), "test");
assert_none!(obj.get("X-Api-Key"));
assert_none!(obj.get("HEADER-1"));
}
#[test]
fn serialize_header_map_none_is_null() {
let config = ServarrConfig::default();
let v: Value = serde_json::to_value(&config).expect("serialize ok");
assert!(v.get("custom_headers").unwrap().is_null());
}
#[test]
@@ -369,7 +414,7 @@ mod tests {
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.host, Some("localhost".to_string()));
assert_some_eq_x!(&config.host, "localhost");
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION") };
}
@@ -384,7 +429,7 @@ mod tests {
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.host, Some("www.example.com".to_string()));
assert_some_eq_x!(&config.host, "www.example.com");
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_NO_OVERWRITE") };
}
@@ -396,7 +441,67 @@ mod tests {
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.port, None);
assert_none!(config.port);
}
#[test]
#[serial]
fn test_deserialize_optional_env_var_header_map_is_present() {
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_HEADER_OPTION", "localhost") };
let expected_custom_headers = {
let mut headers = HeaderMap::new();
headers.insert("X-Api-Host", "localhost".parse().unwrap());
headers.insert("api-token", "test123".parse().unwrap());
headers
};
let yaml_data = r#"
custom_headers:
X-Api-Host: ${TEST_VAR_DESERIALIZE_HEADER_OPTION}
api-token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_some_eq_x!(&config.custom_headers, &expected_custom_headers);
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_HEADER_OPTION") };
}
#[test]
#[serial]
fn test_deserialize_optional_env_var_header_map_does_not_overwrite_non_env_value() {
unsafe {
std::env::set_var(
"TEST_VAR_DESERIALIZE_HEADER_OPTION_NO_OVERWRITE",
"localhost",
)
};
let expected_custom_headers = {
let mut headers = HeaderMap::new();
headers.insert("X-Api-Host", "www.example.com".parse().unwrap());
headers.insert("api-token", "test123".parse().unwrap());
headers
};
let yaml_data = r#"
custom_headers:
X-Api-Host: www.example.com
api-token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_some_eq_x!(&config.custom_headers, &expected_custom_headers);
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_HEADER_OPTION_NO_OVERWRITE") };
}
#[test]
fn test_deserialize_optional_env_var_header_map_empty() {
let yaml_data = r#"
api_token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_none!(config.custom_headers);
}
#[test]
@@ -410,7 +515,7 @@ mod tests {
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.port, Some(1));
assert_some_eq_x!(config.port, 1);
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_U16") };
}
@@ -425,7 +530,7 @@ mod tests {
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.port, Some(1234));
assert_some_eq_x!(config.port, 1234);
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_U16_UNUSED") };
}
@@ -437,9 +542,9 @@ mod tests {
"#;
let result: Result<ServarrConfig, _> = serde_yaml::from_str(yaml_data);
assert!(result.is_err());
assert_err!(&result);
let err = result.unwrap_err().to_string();
assert!(err.contains("invalid digit found in string"));
assert_contains!(err, "invalid digit found in string");
}
#[test]
@@ -450,7 +555,7 @@ mod tests {
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.port, None);
assert_none!(config.port);
}
#[test]
@@ -512,8 +617,11 @@ mod tests {
let api_token = "thisisatest".to_owned();
let api_token_file = "/root/.config/api_token".to_owned();
let ssl_cert_path = "/some/path".to_owned();
let expected_str = format!("ServarrConfig {{ name: Some(\"{}\"), host: Some(\"{}\"), port: Some({}), uri: Some(\"{}\"), weight: Some({}), api_token: Some(\"***********\"), api_token_file: Some(\"{}\"), ssl_cert_path: Some(\"{}\") }}",
name, host, port, uri, weight, api_token_file, ssl_cert_path);
let mut custom_headers = HeaderMap::new();
custom_headers.insert("X-Custom-Header", "value".parse().unwrap());
let expected_str = format!(
"ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}) }}"
);
let servarr_config = ServarrConfig {
name: Some(name),
host: Some(host),
@@ -523,6 +631,7 @@ mod tests {
api_token: Some(api_token),
api_token_file: Some(api_token_file),
ssl_cert_path: Some(ssl_cert_path),
custom_headers: Some(custom_headers),
};
assert_str_eq!(format!("{servarr_config:?}"), expected_str);
+31 -9
View File
@@ -1,20 +1,41 @@
use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS};
use crate::app::App;
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider;
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
use crate::models::Route;
#[cfg(test)]
#[path = "context_clues_tests.rs"]
mod context_clues_tests;
pub(in crate::app) type ContextClue = (KeyBinding, &'static str);
pub type ContextClue = (KeyBinding, &'static str);
pub fn build_context_clue_string(context_clues: &[(KeyBinding, &str)]) -> String {
context_clues
.iter()
.map(|(key_binding, desc)| format!("{} {desc}", key_binding.key))
.collect::<Vec<String>>()
.join(" | ")
pub trait ContextClueProvider {
fn get_context_clues(_app: &mut App<'_>) -> Option<&'static [ContextClue]>;
}
pub static SERVARR_CONTEXT_CLUES: [ContextClue; 3] = [
pub struct ServarrContextClueProvider;
impl ContextClueProvider for ServarrContextClueProvider {
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
match app.get_current_route() {
Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app),
Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app),
_ => None,
}
}
}
pub static SERVARR_CONTEXT_CLUES: [ContextClue; 10] = [
(DEFAULT_KEYBINDINGS.up, "scroll up"),
(DEFAULT_KEYBINDINGS.down, "scroll down"),
(DEFAULT_KEYBINDINGS.left, "previous tab"),
(DEFAULT_KEYBINDINGS.right, "next tab"),
(DEFAULT_KEYBINDINGS.pg_up, DEFAULT_KEYBINDINGS.pg_up.desc),
(
DEFAULT_KEYBINDINGS.pg_down,
DEFAULT_KEYBINDINGS.pg_down.desc,
),
(
DEFAULT_KEYBINDINGS.next_servarr,
DEFAULT_KEYBINDINGS.next_servarr.desc,
@@ -24,6 +45,7 @@ pub static SERVARR_CONTEXT_CLUES: [ContextClue; 3] = [
DEFAULT_KEYBINDINGS.previous_servarr.desc,
),
(DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc),
(DEFAULT_KEYBINDINGS.help, DEFAULT_KEYBINDINGS.help.desc),
];
pub static BARE_POPUP_CONTEXT_CLUES: [ContextClue; 1] =
+209 -149
View File
@@ -1,212 +1,272 @@
#[cfg(test)]
mod test {
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
ServarrContextClueProvider,
};
use crate::app::{context_clues::build_context_clue_string, key_binding::DEFAULT_KEYBINDINGS};
#[test]
fn test_build_context_clue_string() {
let test_context_clues_array = [
(DEFAULT_KEYBINDINGS.add, "add"),
(DEFAULT_KEYBINDINGS.delete, "delete"),
];
assert_str_eq!(
build_context_clue_string(&test_context_clues_array),
"<a> add | <del> delete"
);
}
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::sonarr::sonarr_data::ActiveSonarrBlock;
#[test]
fn test_servarr_context_clues() {
let mut servarr_context_clues_iter = SERVARR_CONTEXT_CLUES.iter();
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.next_servarr);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.next_servarr.desc);
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.previous_servarr);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.previous_servarr.desc);
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.quit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.quit.desc);
assert_eq!(servarr_context_clues_iter.next(), None);
assert_some_eq_x!(
servarr_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.up, "scroll up")
);
assert_some_eq_x!(
servarr_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.down, "scroll down")
);
assert_some_eq_x!(
servarr_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.left, "previous tab")
);
assert_some_eq_x!(
servarr_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.right, "next tab")
);
assert_some_eq_x!(
servarr_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.pg_up, DEFAULT_KEYBINDINGS.pg_up.desc)
);
assert_some_eq_x!(
servarr_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.pg_down,
DEFAULT_KEYBINDINGS.pg_down.desc
)
);
assert_some_eq_x!(
servarr_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.next_servarr,
DEFAULT_KEYBINDINGS.next_servarr.desc
)
);
assert_some_eq_x!(
servarr_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.previous_servarr,
DEFAULT_KEYBINDINGS.previous_servarr.desc
)
);
assert_some_eq_x!(
servarr_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc)
);
assert_some_eq_x!(
servarr_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.help, DEFAULT_KEYBINDINGS.help.desc)
);
assert_none!(servarr_context_clues_iter.next());
}
#[test]
fn test_bare_popup_context_clues() {
let mut bare_popup_context_clues_iter = BARE_POPUP_CONTEXT_CLUES.iter();
let (key_binding, description) = bare_popup_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(bare_popup_context_clues_iter.next(), None);
assert_some_eq_x!(
bare_popup_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(bare_popup_context_clues_iter.next());
}
#[test]
fn test_downloads_context_clues() {
let mut downloads_context_clues_iter = DOWNLOADS_CONTEXT_CLUES.iter();
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, "update downloads");
assert_eq!(downloads_context_clues_iter.next(), None);
assert_some_eq_x!(
downloads_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
downloads_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
);
assert_some_eq_x!(
downloads_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, "update downloads")
);
assert_none!(downloads_context_clues_iter.next());
}
#[test]
fn test_blocklist_context_clues() {
let mut blocklist_context_clues_iter = BLOCKLIST_CONTEXT_CLUES.iter();
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.clear);
assert_str_eq!(*description, "clear blocklist");
assert_eq!(blocklist_context_clues_iter.next(), None);
assert_some_eq_x!(
blocklist_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
blocklist_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
blocklist_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
blocklist_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
);
assert_some_eq_x!(
blocklist_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.clear, "clear blocklist")
);
assert_none!(blocklist_context_clues_iter.next());
}
#[test]
fn test_confirmation_prompt_context_clues() {
let mut confirmation_prompt_context_clues_iter = CONFIRMATION_PROMPT_CONTEXT_CLUES.iter();
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.confirm);
assert_str_eq!(*description, "submit");
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel");
assert_eq!(confirmation_prompt_context_clues_iter.next(), None);
assert_some_eq_x!(
confirmation_prompt_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.confirm, "submit")
);
assert_some_eq_x!(
confirmation_prompt_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel")
);
assert_none!(confirmation_prompt_context_clues_iter.next());
}
#[test]
fn test_root_folders_context_clues() {
let mut root_folders_context_clues_iter = ROOT_FOLDERS_CONTEXT_CLUES.iter();
let (key_binding, description) = root_folders_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.add);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.add.desc);
let (key_binding, description) = root_folders_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = root_folders_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(root_folders_context_clues_iter.next(), None);
assert_some_eq_x!(
root_folders_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc)
);
assert_some_eq_x!(
root_folders_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
);
assert_some_eq_x!(
root_folders_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_none!(root_folders_context_clues_iter.next());
}
#[test]
fn test_indexers_context_clues() {
let mut indexers_context_clues_iter = INDEXERS_CONTEXT_CLUES.iter();
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "edit indexer");
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.settings);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.settings.desc);
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.test);
assert_str_eq!(*description, "test indexer");
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.test_all);
assert_str_eq!(*description, "test all indexers");
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(indexers_context_clues_iter.next(), None);
assert_some_eq_x!(
indexers_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "edit indexer")
);
assert_some_eq_x!(
indexers_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.settings,
DEFAULT_KEYBINDINGS.settings.desc
)
);
assert_some_eq_x!(
indexers_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
);
assert_some_eq_x!(
indexers_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.test, "test indexer")
);
assert_some_eq_x!(
indexers_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.test_all, "test all indexers")
);
assert_some_eq_x!(
indexers_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_none!(indexers_context_clues_iter.next());
}
#[test]
fn test_system_context_clues() {
let mut system_context_clues_iter = SYSTEM_CONTEXT_CLUES.iter();
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_some_eq_x!(
system_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.tasks, "open tasks")
);
assert_some_eq_x!(
system_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.events, "open events")
);
assert_some_eq_x!(
system_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.logs, "open logs")
);
assert_some_eq_x!(
system_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, "open updates")
);
assert_some_eq_x!(
system_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_none!(system_context_clues_iter.next());
}
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.tasks);
assert_str_eq!(*description, "open tasks");
#[test]
fn test_servarr_context_clue_provider_delegates_to_radarr_provider() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
let (key_binding, description) = system_context_clues_iter.next().unwrap();
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.events);
assert_str_eq!(*description, "open events");
assert_some_eq_x!(
context_clues,
&crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
);
}
let (key_binding, description) = system_context_clues_iter.next().unwrap();
#[test]
fn test_servarr_context_clue_provider_delegates_to_sonarr_provider() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into());
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.logs);
assert_str_eq!(*description, "open logs");
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_some_eq_x!(
context_clues,
&crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
);
}
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, "open updates");
#[test]
fn test_servarr_context_clue_provider_unsupported_route_returns_none() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveKeybindingBlock::Help.into());
let (key_binding, description) = system_context_clues_iter.next().unwrap();
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(system_context_clues_iter.next(), None);
assert_none!(context_clues);
}
}
+79 -1
View File
@@ -14,6 +14,8 @@ generate_keybindings! {
down,
left,
right,
pg_down,
pg_up,
backspace,
next_servarr,
previous_servarr,
@@ -21,6 +23,7 @@ generate_keybindings! {
search,
auto_search,
settings,
help,
filter,
sort,
edit,
@@ -44,128 +47,203 @@ generate_keybindings! {
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct KeyBinding {
pub key: Key,
pub alt: Option<Key>,
pub desc: &'static str,
}
pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
add: KeyBinding {
key: Key::Char('a'),
alt: None,
desc: "add",
},
up: KeyBinding {
key: Key::Up,
alt: Some(Key::Char('k')),
desc: "up",
},
down: KeyBinding {
key: Key::Down,
alt: Some(Key::Char('j')),
desc: "down",
},
left: KeyBinding {
key: Key::Left,
alt: Some(Key::Char('h')),
desc: "left",
},
right: KeyBinding {
key: Key::Right,
alt: Some(Key::Char('l')),
desc: "right",
},
pg_down: KeyBinding {
key: Key::PgDown,
alt: Some(Key::Ctrl('d')),
desc: "page down",
},
pg_up: KeyBinding {
key: Key::PgUp,
alt: Some(Key::Ctrl('u')),
desc: "page up",
},
backspace: KeyBinding {
key: Key::Backspace,
alt: Some(Key::Ctrl('h')),
desc: "backspace",
},
next_servarr: KeyBinding {
key: Key::Tab,
alt: None,
desc: "next servarr",
},
previous_servarr: KeyBinding {
key: Key::BackTab,
alt: None,
desc: "previous servarr",
},
clear: KeyBinding {
key: Key::Char('c'),
alt: None,
desc: "clear",
},
auto_search: KeyBinding {
key: Key::Char('S'),
alt: None,
desc: "auto search",
},
search: KeyBinding {
key: Key::Char('s'),
alt: None,
desc: "search",
},
settings: KeyBinding {
key: Key::Char('S'),
alt: None,
desc: "settings",
},
help: KeyBinding {
key: Key::Char('?'),
alt: None,
desc: "show/hide keybindings",
},
filter: KeyBinding {
key: Key::Char('f'),
alt: None,
desc: "filter",
},
sort: KeyBinding {
key: Key::Char('o'),
alt: None,
desc: "sort",
},
edit: KeyBinding {
key: Key::Char('e'),
alt: None,
desc: "edit",
},
events: KeyBinding {
key: Key::Char('e'),
alt: None,
desc: "events",
},
logs: KeyBinding {
key: Key::Char('l'),
key: Key::Char('L'),
alt: None,
desc: "logs",
},
tasks: KeyBinding {
key: Key::Char('t'),
alt: None,
desc: "tasks",
},
test: KeyBinding {
key: Key::Char('t'),
alt: None,
desc: "test",
},
test_all: KeyBinding {
key: Key::Char('T'),
alt: None,
desc: "test all",
},
toggle_monitoring: KeyBinding {
key: Key::Char('m'),
alt: None,
desc: "toggle monitoring",
},
refresh: KeyBinding {
key: Key::Ctrl('r'),
alt: None,
desc: "refresh",
},
update: KeyBinding {
key: Key::Char('u'),
alt: None,
desc: "update",
},
home: KeyBinding {
key: Key::Home,
alt: None,
desc: "home",
},
end: KeyBinding {
key: Key::End,
alt: None,
desc: "end",
},
delete: KeyBinding {
key: Key::Delete,
alt: None,
desc: "delete",
},
submit: KeyBinding {
key: Key::Enter,
alt: None,
desc: "submit",
},
confirm: KeyBinding {
key: Key::Ctrl('s'),
alt: None,
desc: "submit",
},
quit: KeyBinding {
key: Key::Char('q'),
alt: None,
desc: "quit",
},
esc: KeyBinding {
key: Key::Esc,
alt: None,
desc: "close",
},
};
#[macro_export]
macro_rules! matches_key {
($binding:ident, $key:expr) => {
$crate::app::key_binding::DEFAULT_KEYBINDINGS.$binding.key == $key
|| ($crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.is_some()
&& $crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.unwrap()
== $key)
};
($binding:ident, $key:expr, $ignore_special_keys:expr) => {
$crate::app::key_binding::DEFAULT_KEYBINDINGS.$binding.key == $key
|| !$ignore_special_keys
&& ($crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.is_some()
&& $crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.unwrap()
== $key)
};
}
+75 -31
View File
@@ -3,46 +3,90 @@ mod test {
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS};
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
use crate::event::Key;
use crate::matches_key;
#[rstest]
#[case(DEFAULT_KEYBINDINGS.add, Key::Char('a'), "add")]
#[case(DEFAULT_KEYBINDINGS.up, Key::Up, "up")]
#[case(DEFAULT_KEYBINDINGS.down, Key::Down, "down")]
#[case(DEFAULT_KEYBINDINGS.left, Key::Left, "left")]
#[case(DEFAULT_KEYBINDINGS.right, Key::Right, "right")]
#[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, "backspace")]
#[case(DEFAULT_KEYBINDINGS.next_servarr, Key::Tab, "next servarr")]
#[case(DEFAULT_KEYBINDINGS.previous_servarr, Key::BackTab, "previous servarr")]
#[case(DEFAULT_KEYBINDINGS.clear, Key::Char('c'), "clear")]
#[case(DEFAULT_KEYBINDINGS.auto_search, Key::Char('S'), "auto search")]
#[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), "search")]
#[case(DEFAULT_KEYBINDINGS.settings, Key::Char('S'), "settings")]
#[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), "filter")]
#[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), "sort")]
#[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), "edit")]
#[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), "events")]
#[case(DEFAULT_KEYBINDINGS.logs, Key::Char('l'), "logs")]
#[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), "tasks")]
#[case(DEFAULT_KEYBINDINGS.test, Key::Char('t'), "test")]
#[case(DEFAULT_KEYBINDINGS.test_all, Key::Char('T'), "test all")]
#[case(DEFAULT_KEYBINDINGS.toggle_monitoring, Key::Char('m'), "toggle monitoring")]
#[case(DEFAULT_KEYBINDINGS.refresh, Key::Ctrl('r'), "refresh")]
#[case(DEFAULT_KEYBINDINGS.update, Key::Char('u'), "update")]
#[case(DEFAULT_KEYBINDINGS.home, Key::Home, "home")]
#[case(DEFAULT_KEYBINDINGS.end, Key::End, "end")]
#[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, "delete")]
#[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, "submit")]
#[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), "submit")]
#[case(DEFAULT_KEYBINDINGS.quit, Key::Char('q'), "quit")]
#[case(DEFAULT_KEYBINDINGS.esc, Key::Esc, "close")]
#[case(DEFAULT_KEYBINDINGS.add, Key::Char('a'), None, "add")]
#[case(DEFAULT_KEYBINDINGS.up, Key::Up, Some(Key::Char('k')), "up")]
#[case(DEFAULT_KEYBINDINGS.down, Key::Down, Some(Key::Char('j')), "down")]
#[case(DEFAULT_KEYBINDINGS.left, Key::Left, Some(Key::Char('h')), "left")]
#[case(DEFAULT_KEYBINDINGS.right, Key::Right, Some(Key::Char('l')), "right")]
#[case(DEFAULT_KEYBINDINGS.pg_down, Key::PgDown, Some(Key::Ctrl('d')), "page down")]
#[case(DEFAULT_KEYBINDINGS.pg_up, Key::PgUp, Some(Key::Ctrl('u')), "page up")]
#[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, Some(Key::Ctrl('h')), "backspace")]
#[case(DEFAULT_KEYBINDINGS.next_servarr, Key::Tab, None, "next servarr")]
#[case(DEFAULT_KEYBINDINGS.previous_servarr, Key::BackTab, None, "previous servarr")]
#[case(DEFAULT_KEYBINDINGS.clear, Key::Char('c'), None, "clear")]
#[case(DEFAULT_KEYBINDINGS.auto_search, Key::Char('S'), None, "auto search")]
#[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), None, "search")]
#[case(DEFAULT_KEYBINDINGS.settings, Key::Char('S'), None, "settings")]
#[case(DEFAULT_KEYBINDINGS.help, Key::Char('?'), None, "show/hide keybindings")]
#[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.events, Key::Char('e'), None, "events")]
#[case(DEFAULT_KEYBINDINGS.logs, Key::Char('L'), None, "logs")]
#[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), None, "tasks")]
#[case(DEFAULT_KEYBINDINGS.test, Key::Char('t'), None, "test")]
#[case(DEFAULT_KEYBINDINGS.test_all, Key::Char('T'), None, "test all")]
#[case(DEFAULT_KEYBINDINGS.toggle_monitoring, Key::Char('m'), None, "toggle monitoring")]
#[case(DEFAULT_KEYBINDINGS.refresh, Key::Ctrl('r'), None, "refresh")]
#[case(DEFAULT_KEYBINDINGS.update, Key::Char('u'), None, "update")]
#[case(DEFAULT_KEYBINDINGS.home, Key::Home, None, "home")]
#[case(DEFAULT_KEYBINDINGS.end, Key::End, None, "end")]
#[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, None, "delete")]
#[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, None, "submit")]
#[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), None, "submit")]
#[case(DEFAULT_KEYBINDINGS.quit, Key::Char('q'), None, "quit")]
#[case(DEFAULT_KEYBINDINGS.esc, Key::Esc, None, "close")]
fn test_default_key_bindings_and_descriptions(
#[case] key_binding: KeyBinding,
#[case] expected_key: Key,
#[case] expected_alt_key: Option<Key>,
#[case] expected_desc: &str,
) {
assert_eq!(key_binding.key, expected_key);
assert_eq!(key_binding.alt, expected_alt_key);
assert_str_eq!(key_binding.desc, expected_desc);
}
#[test]
fn test_matches_key_macro() {
let key = Key::Char('t');
assert!(matches_key!(test, key));
assert!(!matches_key!(test, Key::Char('T')));
}
#[test]
fn test_matches_key_macro_with_alt_keybinding() {
let alt_key = Key::Char('k');
let key = Key::Up;
assert!(matches_key!(up, key));
assert!(matches_key!(up, alt_key));
assert!(!matches_key!(up, Key::Char('t')));
}
#[test]
fn test_matches_key_macro_with_alt_keybinding_uses_alt_key_when_ignore_special_keys_is_false() {
let alt_key = Key::Char('k');
let key = Key::Up;
assert!(matches_key!(up, key, false));
assert!(matches_key!(up, alt_key, false));
assert!(!matches_key!(up, Key::Char('t'), false));
}
#[test]
fn test_matches_key_macro_with_alt_keybinding_ignores_alt_key_when_ignore_special_keys_is_true() {
let alt_key = Key::Char('k');
let key = Key::Up;
assert!(matches_key!(up, key, true));
assert!(!matches_key!(up, alt_key, true));
assert!(!matches_key!(up, Key::Char('t'), true));
}
}
+145 -56
View File
@@ -1,24 +1,27 @@
use anyhow::{anyhow, Error, Result};
use anyhow::{Error, Result, anyhow};
use colored::Colorize;
use itertools::Itertools;
use log::{debug, error};
use regex::Regex;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::{fs, process};
use tachyonfx::{Duration, EffectManager};
use tokio::sync::mpsc::Sender;
use tokio_util::sync::CancellationToken;
use veil::Redact;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
use crate::cli::Command;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::servarr_models::KeybindingItem;
use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState};
use crate::network::NetworkEvent;
#[cfg(test)]
#[path = "app_tests.rs"]
mod app_tests;
pub mod context_clues;
pub mod key_binding;
@@ -32,14 +35,19 @@ pub struct App<'a> {
pub cancellation_token: CancellationToken,
pub is_first_render: bool,
pub server_tabs: TabState,
pub keymapping_table: Option<StatefulTable<KeybindingItem>>,
pub error: HorizontallyScrollableText,
pub tick_until_poll: u64,
pub ticks_until_scroll: u64,
pub tick_count: u64,
pub ui_scroll_tick_count: u64,
pub last_tick: Duration,
pub effects: EffectManager<()>,
pub has_active_effect: bool,
pub is_routing: bool,
pub is_loading: bool,
pub should_refresh: bool,
pub should_ignore_quit_key: bool,
pub ignore_special_keys_for_textbox_input: bool,
pub cli_mode: bool,
pub data: Data<'a>,
}
@@ -51,50 +59,45 @@ impl App<'_> {
cancellation_token: CancellationToken,
) -> Self {
let mut server_tabs = Vec::new();
let help = format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
);
if let Some(radarr_configs) = config.radarr {
let mut idx = 0;
for radarr_config in radarr_configs {
let mut unnamed_idx = 0;
let radarr_tabs = radarr_configs.into_iter().map(|radarr_config| {
let name = if let Some(name) = radarr_config.name.clone() {
name
} else {
idx += 1;
format!("Radarr {}", idx)
unnamed_idx += 1;
format!("Radarr {unnamed_idx}")
};
server_tabs.push(TabRoute {
TabRoute {
title: name,
route: ActiveRadarrBlock::Movies.into(),
help: help.clone(),
contextual_help: None,
config: Some(radarr_config),
});
}
}
});
server_tabs.extend(radarr_tabs);
}
if let Some(sonarr_configs) = config.sonarr {
let mut idx = 0;
for sonarr_config in sonarr_configs {
let mut unnamed_idx = 0;
let sonarr_tabs = sonarr_configs.into_iter().map(|sonarr_config| {
let name = if let Some(name) = sonarr_config.name.clone() {
name
} else {
idx += 1;
format!("Sonarr {}", idx)
unnamed_idx += 1;
format!("Sonarr {unnamed_idx}")
};
server_tabs.push(TabRoute {
TabRoute {
title: name,
route: ActiveSonarrBlock::Series.into(),
help: help.clone(),
contextual_help: None,
config: Some(sonarr_config),
});
}
}
});
server_tabs.extend(sonarr_tabs);
}
let weight_sorted_tabs = server_tabs
@@ -134,12 +137,12 @@ impl App<'_> {
self.is_loading = true;
}
if let Some(network_tx) = &self.network_tx {
if let Err(e) = network_tx.send(action).await {
self.is_loading = false;
error!("Failed to send event. {e:?}");
self.handle_error(anyhow!(e));
}
if let Some(network_tx) = &self.network_tx
&& let Err(e) = network_tx.send(action).await
{
self.is_loading = false;
error!("Failed to send event. {e:?}");
self.handle_error(anyhow!(e));
}
}
@@ -147,6 +150,14 @@ impl App<'_> {
self.tick_count = 0;
}
pub fn on_ui_scroll_tick(&mut self) {
if self.ui_scroll_tick_count == self.ticks_until_scroll {
self.ui_scroll_tick_count = 0;
} else {
self.ui_scroll_tick_count += 1;
}
}
#[allow(dead_code)]
pub fn reset(&mut self) {
self.reset_tick_count();
@@ -162,7 +173,10 @@ impl App<'_> {
}
pub async fn on_tick(&mut self) {
if self.tick_count % self.tick_until_poll == 0 || self.is_routing || self.should_refresh {
if self.tick_count.is_multiple_of(self.tick_until_poll)
|| self.is_routing
|| self.should_refresh
{
match self.get_current_route() {
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,
@@ -179,6 +193,7 @@ impl App<'_> {
pub fn push_navigation_stack(&mut self, route: Route) {
self.navigation_stack.push(route);
self.is_routing = true;
self.has_active_effect = false;
}
pub fn pop_navigation_stack(&mut self) {
@@ -202,10 +217,14 @@ impl App<'_> {
}
pub fn get_current_route(&self) -> Route {
*self
.navigation_stack
.last()
.unwrap_or(&self.server_tabs.tabs.first().unwrap().route)
*self.navigation_stack.last().unwrap_or(
&self
.server_tabs
.tabs
.first()
.expect("At least one server tab must exist")
.route,
)
}
}
@@ -215,16 +234,21 @@ impl Default for App<'_> {
navigation_stack: Vec::new(),
network_tx: None,
cancellation_token: CancellationToken::new(),
keymapping_table: None,
error: HorizontallyScrollableText::default(),
is_first_render: true,
server_tabs: TabState::new(Vec::new()),
tick_until_poll: 400,
ticks_until_scroll: 4,
tick_count: 0,
ui_scroll_tick_count: 0,
last_tick: Duration::ZERO,
effects: EffectManager::default(),
has_active_effect: false,
is_loading: false,
is_routing: false,
should_refresh: false,
should_ignore_quit_key: false,
ignore_special_keys_for_textbox_input: false,
cli_mode: false,
data: Data::default(),
}
@@ -239,20 +263,36 @@ impl App<'_> {
TabRoute {
title: "Radarr".to_owned(),
route: ActiveRadarrBlock::Movies.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
TabRoute {
title: "Sonarr".to_owned(),
route: ActiveSonarrBlock::Series.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
]),
..App::default()
}
}
pub fn test_default_fully_populated() -> Self {
App {
data: Data {
radarr_data: RadarrData::test_default_fully_populated(),
sonarr_data: SonarrData::test_default_fully_populated(),
},
server_tabs: TabState::new(vec![
TabRoute {
title: "Radarr".to_owned(),
route: ActiveRadarrBlock::Movies.into(),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
TabRoute {
title: "Sonarr".to_owned(),
route: ActiveSonarrBlock::Series.into(),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
@@ -270,6 +310,7 @@ pub struct Data<'a> {
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct AppConfig {
pub theme: Option<String>,
pub radarr: Option<Vec<ServarrConfig>>,
pub sonarr: Option<Vec<ServarrConfig>>,
}
@@ -295,8 +336,7 @@ impl AppConfig {
pub fn verify_config_present_for_cli(&self, command: &Command) {
let msg = |servarr: &str| {
log_and_print_error(format!(
"{} configuration missing; Unable to run any {} commands.",
servarr, servarr
"{servarr} configuration missing; Unable to run any {servarr} commands."
))
};
match command {
@@ -346,6 +386,12 @@ pub struct ServarrConfig {
pub api_token_file: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub ssl_cert_path: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_optional_env_var_header_map",
serialize_with = "serialize_header_map"
)]
pub custom_headers: Option<HeaderMap>,
}
impl ServarrConfig {
@@ -367,8 +413,7 @@ impl ServarrConfig {
if let Some(api_token_file) = self.api_token_file.as_ref() {
if !PathBuf::from(api_token_file).exists() {
log_and_print_error(format!(
"The specified {} API token file does not exist",
api_token_file
"The specified {api_token_file} API token file does not exist"
));
process::exit(1);
}
@@ -392,15 +437,37 @@ impl Default for ServarrConfig {
api_token: Some(String::new()),
api_token_file: None,
ssl_cert_path: None,
custom_headers: None,
}
}
}
pub fn log_and_print_error(error: String) {
error!("{}", error);
error!("{error}");
eprintln!("error: {}", error.red());
}
fn serialize_header_map<S>(headers: &Option<HeaderMap>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if let Some(headers) = headers {
let mut map = HashMap::new();
for (name, value) in headers.iter() {
let name_str = name.as_str().to_string();
let value_str = value
.to_str()
.map_err(serde::ser::Error::custom)?
.to_string();
map.insert(name_str, value_str);
}
map.serialize(serializer)
} else {
serializer.serialize_none()
}
}
fn deserialize_optional_env_var<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
@@ -415,6 +482,28 @@ where
}
}
fn deserialize_optional_env_var_header_map<'de, D>(
deserializer: D,
) -> Result<Option<HeaderMap>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<HashMap<String, String>> = Option::deserialize(deserializer)?;
match opt {
Some(map) => {
let mut header_map = HeaderMap::new();
for (k, v) in map.iter() {
let name = HeaderName::from_bytes(k.as_bytes()).map_err(serde::de::Error::custom)?;
let value_str = interpolate_env_vars(v);
let value = HeaderValue::from_str(&value_str).map_err(serde::de::Error::custom)?;
header_map.insert(name, value);
}
Ok(Some(header_map))
}
None => Ok(None),
}
}
fn deserialize_u16_env_var<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
where
D: serde::Deserializer<'de>,
@@ -434,15 +523,15 @@ where
fn interpolate_env_vars(s: &str) -> String {
let result = s.to_string();
let scrubbing_regex = Regex::new(r#"[\s\{\}!\$^\(\)\[\]\\\|`'"]+"#).unwrap();
let var_regex = Regex::new(r"\$\{(.*?)\}").unwrap();
let scrubbing_regex = Regex::new(r#"[\s{}!$^()\[\]\\|`'"]+"#).unwrap();
let var_regex = Regex::new(r"\$\{(.*?)}").unwrap();
var_regex
.replace_all(s, |caps: &regex::Captures<'_>| {
if let Some(mat) = caps.get(1) {
if let Ok(value) = std::env::var(mat.as_str()) {
return scrubbing_regex.replace_all(&value, "").to_string();
}
if let Some(mat) = caps.get(1)
&& let Ok(value) = std::env::var(mat.as_str())
{
return scrubbing_regex.replace_all(&value, "").to_string();
}
scrubbing_regex.replace_all(&result, "").to_string()
+4 -4
View File
@@ -40,7 +40,7 @@ impl App<'_> {
}
ActiveRadarrBlock::Downloads => {
self
.dispatch_network_event(RadarrEvent::GetDownloads.into())
.dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await;
}
ActiveRadarrBlock::RootFolders => {
@@ -59,7 +59,7 @@ impl App<'_> {
.dispatch_network_event(RadarrEvent::GetMovies.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetDownloads.into())
.dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await;
}
ActiveRadarrBlock::Indexers => {
@@ -185,7 +185,7 @@ impl App<'_> {
}
}
if self.tick_count % self.tick_until_poll == 0 {
if self.tick_count.is_multiple_of(self.tick_until_poll) {
self.refresh_radarr_metadata().await;
}
}
@@ -201,7 +201,7 @@ impl App<'_> {
.dispatch_network_event(RadarrEvent::GetRootFolders.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetDownloads.into())
.dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await;
self
.dispatch_network_event(RadarrEvent::GetDiskSpace.into())
+67 -6
View File
@@ -1,13 +1,25 @@
use crate::app::context_clues::ContextClue;
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider,
};
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,
};
#[cfg(test)]
#[path = "radarr_context_clues_tests.rs"]
mod radarr_context_clues_tests;
pub static LIBRARY_CONTEXT_CLUES: [ContextClue; 10] = [
pub static LIBRARY_CONTEXT_CLUES: [ContextClue; 11] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
@@ -49,7 +61,7 @@ pub static MOVIE_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 6] = [
pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
@@ -61,12 +73,10 @@ pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 6] = [
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 1] =
[(DEFAULT_KEYBINDINGS.submit, "details")];
pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "edit search"),
@@ -82,3 +92,54 @@ pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.edit, "edit collection"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub(in crate::app) struct RadarrContextClueProvider;
impl ContextClueProvider for RadarrContextClueProvider {
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
let Route::Radarr(active_radarr_block, context_option) = app.get_current_route() else {
panic!("RadarrContextClueProvider::get_context_clues called with non-Radarr route");
};
match active_radarr_block {
_ if MOVIE_DETAILS_BLOCKS.contains(&active_radarr_block) => app
.data
.radarr_data
.movie_info_tabs
.get_active_route_contextual_help(),
ActiveRadarrBlock::TestAllIndexers
| ActiveRadarrBlock::AddMovieSearchInput
| ActiveRadarrBlock::AddMovieEmptySearchResults
| ActiveRadarrBlock::SystemLogs
| ActiveRadarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES),
_ if context_option.unwrap_or(active_radarr_block)
== ActiveRadarrBlock::ViewMovieOverview =>
{
Some(&BARE_POPUP_CONTEXT_CLUES)
}
ActiveRadarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES),
_ if EDIT_COLLECTION_BLOCKS.contains(&active_radarr_block)
|| EDIT_INDEXER_BLOCKS.contains(&active_radarr_block)
|| INDEXER_SETTINGS_BLOCKS.contains(&active_radarr_block)
|| EDIT_MOVIE_BLOCKS.contains(&active_radarr_block) =>
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
ActiveRadarrBlock::AddMoviePrompt
| ActiveRadarrBlock::AddMovieSelectMonitor
| ActiveRadarrBlock::AddMovieSelectMinimumAvailability
| ActiveRadarrBlock::AddMovieSelectQualityProfile
| ActiveRadarrBlock::AddMovieSelectRootFolder
| ActiveRadarrBlock::AddMovieTagsInput
| ActiveRadarrBlock::SystemTaskStartConfirmPrompt => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
_ if ADD_MOVIE_BLOCKS.contains(&active_radarr_block) => {
Some(&ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES)
}
ActiveRadarrBlock::CollectionDetails => Some(&COLLECTION_DETAILS_CONTEXT_CLUES),
_ => app
.data
.radarr_data
.main_tabs
.get_active_route_contextual_help(),
}
}
}
+424 -197
View File
@@ -1,114 +1,118 @@
#[cfg(test)]
mod tests {
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MOVIE_DETAILS_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTION_DETAILS_CONTEXT_CLUES,
COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MOVIE_DETAILS_CONTEXT_CLUES, RadarrContextClueProvider, SYSTEM_TASKS_CONTEXT_CLUES,
};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_library_context_clues() {
let mut library_context_clues_iter = LIBRARY_CONTEXT_CLUES.iter();
let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.add);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.add.desc);
let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc);
let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc);
let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc);
let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, "update all");
let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel filter");
assert_eq!(library_context_clues_iter.next(), None);
assert_some_eq_x!(
library_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc)
);
assert_some_eq_x!(
library_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
);
assert_some_eq_x!(
library_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc
)
);
assert_some_eq_x!(
library_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
library_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc)
);
assert_some_eq_x!(
library_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
library_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
library_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
library_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, "update all")
);
assert_some_eq_x!(
library_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
library_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
);
assert_none!(library_context_clues_iter.next());
}
#[test]
fn test_collections_context_clues() {
let mut collections_context_clues = COLLECTIONS_CONTEXT_CLUES.iter();
let (key_binding, description) = collections_context_clues.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc);
let (key_binding, description) = collections_context_clues.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc);
let (key_binding, description) = collections_context_clues.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = collections_context_clues.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc);
let (key_binding, description) = collections_context_clues.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = collections_context_clues.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, "update all");
let (key_binding, description) = collections_context_clues.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = collections_context_clues.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel filter");
assert_some_eq_x!(
collections_context_clues.next(),
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
);
assert_some_eq_x!(
collections_context_clues.next(),
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
);
assert_some_eq_x!(
collections_context_clues.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
collections_context_clues.next(),
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
);
assert_some_eq_x!(
collections_context_clues.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
collections_context_clues.next(),
&(DEFAULT_KEYBINDINGS.update, "update all")
);
assert_some_eq_x!(
collections_context_clues.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
collections_context_clues.next(),
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
);
assert_eq!(collections_context_clues.next(), None);
}
@@ -116,30 +120,32 @@ mod tests {
fn test_movie_details_context_clues() {
let mut movie_details_context_clues_iter = MOVIE_DETAILS_CONTEXT_CLUES.iter();
let (key_binding, description) = movie_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = movie_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.update.desc);
let (key_binding, description) = movie_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc);
let (key_binding, description) = movie_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = movie_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_some_eq_x!(
movie_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
movie_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
movie_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
);
assert_some_eq_x!(
movie_details_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
movie_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_eq!(movie_details_context_clues_iter.next(), None);
}
@@ -147,53 +153,41 @@ mod tests {
fn test_manual_movie_search_context_clues() {
let mut manual_movie_search_context_clues_iter = MANUAL_MOVIE_SEARCH_CONTEXT_CLUES.iter();
let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.update.desc);
let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc);
let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(manual_movie_search_context_clues_iter.next(), None);
}
#[test]
fn test_manual_movie_search_contextual_context_clues() {
let mut manual_movie_search_contextual_context_clues_iter =
MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES.iter();
let (key_binding, description) = manual_movie_search_contextual_context_clues_iter
.next()
.unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
assert_eq!(
manual_movie_search_contextual_context_clues_iter.next(),
None
assert_some_eq_x!(
manual_movie_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
manual_movie_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc)
);
assert_some_eq_x!(
manual_movie_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc)
);
assert_some_eq_x!(
manual_movie_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
);
assert_some_eq_x!(
manual_movie_search_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc
)
);
assert_some_eq_x!(
manual_movie_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
manual_movie_search_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_eq!(manual_movie_search_context_clues_iter.next(), None);
}
#[test]
@@ -201,15 +195,14 @@ mod tests {
let mut add_movie_search_results_context_clues_iter =
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES.iter();
let (key_binding, description) = add_movie_search_results_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = add_movie_search_results_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "edit search");
assert_some_eq_x!(
add_movie_search_results_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "details")
);
assert_some_eq_x!(
add_movie_search_results_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, "edit search")
);
assert_eq!(add_movie_search_results_context_clues_iter.next(), None);
}
@@ -217,15 +210,14 @@ mod tests {
fn test_system_tasks_context_clues() {
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter();
let (key_binding, description) = system_tasks_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "start task");
let (key_binding, description) = system_tasks_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_some_eq_x!(
system_tasks_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "start task")
);
assert_some_eq_x!(
system_tasks_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_eq!(system_tasks_context_clues_iter.next(), None);
}
@@ -233,20 +225,255 @@ mod tests {
fn test_collection_details_context_clues() {
let mut collection_details_context_clues_iter = COLLECTION_DETAILS_CONTEXT_CLUES.iter();
let (key_binding, description) = collection_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "show overview/add movie");
let (key_binding, description) = collection_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit);
assert_str_eq!(*description, "edit collection");
let (key_binding, description) = collection_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_some_eq_x!(
collection_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.submit, "show overview/add movie")
);
assert_some_eq_x!(
collection_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.edit, "edit collection")
);
assert_some_eq_x!(
collection_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_eq!(collection_details_context_clues_iter.next(), None);
}
#[test]
#[should_panic(
expected = "RadarrContextClueProvider::get_context_clues called with non-Radarr route"
)]
fn test_radarr_context_clue_provider_get_context_clues_non_radarr_route() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::default().into());
// This should panic because the route is not a Radarr route
RadarrContextClueProvider::get_context_clues(&mut app);
}
#[rstest]
#[case(ActiveRadarrBlock::TestAllIndexers, None)]
#[case(ActiveRadarrBlock::AddMovieSearchInput, None)]
#[case(ActiveRadarrBlock::AddMovieEmptySearchResults, None)]
#[case(ActiveRadarrBlock::SystemLogs, None)]
#[case(ActiveRadarrBlock::SystemUpdates, None)]
#[case(ActiveRadarrBlock::ViewMovieOverview, None)]
#[case(
ActiveRadarrBlock::CollectionDetails,
Some(ActiveRadarrBlock::ViewMovieOverview)
)]
fn test_radarr_context_clue_provider_bare_popup_context_clues(
#[case] active_radarr_block: ActiveRadarrBlock,
#[case] context_option: Option<ActiveRadarrBlock>,
) {
let mut app = App::test_default();
app.push_navigation_stack((active_radarr_block, context_option).into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &BARE_POPUP_CONTEXT_CLUES);
}
#[rstest]
#[case(0, ActiveRadarrBlock::MovieDetails, &MOVIE_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveRadarrBlock::MovieHistory, &MOVIE_DETAILS_CONTEXT_CLUES)]
#[case(2, ActiveRadarrBlock::FileInfo, &MOVIE_DETAILS_CONTEXT_CLUES)]
#[case(3, ActiveRadarrBlock::Cast, &MOVIE_DETAILS_CONTEXT_CLUES)]
#[case(4, ActiveRadarrBlock::Crew, &MOVIE_DETAILS_CONTEXT_CLUES)]
#[case(5, ActiveRadarrBlock::ManualSearch, &MANUAL_MOVIE_SEARCH_CONTEXT_CLUES)]
fn test_radarr_context_clue_provider_movie_details_block_context_clues(
#[case] index: usize,
#[case] active_radarr_block: ActiveRadarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.data.radarr_data.movie_info_tabs.set_index(index);
app.push_navigation_stack(active_radarr_block.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, expected_context_clues);
}
#[rstest]
fn test_radarr_context_clue_provider_confirmation_prompt_context_clues(
#[values(
ActiveRadarrBlock::AddMoviePrompt,
ActiveRadarrBlock::AddMovieSelectMonitor,
ActiveRadarrBlock::AddMovieSelectMinimumAvailability,
ActiveRadarrBlock::AddMovieSelectQualityProfile,
ActiveRadarrBlock::AddMovieSelectRootFolder,
ActiveRadarrBlock::AddMovieTagsInput,
ActiveRadarrBlock::SystemTaskStartConfirmPrompt
)]
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_confirmation_prompt_context_clues_edit_collection_blocks(
#[values(
ActiveRadarrBlock::EditCollectionPrompt,
ActiveRadarrBlock::EditCollectionConfirmPrompt,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
ActiveRadarrBlock::EditCollectionSelectMinimumAvailability,
ActiveRadarrBlock::EditCollectionSelectQualityProfile,
ActiveRadarrBlock::EditCollectionToggleSearchOnAdd,
ActiveRadarrBlock::EditCollectionToggleMonitored
)]
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_confirmation_prompt_context_clues_edit_indexer_blocks(
#[values(
ActiveRadarrBlock::EditIndexerPrompt,
ActiveRadarrBlock::EditIndexerConfirmPrompt,
ActiveRadarrBlock::EditIndexerApiKeyInput,
ActiveRadarrBlock::EditIndexerNameInput,
ActiveRadarrBlock::EditIndexerSeedRatioInput,
ActiveRadarrBlock::EditIndexerToggleEnableRss,
ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveRadarrBlock::EditIndexerPriorityInput,
ActiveRadarrBlock::EditIndexerUrlInput,
ActiveRadarrBlock::EditIndexerTagsInput
)]
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_confirmation_prompt_context_clues_indexer_settings_blocks(
#[values(
ActiveRadarrBlock::AllIndexerSettingsPrompt,
ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
ActiveRadarrBlock::IndexerSettingsConfirmPrompt,
ActiveRadarrBlock::IndexerSettingsMaximumSizeInput,
ActiveRadarrBlock::IndexerSettingsMinimumAgeInput,
ActiveRadarrBlock::IndexerSettingsRetentionInput,
ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput,
ActiveRadarrBlock::IndexerSettingsToggleAllowHardcodedSubs,
ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput
)]
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_confirmation_prompt_context_clues_edit_movie_blocks(
#[values(
ActiveRadarrBlock::EditMoviePrompt,
ActiveRadarrBlock::EditMovieConfirmPrompt,
ActiveRadarrBlock::EditMoviePathInput,
ActiveRadarrBlock::EditMovieSelectMinimumAvailability,
ActiveRadarrBlock::EditMovieSelectQualityProfile,
ActiveRadarrBlock::EditMovieTagsInput,
ActiveRadarrBlock::EditMovieToggleMonitored
)]
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(
ActiveRadarrBlock::AddMovieSearchResults,
ActiveRadarrBlock::AddMovieAlreadyInLibrary
)]
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, &ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES);
}
#[test]
fn test_radarr_context_clue_provider_collection_details_context_clues() {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &COLLECTION_DETAILS_CONTEXT_CLUES);
}
#[test]
fn test_radarr_context_clue_provider_system_tasks_context_clues() {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES);
}
#[rstest]
#[case(0, ActiveRadarrBlock::Movies, &LIBRARY_CONTEXT_CLUES)]
#[case(1, ActiveRadarrBlock::Collections, &COLLECTIONS_CONTEXT_CLUES)]
#[case(2, ActiveRadarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)]
#[case(3, ActiveRadarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)]
#[case(4, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)]
#[case(5, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)]
#[case(6, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)]
fn test_radarr_context_clue_provider_radarr_blocks_context_clues(
#[case] index: usize,
#[case] active_radarr_block: ActiveRadarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.data.radarr_data.main_tabs.set_index(index);
app.push_navigation_stack(active_radarr_block.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, expected_context_clues);
}
}
+10 -10
View File
@@ -3,15 +3,15 @@ mod tests {
use pretty_assertions::assert_eq;
use tokio::sync::mpsc;
use crate::app::radarr::ActiveRadarrBlock;
use crate::app::App;
use crate::app::radarr::ActiveRadarrBlock;
use crate::models::radarr_models::{
AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, Movie, RadarrRelease,
};
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_models::Indexer;
use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkEvent;
use crate::network::radarr_network::RadarrEvent;
#[tokio::test]
async fn test_dispatch_by_blocklist_block() {
@@ -140,7 +140,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -186,7 +186,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -591,7 +591,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -625,7 +625,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -650,7 +650,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
}
@@ -675,7 +675,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(app.should_refresh);
assert!(!app.data.radarr_data.prompt_confirm);
@@ -692,7 +692,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
assert!(app.should_refresh);
@@ -722,7 +722,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
}
+13 -14
View File
@@ -53,7 +53,7 @@ impl App<'_> {
)
.await;
self
.dispatch_network_event(SonarrEvent::GetDownloads.into())
.dispatch_network_event(SonarrEvent::GetDownloads(500).into())
.await;
}
ActiveSonarrBlock::SeasonHistory => {
@@ -94,21 +94,20 @@ impl App<'_> {
.await;
}
ActiveSonarrBlock::ManualEpisodeSearch => {
if let Some(season_details_modal) = self.data.sonarr_data.season_details_modal.as_ref() {
if let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref() {
if episode_details_modal.episode_releases.is_empty() {
self
.dispatch_network_event(
SonarrEvent::GetEpisodeReleases(self.extract_episode_id().await).into(),
)
.await;
}
}
if let Some(season_details_modal) = self.data.sonarr_data.season_details_modal.as_ref()
&& let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref()
&& episode_details_modal.episode_releases.is_empty()
{
self
.dispatch_network_event(
SonarrEvent::GetEpisodeReleases(self.extract_episode_id().await).into(),
)
.await;
}
}
ActiveSonarrBlock::Downloads => {
self
.dispatch_network_event(SonarrEvent::GetDownloads.into())
.dispatch_network_event(SonarrEvent::GetDownloads(500).into())
.await;
}
ActiveSonarrBlock::Blocklist => {
@@ -215,7 +214,7 @@ impl App<'_> {
}
}
if self.tick_count % self.tick_until_poll == 0 {
if self.tick_count.is_multiple_of(self.tick_until_poll) {
self.refresh_sonarr_metadata().await;
}
}
@@ -234,7 +233,7 @@ impl App<'_> {
.dispatch_network_event(SonarrEvent::GetRootFolders.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetDownloads.into())
.dispatch_network_event(SonarrEvent::GetDownloads(500).into())
.await;
self
.dispatch_network_event(SonarrEvent::GetDiskSpace.into())
+100 -14
View File
@@ -1,4 +1,12 @@
use crate::app::{context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS};
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider,
};
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,
};
#[cfg(test)]
#[path = "sonarr_context_clues_tests.rs"]
@@ -9,9 +17,13 @@ pub static ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.esc, "edit search"),
];
pub static SERIES_CONTEXT_CLUES: [ContextClue; 10] = [
pub static SERIES_CONTEXT_CLUES: [ContextClue; 11] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
@@ -75,12 +87,7 @@ pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "episode details"),
(DEFAULT_KEYBINDINGS.delete, "delete episode"),
];
pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
@@ -95,9 +102,11 @@ pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
(DEFAULT_KEYBINDINGS.submit, "episode details"),
(DEFAULT_KEYBINDINGS.delete, "delete episode"),
];
pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
@@ -109,10 +118,11 @@ pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
@@ -122,10 +132,11 @@ pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
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 MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
@@ -135,12 +146,10 @@ pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
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 DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 1] =
[(DEFAULT_KEYBINDINGS.submit, "details")];
pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(
DEFAULT_KEYBINDINGS.refresh,
@@ -153,7 +162,84 @@ pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 4] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub(in crate::app) struct SonarrContextClueProvider;
impl ContextClueProvider for SonarrContextClueProvider {
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() else {
panic!("SonarrContextClueProvider::get_context_clues called with non-Sonarr route");
};
match active_sonarr_block {
_ if SERIES_DETAILS_BLOCKS.contains(&active_sonarr_block) => app
.data
.sonarr_data
.series_info_tabs
.get_active_route_contextual_help(),
_ if SEASON_DETAILS_BLOCKS.contains(&active_sonarr_block) => app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_details_tabs
.get_active_route_contextual_help(),
_ if EPISODE_DETAILS_BLOCKS.contains(&active_sonarr_block) => app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.episode_details_modal
.as_ref()
.unwrap()
.episode_details_tabs
.get_active_route_contextual_help(),
ActiveSonarrBlock::TestAllIndexers
| ActiveSonarrBlock::AddSeriesSearchInput
| ActiveSonarrBlock::AddSeriesEmptySearchResults
| ActiveSonarrBlock::SystemLogs
| ActiveSonarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES),
_ if EDIT_INDEXER_BLOCKS.contains(&active_sonarr_block)
|| INDEXER_SETTINGS_BLOCKS.contains(&active_sonarr_block)
|| EDIT_SERIES_BLOCKS.contains(&active_sonarr_block) =>
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
ActiveSonarrBlock::AddSeriesPrompt
| ActiveSonarrBlock::AddSeriesSelectMonitor
| ActiveSonarrBlock::AddSeriesSelectSeriesType
| ActiveSonarrBlock::AddSeriesSelectQualityProfile
| ActiveSonarrBlock::AddSeriesSelectLanguageProfile
| ActiveSonarrBlock::AddSeriesSelectRootFolder
| ActiveSonarrBlock::AddSeriesTagsInput
| ActiveSonarrBlock::SystemTaskStartConfirmPrompt => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
_ if ADD_SERIES_BLOCKS.contains(&active_sonarr_block) => {
Some(&ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES)
}
ActiveSonarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES),
_ => app
.data
.sonarr_data
.main_tabs
.get_active_route_contextual_help(),
}
}
}
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -16,7 +16,7 @@ mod tests {
},
sonarr_models::{Season, Series, SonarrRelease},
},
network::{sonarr_network::SonarrEvent, NetworkEvent},
network::{NetworkEvent, sonarr_network::SonarrEvent},
};
#[tokio::test]
@@ -107,7 +107,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -366,7 +366,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -604,7 +604,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -642,7 +642,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -667,7 +667,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
}
@@ -692,7 +692,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(app.should_refresh);
assert!(!app.data.sonarr_data.prompt_confirm);
@@ -709,7 +709,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
assert!(app.should_refresh);
@@ -743,7 +743,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
}
+12 -12
View File
@@ -2,16 +2,18 @@
mod tests {
use std::sync::Arc;
use clap::{error::ErrorKind, CommandFactory};
use clap::{CommandFactory, error::ErrorKind};
use mockall::predicate::eq;
use rstest::rstest;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
Cli,
app::App,
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand, sonarr::SonarrCommand},
models::{
Serdeable,
radarr_models::{
BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse,
RadarrSerdeable,
@@ -20,12 +22,10 @@ mod tests {
BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse,
SonarrSerdeable,
},
Serdeable,
},
network::{
radarr_network::RadarrEvent, sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent,
MockNetworkTrait, NetworkEvent, radarr_network::RadarrEvent, sonarr_network::SonarrEvent,
},
Cli,
};
use pretty_assertions::assert_eq;
@@ -33,7 +33,7 @@ mod tests {
fn test_servarr_subcommand_requires_subcommand(#[values("radarr", "sonarr")] subcommand: &str) {
let result = Cli::command().try_get_matches_from(["managarr", subcommand]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
@@ -45,21 +45,21 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "all-indexer-settings"]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_sonarr_subcommand_delegates_to_sonarr() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "series"]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_completions_requires_argument() {
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
@@ -70,7 +70,7 @@ mod tests {
fn test_completions_invalid_argument() {
let result = Cli::command().try_get_matches_from(["managarr", "completions", "test"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -78,7 +78,7 @@ mod tests {
fn test_completions_satisfied_with_argument() {
let result = Cli::command().try_get_matches_from(["managarr", "completions", "bash"]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[rstest]
@@ -141,7 +141,7 @@ mod tests {
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -172,6 +172,6 @@ mod tests {
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{command, Subcommand};
use clap::{Subcommand, command};
use clap_complete::Shell;
use radarr::{RadarrCliHandler, RadarrCommand};
use sonarr::{SonarrCliHandler, SonarrCommand};
+2 -2
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{arg, command, ArgAction, Subcommand};
use clap::{ArgAction, Subcommand, arg, command};
use tokio::sync::Mutex;
use super::RadarrCommand;
@@ -10,7 +10,7 @@ use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::radarr_models::{AddMovieBody, AddMovieOptions, MinimumAvailability, MovieMonitor},
network::{radarr_network::RadarrEvent, NetworkTrait},
network::{NetworkTrait, radarr_network::RadarrEvent},
};
#[cfg(test)]
+44 -39
View File
@@ -1,14 +1,14 @@
#[cfg(test)]
mod tests {
use clap::{error::ErrorKind, CommandFactory, Parser};
use clap::{CommandFactory, Parser, error::ErrorKind};
use crate::{
Cli,
cli::{
radarr::{add_command_handler::RadarrAddCommand, RadarrCommand},
Command,
radarr::{RadarrCommand, add_command_handler::RadarrAddCommand},
},
models::radarr_models::{MinimumAvailability, MovieMonitor},
Cli,
};
use pretty_assertions::assert_eq;
@@ -32,7 +32,7 @@ mod tests {
fn test_add_movie_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "add", "movie"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -52,7 +52,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -72,7 +72,7 @@ mod tests {
"/test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -92,7 +92,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -117,7 +117,7 @@ mod tests {
flag,
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -136,7 +136,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -156,7 +156,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -177,7 +177,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -207,10 +207,11 @@ mod tests {
"1",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
assert_ok!(&result);
let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
@@ -243,10 +244,11 @@ mod tests {
"2",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
assert_ok!(&result);
let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
@@ -285,10 +287,11 @@ mod tests {
"--no-search-for-movie",
]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
assert_ok!(&result);
let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
#[test]
@@ -296,7 +299,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "add", "root-folder"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -318,18 +321,19 @@ mod tests {
"/nfs/test",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::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", "radarr", "add", "tag"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -344,11 +348,12 @@ mod tests {
let result = Cli::try_parse_from(["managarr", "radarr", "add", "tag", "--name", "test"]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type")
};
assert_eq!(add_command, expected_args);
}
}
@@ -357,12 +362,12 @@ mod tests {
use crate::{
app::App,
cli::{radarr::add_command_handler::RadarrAddCommandHandler, CliCommandHandler},
cli::{CliCommandHandler, radarr::add_command_handler::RadarrAddCommandHandler},
models::{
radarr_models::{AddMovieBody, AddMovieOptions, RadarrSerdeable},
Serdeable,
radarr_models::{AddMovieBody, AddMovieOptions, RadarrSerdeable},
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
network::{MockNetworkTrait, NetworkEvent, radarr_network::RadarrEvent},
};
use super::*;
@@ -416,7 +421,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -447,7 +452,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -474,7 +479,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::radarr_models::DeleteMovieParams,
network::{radarr_network::RadarrEvent, NetworkTrait},
network::{NetworkTrait, radarr_network::RadarrEvent},
};
use super::RadarrCommand;
+62 -55
View File
@@ -1,13 +1,13 @@
#[cfg(test)]
mod tests {
use crate::{
cli::{
radarr::{delete_command_handler::RadarrDeleteCommand, RadarrCommand},
Command,
},
Cli,
cli::{
Command,
radarr::{RadarrCommand, delete_command_handler::RadarrDeleteCommand},
},
};
use clap::{error::ErrorKind, CommandFactory, Parser};
use clap::{CommandFactory, Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
#[test]
@@ -30,7 +30,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "delete", "blocklist-item"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -52,12 +52,13 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
@@ -65,7 +66,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "delete", "download"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -85,19 +86,20 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::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", "radarr", "delete", "indexer"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -117,19 +119,20 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_movie_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "delete", "movie"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -147,12 +150,13 @@ mod tests {
let result =
Cli::try_parse_from(["managarr", "radarr", "delete", "movie", "--movie-id", "1"]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
@@ -174,12 +178,13 @@ mod tests {
"--add-list-exclusion",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
@@ -187,7 +192,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "delete", "root-folder"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -207,19 +212,20 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::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", "radarr", "delete", "tag"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -232,12 +238,13 @@ mod tests {
let result = Cli::try_parse_from(["managarr", "radarr", "delete", "tag", "--tag-id", "1"]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
}
@@ -251,14 +258,14 @@ mod tests {
use crate::{
app::App,
cli::{
radarr::delete_command_handler::{RadarrDeleteCommand, RadarrDeleteCommandHandler},
CliCommandHandler,
radarr::delete_command_handler::{RadarrDeleteCommand, RadarrDeleteCommandHandler},
},
models::{
radarr_models::{DeleteMovieParams, RadarrSerdeable},
Serdeable,
radarr_models::{DeleteMovieParams, RadarrSerdeable},
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
network::{MockNetworkTrait, NetworkEvent, radarr_network::RadarrEvent},
};
#[tokio::test]
@@ -289,7 +296,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -315,7 +322,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -341,7 +348,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -375,7 +382,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -401,7 +408,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -427,7 +434,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+3 -3
View File
@@ -6,15 +6,15 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{mutex_flags_or_default, mutex_flags_or_option, CliCommandHandler, Command},
cli::{CliCommandHandler, Command, mutex_flags_or_default, mutex_flags_or_option},
models::{
Serdeable,
radarr_models::{
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability, RadarrSerdeable,
},
servarr_models::EditIndexerParams,
Serdeable,
},
network::{radarr_network::RadarrEvent, NetworkTrait},
network::{NetworkTrait, radarr_network::RadarrEvent},
};
use super::RadarrCommand;
+95 -85
View File
@@ -1,13 +1,13 @@
#[cfg(test)]
mod tests {
use crate::{
cli::{
radarr::{edit_command_handler::RadarrEditCommand, RadarrCommand},
Command,
},
Cli,
cli::{
Command,
radarr::{RadarrCommand, edit_command_handler::RadarrEditCommand},
},
};
use clap::{error::ErrorKind, CommandFactory, Parser};
use clap::{CommandFactory, Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
#[test]
@@ -42,7 +42,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "edit", "all-indexer-settings"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -60,7 +60,7 @@ mod tests {
"--disable-allow-hardcoded-subs",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -75,7 +75,7 @@ mod tests {
"--disable-prefer-indexer-flags",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -99,7 +99,7 @@ mod tests {
flag,
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -126,11 +126,12 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -168,11 +169,12 @@ mod tests {
"test",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -180,7 +182,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "edit", "collection"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -198,7 +200,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -218,7 +220,7 @@ mod tests {
"--disable-monitoring",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -235,7 +237,7 @@ mod tests {
"--disable-search-on-add",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -252,7 +254,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -270,7 +272,7 @@ mod tests {
flag,
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -298,11 +300,12 @@ mod tests {
"/test",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -334,18 +337,19 @@ mod tests {
"--search-on-add",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_indexer_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "edit", "indexer"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -363,7 +367,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -383,7 +387,7 @@ mod tests {
"--disable-rss",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -400,7 +404,7 @@ mod tests {
"--disable-automatic-search",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -417,7 +421,7 @@ mod tests {
"--disable-interactive-search",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -435,7 +439,7 @@ mod tests {
"--clear-tags",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -453,7 +457,7 @@ mod tests {
flag,
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -487,11 +491,12 @@ mod tests {
"Test",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -526,11 +531,12 @@ mod tests {
"2",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -578,18 +584,19 @@ mod tests {
"25",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_movie_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "edit", "movie"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -607,7 +614,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -627,7 +634,7 @@ mod tests {
"--disable-monitoring",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -645,7 +652,7 @@ mod tests {
"--clear-tags",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -669,7 +676,7 @@ mod tests {
flag,
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -686,7 +693,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -714,11 +721,12 @@ mod tests {
"/nfs/test",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -747,11 +755,12 @@ mod tests {
"2",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -787,11 +796,12 @@ mod tests {
"2",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
}
@@ -805,18 +815,18 @@ mod tests {
use crate::{
app::App,
cli::{
radarr::edit_command_handler::{RadarrEditCommand, RadarrEditCommandHandler},
CliCommandHandler,
radarr::edit_command_handler::{RadarrEditCommand, RadarrEditCommandHandler},
},
models::{
Serdeable,
radarr_models::{
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability,
RadarrSerdeable,
},
servarr_models::EditIndexerParams,
Serdeable,
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
network::{MockNetworkTrait, NetworkEvent, radarr_network::RadarrEvent},
};
#[tokio::test]
@@ -887,7 +897,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -958,12 +968,12 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_edit_all_indexer_settings_command_unprovided_values_default_to_previous_values(
) {
async fn test_handle_edit_all_indexer_settings_command_unprovided_values_default_to_previous_values()
{
let expected_edit_all_indexer_settings = IndexerSettings {
allow_hardcoded_subs: true,
availability_delay: 2,
@@ -1030,7 +1040,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -1072,7 +1082,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -1114,7 +1124,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -1156,7 +1166,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -1210,7 +1220,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -1264,7 +1274,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -1318,7 +1328,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -1361,7 +1371,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -1404,7 +1414,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -1447,7 +1457,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+2 -2
View File
@@ -1,13 +1,13 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{command, Subcommand};
use clap::{Subcommand, command};
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{radarr_network::RadarrEvent, NetworkTrait},
network::{NetworkTrait, radarr_network::RadarrEvent},
};
use super::RadarrCommand;
+21 -21
View File
@@ -1,12 +1,12 @@
#[cfg(test)]
mod tests {
use clap::error::ErrorKind;
use clap::CommandFactory;
use clap::error::ErrorKind;
use crate::cli::radarr::get_command_handler::RadarrGetCommand;
use crate::cli::radarr::RadarrCommand;
use crate::cli::Command;
use crate::Cli;
use crate::cli::Command;
use crate::cli::radarr::RadarrCommand;
use crate::cli::radarr::get_command_handler::RadarrGetCommand;
use pretty_assertions::assert_eq;
#[test]
@@ -27,7 +27,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "all-indexer-settings"]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -35,7 +35,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "host-config"]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -43,7 +43,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "movie-details"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -61,7 +61,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -69,7 +69,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "movie-history"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -87,7 +87,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -95,7 +95,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "security-config"]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -103,7 +103,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "get", "system-status"]);
assert!(result.is_ok());
assert_ok!(&result);
}
}
@@ -117,11 +117,11 @@ mod tests {
use crate::{
app::App,
cli::{
radarr::get_command_handler::{RadarrGetCommand, RadarrGetCommandHandler},
CliCommandHandler,
radarr::get_command_handler::{RadarrGetCommand, RadarrGetCommandHandler},
},
models::{radarr_models::RadarrSerdeable, Serdeable},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
models::{Serdeable, radarr_models::RadarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, radarr_network::RadarrEvent},
};
#[tokio::test]
@@ -149,7 +149,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -172,7 +172,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -198,7 +198,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -224,7 +224,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -247,7 +247,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -270,7 +270,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+8 -5
View File
@@ -1,13 +1,13 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{command, Subcommand};
use clap::{Subcommand, command};
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{radarr_network::RadarrEvent, NetworkTrait},
network::{NetworkTrait, radarr_network::RadarrEvent},
};
use super::RadarrCommand;
@@ -23,7 +23,10 @@ pub enum RadarrListCommand {
#[command(about = "List all Radarr collections")]
Collections,
#[command(about = "List all active downloads in Radarr")]
Downloads,
Downloads {
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
count: u64,
},
#[command(about = "List disk space details for all provisioned root folders in Radarr")]
DiskSpace,
#[command(about = "List all Radarr indexers")]
@@ -104,10 +107,10 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Downloads => {
RadarrListCommand::Downloads { count } => {
let resp = self
.network
.handle_network_event(RadarrEvent::GetDownloads.into())
.handle_network_event(RadarrEvent::GetDownloads(count).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
+73 -22
View File
@@ -1,12 +1,12 @@
#[cfg(test)]
mod tests {
use clap::error::ErrorKind;
use clap::CommandFactory;
use clap::error::ErrorKind;
use crate::cli::radarr::list_command_handler::RadarrListCommand;
use crate::cli::radarr::RadarrCommand;
use crate::cli::Command;
use crate::Cli;
use crate::cli::Command;
use crate::cli::radarr::RadarrCommand;
use crate::cli::radarr::list_command_handler::RadarrListCommand;
use pretty_assertions::assert_eq;
#[test]
@@ -29,7 +29,6 @@ mod tests {
#[values(
"blocklist",
"collections",
"downloads",
"disk-space",
"indexers",
"movies",
@@ -44,7 +43,7 @@ mod tests {
) {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "list", subcommand]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -52,19 +51,28 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "list", "movie-credits"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_downloads_count_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "list", "downloads", "--count"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_logs_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "list", "logs", "--events"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -80,11 +88,27 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::List(credits_command))) = result.unwrap().command {
assert_eq!(credits_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::List(credits_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(credits_command, expected_args);
}
#[test]
fn test_list_downloads_default_values() {
let expected_args = RadarrListCommand::Downloads { count: 500 };
let result = Cli::try_parse_from(["managarr", "radarr", "list", "downloads"]);
assert_ok!(&result);
let Some(Command::Radarr(RadarrCommand::List(refresh_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(refresh_command, expected_args);
}
#[test]
@@ -95,11 +119,13 @@ mod tests {
};
let result = Cli::try_parse_from(["managarr", "radarr", "list", "logs"]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::List(refresh_command))) = result.unwrap().command {
assert_eq!(refresh_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::List(refresh_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(refresh_command, expected_args);
}
}
@@ -115,14 +141,13 @@ mod tests {
use crate::{
app::App,
cli::radarr::list_command_handler::{RadarrListCommand, RadarrListCommandHandler},
models::{radarr_models::RadarrSerdeable, Serdeable},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
models::{Serdeable, radarr_models::RadarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, radarr_network::RadarrEvent},
};
#[rstest]
#[case(RadarrListCommand::Blocklist, RadarrEvent::GetBlocklist)]
#[case(RadarrListCommand::Collections, RadarrEvent::GetCollections)]
#[case(RadarrListCommand::Downloads, RadarrEvent::GetDownloads)]
#[case(RadarrListCommand::DiskSpace, RadarrEvent::GetDiskSpace)]
#[case(RadarrListCommand::Indexers, RadarrEvent::GetIndexers)]
#[case(RadarrListCommand::Movies, RadarrEvent::GetMovies)]
@@ -153,7 +178,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -179,7 +204,33 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
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>(
RadarrEvent::GetDownloads(expected_count).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_downloads_command = RadarrListCommand::Downloads { count: 1000 };
let result =
RadarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
@@ -207,7 +258,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+19 -1
View File
@@ -13,8 +13,8 @@ use crate::app::App;
use crate::cli::CliCommandHandler;
use crate::models::radarr_models::{RadarrReleaseDownloadBody, RadarrTaskName};
use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkTrait;
use crate::network::radarr_network::RadarrEvent;
use anyhow::Result;
use super::Command;
@@ -118,6 +118,17 @@ pub enum RadarrCommand {
},
#[command(about = "Test all Radarr indexers")]
TestAllIndexers,
#[command(
about = "Toggle monitoring for the specified movie corresponding to the given movie ID"
)]
ToggleMovieMonitoring {
#[arg(
long,
help = "The Radarr ID of the movie to toggle monitoring on",
required = true
)]
movie_id: i64,
},
#[command(about = "Trigger an automatic search for the movie with the specified ID")]
TriggerAutomaticSearch {
#[arg(
@@ -250,6 +261,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::ToggleMovieMonitoring { movie_id } => {
let resp = self
.network
.handle_network_event(RadarrEvent::ToggleMovieMonitoring(movie_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::TriggerAutomaticSearch { movie_id } => {
let resp = self
.network
+92 -41
View File
@@ -1,11 +1,11 @@
#[cfg(test)]
mod tests {
use clap::error::ErrorKind;
use clap::CommandFactory;
use clap::error::ErrorKind;
use crate::cli::radarr::RadarrCommand;
use crate::cli::Command;
use crate::Cli;
use crate::cli::Command;
use crate::cli::radarr::RadarrCommand;
use pretty_assertions::assert_eq;
#[test]
@@ -28,7 +28,7 @@ mod tests {
) {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", subcommand]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -43,7 +43,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -62,7 +62,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -81,7 +81,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -102,14 +102,14 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_manual_search_requires_movie_id() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "manual-search"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -126,14 +126,14 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_search_new_movie_requires_query() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "search-new-movie"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -150,14 +150,14 @@ mod tests {
"halo",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_start_task_requires_task_name() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "start-task"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -174,7 +174,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -188,14 +188,14 @@ mod tests {
"application-check-update",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_test_indexer_requires_indexer_id() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "test-indexer"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -212,7 +212,32 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_toggle_movie_monitoring_requires_movie_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "toggle-movie-monitoring"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_movie_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"toggle-movie-monitoring",
"--movie-id",
"1",
]);
assert_ok!(&result);
}
#[test]
@@ -220,7 +245,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "trigger-automatic-search"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -237,7 +262,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
}
@@ -251,22 +276,22 @@ mod tests {
use crate::{
app::App,
cli::{
radarr::{
add_command_handler::RadarrAddCommand, delete_command_handler::RadarrDeleteCommand,
edit_command_handler::RadarrEditCommand, get_command_handler::RadarrGetCommand,
list_command_handler::RadarrListCommand, refresh_command_handler::RadarrRefreshCommand,
RadarrCliHandler, RadarrCommand,
},
CliCommandHandler,
radarr::{
RadarrCliHandler, RadarrCommand, add_command_handler::RadarrAddCommand,
delete_command_handler::RadarrDeleteCommand, edit_command_handler::RadarrEditCommand,
get_command_handler::RadarrGetCommand, list_command_handler::RadarrListCommand,
refresh_command_handler::RadarrRefreshCommand,
},
},
models::{
Serdeable,
radarr_models::{
BlocklistItem, BlocklistResponse, IndexerSettings, RadarrReleaseDownloadBody,
RadarrSerdeable, RadarrTaskName,
},
Serdeable,
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
network::{MockNetworkTrait, NetworkEvent, radarr_network::RadarrEvent},
};
#[tokio::test]
@@ -299,7 +324,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -332,7 +357,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -357,7 +382,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -384,7 +409,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -411,7 +436,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -436,7 +461,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -458,7 +483,33 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
async fn test_toggle_movie_monitoring_command() {
let expected_movie_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::ToggleMovieMonitoring(expected_movie_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let toggle_movie_monitoring_command = RadarrCommand::ToggleMovieMonitoring { movie_id: 1 };
let result =
RadarrCliHandler::with(&app_arc, toggle_movie_monitoring_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
@@ -487,7 +538,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -514,7 +565,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -543,7 +594,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -615,7 +666,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -644,7 +695,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -670,7 +721,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -696,7 +747,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{radarr_network::RadarrEvent, NetworkTrait},
network::{NetworkTrait, radarr_network::RadarrEvent},
};
use super::RadarrCommand;
+16 -16
View File
@@ -1,12 +1,12 @@
#[cfg(test)]
mod tests {
use clap::error::ErrorKind;
use clap::CommandFactory;
use clap::error::ErrorKind;
use crate::cli::radarr::refresh_command_handler::RadarrRefreshCommand;
use crate::cli::radarr::RadarrCommand;
use crate::cli::Command;
use crate::Cli;
use crate::cli::Command;
use crate::cli::radarr::RadarrCommand;
use crate::cli::radarr::refresh_command_handler::RadarrRefreshCommand;
use pretty_assertions::assert_eq;
#[test]
@@ -31,14 +31,14 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "refresh", subcommand]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_refresh_movie_requires_movie_id() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "refresh", "movie"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -51,13 +51,13 @@ mod tests {
let result =
Cli::try_parse_from(["managarr", "radarr", "refresh", "movie", "--movie-id", "1"]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Radarr(RadarrCommand::Refresh(refresh_command))) =
result.unwrap().command
{
assert_eq!(refresh_command, expected_args);
}
let Some(Command::Radarr(RadarrCommand::Refresh(refresh_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(refresh_command, expected_args);
}
}
@@ -73,8 +73,8 @@ mod tests {
use crate::{
app::App,
cli::radarr::refresh_command_handler::{RadarrRefreshCommand, RadarrRefreshCommandHandler},
models::{radarr_models::RadarrSerdeable, Serdeable},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
models::{Serdeable, radarr_models::RadarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, radarr_network::RadarrEvent},
};
#[rstest]
@@ -102,7 +102,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -128,7 +128,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::sonarr_models::{AddSeriesBody, AddSeriesOptions, SeriesMonitor, SeriesType},
network::{sonarr_network::SonarrEvent, NetworkTrait},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
#[cfg(test)]
+48 -43
View File
@@ -1,14 +1,14 @@
#[cfg(test)]
mod tests {
use clap::{error::ErrorKind, CommandFactory, Parser};
use clap::{CommandFactory, Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use crate::{
cli::{
sonarr::{add_command_handler::SonarrAddCommand, SonarrCommand},
Command,
},
Cli,
cli::{
Command,
sonarr::{SonarrCommand, add_command_handler::SonarrAddCommand},
},
};
#[test]
@@ -34,7 +34,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "add", "root-folder"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -56,18 +56,19 @@ mod tests {
"/nfs/test",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(add_command, expected_args);
}
#[test]
fn test_add_series_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "add", "series"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -91,7 +92,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -115,7 +116,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -139,7 +140,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -163,7 +164,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -187,7 +188,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -216,7 +217,7 @@ mod tests {
flag,
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -239,7 +240,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -263,7 +264,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -288,7 +289,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -325,10 +326,11 @@ mod tests {
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
assert_ok!(&result);
let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(add_command, expected_args);
}
#[test]
@@ -368,10 +370,11 @@ mod tests {
"2",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
assert_ok!(&result);
let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(add_command, expected_args);
}
#[test]
@@ -418,17 +421,18 @@ mod tests {
"--no-search-for-series",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
assert_ok!(&result);
let Some(Command::Sonarr(SonarrCommand::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", "sonarr", "add", "tag"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -443,11 +447,12 @@ mod tests {
let result = Cli::try_parse_from(["managarr", "sonarr", "add", "tag", "--name", "test"]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(add_command, expected_args);
}
}
@@ -456,14 +461,14 @@ mod tests {
use crate::{
app::App,
cli::{sonarr::add_command_handler::SonarrAddCommandHandler, CliCommandHandler},
cli::{CliCommandHandler, sonarr::add_command_handler::SonarrAddCommandHandler},
models::{
Serdeable,
sonarr_models::{
AddSeriesBody, AddSeriesOptions, SeriesMonitor, SeriesType, SonarrSerdeable,
},
Serdeable,
},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
};
use super::*;
@@ -501,7 +506,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -554,7 +559,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -581,7 +586,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::sonarr_models::DeleteSeriesParams,
network::{sonarr_network::SonarrEvent, NetworkTrait},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
use super::SonarrCommand;
+70 -62
View File
@@ -1,13 +1,13 @@
#[cfg(test)]
mod tests {
use crate::{
cli::{
sonarr::{delete_command_handler::SonarrDeleteCommand, SonarrCommand},
Command,
},
Cli,
cli::{
Command,
sonarr::{SonarrCommand, delete_command_handler::SonarrDeleteCommand},
},
};
use clap::{error::ErrorKind, CommandFactory, Parser};
use clap::{CommandFactory, Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
#[test]
@@ -30,7 +30,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "blocklist-item"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -52,12 +52,13 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
@@ -65,7 +66,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "download"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -85,12 +86,13 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
@@ -98,7 +100,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "episode-file"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -118,19 +120,20 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::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", "sonarr", "delete", "indexer"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -150,12 +153,13 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
@@ -163,7 +167,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "root-folder"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -183,19 +187,20 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_series_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "series"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -213,12 +218,13 @@ mod tests {
let result =
Cli::try_parse_from(["managarr", "sonarr", "delete", "series", "--series-id", "1"]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
#[test]
@@ -240,19 +246,20 @@ mod tests {
"--add-list-exclusion",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::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", "sonarr", "delete", "tag"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -265,12 +272,13 @@ mod tests {
let result = Cli::try_parse_from(["managarr", "sonarr", "delete", "tag", "--tag-id", "1"]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(delete_command, expected_args);
}
}
@@ -284,14 +292,14 @@ mod tests {
use crate::{
app::App,
cli::{
sonarr::delete_command_handler::{SonarrDeleteCommand, SonarrDeleteCommandHandler},
CliCommandHandler,
sonarr::delete_command_handler::{SonarrDeleteCommand, SonarrDeleteCommandHandler},
},
models::{
sonarr_models::{DeleteSeriesParams, SonarrSerdeable},
Serdeable,
sonarr_models::{DeleteSeriesParams, SonarrSerdeable},
},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
};
#[tokio::test]
@@ -322,7 +330,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -348,7 +356,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -374,7 +382,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -400,7 +408,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -426,7 +434,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -460,7 +468,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -486,7 +494,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::sonarr_models::SonarrReleaseDownloadBody,
network::{sonarr_network::SonarrEvent, NetworkTrait},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
use super::SonarrCommand;
@@ -1,11 +1,11 @@
#[cfg(test)]
mod tests {
use crate::{
cli::{
sonarr::{download_command_handler::SonarrDownloadCommand, SonarrCommand},
Command,
},
Cli,
cli::{
Command,
sonarr::{SonarrCommand, download_command_handler::SonarrDownloadCommand},
},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
@@ -41,7 +41,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -61,7 +61,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -81,7 +81,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -103,7 +103,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -121,7 +121,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -143,7 +143,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -165,7 +165,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -187,7 +187,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -211,7 +211,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -227,7 +227,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -247,7 +247,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -267,7 +267,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -289,7 +289,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
}
@@ -303,14 +303,14 @@ mod tests {
use crate::{
app::App,
cli::{
sonarr::download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler},
CliCommandHandler,
sonarr::download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler},
},
models::{
sonarr_models::{SonarrReleaseDownloadBody, SonarrSerdeable},
Serdeable,
sonarr_models::{SonarrReleaseDownloadBody, SonarrSerdeable},
},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
};
#[tokio::test]
@@ -345,7 +345,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -382,7 +382,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -417,7 +417,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+3 -3
View File
@@ -6,13 +6,13 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{mutex_flags_or_option, CliCommandHandler, Command},
cli::{CliCommandHandler, Command, mutex_flags_or_option},
models::{
Serdeable,
servarr_models::EditIndexerParams,
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
Serdeable,
},
network::{sonarr_network::SonarrEvent, NetworkTrait},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
use super::SonarrCommand;
+67 -59
View File
@@ -1,8 +1,8 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{edit_command_handler::SonarrEditCommand, SonarrCommand},
Command,
sonarr::{SonarrCommand, edit_command_handler::SonarrEditCommand},
};
#[test]
@@ -20,10 +20,10 @@ mod tests {
}
mod cli {
use crate::{models::sonarr_models::SeriesType, Cli};
use crate::{Cli, models::sonarr_models::SeriesType};
use super::*;
use clap::{error::ErrorKind, CommandFactory, Parser};
use clap::{CommandFactory, Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
@@ -32,7 +32,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "all-indexer-settings"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -57,7 +57,7 @@ mod tests {
flag,
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -78,11 +78,12 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -108,18 +109,19 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_indexer_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "indexer"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -137,7 +139,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -157,7 +159,7 @@ mod tests {
"--disable-rss",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -174,7 +176,7 @@ mod tests {
"--disable-automatic-search",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -191,7 +193,7 @@ mod tests {
"--disable-interactive-search",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -209,7 +211,7 @@ mod tests {
"--clear-tags",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -227,7 +229,7 @@ mod tests {
flag,
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -261,11 +263,12 @@ mod tests {
"Test",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -300,11 +303,12 @@ mod tests {
"2",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -352,18 +356,19 @@ mod tests {
"25",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
fn test_edit_series_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "series"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -381,7 +386,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -401,7 +406,7 @@ mod tests {
"--disable-monitoring",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -418,7 +423,7 @@ mod tests {
"--disable-season-folders",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -436,7 +441,7 @@ mod tests {
"--clear-tags",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
@@ -461,7 +466,7 @@ mod tests {
flag,
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -478,7 +483,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -509,11 +514,12 @@ mod tests {
"/nfs/test",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -545,11 +551,12 @@ mod tests {
"2",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
#[test]
@@ -591,11 +598,12 @@ mod tests {
"2",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(edit_command, expected_args);
}
}
@@ -609,15 +617,15 @@ mod tests {
use crate::{
app::App,
cli::{
sonarr::edit_command_handler::{SonarrEditCommand, SonarrEditCommandHandler},
CliCommandHandler,
sonarr::edit_command_handler::{SonarrEditCommand, SonarrEditCommandHandler},
},
models::{
Serdeable,
servarr_models::EditIndexerParams,
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
Serdeable,
},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
};
#[tokio::test]
@@ -674,7 +682,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -728,7 +736,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -776,7 +784,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -824,7 +832,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -872,7 +880,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{sonarr_network::SonarrEvent, NetworkTrait},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
use super::SonarrCommand;
+21 -21
View File
@@ -1,10 +1,10 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{get_command_handler::SonarrGetCommand, SonarrCommand},
Command,
};
use crate::Cli;
use crate::cli::{
Command,
sonarr::{SonarrCommand, get_command_handler::SonarrGetCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
@@ -28,7 +28,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "all-indexer-settings"]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -36,7 +36,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "system-status"]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -44,7 +44,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "episode-details"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -62,7 +62,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -70,7 +70,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "host-config"]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -78,7 +78,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "security-config"]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -86,7 +86,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "series-details"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -104,7 +104,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
}
@@ -118,11 +118,11 @@ mod tests {
use crate::{
app::App,
cli::{
sonarr::get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler},
CliCommandHandler,
sonarr::get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler},
},
models::{sonarr_models::SonarrSerdeable, Serdeable},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
models::{Serdeable, sonarr_models::SonarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
};
#[tokio::test]
@@ -150,7 +150,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -176,7 +176,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -199,7 +199,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -222,7 +222,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -248,7 +248,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -271,7 +271,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+7 -4
View File
@@ -7,7 +7,7 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{sonarr_network::SonarrEvent, NetworkTrait},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
use super::SonarrCommand;
@@ -21,7 +21,10 @@ pub enum SonarrListCommand {
#[command(about = "List all items in the Sonarr blocklist")]
Blocklist,
#[command(about = "List all active downloads in Sonarr")]
Downloads,
Downloads {
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
count: u64,
},
#[command(about = "List disk space details for all provisioned root folders in Sonarr")]
DiskSpace,
#[command(about = "List the episodes for the series with the given ID")]
@@ -146,10 +149,10 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::Downloads => {
SonarrListCommand::Downloads { count } => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetDownloads.into())
.handle_network_event(SonarrEvent::GetDownloads(count).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
+109 -54
View File
@@ -1,10 +1,10 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{list_command_handler::SonarrListCommand, SonarrCommand},
Command,
};
use crate::Cli;
use crate::cli::{
Command,
sonarr::{SonarrCommand, list_command_handler::SonarrListCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
@@ -19,7 +19,7 @@ mod tests {
mod cli {
use super::*;
use clap::{error::ErrorKind, Parser};
use clap::{Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
@@ -28,7 +28,6 @@ mod tests {
#[values(
"blocklist",
"series",
"downloads",
"disk-space",
"quality-profiles",
"indexers",
@@ -43,14 +42,14 @@ mod tests {
) {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", subcommand]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_list_episodes_requires_series_id() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "episodes"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -62,7 +61,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "episode-files"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -74,7 +73,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "episode-history"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -93,13 +92,37 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::List(episode_history_command))) =
let Some(Command::Sonarr(SonarrCommand::List(episode_history_command))) =
result.unwrap().command
{
assert_eq!(episode_history_command, expected_args);
}
else {
panic!("Unexpected command type");
};
assert_eq!(episode_history_command, expected_args);
}
#[test]
fn test_list_downloads_count_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "downloads", "--count"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_downloads_default_values() {
let expected_args = SonarrListCommand::Downloads { count: 500 };
let result = Cli::try_parse_from(["managarr", "sonarr", "list", "downloads"]);
assert_ok!(&result);
let Some(Command::Sonarr(SonarrCommand::List(downloads_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(downloads_command, expected_args);
}
#[test]
@@ -107,7 +130,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "history", "--events"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -116,11 +139,13 @@ mod tests {
let expected_args = SonarrListCommand::History { events: 500 };
let result = Cli::try_parse_from(["managarr", "sonarr", "list", "history"]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::List(history_command))) = result.unwrap().command {
assert_eq!(history_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::List(history_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(history_command, expected_args);
}
#[test]
@@ -128,7 +153,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "logs", "--events"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -140,11 +165,12 @@ mod tests {
};
let result = Cli::try_parse_from(["managarr", "sonarr", "list", "logs"]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::List(logs_command))) = result.unwrap().command {
assert_eq!(logs_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::List(logs_command))) = result.unwrap().command else {
panic!("Unexpected command type");
};
assert_eq!(logs_command, expected_args);
}
#[test]
@@ -153,12 +179,13 @@ mod tests {
let result =
Cli::try_parse_from(["managarr", "sonarr", "list", "episodes", "--series-id", "1"]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::List(episodes_command))) = result.unwrap().command
{
assert_eq!(episodes_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::List(episodes_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(episodes_command, expected_args);
}
#[test]
@@ -173,13 +200,14 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::List(episode_files_command))) =
let Some(Command::Sonarr(SonarrCommand::List(episode_files_command))) =
result.unwrap().command
{
assert_eq!(episode_files_command, expected_args);
}
else {
panic!("Unexpected command type");
};
assert_eq!(episode_files_command, expected_args);
}
#[test]
@@ -193,7 +221,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -211,7 +239,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -231,7 +259,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -239,7 +267,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "series-history"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -258,11 +286,13 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::List(series_command))) = result.unwrap().command {
assert_eq!(series_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::List(series_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(series_command, expected_args);
}
}
@@ -275,10 +305,10 @@ mod tests {
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::sonarr::list_command_handler::{SonarrListCommand, SonarrListCommandHandler};
use crate::cli::CliCommandHandler;
use crate::models::sonarr_models::SonarrSerdeable;
use crate::cli::sonarr::list_command_handler::{SonarrListCommand, SonarrListCommandHandler};
use crate::models::Serdeable;
use crate::models::sonarr_models::SonarrSerdeable;
use crate::network::sonarr_network::SonarrEvent;
use crate::{
app::App,
@@ -287,7 +317,6 @@ mod tests {
#[rstest]
#[case(SonarrListCommand::Blocklist, SonarrEvent::GetBlocklist)]
#[case(SonarrListCommand::Downloads, SonarrEvent::GetDownloads)]
#[case(SonarrListCommand::DiskSpace, SonarrEvent::GetDiskSpace)]
#[case(SonarrListCommand::Indexers, SonarrEvent::GetIndexers)]
#[case(SonarrListCommand::QualityProfiles, SonarrEvent::GetQualityProfiles)]
@@ -319,7 +348,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -345,7 +374,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -371,7 +400,33 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
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>(
SonarrEvent::GetDownloads(expected_count).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_downloads_command = SonarrListCommand::Downloads { count: 1000 };
let result =
SonarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
@@ -397,7 +452,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -425,7 +480,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -451,7 +506,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -477,7 +532,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -507,7 +562,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
@@ -7,7 +7,7 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{sonarr_network::SonarrEvent, NetworkTrait},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
use super::SonarrCommand;
@@ -1,10 +1,10 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{manual_search_command_handler::SonarrManualSearchCommand, SonarrCommand},
Command,
};
use crate::Cli;
use crate::cli::{
Command,
sonarr::{SonarrCommand, manual_search_command_handler::SonarrManualSearchCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
@@ -36,7 +36,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -54,7 +54,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -74,7 +74,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -82,7 +82,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "manual-search", "episode"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -100,7 +100,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
}
@@ -114,13 +114,13 @@ mod tests {
use crate::{
app::App,
cli::{
CliCommandHandler,
sonarr::manual_search_command_handler::{
SonarrManualSearchCommand, SonarrManualSearchCommandHandler,
},
CliCommandHandler,
},
models::{sonarr_models::SonarrSerdeable, Serdeable},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
models::{Serdeable, sonarr_models::SonarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
};
#[tokio::test]
@@ -149,7 +149,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -182,7 +182,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+19 -1
View File
@@ -18,7 +18,7 @@ use trigger_automatic_search_command_handler::{
use crate::{
app::App,
models::sonarr_models::SonarrTaskName,
network::{sonarr_network::SonarrEvent, NetworkTrait},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
use super::{CliCommandHandler, Command};
@@ -146,6 +146,17 @@ pub enum SonarrCommand {
)]
season_number: i64,
},
#[command(
about = "Toggle monitoring for the specified series corresponding to the given series ID"
)]
ToggleSeriesMonitoring {
#[arg(
long,
help = "The Sonarr ID of the series to toggle monitoring on",
required = true
)]
series_id: i64,
},
}
impl From<SonarrCommand> for Command {
@@ -290,6 +301,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrCommand::ToggleSeriesMonitoring { series_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::ToggleSeriesMonitoring(series_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
+1 -1
View File
@@ -6,7 +6,7 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{sonarr_network::SonarrEvent, NetworkTrait},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
use super::SonarrCommand;
+17 -17
View File
@@ -2,11 +2,11 @@
mod tests {
use pretty_assertions::assert_eq;
use crate::cli::{
sonarr::{refresh_command_handler::SonarrRefreshCommand, SonarrCommand},
Command,
};
use crate::Cli;
use crate::cli::{
Command,
sonarr::{SonarrCommand, refresh_command_handler::SonarrRefreshCommand},
};
use clap::CommandFactory;
#[test]
@@ -20,7 +20,7 @@ mod tests {
mod cli {
use super::*;
use clap::{error::ErrorKind, Parser};
use clap::{Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
@@ -31,14 +31,14 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "refresh", subcommand]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_refresh_series_requires_series_id() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "refresh", "series"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -57,13 +57,13 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
if let Some(Command::Sonarr(SonarrCommand::Refresh(refresh_command))) =
result.unwrap().command
{
assert_eq!(refresh_command, expected_args);
}
let Some(Command::Sonarr(SonarrCommand::Refresh(refresh_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(refresh_command, expected_args);
}
}
@@ -77,11 +77,11 @@ mod tests {
use crate::{app::App, cli::sonarr::refresh_command_handler::SonarrRefreshCommandHandler};
use crate::{
cli::{sonarr::refresh_command_handler::SonarrRefreshCommand, CliCommandHandler},
cli::{CliCommandHandler, sonarr::refresh_command_handler::SonarrRefreshCommand},
network::sonarr_network::SonarrEvent,
};
use crate::{
models::{sonarr_models::SonarrSerdeable, Serdeable},
models::{Serdeable, sonarr_models::SonarrSerdeable},
network::{MockNetworkTrait, NetworkEvent},
};
@@ -109,7 +109,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -135,7 +135,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+101 -47
View File
@@ -1,10 +1,10 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{list_command_handler::SonarrListCommand, SonarrCommand},
Command,
};
use crate::Cli;
use crate::cli::{
Command,
sonarr::{SonarrCommand, list_command_handler::SonarrListCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
@@ -29,7 +29,7 @@ mod tests {
) {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", subcommand]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -37,7 +37,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "mark-history-item-as-failed"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -54,14 +54,14 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_search_new_series_requires_query() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "search-new-series"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -78,14 +78,14 @@ mod tests {
"halo",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_start_task_requires_task_name() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "start-task"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -102,7 +102,7 @@ mod tests {
"test",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
@@ -116,14 +116,14 @@ mod tests {
"application-update-check",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_test_indexer_requires_indexer_id() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "test-indexer"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -140,7 +140,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -148,7 +148,7 @@ mod tests {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "toggle-episode-monitoring"]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -165,7 +165,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -178,7 +178,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -195,7 +195,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -214,7 +214,32 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
fn test_toggle_series_monitoring_requires_series_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "toggle-series-monitoring"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_series_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"toggle-series-monitoring",
"--series-id",
"1",
]);
assert_ok!(&result);
}
}
@@ -228,25 +253,25 @@ mod tests {
use crate::{
app::App,
cli::{
CliCommandHandler,
sonarr::{
add_command_handler::SonarrAddCommand, delete_command_handler::SonarrDeleteCommand,
SonarrCliHandler, SonarrCommand, add_command_handler::SonarrAddCommand,
delete_command_handler::SonarrDeleteCommand,
download_command_handler::SonarrDownloadCommand, edit_command_handler::SonarrEditCommand,
get_command_handler::SonarrGetCommand, list_command_handler::SonarrListCommand,
manual_search_command_handler::SonarrManualSearchCommand,
refresh_command_handler::SonarrRefreshCommand,
trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand,
SonarrCliHandler, SonarrCommand,
},
CliCommandHandler,
},
models::{
Serdeable,
sonarr_models::{
BlocklistItem, BlocklistResponse, IndexerSettings, Series, SonarrReleaseDownloadBody,
SonarrSerdeable, SonarrTaskName,
},
Serdeable,
},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
};
#[tokio::test]
@@ -279,7 +304,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -309,7 +334,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -336,7 +361,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -365,7 +390,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -401,7 +426,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -459,12 +484,12 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
async fn test_sonarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler(
) {
async fn test_sonarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler()
{
let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
@@ -487,12 +512,12 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
async fn test_sonarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler(
) {
async fn test_sonarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler()
{
let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
@@ -517,7 +542,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -539,7 +564,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -561,7 +586,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -587,7 +612,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -614,7 +639,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -641,7 +666,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -666,7 +691,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -688,11 +713,11 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
async fn test_list_toggle_episode_monitoring_command() {
async fn test_toggle_episode_monitoring_command() {
let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
@@ -718,11 +743,11 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
async fn test_list_toggle_season_monitoring_command() {
async fn test_toggle_season_monitoring_command() {
let expected_series_id = 1;
let expected_season_number = 1;
let mut mock_network = MockNetworkTrait::new();
@@ -751,7 +776,36 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
async fn test_toggle_series_monitoring_command() {
let expected_series_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::ToggleSeriesMonitoring(expected_series_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let toggle_series_monitoring_command = SonarrCommand::ToggleSeriesMonitoring { series_id: 1 };
let result = SonarrCliHandler::with(
&app_arc,
toggle_series_monitoring_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
}
}
@@ -7,7 +7,7 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{sonarr_network::SonarrEvent, NetworkTrait},
network::{NetworkTrait, sonarr_network::SonarrEvent},
};
use super::SonarrCommand;
@@ -1,12 +1,12 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{
trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand, SonarrCommand,
},
Command,
};
use crate::Cli;
use crate::cli::{
Command,
sonarr::{
SonarrCommand, trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand,
},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
@@ -36,7 +36,7 @@ mod tests {
"series",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -54,7 +54,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -68,7 +68,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -86,7 +86,7 @@ mod tests {
"1",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -106,7 +106,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
#[test]
@@ -118,7 +118,7 @@ mod tests {
"episode",
]);
assert!(result.is_err());
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
@@ -136,7 +136,7 @@ mod tests {
"1",
]);
assert!(result.is_ok());
assert_ok!(&result);
}
}
@@ -150,13 +150,13 @@ mod tests {
use crate::{
app::App,
cli::{
CliCommandHandler,
sonarr::trigger_automatic_search_command_handler::{
SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler,
},
CliCommandHandler,
},
models::{sonarr_models::SonarrSerdeable, Serdeable},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
models::{Serdeable, sonarr_models::SonarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent},
};
#[tokio::test]
@@ -186,7 +186,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -220,7 +220,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
#[tokio::test]
@@ -250,7 +250,7 @@ mod tests {
.handle()
.await;
assert!(result.is_ok());
assert_ok!(&result);
}
}
}
+13 -5
View File
@@ -1,10 +1,11 @@
use anyhow::Result;
use std::sync::mpsc;
use std::sync::mpsc::Receiver;
use std::thread;
use std::time::{Duration, Instant};
use crossterm::event;
use crossterm::event::Event as CrosstermEvent;
use crossterm::event::{Event as CrosstermEvent, KeyEventKind};
use crate::event::Key;
@@ -28,8 +29,12 @@ impl Events {
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout).unwrap() {
if let CrosstermEvent::Key(key_event) = event::read().unwrap() {
if event::poll(timeout).unwrap()
&& let CrosstermEvent::Key(key_event) = event::read().unwrap()
{
// Only process the key event if it's a press event
// Source: https://ratatui.rs/faq/ Why am I getting duplicate key events on Windows?
if key_event.kind == KeyEventKind::Press {
let key = Key::from(key_event);
tx.send(InputEvent::KeyEvent(key)).unwrap();
}
@@ -45,7 +50,10 @@ impl Events {
Events { rx }
}
pub fn next(&self) -> Result<InputEvent<Key>, mpsc::RecvError> {
self.rx.recv()
pub fn next(&self) -> Result<Option<InputEvent<Key>>> {
match self.rx.try_recv() {
Ok(event) => Ok(Some(event)),
_ => Ok(None),
}
}
}
+27 -15
View File
@@ -13,6 +13,8 @@ pub enum Key {
Down,
Left,
Right,
PgDown,
PgUp,
Enter,
Esc,
Backspace,
@@ -29,21 +31,23 @@ pub enum Key {
impl Display for Key {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Key::Char(c) => write!(f, "<{c}>"),
Key::Ctrl(c) => write!(f, "<ctrl-{c}>"),
Key::Up => write!(f, "<>"),
Key::Down => write!(f, "<>"),
Key::Left => write!(f, "<>"),
Key::Right => write!(f, "<>"),
Key::Enter => write!(f, "<enter>"),
Key::Esc => write!(f, "<esc>"),
Key::Backspace => write!(f, "<backspace>"),
Key::Home => write!(f, "<home>"),
Key::End => write!(f, "<end>"),
Key::Tab => write!(f, "<tab>"),
Key::BackTab => write!(f, "<shift-tab>"),
Key::Delete => write!(f, "<del>"),
_ => write!(f, "<{self:?}>"),
Key::Char(c) => write!(f, "{c}"),
Key::Ctrl(c) => write!(f, "ctrl-{c}"),
Key::Up => write!(f, ""),
Key::Down => write!(f, ""),
Key::Left => write!(f, ""),
Key::Right => write!(f, ""),
Key::PgDown => write!(f, "pgDown"),
Key::PgUp => write!(f, "pgUp"),
Key::Enter => write!(f, "enter"),
Key::Esc => write!(f, "esc"),
Key::Backspace => write!(f, "backspace"),
Key::Home => write!(f, "home"),
Key::End => write!(f, "end"),
Key::Tab => write!(f, "tab"),
Key::BackTab => write!(f, "shift-tab"),
Key::Delete => write!(f, "del"),
_ => write!(f, "{self:?}"),
}
}
}
@@ -66,6 +70,14 @@ impl From<KeyEvent> for Key {
code: KeyCode::Right,
..
} => Key::Right,
KeyEvent {
code: KeyCode::PageDown,
..
} => Key::PgDown,
KeyEvent {
code: KeyCode::PageUp,
..
} => Key::PgUp,
KeyEvent {
code: KeyCode::Backspace,
..
+13 -1
View File
@@ -11,6 +11,8 @@ mod tests {
#[case(Key::Down, "")]
#[case(Key::Left, "")]
#[case(Key::Right, "")]
#[case(Key::PgDown, "pgDown")]
#[case(Key::PgUp, "pgUp")]
#[case(Key::Enter, "enter")]
#[case(Key::Esc, "esc")]
#[case(Key::Backspace, "backspace")]
@@ -22,7 +24,7 @@ mod tests {
#[case(Key::Char('q'), "q")]
#[case(Key::Ctrl('q'), "ctrl-q")]
fn test_key_formatter(#[case] key: Key, #[case] expected_str: &str) {
assert_str_eq!(format!("{key}"), format!("<{expected_str}>"));
assert_str_eq!(format!("{key}"), format!("{expected_str}"));
}
#[test]
@@ -45,6 +47,16 @@ mod tests {
assert_eq!(Key::from(KeyEvent::from(KeyCode::Right)), Key::Right);
}
#[test]
fn test_key_from_page_down() {
assert_eq!(Key::from(KeyEvent::from(KeyCode::PageDown)), Key::PgDown);
}
#[test]
fn test_key_from_page_up() {
assert_eq!(Key::from(KeyEvent::from(KeyCode::PageUp)), Key::PgUp);
}
#[test]
fn test_key_from_backspace() {
assert_eq!(
+221
View File
@@ -0,0 +1,221 @@
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
use crate::app::App;
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::*;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::StatefulTable;
use crate::models::radarr_models::Movie;
use crate::models::{Scrollable, Paginated};
proptest! {
/// Property test: Table never panics on index selection
#[test]
fn test_table_index_selection_safety(
list_size in list_size(),
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();
table.set_items(movies);
// Try to select an arbitrary index
if index < list_size {
table.select_index(Some(index));
let selected = table.current_selection();
prop_assert_eq!(selected.id, index as i64);
} else {
// Out of bounds selection should be safe
table.select_index(Some(index));
// Should not panic, selection stays valid
}
}
/// Property test: Table state remains consistent after scroll operations
#[test]
fn test_table_scroll_consistency(
list_size in list_size(),
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();
table.set_items(movies);
let initial_id = table.current_selection().id;
// Scroll down multiple times
for _ in 0..scroll_amount {
table.scroll_down();
}
let after_down_id = table.current_selection().id;
// Position should increase (up to max)
prop_assert!(after_down_id >= initial_id);
prop_assert!(after_down_id < list_size as i64);
// Scroll back up
for _ in 0..scroll_amount {
table.scroll_up();
}
// Should return to initial position (or 0 if we hit the top)
prop_assert!(table.current_selection().id <= initial_id);
}
/// Property test: Empty tables handle operations gracefully
#[test]
fn test_empty_table_safety(_scroll_ops in 0usize..50) {
let table = StatefulTable::<Movie>::default();
// Empty table operations should be safe
prop_assert!(table.is_empty());
prop_assert!(table.items.is_empty());
}
/// Property test: Navigation operations maintain consistency
#[test]
fn test_navigation_consistency(pushes in 1usize..20) {
let mut app = App::test_default();
let initial_route = app.get_current_route();
// Push multiple routes
let routes = vec![
ActiveRadarrBlock::Movies,
ActiveRadarrBlock::Collections,
ActiveRadarrBlock::Downloads,
ActiveRadarrBlock::Blocklist,
];
for i in 0..pushes {
let route = routes[i % routes.len()];
app.push_navigation_stack(route.into());
}
// Current route should be the last pushed
let last_pushed = routes[(pushes - 1) % routes.len()];
prop_assert_eq!(app.get_current_route(), last_pushed.into());
// Pop all routes
for _ in 0..pushes {
app.pop_navigation_stack();
}
// Should return to initial route
prop_assert_eq!(app.get_current_route(), initial_route);
}
/// Property test: String input handling is safe
#[test]
fn test_string_input_safety(input in text_input_string()) {
// String operations should never panic
let _lowercase = input.to_lowercase();
let _uppercase = input.to_uppercase();
let _trimmed = input.trim();
let _len = input.len();
let _chars: Vec<char> = input.chars().collect();
// All operations completed without panic
prop_assert!(true);
}
/// Property test: Table maintains data integrity after operations
#[test]
fn test_table_data_integrity(
list_size in 1usize..100
) {
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();
table.set_items(movies.clone());
let original_count = table.items.len();
// Count should remain the same after various operations
prop_assert_eq!(table.items.len(), original_count);
// All original items should still be present
for movie in &movies {
prop_assert!(table.items.iter().any(|m| m.id == movie.id));
}
}
/// Property test: Page up/down maintains bounds
#[test]
fn test_page_navigation_bounds(
list_size in list_size(),
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();
table.set_items(movies);
// Perform page operations
for i in 0..page_ops {
if i % 2 == 0 {
table.page_down();
} else {
table.page_up();
}
// Should never exceed bounds
let current = table.current_selection();
prop_assert!(current.id >= 0);
prop_assert!(current.id < list_size as i64);
}
}
/// Property test: Table filtering reduces or maintains size
#[test]
fn test_table_filter_size_invariant(
list_size in list_size(),
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();
table.set_items(movies.clone());
let original_size = table.items.len();
// Apply filter
if !filter_term.is_empty() {
let filtered: Vec<Movie> = movies.into_iter()
.filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase()))
.collect();
table.set_items(filtered);
}
// Filtered size should be <= original
prop_assert!(table.items.len() <= original_size);
// Selection should still be valid if table not empty
if !table.items.is_empty() {
let current = table.current_selection();
prop_assert!(current.id >= 0);
}
}
}
}
+36
View File
@@ -452,4 +452,40 @@ mod test_utils {
assert!(app.should_refresh);
};
}
#[macro_export]
macro_rules! assert_modal_present {
($modal:expr) => {
assert!($modal.is_some(), "Expected modal to be present");
};
}
#[macro_export]
macro_rules! assert_modal_absent {
($modal:expr) => {
assert!($modal.is_none(), "Expected modal to be absent");
};
}
#[macro_export]
macro_rules! assert_navigation_pushed {
($app:expr, $expected_route:expr) => {
pretty_assertions::assert_eq!(
$app.get_current_route(),
$expected_route,
"Expected route to be pushed onto navigation stack"
);
};
}
#[macro_export]
macro_rules! assert_navigation_popped {
($app:expr, $expected_route:expr) => {
pretty_assertions::assert_eq!(
$app.get_current_route(),
$expected_route,
"Expected route after popping navigation stack"
);
};
}
}
+185 -8
View File
@@ -1,20 +1,30 @@
#[cfg(test)]
mod tests {
use crate::assert_modal_absent;
use crate::assert_navigation_pushed;
use crate::models::radarr_models::Movie;
use crate::models::sonarr_models::Series;
use crate::{assert_modal_present, assert_navigation_popped};
use pretty_assertions::assert_eq;
use rstest::rstest;
use tokio_util::sync::CancellationToken;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::app::context_clues::SERVARR_CONTEXT_CLUES;
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
use crate::app::radarr::radarr_context_clues::{
LIBRARY_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES,
};
use crate::event::Key;
use crate::handlers::handle_events;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::handlers::{handle_events, populate_keymapping_table};
use crate::models::HorizontallyScrollableText;
use crate::models::Route;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::servarr_models::KeybindingItem;
use crate::models::stateful_table::StatefulTable;
#[test]
fn test_handle_clear_errors() {
@@ -23,7 +33,7 @@ mod tests {
handle_clear_errors(&mut app);
assert!(app.error.text.is_empty());
assert_is_empty!(app.error.text);
}
#[rstest]
@@ -46,7 +56,7 @@ mod tests {
handle_events(DEFAULT_KEYBINDINGS.esc.key, &mut app);
assert_eq!(app.get_current_route(), base_block);
assert_navigation_popped!(app, base_block);
}
#[rstest]
@@ -63,7 +73,7 @@ mod tests {
handle_events(DEFAULT_KEYBINDINGS.previous_servarr.key, &mut app);
assert_eq!(app.server_tabs.get_active_route(), left_block.into());
assert_eq!(app.get_current_route(), left_block.into());
assert_navigation_pushed!(app, left_block.into());
assert!(app.is_first_render);
assert_eq!(app.error, HorizontallyScrollableText::default());
assert!(app.cancellation_token.is_cancelled());
@@ -76,12 +86,88 @@ mod tests {
handle_events(DEFAULT_KEYBINDINGS.next_servarr.key, &mut app);
assert_eq!(app.server_tabs.get_active_route(), right_block.into());
assert_eq!(app.get_current_route(), right_block.into());
assert_navigation_pushed!(app, right_block.into());
assert!(app.is_first_render);
assert_eq!(app.error, HorizontallyScrollableText::default());
assert!(app.cancellation_token.is_cancelled());
}
#[test]
fn test_handle_populate_keybindings_table_on_help_button_press() {
let mut app = App::test_default();
let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
app.push_navigation_stack(ActiveKeybindingBlock::Help.into());
handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app);
assert_modal_present!(app.keymapping_table);
assert_eq!(
expected_keybinding_items,
app.keymapping_table.unwrap().items
);
}
#[test]
fn test_handle_ignore_help_button_when_ignore_special_keys_for_textbox_input_is_true() {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveRadarrBlock::default().into());
handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app);
assert_modal_absent!(app.keymapping_table);
}
#[test]
fn test_handle_empties_keybindings_table_on_help_button_press_when_keybindings_table_is_already_populated()
{
let mut app = App::test_default();
let keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(keybinding_items);
app.keymapping_table = Some(stateful_table);
app.push_navigation_stack(ActiveRadarrBlock::default().into());
handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app);
assert_modal_absent!(app.keymapping_table);
}
#[test]
fn test_handle_shows_keymapping_popup_when_keymapping_table_is_populated() {
let mut app = App::test_default();
let keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(keybinding_items);
app.keymapping_table = Some(stateful_table);
app.push_navigation_stack(ActiveRadarrBlock::default().into());
let expected_selection = KeybindingItem {
key: SERVARR_CONTEXT_CLUES[1].0.key.to_string(),
alt_key: SERVARR_CONTEXT_CLUES[1]
.0
.alt
.map_or(String::new(), |k| k.to_string()),
desc: SERVARR_CONTEXT_CLUES[1].1.to_string(),
};
handle_events(DEFAULT_KEYBINDINGS.down.key, &mut app);
assert_modal_present!(app.keymapping_table);
assert_eq!(
&expected_selection,
app.keymapping_table.unwrap().current_selection()
);
}
#[rstest]
fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::test_default();
@@ -113,4 +199,95 @@ mod tests {
assert!(!app.data.sonarr_data.prompt_confirm);
}
#[test]
fn test_populate_keymapping_table_global_options() {
let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| {
let (key, alt_key) = if key.alt.is_some() {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
} else {
(key.key.to_string(), String::new())
};
KeybindingItem {
key,
alt_key,
desc: desc.to_string(),
}
})
.collect::<Vec<_>>();
let mut app = App::test_default();
app.push_navigation_stack(ActiveKeybindingBlock::Help.into());
populate_keymapping_table(&mut app);
assert_modal_present!(app.keymapping_table);
assert_eq!(
expected_keybinding_items,
app.keymapping_table.unwrap().items
);
}
#[test]
fn test_populate_keymapping_table_populates_servarr_specific_tab_info_before_global_options() {
let mut expected_keybinding_items = LIBRARY_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
expected_keybinding_items.extend(
SERVARR_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(ActiveRadarrBlock::default().into());
populate_keymapping_table(&mut app);
assert_modal_present!(app.keymapping_table);
assert_eq!(
expected_keybinding_items,
app.keymapping_table.unwrap().items
);
}
#[test]
fn test_populate_keymapping_table_populates_delegated_servarr_context_provider_options_before_global_options()
{
let mut expected_keybinding_items = MOVIE_DETAILS_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
expected_keybinding_items.extend(
SERVARR_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into());
populate_keymapping_table(&mut app);
assert_modal_present!(app.keymapping_table);
assert_eq!(
expected_keybinding_items,
app.keymapping_table.unwrap().items
);
}
fn context_clue_to_keybinding_item(key: &KeyBinding, desc: &&str) -> KeybindingItem {
let (key, alt_key) = if key.alt.is_some() {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
} else {
(key.key.to_string(), String::new())
};
KeybindingItem {
key,
alt_key,
desc: desc.to_string(),
}
}
}
+81
View File
@@ -0,0 +1,81 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::models::servarr_data::ActiveKeybindingBlock;
#[cfg(test)]
#[path = "keybinding_handler_tests.rs"]
mod keybinding_handler_tests;
pub(super) struct KeybindingHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandler<'a, 'b> {
fn handle(&mut self) {
let keybinding_table_handling_config = TableHandlingConfig::new(self.app.get_current_route());
if !handle_table(
self,
|app| app.keymapping_table.as_mut().unwrap(),
keybinding_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(_active_block: ActiveKeybindingBlock) -> bool {
true
}
fn new(
key: Key,
app: &'a mut App<'b>,
_active_block: ActiveKeybindingBlock,
_context: Option<ActiveKeybindingBlock>,
) -> KeybindingHandler<'a, 'b> {
KeybindingHandler { key, app }
}
fn get_key(&self) -> Key {
self.key
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn is_ready(&self) -> bool {
self.app.keymapping_table.is_some()
}
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) {}
fn handle_submit(&mut self) {}
fn handle_esc(&mut self) {
self.app.keymapping_table = None;
}
fn handle_char_key_event(&mut self) {}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> crate::models::Route {
self.app.get_current_route()
}
}
+70
View File
@@ -0,0 +1,70 @@
#[cfg(test)]
mod tests {
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::KeybindingHandler;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::stateful_table::StatefulTable;
use rstest::rstest;
mod test_handle_esc {
use super::*;
use crate::assert_modal_absent;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use pretty_assertions::assert_eq;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[test]
fn test_esc_empties_keymapping_table() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
KeybindingHandler::new(ESC_KEY, &mut app, ActiveKeybindingBlock::Help, None).handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
assert_modal_absent!(app.keymapping_table);
}
}
#[test]
fn test_keybinding_handler_accepts() {
assert!(KeybindingHandler::accepts(ActiveKeybindingBlock::Help));
}
#[test]
fn test_keybinding_handler_not_ready_when_keybinding_is_empty() {
let mut app = App::test_default();
app.is_loading = false;
let handler = KeybindingHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveKeybindingBlock::Help,
None,
);
assert!(!handler.is_ready());
}
#[rstest]
fn test_keybinding_handler_ready_when_keymapping_table_is_not_empty(
#[values(true, false)] is_loading: bool,
) {
let mut app = App::test_default();
app.keymapping_table = Some(StatefulTable::default());
app.is_loading = is_loading;
let handler = KeybindingHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveKeybindingBlock::Help,
None,
);
assert!(handler.is_ready());
}
}
+86 -23
View File
@@ -1,11 +1,20 @@
use radarr_handlers::RadarrHandler;
use sonarr_handlers::SonarrHandler;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::app::context_clues::{
ContextClueProvider, SERVARR_CONTEXT_CLUES, ServarrContextClueProvider,
};
use crate::app::key_binding::KeyBinding;
use crate::event::Key;
use crate::handlers::keybinding_handler::KeybindingHandler;
use crate::matches_key;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_models::KeybindingItem;
use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, Route};
mod keybinding_handler;
mod radarr_handlers;
mod sonarr_handlers;
@@ -22,40 +31,42 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
fn handle_key_event(&mut self) {
let key = self.get_key();
match key {
_ if key == DEFAULT_KEYBINDINGS.up.key => {
_ if matches_key!(up, key, self.ignore_special_keys()) => {
if self.is_ready() {
self.handle_scroll_up();
}
}
_ if key == DEFAULT_KEYBINDINGS.down.key => {
_ if matches_key!(down, key, self.ignore_special_keys()) => {
if self.is_ready() {
self.handle_scroll_down();
}
}
_ if key == DEFAULT_KEYBINDINGS.home.key => {
_ if matches_key!(home, key) => {
if self.is_ready() {
self.handle_home();
}
}
_ if key == DEFAULT_KEYBINDINGS.end.key => {
_ if matches_key!(end, key) => {
if self.is_ready() {
self.handle_end();
}
}
_ if key == DEFAULT_KEYBINDINGS.delete.key => {
_ if matches_key!(delete, key) => {
if self.is_ready() {
self.handle_delete();
}
}
_ if key == DEFAULT_KEYBINDINGS.left.key || key == DEFAULT_KEYBINDINGS.right.key => {
_ if matches_key!(left, key, self.ignore_special_keys())
|| matches_key!(right, key, self.ignore_special_keys()) =>
{
self.handle_left_right_action()
}
_ if key == DEFAULT_KEYBINDINGS.submit.key => {
_ if matches_key!(submit, key) => {
if self.is_ready() {
self.handle_submit();
}
}
_ if key == DEFAULT_KEYBINDINGS.esc.key => self.handle_esc(),
_ if matches_key!(esc, key) => self.handle_esc(),
_ => {
if self.is_ready() {
self.handle_char_key_event();
@@ -71,6 +82,7 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
fn accepts(active_block: T) -> bool;
fn new(key: Key, app: &'a mut App<'b>, active_block: T, context: Option<T>) -> Self;
fn get_key(&self) -> Key;
fn ignore_special_keys(&self) -> bool;
fn is_ready(&self) -> bool;
fn handle_scroll_up(&mut self);
fn handle_scroll_down(&mut self);
@@ -81,21 +93,32 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
fn handle_submit(&mut self);
fn handle_esc(&mut self);
fn handle_char_key_event(&mut self);
fn app_mut(&mut self) -> &mut App<'b>;
fn current_route(&self) -> Route;
}
pub fn handle_events(key: Key, app: &mut App<'_>) {
if key == DEFAULT_KEYBINDINGS.next_servarr.key {
if matches_key!(next_servarr, key) {
app.reset();
app.server_tabs.next();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
app.cancellation_token.cancel();
} else if key == DEFAULT_KEYBINDINGS.previous_servarr.key {
} else if matches_key!(previous_servarr, key) {
app.reset();
app.server_tabs.previous();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
app.cancellation_token.cancel();
} else if matches_key!(help, key) && !app.ignore_special_keys_for_textbox_input {
if app.keymapping_table.is_none() {
populate_keymapping_table(app);
} else {
app.keymapping_table = None;
}
} else {
match app.get_current_route() {
_ if app.keymapping_table.is_some() => {
KeybindingHandler::new(key, app, ActiveKeybindingBlock::Help, None).handle();
}
Route::Radarr(active_radarr_block, context) => {
RadarrHandler::new(key, app, active_radarr_block, context).handle()
}
@@ -107,6 +130,48 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
}
}
pub fn populate_keymapping_table(app: &mut App<'_>) {
let context_clue_to_keybinding_item = |key: &KeyBinding, desc: &&str| {
let (key, alt_key) = if key.alt.is_some() {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
} else {
(key.key.to_string(), String::new())
};
KeybindingItem {
key,
alt_key,
desc: desc.to_string(),
}
};
let mut keybindings = Vec::new();
let global_keybindings = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
if let Some(contextual_help) = app.server_tabs.get_active_route_contextual_help() {
keybindings.extend(
contextual_help
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
}
if let Some(contextual_help) = ServarrContextClueProvider::get_context_clues(app) {
keybindings.extend(
contextual_help
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
}
keybindings.extend(global_keybindings);
let mut table = StatefulTable::default();
table.set_items(keybindings);
app.keymapping_table = Some(table);
}
fn handle_clear_errors(app: &mut App<'_>) {
if !app.error.text.is_empty() {
app.error = HorizontallyScrollableText::default();
@@ -115,17 +180,15 @@ fn handle_clear_errors(app: &mut App<'_>) {
fn handle_prompt_toggle(app: &mut App<'_>, key: Key) {
match key {
_ if key == DEFAULT_KEYBINDINGS.left.key || key == DEFAULT_KEYBINDINGS.right.key => {
match app.get_current_route() {
Route::Radarr(_, _) => {
app.data.radarr_data.prompt_confirm = !app.data.radarr_data.prompt_confirm
}
Route::Sonarr(_, _) => {
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
}
_ => (),
_ if matches_key!(left, key) || matches_key!(right, key) => match app.get_current_route() {
Route::Radarr(_, _) => {
app.data.radarr_data.prompt_confirm = !app.data.radarr_data.prompt_confirm
}
}
Route::Sonarr(_, _) => {
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
}
_ => (),
},
_ => (),
}
}
@@ -149,7 +212,7 @@ macro_rules! handle_text_box_left_right_keys {
macro_rules! handle_text_box_keys {
($self:expr, $key:expr, $input:expr) => {
match $self.key {
_ if $key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.backspace.key => {
_ if $crate::matches_key!(backspace, $key) => {
$input.pop();
}
Key::Char(character) => {
@@ -165,7 +228,7 @@ macro_rules! handle_prompt_left_right_keys {
($self:expr, $confirm_prompt:expr, $data:ident) => {
if $self.app.data.$data.selected_block.get_active_block() == $confirm_prompt {
handle_prompt_toggle($self.app, $self.key);
} else if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.left.key {
} else if $crate::matches_key!(left, $self.key) {
$self.app.data.$data.selected_block.left();
} else {
$self.app.data.$data.selected_block.right();
@@ -4,13 +4,15 @@ mod tests {
use chrono::DateTime;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_pushed;
use crate::event::Key;
use crate::handlers::radarr_handlers::blocklist::{blocklist_sorting_options, BlocklistHandler};
use crate::handlers::KeyEventHandler;
use crate::handlers::radarr_handlers::blocklist::{BlocklistHandler, blocklist_sorting_options};
use crate::models::radarr_models::{BlocklistItem, BlocklistItemMovie};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::servarr_models::{Language, Quality, QualityWrapper};
@@ -29,10 +31,7 @@ mod tests {
BlocklistHandler::new(DELETE_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::DeleteBlocklistItemPrompt.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::DeleteBlocklistItemPrompt.into());
}
#[test]
@@ -53,6 +52,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::assert_navigation_pushed;
#[rstest]
fn test_blocklist_tab_left(#[values(true, false)] is_ready: bool) {
@@ -72,7 +72,7 @@ mod tests {
app.data.radarr_data.main_tabs.get_active_route(),
ActiveRadarrBlock::Downloads.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
assert_navigation_pushed!(app, ActiveRadarrBlock::Downloads.into());
}
#[rstest]
@@ -93,10 +93,7 @@ mod tests {
app.data.radarr_data.main_tabs.get_active_route(),
ActiveRadarrBlock::RootFolders.into()
);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::RootFolders.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::RootFolders.into());
}
#[rstest]
@@ -121,11 +118,11 @@ mod tests {
}
mod test_handle_submit {
use crate::assert_navigation_popped;
use crate::network::radarr_network::RadarrEvent;
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::network::radarr_network::RadarrEvent;
use super::*;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
@@ -138,10 +135,7 @@ mod tests {
BlocklistHandler::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::BlocklistItemDetails.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::BlocklistItemDetails.into());
}
#[test]
@@ -185,7 +179,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), base_route.into());
assert_navigation_popped!(app, base_route.into());
}
#[rstest]
@@ -204,18 +198,18 @@ mod tests {
BlocklistHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
assert_none!(app.data.radarr_data.prompt_confirm_action);
assert_navigation_popped!(app, ActiveRadarrBlock::Blocklist.into());
}
}
mod test_handle_esc {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::handlers::radarr_handlers::downloads::DownloadsHandler;
use super::*;
use crate::assert_navigation_popped;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
@@ -239,7 +233,7 @@ mod tests {
BlocklistHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
assert_eq!(app.get_current_route(), base_block.into());
assert_navigation_popped!(app, base_block.into());
assert!(!app.data.radarr_data.prompt_confirm);
}
@@ -257,7 +251,7 @@ mod tests {
)
.handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
assert_navigation_popped!(app, ActiveRadarrBlock::Blocklist.into());
}
#[rstest]
@@ -270,7 +264,7 @@ mod tests {
DownloadsHandler::new(ESC_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
assert_navigation_popped!(app, ActiveRadarrBlock::Blocklist.into());
assert!(app.error.text.is_empty());
}
}
@@ -282,6 +276,7 @@ mod tests {
use crate::network::radarr_network::RadarrEvent;
use super::*;
use crate::{assert_navigation_popped, assert_navigation_pushed};
#[test]
fn test_refresh_blocklist_key() {
@@ -297,7 +292,7 @@ mod tests {
)
.handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
assert_navigation_pushed!(app, ActiveRadarrBlock::Blocklist.into());
assert!(app.should_refresh);
}
@@ -333,10 +328,7 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into());
}
#[test]
@@ -391,7 +383,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), base_route.into());
assert_navigation_popped!(app, base_route.into());
}
}
@@ -541,6 +533,25 @@ mod tests {
})
}
#[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,
ActiveRadarrBlock::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();
+24 -17
View File
@@ -1,10 +1,9 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
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::radarr_models::BlocklistItem;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::stateful_table::SortOption;
@@ -22,13 +21,6 @@ pub(super) struct BlocklistHandler<'a, 'b> {
}
impl BlocklistHandler<'_, '_> {
handle_table_events!(
self,
blocklist,
self.app.data.radarr_data.blocklist,
BlocklistItem
);
fn extract_blocklist_item_id(&self) -> i64 {
self.app.data.radarr_data.blocklist.current_selection().id
}
@@ -39,10 +31,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
let blocklist_table_handling_config =
TableHandlingConfig::new(ActiveRadarrBlock::Blocklist.into())
.sorting_block(ActiveRadarrBlock::BlocklistSortPrompt.into())
.sort_by_fn(|a: &BlocklistItem, b: &BlocklistItem| a.id.cmp(&b.id))
.sort_options(blocklist_sorting_options());
if !self.handle_blocklist_table_events(blocklist_table_handling_config) {
if !handle_table(
self,
|app| &mut app.data.radarr_data.blocklist,
blocklist_table_handling_config,
) {
self.handle_key_event();
}
}
@@ -51,6 +46,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
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>,
@@ -143,10 +142,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ if key == DEFAULT_KEYBINDINGS.clear.key => {
_ if matches_key!(clear, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into());
@@ -154,7 +153,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
_ => (),
},
ActiveRadarrBlock::DeleteBlocklistItemPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem(
self.extract_blocklist_item_id(),
@@ -164,7 +163,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
}
}
ActiveRadarrBlock::BlocklistClearAllItemsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist);
@@ -174,6 +173,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> crate::models::Route {
self.app.get_current_route()
}
}
fn blocklist_sorting_options() -> Vec<SortOption<BlocklistItem>> {
@@ -1,16 +1,14 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::CollectionMovie;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::matches_key;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, ADD_MOVIE_SELECTION_BLOCKS, COLLECTION_DETAILS_BLOCKS,
ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
EDIT_COLLECTION_SELECTION_BLOCKS,
};
use crate::models::stateful_table::StatefulTable;
use crate::models::BlockSelectionState;
#[cfg(test)]
#[path = "collection_details_handler_tests.rs"]
@@ -23,21 +21,18 @@ pub(super) struct CollectionDetailsHandler<'a, 'b> {
_context: Option<ActiveRadarrBlock>,
}
impl CollectionDetailsHandler<'_, '_> {
handle_table_events!(
self,
collection_movies,
self.app.data.radarr_data.collection_movies,
CollectionMovie
);
}
impl CollectionDetailsHandler<'_, '_> {}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHandler<'a, 'b> {
fn handle(&mut self) {
let collection_movies_table_handling_config =
TableHandlingConfig::new(ActiveRadarrBlock::CollectionDetails.into());
if !self.handle_collection_movies_table_events(collection_movies_table_handling_config) {
if !handle_table(
self,
|app| &mut app.data.radarr_data.collection_movies,
collection_movies_table_handling_config,
) {
self.handle_key_event();
}
}
@@ -46,6 +41,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
COLLECTION_DETAILS_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>,
@@ -130,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
fn handle_char_key_event(&mut self) {
if self.active_radarr_block == ActiveRadarrBlock::CollectionDetails
&& self.key == DEFAULT_KEYBINDINGS.edit.key
&& matches_key!(edit, self.key)
{
self.app.push_navigation_stack(
(
@@ -144,4 +143,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> crate::models::Route {
self.app.get_current_route()
}
}
@@ -1,25 +1,27 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_str_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_modal_absent;
use crate::event::Key;
use crate::handlers::radarr_handlers::collections::collection_details_handler::CollectionDetailsHandler;
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::radarr_data::{
ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
};
mod test_handle_submit {
use bimap::BiMap;
use pretty_assertions::assert_eq;
use crate::assert_navigation_pushed;
use crate::models::BlockSelectionState;
use crate::models::radarr_models::Movie;
use crate::models::servarr_data::radarr::radarr_data::ADD_MOVIE_SELECTION_BLOCKS;
use crate::models::BlockSelectionState;
use bimap::BiMap;
use pretty_assertions::assert_eq;
use super::*;
@@ -50,45 +52,51 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
assert_navigation_pushed!(
app,
(
ActiveRadarrBlock::AddMoviePrompt,
Some(ActiveRadarrBlock::CollectionDetails)
)
.into()
);
assert!(!app
.data
.radarr_data
.add_movie_modal
.as_ref()
.unwrap()
.monitor_list
.items
.is_empty());
assert!(
!app
.data
.radarr_data
.add_movie_modal
.as_ref()
.unwrap()
.monitor_list
.items
.is_empty()
);
assert_eq!(
app.data.radarr_data.selected_block.get_active_block(),
ActiveRadarrBlock::AddMovieSelectRootFolder
);
assert!(!app
.data
.radarr_data
.add_movie_modal
.as_ref()
.unwrap()
.minimum_availability_list
.items
.is_empty());
assert!(!app
.data
.radarr_data
.add_movie_modal
.as_ref()
.unwrap()
.quality_profile_list
.items
.is_empty());
assert!(
!app
.data
.radarr_data
.add_movie_modal
.as_ref()
.unwrap()
.minimum_availability_list
.items
.is_empty()
);
assert!(
!app
.data
.radarr_data
.add_movie_modal
.as_ref()
.unwrap()
.quality_profile_list
.items
.is_empty()
);
assert_str_eq!(
app
.data
@@ -125,7 +133,7 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::CollectionDetails.into()
);
assert!(app.data.radarr_data.add_movie_modal.is_none());
assert_modal_absent!(app.data.radarr_data.add_movie_modal);
}
#[test]
@@ -150,16 +158,13 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::ViewMovieOverview.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::ViewMovieOverview.into());
}
}
mod test_handle_esc {
use super::*;
use pretty_assertions::assert_eq;
use crate::assert_navigation_popped;
use rstest::rstest;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
@@ -184,11 +189,8 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert!(app.data.radarr_data.collection_movies.items.is_empty());
assert_navigation_popped!(app, ActiveRadarrBlock::Collections.into());
assert_is_empty!(app.data.radarr_data.collection_movies.items);
}
#[test]
@@ -205,10 +207,7 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::CollectionDetails.into()
);
assert_navigation_popped!(app, ActiveRadarrBlock::CollectionDetails.into());
}
}
@@ -220,7 +219,7 @@ mod tests {
use crate::models::radarr_models::{Collection, MinimumAvailability};
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
use crate::models::servarr_data::radarr::radarr_data::{
RadarrData, EDIT_COLLECTION_SELECTION_BLOCKS,
EDIT_COLLECTION_SELECTION_BLOCKS, RadarrData,
};
use crate::test_edit_collection_key;
@@ -263,7 +262,7 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::CollectionDetails.into()
);
assert!(app.data.radarr_data.edit_collection_modal.is_none());
assert_modal_absent!(app.data.radarr_data.edit_collection_modal);
}
}
@@ -278,6 +277,25 @@ mod tests {
});
}
#[rstest]
fn test_collection_details_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 = CollectionDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_collection_details_handler_not_ready_when_loading() {
let mut app = App::test_default();
@@ -7,16 +7,18 @@ mod tests {
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
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::radarr_handlers::collections::{
collections_sorting_options, CollectionsHandler,
};
use crate::handlers::KeyEventHandler;
use crate::handlers::radarr_handlers::collections::{
CollectionsHandler, collections_sorting_options,
};
use crate::models::radarr_models::{Collection, CollectionMovie};
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, COLLECTIONS_BLOCKS, COLLECTION_DETAILS_BLOCKS, EDIT_COLLECTION_BLOCKS,
ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, COLLECTIONS_BLOCKS, EDIT_COLLECTION_BLOCKS,
};
use crate::test_handler_delegation;
@@ -25,6 +27,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::assert_navigation_pushed;
#[rstest]
fn test_collections_tab_left(#[values(true, false)] is_ready: bool) {
@@ -44,7 +47,7 @@ mod tests {
app.data.radarr_data.main_tabs.get_active_route(),
ActiveRadarrBlock::Movies.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
assert_navigation_pushed!(app, ActiveRadarrBlock::Movies.into());
}
#[rstest]
@@ -65,7 +68,7 @@ mod tests {
app.data.radarr_data.main_tabs.get_active_route(),
ActiveRadarrBlock::Downloads.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
assert_navigation_pushed!(app, ActiveRadarrBlock::Downloads.into());
}
#[rstest]
@@ -97,9 +100,9 @@ mod tests {
}
mod test_handle_submit {
use pretty_assertions::assert_eq;
use crate::assert_navigation_popped;
use crate::network::radarr_network::RadarrEvent;
use pretty_assertions::assert_eq;
use super::*;
@@ -116,10 +119,7 @@ mod tests {
CollectionsHandler::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Collections, None).handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::CollectionDetails.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::CollectionDetails.into());
}
#[test]
@@ -166,10 +166,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::UpdateCollections)
);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert_navigation_popped!(app, ActiveRadarrBlock::Collections.into());
}
#[test]
@@ -192,17 +189,13 @@ mod tests {
.handle();
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert_none!(app.data.radarr_data.prompt_confirm_action);
assert_navigation_popped!(app, ActiveRadarrBlock::Collections.into());
}
}
mod test_handle_esc {
use pretty_assertions::assert_eq;
use crate::assert_navigation_popped;
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
use super::*;
@@ -224,10 +217,7 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert_navigation_popped!(app, ActiveRadarrBlock::Collections.into());
assert!(!app.data.radarr_data.prompt_confirm);
}
@@ -242,11 +232,8 @@ mod tests {
CollectionsHandler::new(ESC_KEY, &mut app, ActiveRadarrBlock::Collections, None).handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert!(app.error.text.is_empty());
assert_navigation_popped!(app, ActiveRadarrBlock::Collections.into());
assert_is_empty!(app.error.text);
}
}
@@ -258,10 +245,10 @@ mod tests {
use crate::models::radarr_models::MinimumAvailability;
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
use crate::models::servarr_data::radarr::radarr_data::{
RadarrData, EDIT_COLLECTION_SELECTION_BLOCKS,
EDIT_COLLECTION_SELECTION_BLOCKS, RadarrData,
};
use crate::network::radarr_network::RadarrEvent;
use crate::test_edit_collection_key;
use crate::{assert_navigation_popped, test_edit_collection_key};
use super::*;
@@ -298,7 +285,7 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert!(app.data.radarr_data.edit_collection_modal.is_none());
assert_modal_absent!(app.data.radarr_data.edit_collection_modal);
}
#[test]
@@ -318,10 +305,7 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::UpdateAllCollectionsPrompt.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::UpdateAllCollectionsPrompt.into());
}
#[test]
@@ -367,10 +351,7 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::Collections.into());
assert!(app.should_refresh);
}
@@ -424,10 +405,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::UpdateCollections)
);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert_navigation_popped!(app, ActiveRadarrBlock::Collections.into());
}
}
@@ -589,6 +567,25 @@ mod tests {
});
}
#[rstest]
fn test_collections_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 = CollectionsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_collections_handler_not_ready_when_loading() {
let mut app = App::test_default();
@@ -644,7 +641,7 @@ mod tests {
Collection {
id: 3,
title: "test 1".into(),
movies: Some(iter::repeat(CollectionMovie::default()).take(3).collect()),
movies: Some(iter::repeat_n(CollectionMovie::default(), 3).collect()),
root_folder_path: Some("/nfs/movies".into()),
quality_profile_id: 1,
search_on_add: false,
@@ -654,7 +651,7 @@ mod tests {
Collection {
id: 2,
title: "test 2".into(),
movies: Some(iter::repeat(CollectionMovie::default()).take(7).collect()),
movies: Some(iter::repeat_n(CollectionMovie::default(), 7).collect()),
root_folder_path: Some("/htpc/movies".into()),
quality_profile_id: 3,
search_on_add: true,
@@ -664,7 +661,7 @@ mod tests {
Collection {
id: 1,
title: "test 3".into(),
movies: Some(iter::repeat(CollectionMovie::default()).take(1).collect()),
movies: Some(iter::repeat_n(CollectionMovie::default(), 1).collect()),
root_folder_path: Some("/nfs/some/stupidly/long/path/to/test/with".into()),
quality_profile_id: 1,
search_on_add: false,
@@ -1,13 +1,12 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::Scrollable;
use crate::models::radarr_models::EditCollectionParams;
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS};
use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys};
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
#[cfg(test)]
#[path = "edit_collection_handler_tests.rs"]
@@ -69,6 +68,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
EDIT_COLLECTION_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>,
@@ -262,7 +265,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
)
.into(),
);
self.app.should_ignore_quit_key = true;
self.app.ignore_special_keys_for_textbox_input = true;
}
ActiveRadarrBlock::EditCollectionToggleMonitored => {
self
@@ -311,7 +314,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
| ActiveRadarrBlock::EditCollectionSelectQualityProfile => self.app.pop_navigation_stack(),
ActiveRadarrBlock::EditCollectionRootFolderPathInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
@@ -321,7 +324,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
match self.active_radarr_block {
ActiveRadarrBlock::EditCollectionRootFolderPathInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveRadarrBlock::EditCollectionPrompt => {
self.app.pop_navigation_stack();
@@ -354,7 +357,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
ActiveRadarrBlock::EditCollectionPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::EditCollectionConfirmPrompt
&& key == DEFAULT_KEYBINDINGS.confirm.key
&& matches_key!(confirm, key)
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(
@@ -368,4 +371,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> crate::models::Route {
self.app.get_current_route()
}
}
@@ -2,14 +2,17 @@
mod tests {
use bimap::BiMap;
use pretty_assertions::assert_str_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
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::radarr_handlers::collections::edit_collection_handler::EditCollectionHandler;
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::collection;
use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::{Collection, EditCollectionParams, MinimumAvailability};
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::models::servarr_data::radarr::radarr_data::{
@@ -21,9 +24,9 @@ mod tests {
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::models::servarr_data::radarr::radarr_data::EDIT_COLLECTION_SELECTION_BLOCKS;
use crate::models::BlockSelectionState;
use super::*;
@@ -442,13 +445,13 @@ mod tests {
}
mod test_handle_submit {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::assert_navigation_popped;
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::models::servarr_data::radarr::radarr_data::EDIT_COLLECTION_SELECTION_BLOCKS;
use crate::models::{BlockSelectionState, Route};
use crate::network::radarr_network::RadarrEvent;
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
@@ -457,7 +460,7 @@ mod tests {
#[test]
fn test_edit_collection_root_folder_path_input_submit() {
let mut app = App::test_default();
app.should_ignore_quit_key = true;
app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal {
path: "Test Path".into(),
..EditCollectionModal::default()
@@ -473,20 +476,19 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app
.data
.radarr_data
.edit_collection_modal
.as_ref()
.unwrap()
.path
.text
.is_empty());
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::EditCollectionPrompt.into()
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(
!app
.data
.radarr_data
.edit_collection_modal
.as_ref()
.unwrap()
.path
.text
.is_empty()
);
assert_navigation_popped!(app, ActiveRadarrBlock::EditCollectionPrompt.into());
}
#[test]
@@ -511,11 +513,8 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_navigation_popped!(app, ActiveRadarrBlock::Collections.into());
assert_none!(app.data.radarr_data.prompt_confirm_action);
}
#[test]
@@ -568,16 +567,13 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert_navigation_popped!(app, ActiveRadarrBlock::Collections.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditCollection(expected_edit_collection_params))
);
assert!(app.should_refresh);
assert!(app.data.radarr_data.edit_collection_modal.is_none());
assert_modal_absent!(app.data.radarr_data.edit_collection_modal);
}
#[test]
@@ -608,7 +604,7 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::EditCollectionPrompt.into()
);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_none!(app.data.radarr_data.prompt_confirm_action);
assert!(!app.should_refresh);
}
@@ -633,7 +629,7 @@ mod tests {
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(
assert_some_eq_x!(
app
.data
.radarr_data
@@ -641,7 +637,7 @@ mod tests {
.as_ref()
.unwrap()
.monitored,
Some(true)
true
);
EditCollectionHandler::new(
@@ -653,7 +649,7 @@ mod tests {
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(
assert_some_eq_x!(
app
.data
.radarr_data
@@ -661,7 +657,7 @@ mod tests {
.as_ref()
.unwrap()
.monitored,
Some(false)
false
);
}
@@ -691,7 +687,7 @@ mod tests {
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(
assert_some_eq_x!(
app
.data
.radarr_data
@@ -699,7 +695,7 @@ mod tests {
.as_ref()
.unwrap()
.search_on_add,
Some(true)
true
);
EditCollectionHandler::new(
@@ -711,7 +707,7 @@ mod tests {
.handle();
assert_eq!(app.get_current_route(), current_route);
assert_eq!(
assert_some_eq_x!(
app
.data
.radarr_data
@@ -719,7 +715,7 @@ mod tests {
.as_ref()
.unwrap()
.search_on_add,
Some(false)
false
);
}
@@ -752,14 +748,14 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
assert_navigation_pushed!(
app,
(selected_block, Some(ActiveRadarrBlock::Collections)).into()
);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_none!(app.data.radarr_data.prompt_confirm_action);
if selected_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput {
assert!(app.should_ignore_quit_key);
assert!(app.ignore_special_keys_for_textbox_input);
}
}
@@ -785,22 +781,18 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::EditCollectionPrompt.into()
);
assert_navigation_popped!(app, ActiveRadarrBlock::EditCollectionPrompt.into());
if active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput {
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
}
}
}
mod test_handle_esc {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::assert_navigation_popped;
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
use rstest::rstest;
use super::*;
@@ -811,7 +803,7 @@ mod tests {
let mut app = App::test_default();
app.data.radarr_data = create_test_radarr_data();
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
app.should_ignore_quit_key = true;
app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionRootFolderPathInput.into());
@@ -823,11 +815,8 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::EditCollectionPrompt.into()
);
assert!(!app.ignore_special_keys_for_textbox_input);
assert_navigation_popped!(app, ActiveRadarrBlock::EditCollectionPrompt.into());
}
#[test]
@@ -845,13 +834,10 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert_navigation_popped!(app, ActiveRadarrBlock::Collections.into());
let radarr_data = &app.data.radarr_data;
assert!(radarr_data.edit_collection_modal.is_none());
assert_modal_absent!(radarr_data.edit_collection_modal);
assert!(!radarr_data.prompt_confirm);
}
@@ -872,21 +858,19 @@ mod tests {
EditCollectionHandler::new(ESC_KEY, &mut app, active_radarr_block, None).handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert_navigation_popped!(app, ActiveRadarrBlock::Collections.into());
}
}
mod test_handle_key_char {
use super::*;
use crate::{
assert_navigation_popped,
models::{
BlockSelectionState,
servarr_data::radarr::{
modals::EditCollectionModal, radarr_data::EDIT_COLLECTION_SELECTION_BLOCKS,
},
BlockSelectionState,
},
network::radarr_network::RadarrEvent,
};
@@ -926,7 +910,7 @@ mod tests {
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
EditCollectionHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
None,
@@ -942,7 +926,7 @@ mod tests {
.unwrap()
.path
.text,
"h"
"a"
);
}
@@ -995,16 +979,13 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert_navigation_popped!(app, ActiveRadarrBlock::Collections.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditCollection(expected_edit_collection_params))
);
assert!(app.should_refresh);
assert!(app.data.radarr_data.edit_collection_modal.is_none());
assert_modal_absent!(app.data.radarr_data.edit_collection_modal);
}
}
@@ -1019,6 +1000,25 @@ mod tests {
});
}
#[rstest]
fn test_edit_collection_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 = EditCollectionHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_edit_collection_params() {
let mut app = App::test_default();
@@ -1060,7 +1060,7 @@ mod tests {
.build_edit_collection_params();
assert_eq!(edit_collection_params, expected_edit_collection_params);
assert!(app.data.radarr_data.edit_collection_modal.is_none());
assert_modal_absent!(app.data.radarr_data.edit_collection_modal);
}
#[test]
+26 -19
View File
@@ -1,18 +1,17 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::collections::collection_details_handler::CollectionDetailsHandler;
use crate::handlers::radarr_handlers::collections::edit_collection_handler::EditCollectionHandler;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
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::BlockSelectionState;
use crate::models::radarr_models::Collection;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
};
use crate::models::stateful_table::SortOption;
use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent;
mod collection_details_handler;
@@ -29,21 +28,13 @@ pub(super) struct CollectionsHandler<'a, 'b> {
context: Option<ActiveRadarrBlock>,
}
impl CollectionsHandler<'_, '_> {
handle_table_events!(
self,
collections,
self.app.data.radarr_data.collections,
Collection
);
}
impl CollectionsHandler<'_, '_> {}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'a, 'b> {
fn handle(&mut self) {
let collections_table_handling_config =
TableHandlingConfig::new(ActiveRadarrBlock::Collections.into())
.sorting_block(ActiveRadarrBlock::CollectionsSortPrompt.into())
.sort_by_fn(|a: &Collection, b: &Collection| a.id.cmp(&b.id))
.sort_options(collections_sorting_options())
.searching_block(ActiveRadarrBlock::SearchCollection.into())
.search_error_block(ActiveRadarrBlock::SearchCollectionError.into())
@@ -52,7 +43,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
.filter_error_block(ActiveRadarrBlock::FilterCollectionsError.into())
.filter_field_fn(|collection| &collection.title.text);
if !self.handle_collections_table_events(collections_table_handling_config) {
if !handle_table(
self,
|app| &mut app.data.radarr_data.collections,
collections_table_handling_config,
) {
match self.active_radarr_block {
_ if CollectionDetailsHandler::accepts(self.active_radarr_block) => {
CollectionDetailsHandler::new(self.key, self.app, self.active_radarr_block, self.context)
@@ -73,6 +68,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
|| COLLECTIONS_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>,
@@ -145,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Collections => match self.key {
_ if key == DEFAULT_KEYBINDINGS.edit.key => {
_ if matches_key!(edit, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
@@ -154,18 +153,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
self.app.data.radarr_data.selected_block =
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
}
_ if key == DEFAULT_KEYBINDINGS.update.key => {
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveRadarrBlock::UpdateAllCollectionsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections);
@@ -175,6 +174,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> crate::models::Route {
self.app.get_current_route()
}
}
fn collections_sorting_options() -> Vec<SortOption<Collection>> {
@@ -1,14 +1,16 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
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::radarr_handlers::downloads::DownloadsHandler;
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::download_record;
use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::DownloadRecord;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
@@ -30,10 +32,7 @@ mod tests {
DownloadsHandler::new(DELETE_KEY, &mut app, ActiveRadarrBlock::Downloads, None).handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::DeleteDownloadPrompt.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::DeleteDownloadPrompt.into());
}
#[test]
@@ -77,10 +76,7 @@ mod tests {
app.data.radarr_data.main_tabs.get_active_route(),
ActiveRadarrBlock::Collections.into()
);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::Collections.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::Collections.into());
}
#[rstest]
@@ -101,7 +97,7 @@ mod tests {
app.data.radarr_data.main_tabs.get_active_route(),
ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
assert_navigation_pushed!(app, ActiveRadarrBlock::Blocklist.into());
}
#[rstest]
@@ -132,6 +128,7 @@ mod tests {
use crate::network::radarr_network::RadarrEvent;
use super::*;
use crate::assert_navigation_popped;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
@@ -168,7 +165,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), base_route.into());
assert_navigation_popped!(app, base_route.into());
}
#[rstest]
@@ -190,16 +187,16 @@ mod tests {
DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_eq!(app.get_current_route(), base_route.into());
assert_none!(app.data.radarr_data.prompt_confirm_action);
assert_navigation_popped!(app, base_route.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;
@@ -217,7 +214,7 @@ mod tests {
DownloadsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
assert_eq!(app.get_current_route(), base_block.into());
assert_navigation_popped!(app, base_block.into());
assert!(!app.data.radarr_data.prompt_confirm);
}
@@ -231,17 +228,17 @@ mod tests {
DownloadsHandler::new(ESC_KEY, &mut app, ActiveRadarrBlock::Downloads, None).handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
assert!(app.error.text.is_empty());
assert_navigation_popped!(app, ActiveRadarrBlock::Downloads.into());
assert_is_empty!(app.error.text);
}
}
mod test_handle_key_char {
use crate::assert_navigation_popped;
use crate::network::radarr_network::RadarrEvent;
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::network::radarr_network::RadarrEvent;
use super::*;
#[test]
@@ -261,10 +258,7 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::UpdateDownloadsPrompt.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::UpdateDownloadsPrompt.into());
}
#[test]
@@ -307,7 +301,7 @@ mod tests {
)
.handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
assert_navigation_pushed!(app, ActiveRadarrBlock::Downloads.into());
assert!(app.should_refresh);
}
@@ -372,7 +366,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), base_route.into());
assert_navigation_popped!(app, base_route.into());
}
}
@@ -387,6 +381,25 @@ mod tests {
})
}
#[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,
ActiveRadarrBlock::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();
+24 -17
View File
@@ -1,11 +1,9 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::DownloadRecord;
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::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
use crate::network::radarr_network::RadarrEvent;
@@ -21,13 +19,6 @@ pub(super) struct DownloadsHandler<'a, 'b> {
}
impl DownloadsHandler<'_, '_> {
handle_table_events!(
self,
downloads,
self.app.data.radarr_data.downloads,
DownloadRecord
);
fn extract_download_id(&self) -> i64 {
self.app.data.radarr_data.downloads.current_selection().id
}
@@ -38,7 +29,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
let downloads_table_handling_config =
TableHandlingConfig::new(ActiveRadarrBlock::Downloads.into());
if !self.handle_downloads_table_events(downloads_table_handling_config) {
if !handle_table(
self,
|app| &mut app.data.radarr_data.downloads,
downloads_table_handling_config,
) {
self.handle_key_event();
}
}
@@ -47,6 +42,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
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>,
@@ -130,18 +129,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Downloads => match self.key {
_ if key == DEFAULT_KEYBINDINGS.update.key => {
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::UpdateDownloadsPrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveRadarrBlock::DeleteDownloadPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteDownload(self.extract_download_id()));
@@ -150,7 +149,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
}
}
ActiveRadarrBlock::UpdateDownloadsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateDownloads);
@@ -160,4 +159,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> crate::models::Route {
self.app.get_current_route()
}
}
@@ -1,12 +1,13 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
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::network::radarr_network::RadarrEvent;
use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys};
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"]
@@ -65,6 +66,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
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>,
@@ -356,7 +361,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
| ActiveRadarrBlock::EditIndexerSeedRatioInput
| ActiveRadarrBlock::EditIndexerTagsInput => {
self.app.push_navigation_stack(selected_block.into());
self.app.should_ignore_quit_key = true;
self.app.ignore_special_keys_for_textbox_input = true;
}
ActiveRadarrBlock::EditIndexerPriorityInput => self
.app
@@ -402,7 +407,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
| ActiveRadarrBlock::EditIndexerSeedRatioInput
| ActiveRadarrBlock::EditIndexerTagsInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveRadarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(),
_ => (),
@@ -423,7 +428,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
| ActiveRadarrBlock::EditIndexerPriorityInput
| ActiveRadarrBlock::EditIndexerTagsInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => self.app.pop_navigation_stack(),
}
@@ -504,7 +509,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
ActiveRadarrBlock::EditIndexerPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::EditIndexerConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key
&& matches_key!(confirm, self.key)
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
@@ -517,4 +522,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
_ => (),
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> crate::models::Route {
self.app.get_current_route()
}
}
@@ -1,15 +1,19 @@
#[cfg(test)]
mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_modal_absent;
use crate::assert_modal_present;
use crate::assert_navigation_pushed;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::indexer;
use crate::handlers::KeyEventHandler;
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 pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
mod test_handle_scroll_up_and_down {
@@ -17,9 +21,9 @@ mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS;
use crate::models::BlockSelectionState;
use super::*;
@@ -418,11 +422,11 @@ mod tests {
use std::sync::atomic::Ordering;
use crate::app::App;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::{
EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
};
use crate::models::BlockSelectionState;
use pretty_assertions::assert_eq;
use rstest::rstest;
@@ -849,9 +853,10 @@ mod tests {
use rstest::rstest;
use crate::app::App;
use crate::assert_navigation_popped;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::{
servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, BlockSelectionState,
BlockSelectionState, servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
};
use crate::network::radarr_network::RadarrEvent;
@@ -881,10 +886,10 @@ mod tests {
)
.handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_navigation_popped!(app, ActiveRadarrBlock::Indexers.into());
assert_none!(app.data.radarr_data.prompt_confirm_action);
assert!(!app.should_refresh);
assert_eq!(app.data.radarr_data.edit_indexer_modal, None);
assert_none!(app.data.radarr_data.edit_indexer_modal);
}
#[test]
@@ -935,8 +940,8 @@ mod tests {
)
.handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert!(app.data.radarr_data.edit_indexer_modal.is_none());
assert_navigation_popped!(app, ActiveRadarrBlock::Indexers.into());
assert_modal_absent!(app.data.radarr_data.edit_indexer_modal);
assert!(app.should_refresh);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
@@ -965,9 +970,9 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
);
assert!(app.data.radarr_data.edit_indexer_modal.is_some());
assert_modal_present!(app.data.radarr_data.edit_indexer_modal);
assert!(!app.should_refresh);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_none!(app.data.radarr_data.prompt_confirm_action);
}
#[rstest]
@@ -1001,8 +1006,8 @@ mod tests {
)
.handle();
assert_eq!(app.get_current_route(), block.into());
assert!(app.should_ignore_quit_key);
assert_navigation_pushed!(app, block.into());
assert!(app.ignore_special_keys_for_textbox_input);
}
#[test]
@@ -1023,11 +1028,8 @@ mod tests {
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPriorityInput.into()
);
assert!(!app.should_ignore_quit_key);
assert_navigation_pushed!(app, ActiveRadarrBlock::EditIndexerPriorityInput.into());
assert!(!app.ignore_special_keys_for_textbox_input);
}
#[test]
@@ -1052,14 +1054,16 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
);
assert!(app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_rss
.unwrap());
assert!(
app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_rss
.unwrap()
);
EditIndexerHandler::new(
SUBMIT_KEY,
@@ -1073,14 +1077,16 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
);
assert!(!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_rss
.unwrap());
assert!(
!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_rss
.unwrap()
);
}
#[test]
@@ -1105,14 +1111,16 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
);
assert!(app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_automatic_search
.unwrap());
assert!(
app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_automatic_search
.unwrap()
);
EditIndexerHandler::new(
SUBMIT_KEY,
@@ -1126,14 +1134,16 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
);
assert!(!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_automatic_search
.unwrap());
assert!(
!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_automatic_search
.unwrap()
);
}
#[test]
@@ -1158,14 +1168,16 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
);
assert!(app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_interactive_search
.unwrap());
assert!(
app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_interactive_search
.unwrap()
);
EditIndexerHandler::new(
SUBMIT_KEY,
@@ -1179,21 +1191,23 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
);
assert!(!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_interactive_search
.unwrap());
assert!(
!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.enable_interactive_search
.unwrap()
);
}
#[test]
fn test_edit_indexer_name_input_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.should_ignore_quit_key = true;
app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal {
name: "Test".into(),
..EditIndexerModal::default()
@@ -1209,27 +1223,26 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.name
.text
.is_empty());
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(
!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.name
.text
.is_empty()
);
assert_navigation_popped!(app, ActiveRadarrBlock::EditIndexerPrompt.into());
}
#[test]
fn test_edit_indexer_url_input_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.should_ignore_quit_key = true;
app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal {
url: "Test".into(),
..EditIndexerModal::default()
@@ -1245,27 +1258,26 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.url
.text
.is_empty());
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(
!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.url
.text
.is_empty()
);
assert_navigation_popped!(app, ActiveRadarrBlock::EditIndexerPrompt.into());
}
#[test]
fn test_edit_indexer_api_key_input_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.should_ignore_quit_key = true;
app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal {
api_key: "Test".into(),
..EditIndexerModal::default()
@@ -1281,27 +1293,26 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.api_key
.text
.is_empty());
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(
!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.api_key
.text
.is_empty()
);
assert_navigation_popped!(app, ActiveRadarrBlock::EditIndexerPrompt.into());
}
#[test]
fn test_edit_indexer_seed_ratio_input_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.should_ignore_quit_key = true;
app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal {
seed_ratio: "Test".into(),
..EditIndexerModal::default()
@@ -1317,27 +1328,26 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.seed_ratio
.text
.is_empty());
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(
!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.seed_ratio
.text
.is_empty()
);
assert_navigation_popped!(app, ActiveRadarrBlock::EditIndexerPrompt.into());
}
#[test]
fn test_edit_indexer_tags_input_submit() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.should_ignore_quit_key = true;
app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal {
tags: "Test".into(),
..EditIndexerModal::default()
@@ -1353,26 +1363,26 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.tags
.text
.is_empty());
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPrompt.into()
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(
!app
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.tags
.text
.is_empty()
);
assert_navigation_popped!(app, ActiveRadarrBlock::EditIndexerPrompt.into());
}
}
mod test_handle_esc {
use super::*;
use crate::app::App;
use crate::assert_navigation_popped;
use crate::event::Key;
use crate::models::servarr_data::modals::EditIndexerModal;
use pretty_assertions::assert_eq;
@@ -1396,9 +1406,9 @@ mod tests {
)
.handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert_navigation_popped!(app, ActiveRadarrBlock::Indexers.into());
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.data.radarr_data.edit_indexer_modal, None);
assert_none!(app.data.radarr_data.edit_indexer_modal);
}
#[rstest]
@@ -1417,12 +1427,12 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(active_radarr_block.into());
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
app.should_ignore_quit_key = true;
app.ignore_special_keys_for_textbox_input = true;
EditIndexerHandler::new(ESC_KEY, &mut app, active_radarr_block, None).handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert!(!app.should_ignore_quit_key);
assert_navigation_popped!(app, ActiveRadarrBlock::Indexers.into());
assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!(
app.data.radarr_data.edit_indexer_modal,
Some(EditIndexerModal::default())
@@ -1431,15 +1441,15 @@ mod tests {
}
mod test_handle_key_char {
use super::*;
use crate::app::App;
use crate::assert_navigation_popped;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS;
use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent;
use pretty_assertions::{assert_eq, assert_str_eq};
use super::*;
#[test]
fn test_edit_indexer_name_input_backspace() {
let mut app = App::test_default();
@@ -1597,7 +1607,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditIndexerNameInput,
None,
@@ -1613,7 +1623,7 @@ mod tests {
.unwrap()
.name
.text,
"h"
"a"
);
}
@@ -1624,7 +1634,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditIndexerUrlInput,
None,
@@ -1640,7 +1650,7 @@ mod tests {
.unwrap()
.url
.text,
"h"
"a"
);
}
@@ -1651,7 +1661,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditIndexerApiKeyInput,
None,
@@ -1667,7 +1677,7 @@ mod tests {
.unwrap()
.api_key
.text,
"h"
"a"
);
}
@@ -1678,7 +1688,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditIndexerSeedRatioInput,
None,
@@ -1694,7 +1704,7 @@ mod tests {
.unwrap()
.seed_ratio
.text,
"h"
"a"
);
}
@@ -1705,7 +1715,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditIndexerTagsInput,
None,
@@ -1721,7 +1731,7 @@ mod tests {
.unwrap()
.tags
.text,
"h"
"a"
);
}
@@ -1772,8 +1782,8 @@ mod tests {
)
.handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert!(app.data.radarr_data.edit_indexer_modal.is_none());
assert_navigation_popped!(app, ActiveRadarrBlock::Indexers.into());
assert_modal_absent!(app.data.radarr_data.edit_indexer_modal);
assert!(app.should_refresh);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
@@ -1793,6 +1803,25 @@ mod tests {
})
}
#[rstest]
fn test_edit_indexer_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 = EditIndexerHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_edit_indexer_params() {
let mut app = App::test_default();
@@ -1832,7 +1861,7 @@ mod tests {
.build_edit_indexer_params();
assert_eq!(edit_indexer_params, expected_edit_indexer_params);
assert!(app.data.radarr_data.edit_indexer_modal.is_none());
assert_modal_absent!(app.data.radarr_data.edit_indexer_modal);
}
#[test]

Some files were not shown because too many files have changed in this diff Show More