Lidarr support #1
@@ -35,6 +35,7 @@ mod tests {
|
||||
theme: None,
|
||||
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
|
||||
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
|
||||
lidarr: None,
|
||||
};
|
||||
let expected_tab_routes = vec![
|
||||
TabRoute {
|
||||
|
||||
@@ -285,6 +285,12 @@ impl App<'_> {
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Lidarr".to_owned(),
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
]),
|
||||
..App::default()
|
||||
}
|
||||
@@ -309,6 +315,12 @@ impl App<'_> {
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
TabRoute {
|
||||
title: "Lidarr".to_owned(),
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: None,
|
||||
config: Some(ServarrConfig::default()),
|
||||
},
|
||||
]),
|
||||
..App::default()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::{
|
||||
lidarr::{list_command_handler::LidarrListCommand, LidarrCommand},
|
||||
Command,
|
||||
};
|
||||
use crate::Cli;
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_command_from() {
|
||||
let command = LidarrCommand::List(LidarrListCommand::Artists);
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(command));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_list_artists_has_no_arg_requirements() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_list_subcommand_requires_subcommand() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list"]);
|
||||
|
||||
assert_err!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,10 @@ use crate::{
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "list_command_handler_tests.rs"]
|
||||
mod list_command_handler_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrListCommand {
|
||||
#[command(about = "List all artists in your Lidarr library")]
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Cli;
|
||||
use crate::cli::{
|
||||
Command,
|
||||
lidarr::{LidarrCommand, list_command_handler::LidarrListCommand},
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_list_command_from() {
|
||||
let command = LidarrListCommand::Artists;
|
||||
|
||||
let result = Command::from(command.clone());
|
||||
|
||||
assert_eq!(result, Command::Lidarr(LidarrCommand::List(command)));
|
||||
}
|
||||
|
||||
mod cli {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_list_artists_has_no_arg_requirements() {
|
||||
let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]);
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
|
||||
mod handler {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mockall::predicate::eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::cli::CliCommandHandler;
|
||||
use crate::cli::lidarr::list_command_handler::{LidarrListCommand, LidarrListCommandHandler};
|
||||
use crate::models::Serdeable;
|
||||
use crate::models::lidarr_models::LidarrSerdeable;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::{
|
||||
app::App,
|
||||
network::{MockNetworkTrait, NetworkEvent},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_artists_command() {
|
||||
let mut mock_network = MockNetworkTrait::new();
|
||||
mock_network
|
||||
.expect_handle_network_event()
|
||||
.with(eq::<NetworkEvent>(LidarrEvent::ListArtists.into()))
|
||||
.times(1)
|
||||
.returning(|_| {
|
||||
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
|
||||
json!({"testResponse": "response"}),
|
||||
)))
|
||||
});
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
|
||||
let result =
|
||||
LidarrListCommandHandler::with(&app_arc, LidarrListCommand::Artists, &mut mock_network)
|
||||
.handle()
|
||||
.await;
|
||||
|
||||
assert_ok!(&result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,10 @@ use super::{CliCommandHandler, Command};
|
||||
|
||||
mod list_command_handler;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_command_tests.rs"]
|
||||
mod lidarr_command_tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
|
||||
pub enum LidarrCommand {
|
||||
#[command(
|
||||
|
||||
@@ -4,13 +4,12 @@ mod property_tests {
|
||||
|
||||
use crate::app::App;
|
||||
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::*;
|
||||
use crate::models::radarr_models::Movie;
|
||||
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};
|
||||
use crate::models::{Paginated, Scrollable};
|
||||
|
||||
proptest! {
|
||||
/// Property test: Table never panics on index selection
|
||||
#[test]
|
||||
fn test_table_index_selection_safety(
|
||||
list_size in list_size(),
|
||||
@@ -25,19 +24,15 @@ mod property_tests {
|
||||
|
||||
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(),
|
||||
@@ -53,42 +48,34 @@ mod property_tests {
|
||||
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,
|
||||
@@ -101,34 +88,27 @@ mod property_tests {
|
||||
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
|
||||
@@ -144,16 +124,13 @@ mod property_tests {
|
||||
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(),
|
||||
@@ -168,7 +145,6 @@ mod property_tests {
|
||||
|
||||
table.set_items(movies);
|
||||
|
||||
// Perform page operations
|
||||
for i in 0..page_ops {
|
||||
if i % 2 == 0 {
|
||||
table.page_down();
|
||||
@@ -176,14 +152,12 @@ mod property_tests {
|
||||
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(),
|
||||
@@ -200,7 +174,6 @@ mod property_tests {
|
||||
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()))
|
||||
@@ -208,10 +181,8 @@ mod property_tests {
|
||||
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);
|
||||
|
||||
@@ -9,22 +9,23 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::SERVARR_CONTEXT_CLUES;
|
||||
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
|
||||
use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS};
|
||||
use crate::app::radarr::radarr_context_clues::{
|
||||
LIBRARY_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::{handle_clear_errors, handle_prompt_toggle};
|
||||
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::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::servarr_data::ActiveKeybindingBlock;
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::Route;
|
||||
|
||||
#[test]
|
||||
fn test_handle_clear_errors() {
|
||||
@@ -60,11 +61,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0, ActiveSonarrBlock::Series, ActiveSonarrBlock::Series)]
|
||||
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Movies)]
|
||||
fn test_handle_change_tabs<T>(#[case] index: usize, #[case] left_block: T, #[case] right_block: T)
|
||||
where
|
||||
#[case(0, ActiveLidarrBlock::Artists, ActiveSonarrBlock::Series)]
|
||||
#[case(1, ActiveRadarrBlock::Movies, ActiveLidarrBlock::Artists)]
|
||||
#[case(2, ActiveSonarrBlock::Series, ActiveRadarrBlock::Movies)]
|
||||
fn test_handle_change_tabs<T, U>(
|
||||
#[case] index: usize,
|
||||
#[case] left_block: T,
|
||||
#[case] right_block: U,
|
||||
) where
|
||||
T: Into<Route> + Copy,
|
||||
U: Into<Route> + Copy,
|
||||
{
|
||||
let mut app = App::test_default();
|
||||
app.error = "Test".into();
|
||||
@@ -122,8 +128,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_empties_keybindings_table_on_help_button_press_when_keybindings_table_is_already_populated()
|
||||
{
|
||||
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()
|
||||
@@ -254,8 +260,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_populate_keymapping_table_populates_delegated_servarr_context_provider_options_before_global_options()
|
||||
{
|
||||
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))
|
||||
|
||||
@@ -6,6 +6,10 @@ use serde_json::{Number, Value};
|
||||
use super::{HorizontallyScrollableText, Serdeable};
|
||||
use crate::serde_enum_from;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_models_tests.rs"]
|
||||
mod lidarr_models_tests;
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Artist {
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::models::{
|
||||
lidarr_models::{Artist, ArtistStatistics, ArtistStatus, LidarrSerdeable, Ratings},
|
||||
Serdeable,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_artist_status_default() {
|
||||
assert_eq!(ArtistStatus::default(), ArtistStatus::Continuing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from() {
|
||||
let lidarr_serdeable = LidarrSerdeable::Value(json!({}));
|
||||
|
||||
let serdeable: Serdeable = Serdeable::from(lidarr_serdeable.clone());
|
||||
|
||||
assert_eq!(serdeable, Serdeable::Lidarr(lidarr_serdeable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_unit() {
|
||||
let lidarr_serdeable = LidarrSerdeable::from(());
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Value(json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_value() {
|
||||
let value = json!({"test": "test"});
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = value.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Value(value));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_serdeable_from_artists() {
|
||||
let artists = vec![Artist {
|
||||
id: 1,
|
||||
..Artist::default()
|
||||
}];
|
||||
|
||||
let lidarr_serdeable: LidarrSerdeable = artists.clone().into();
|
||||
|
||||
assert_eq!(lidarr_serdeable, LidarrSerdeable::Artists(artists));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_deserialization() {
|
||||
let artist_json = json!({
|
||||
"id": 1,
|
||||
"mbId": "test-mb-id",
|
||||
"artistName": "Test Artist",
|
||||
"foreignArtistId": "test-foreign-id",
|
||||
"status": "continuing",
|
||||
"overview": "Test overview",
|
||||
"artistType": "Group",
|
||||
"disambiguation": "UK Band",
|
||||
"path": "/music/test-artist",
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"monitored": true,
|
||||
"genres": ["Rock", "Alternative"],
|
||||
"tags": [1, 2],
|
||||
"added": "2023-01-01T00:00:00Z",
|
||||
"ratings": {
|
||||
"votes": 100,
|
||||
"value": 4.5
|
||||
},
|
||||
"statistics": {
|
||||
"albumCount": 5,
|
||||
"trackFileCount": 50,
|
||||
"trackCount": 60,
|
||||
"totalTrackCount": 70,
|
||||
"sizeOnDisk": 1000000000,
|
||||
"percentOfTracks": 83.33
|
||||
}
|
||||
});
|
||||
|
||||
let artist: Artist = serde_json::from_value(artist_json).unwrap();
|
||||
|
||||
assert_eq!(artist.id, 1);
|
||||
assert_str_eq!(artist.mb_id, "test-mb-id");
|
||||
assert_str_eq!(artist.artist_name.text, "Test Artist");
|
||||
assert_str_eq!(artist.foreign_artist_id, "test-foreign-id");
|
||||
assert_eq!(artist.status, ArtistStatus::Continuing);
|
||||
assert_some_eq_x!(&artist.overview, "Test overview");
|
||||
assert_some_eq_x!(&artist.artist_type, "Group");
|
||||
assert_some_eq_x!(&artist.disambiguation, "UK Band");
|
||||
assert_str_eq!(artist.path, "/music/test-artist");
|
||||
assert_eq!(artist.quality_profile_id, 1);
|
||||
assert_eq!(artist.metadata_profile_id, 1);
|
||||
assert!(artist.monitored);
|
||||
assert_eq!(artist.genres, vec!["Rock", "Alternative"]);
|
||||
assert_eq!(artist.tags.len(), 2);
|
||||
assert_some!(&artist.ratings);
|
||||
assert_some!(&artist.statistics);
|
||||
|
||||
let ratings = artist.ratings.unwrap();
|
||||
assert_eq!(ratings.votes, 100);
|
||||
assert_eq!(ratings.value, 4.5);
|
||||
|
||||
let stats = artist.statistics.unwrap();
|
||||
assert_eq!(stats.album_count, 5);
|
||||
assert_eq!(stats.track_file_count, 50);
|
||||
assert_eq!(stats.track_count, 60);
|
||||
assert_eq!(stats.total_track_count, 70);
|
||||
assert_eq!(stats.size_on_disk, 1000000000);
|
||||
assert_eq!(stats.percent_of_tracks, 83.33);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_status_deserialization() {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<ArtistStatus>("\"continuing\"").unwrap(),
|
||||
ArtistStatus::Continuing
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<ArtistStatus>("\"ended\"").unwrap(),
|
||||
ArtistStatus::Ended
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<ArtistStatus>("\"deleted\"").unwrap(),
|
||||
ArtistStatus::Deleted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ratings_equality() {
|
||||
let ratings1 = Ratings {
|
||||
votes: 100,
|
||||
value: 4.5,
|
||||
};
|
||||
let ratings2 = Ratings {
|
||||
votes: 100,
|
||||
value: 4.5,
|
||||
};
|
||||
let ratings3 = Ratings {
|
||||
votes: 50,
|
||||
value: 3.0,
|
||||
};
|
||||
|
||||
assert_eq!(ratings1, ratings2);
|
||||
assert_ne!(ratings1, ratings3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_statistics_equality() {
|
||||
let stats1 = ArtistStatistics {
|
||||
album_count: 5,
|
||||
track_file_count: 50,
|
||||
track_count: 60,
|
||||
total_track_count: 70,
|
||||
size_on_disk: 1000000000,
|
||||
percent_of_tracks: 83.33,
|
||||
};
|
||||
let stats2 = ArtistStatistics {
|
||||
album_count: 5,
|
||||
track_file_count: 50,
|
||||
track_count: 60,
|
||||
total_track_count: 70,
|
||||
size_on_disk: 1000000000,
|
||||
percent_of_tracks: 83.33,
|
||||
};
|
||||
let stats3 = ArtistStatistics::default();
|
||||
|
||||
assert_eq!(stats1, stats2);
|
||||
assert_ne!(stats1, stats3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artist_with_optional_fields_none() {
|
||||
let artist_json = json!({
|
||||
"id": 1,
|
||||
"mbId": "",
|
||||
"artistName": "Test Artist",
|
||||
"foreignArtistId": "",
|
||||
"status": "continuing",
|
||||
"path": "",
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"monitored": false,
|
||||
"genres": [],
|
||||
"tags": [],
|
||||
"added": "2023-01-01T00:00:00Z"
|
||||
});
|
||||
|
||||
let artist: Artist = serde_json::from_value(artist_json).unwrap();
|
||||
|
||||
assert_none!(&artist.overview);
|
||||
assert_none!(&artist.artist_type);
|
||||
assert_none!(&artist.disambiguation);
|
||||
assert_none!(&artist.ratings);
|
||||
assert_none!(&artist.statistics);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::lidarr_models::{Artist, LidarrSerdeable};
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use crate::network::{NetworkEvent, NetworkResource, lidarr_network::LidarrEvent};
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
|
||||
#[rstest]
|
||||
#[case(LidarrEvent::HealthCheck, "/health")]
|
||||
#[case(LidarrEvent::ListArtists, "/artist")]
|
||||
fn test_resource(#[case] event: LidarrEvent, #[case] expected_uri: &str) {
|
||||
assert_str_eq!(event.resource(), expected_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_lidarr_event() {
|
||||
assert_eq!(
|
||||
NetworkEvent::Lidarr(LidarrEvent::HealthCheck),
|
||||
NetworkEvent::from(LidarrEvent::HealthCheck)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_lidarr_healthcheck_event() {
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.build_for(LidarrEvent::HealthCheck)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let _ = network.handle_lidarr_event(LidarrEvent::HealthCheck).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_artists_event() {
|
||||
let artists_json = json!([{
|
||||
"id": 1,
|
||||
"mbId": "test-mb-id",
|
||||
"artistName": "Test Artist",
|
||||
"foreignArtistId": "test-foreign-id",
|
||||
"status": "continuing",
|
||||
"path": "/music/test-artist",
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"monitored": true,
|
||||
"genres": [],
|
||||
"tags": [],
|
||||
"added": "2023-01-01T00:00:00Z"
|
||||
}]);
|
||||
let response: Vec<Artist> = serde_json::from_value(artists_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(artists_json)
|
||||
.build_for(LidarrEvent::ListArtists)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network.handle_lidarr_event(LidarrEvent::ListArtists).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::Artists(artists) = result.unwrap() else {
|
||||
panic!("Expected Artists");
|
||||
};
|
||||
|
||||
assert_eq!(artists, response);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ use crate::{
|
||||
network::RequestMethod,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_network_tests.rs"]
|
||||
mod lidarr_network_tests;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum LidarrEvent {
|
||||
HealthCheck,
|
||||
|
||||
@@ -810,11 +810,16 @@ pub(in crate::network) mod test_utils {
|
||||
network_event: E,
|
||||
) -> (Mock, Arc<Mutex<App<'static>>>, ServerGuard)
|
||||
where
|
||||
E: Into<NetworkEvent> + NetworkResource,
|
||||
E: Into<NetworkEvent> + NetworkResource + Clone,
|
||||
{
|
||||
let resource = network_event.resource();
|
||||
let network_event_clone: NetworkEvent = network_event.clone().into();
|
||||
let api_version = match &network_event_clone {
|
||||
NetworkEvent::Lidarr(_) => "v1",
|
||||
_ => "v3",
|
||||
};
|
||||
let mut server = Server::new_async().await;
|
||||
let mut uri = format!("/api/v3{resource}");
|
||||
let mut uri = format!("/api/{api_version}{resource}");
|
||||
|
||||
if let Some(path) = &self.path {
|
||||
uri = format!("{uri}{path}");
|
||||
@@ -853,9 +858,10 @@ pub(in crate::network) mod test_utils {
|
||||
..ServarrConfig::default()
|
||||
};
|
||||
|
||||
match network_event.into() {
|
||||
match network_event_clone {
|
||||
NetworkEvent::Radarr(_) => app.server_tabs.tabs[0].config = Some(servarr_config),
|
||||
NetworkEvent::Sonarr(_) => app.server_tabs.tabs[1].config = Some(servarr_config),
|
||||
NetworkEvent::Lidarr(_) => app.server_tabs.tabs[2].config = Some(servarr_config),
|
||||
}
|
||||
|
||||
let app_arc = Arc::new(Mutex::new(app));
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Radarr │ Sonarr <?> to open help│
|
||||
│ Radarr │ Sonarr │ Lidarr <?> to open help│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮
|
||||
│Radarr Version: 1.2.3.4 ││Test Download Title ││ ⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Radarr │ Sonarr <?> to open help│
|
||||
│ Radarr │ Sonarr │ Lidarr <?> to open help│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮
|
||||
│Radarr Version: 1.2.3.4 ╭ Keybindings ──────────────────────────────────────────────────────────────────────────╮ ││ ⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs
|
||||
expression: output
|
||||
---
|
||||
╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Radarr │ Sonarr <?> to open help│
|
||||
│ Radarr │ Sonarr │ Lidarr <?> to open help│
|
||||
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭ Error | <esc> to close ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│Some error │
|
||||
|
||||
Reference in New Issue
Block a user