Compare commits
10
Commits
87edf1fb9e
...
1d936c5cb5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d936c5cb5
|
||
|
|
7b6d595397
|
||
|
|
25da032f02
|
||
|
|
e9c323c4e8
|
||
|
|
289abbd8eb
|
||
|
|
a68de41025
|
||
|
|
9d0973352f
|
||
|
|
76c490f45b
|
||
|
|
9b4b3003b5
|
||
|
|
d678f5777a
|
No files matched your search
@@ -5,3 +5,4 @@
|
|||||||
# It is recommended to check this file in to source control so that
|
# It is recommended to check this file in to source control so that
|
||||||
# everyone who runs the test benefits from these saved cases.
|
# everyone who runs the test benefits from these saved cases.
|
||||||
cc 56330c025ad79db641d0eb9f429ab74e95822e1fb015b58f0e158ea674cd42a1 # shrinks to list_size = 1, page_ops = 1
|
cc 56330c025ad79db641d0eb9f429ab74e95822e1fb015b58f0e158ea674cd42a1 # shrinks to list_size = 1, page_ops = 1
|
||||||
|
cc 29df5ff471eb388b0ffde573a959a539e4cf32acd9e1cea5187bba2268dd57f7 # shrinks to list_size = 1, filter_term = "𑛀"
|
||||||
@@ -3,13 +3,16 @@ mod property_tests {
|
|||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||||
|
use crate::event::Key;
|
||||||
|
use crate::handlers::handle_events;
|
||||||
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::{
|
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::{
|
||||||
list_size, text_input_string,
|
list_size, text_input_string,
|
||||||
};
|
};
|
||||||
use crate::models::radarr_models::Movie;
|
use crate::models::radarr_models::Movie;
|
||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
use crate::models::{Paginated, Scrollable};
|
use crate::models::{Paginated, Scrollable, strip_non_search_characters};
|
||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
#[test]
|
#[test]
|
||||||
@@ -104,13 +107,29 @@ mod property_tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_string_input_safety(input in text_input_string()) {
|
fn test_string_input_safety(input in text_input_string()) {
|
||||||
let _lowercase = input.to_lowercase();
|
let mut app = App::test_default();
|
||||||
let _uppercase = input.to_uppercase();
|
app.data.radarr_data.movies.set_items(vec![Movie::default()]);
|
||||||
let _trimmed = input.trim();
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||||
let _len = input.len();
|
handle_events(DEFAULT_KEYBINDINGS.filter.key, &mut app);
|
||||||
let _chars: Vec<char> = input.chars().collect();
|
|
||||||
|
|
||||||
prop_assert!(true);
|
for character in input.chars() {
|
||||||
|
handle_events(Key::Char(character), &mut app);
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_assert_eq!(app.get_current_route(), ActiveRadarrBlock::FilterMovies.into());
|
||||||
|
prop_assert_eq!(
|
||||||
|
app.data.radarr_data.movies.filter.as_ref().unwrap().text.as_str(),
|
||||||
|
input.as_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
for _ in 0..input.chars().count() {
|
||||||
|
handle_events(DEFAULT_KEYBINDINGS.backspace.key, &mut app);
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_assert_eq!(
|
||||||
|
app.data.radarr_data.movies.filter.as_ref().unwrap().text.as_str(),
|
||||||
|
""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -177,22 +196,36 @@ mod property_tests {
|
|||||||
..Movie::default()
|
..Movie::default()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
table.set_items(movies.clone());
|
table.set_items(movies.clone());
|
||||||
let original_size = table.items.len();
|
table.filter = Some(filter_term.clone().into());
|
||||||
|
let scrubbed_filter = strip_non_search_characters(&filter_term);
|
||||||
|
let matches_filter = |movie: &Movie| {
|
||||||
|
strip_non_search_characters(&movie.title.text).contains(&scrubbed_filter)
|
||||||
|
};
|
||||||
|
|
||||||
if !filter_term.is_empty() {
|
let has_match = table.apply_filter(|movie| &movie.title.text);
|
||||||
let filtered: Vec<Movie> = movies.into_iter()
|
|
||||||
.filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase()))
|
|
||||||
.collect();
|
|
||||||
table.set_items(filtered);
|
|
||||||
}
|
|
||||||
|
|
||||||
prop_assert!(table.items.len() <= original_size);
|
prop_assert_eq!(&table.items, &movies);
|
||||||
|
prop_assert!(table.filter.is_none());
|
||||||
|
|
||||||
if !table.items.is_empty() {
|
if has_match {
|
||||||
let current = table.current_selection();
|
let filtered_items = table.filtered_items.as_ref().unwrap();
|
||||||
prop_assert!(current.id >= 0);
|
|
||||||
|
prop_assert!(!filtered_items.is_empty());
|
||||||
|
prop_assert!(filtered_items.len() <= movies.len());
|
||||||
|
|
||||||
|
for movie in filtered_items {
|
||||||
|
prop_assert!(matches_filter(movie));
|
||||||
|
}
|
||||||
|
|
||||||
|
for movie in movies.iter().filter(|movie| !filtered_items.contains(movie)) {
|
||||||
|
prop_assert!(!matches_filter(movie));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prop_assert!(table.filtered_items.is_none());
|
||||||
|
prop_assert!(
|
||||||
|
filter_term.is_empty() || movies.iter().all(|movie| !matches_filter(movie))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ mod tests {
|
|||||||
#[case(
|
#[case(
|
||||||
ActiveLidarrBlock::Downloads,
|
ActiveLidarrBlock::Downloads,
|
||||||
ActiveLidarrBlock::DeleteDownloadPrompt,
|
ActiveLidarrBlock::DeleteDownloadPrompt,
|
||||||
LidarrEvent::DeleteDownload(1)
|
LidarrEvent::DeleteDownload(9)
|
||||||
)]
|
)]
|
||||||
#[case(
|
#[case(
|
||||||
ActiveLidarrBlock::Downloads,
|
ActiveLidarrBlock::Downloads,
|
||||||
@@ -338,7 +338,7 @@ mod tests {
|
|||||||
#[case(
|
#[case(
|
||||||
ActiveLidarrBlock::Downloads,
|
ActiveLidarrBlock::Downloads,
|
||||||
ActiveLidarrBlock::DeleteDownloadPrompt,
|
ActiveLidarrBlock::DeleteDownloadPrompt,
|
||||||
LidarrEvent::DeleteDownload(1)
|
LidarrEvent::DeleteDownload(9)
|
||||||
)]
|
)]
|
||||||
#[case(
|
#[case(
|
||||||
ActiveLidarrBlock::Downloads,
|
ActiveLidarrBlock::Downloads,
|
||||||
@@ -423,7 +423,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.extract_download_id();
|
.extract_download_id();
|
||||||
|
|
||||||
assert_eq!(download_id, 1);
|
assert_eq!(download_id, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
|||||||
use crate::models::HorizontallyScrollableText;
|
use crate::models::HorizontallyScrollableText;
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{
|
||||||
AddMovieBody, AddMovieOptions, AddMovieSearchResult, Collection, CollectionMovie,
|
AddMovieBody, AddMovieOptions, AddMovieSearchResult, Collection, CollectionMovie,
|
||||||
DownloadRecord, IndexerSettings, MediaInfo, MinimumAvailability, Movie, MovieCollection,
|
DownloadRecord, DownloadStatus, IndexerSettings, MediaInfo, MinimumAvailability, Movie,
|
||||||
MovieFile, MovieMonitor, RadarrRelease, Rating, RatingsList,
|
MovieCollection, MovieFile, MovieMonitor, RadarrRelease, Rating, RatingsList,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
||||||
@@ -407,7 +407,7 @@ pub(in crate::handlers::radarr_handlers) mod utils {
|
|||||||
pub fn download_record() -> DownloadRecord {
|
pub fn download_record() -> DownloadRecord {
|
||||||
DownloadRecord {
|
DownloadRecord {
|
||||||
title: "Test Download Title".to_owned(),
|
title: "Test Download Title".to_owned(),
|
||||||
status: "downloading".to_owned(),
|
status: DownloadStatus::Downloading,
|
||||||
id: 1,
|
id: 1,
|
||||||
movie_id: 1,
|
movie_id: 1,
|
||||||
size: 3543348019,
|
size: 3543348019,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ pub struct Artist {
|
|||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub artist_name: HorizontallyScrollableText,
|
pub artist_name: HorizontallyScrollableText,
|
||||||
pub foreign_artist_id: String,
|
pub foreign_artist_id: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: ArtistStatus,
|
pub status: ArtistStatus,
|
||||||
pub overview: Option<String>,
|
pub overview: Option<String>,
|
||||||
pub artist_type: Option<String>,
|
pub artist_type: Option<String>,
|
||||||
@@ -38,6 +39,7 @@ pub struct Artist {
|
|||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub metadata_profile_id: i64,
|
pub metadata_profile_id: i64,
|
||||||
pub monitored: bool,
|
pub monitored: bool,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub monitor_new_items: NewItemMonitorType,
|
pub monitor_new_items: NewItemMonitorType,
|
||||||
pub genres: Vec<String>,
|
pub genres: Vec<String>,
|
||||||
pub tags: Vec<Number>,
|
pub tags: Vec<Number>,
|
||||||
@@ -161,6 +163,7 @@ pub enum MonitorType {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadRecord {
|
pub struct DownloadRecord {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: DownloadStatus,
|
pub status: DownloadStatus,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
@@ -219,6 +222,7 @@ pub struct DownloadsResponse {
|
|||||||
pub struct AddArtistSearchResult {
|
pub struct AddArtistSearchResult {
|
||||||
pub foreign_artist_id: String,
|
pub foreign_artist_id: String,
|
||||||
pub artist_name: HorizontallyScrollableText,
|
pub artist_name: HorizontallyScrollableText,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: ArtistStatus,
|
pub status: ArtistStatus,
|
||||||
pub overview: Option<String>,
|
pub overview: Option<String>,
|
||||||
pub artist_type: Option<String>,
|
pub artist_type: Option<String>,
|
||||||
@@ -414,6 +418,7 @@ pub struct LidarrHistoryItem {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct LidarrTask {
|
pub struct LidarrTask {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub task_name: LidarrTaskName,
|
pub task_name: LidarrTaskName,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub interval: i64,
|
pub interval: i64,
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
mod tests {
|
mod tests {
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::models::lidarr_models::{
|
use crate::models::lidarr_models::{
|
||||||
AddArtistSearchResult, Album, AudioTags, BlocklistItem, BlocklistResponse, DownloadRecord,
|
AddArtistSearchResult, Album, AudioTags, BlocklistItem, BlocklistResponse, DownloadRecord,
|
||||||
DownloadStatus, DownloadsResponse, LidarrHistoryEventType, LidarrHistoryItem,
|
DownloadStatus, DownloadsResponse, LidarrHistoryEventType, LidarrHistoryItem,
|
||||||
LidarrHistoryWrapper, LidarrRelease, LidarrTask, MediaInfo, Member, MonitorType,
|
LidarrHistoryWrapper, LidarrRelease, LidarrTask, LidarrTaskName, MediaInfo, Member,
|
||||||
NewItemMonitorType, Track, TrackFile,
|
MonitorType, NewItemMonitorType, Track, TrackFile,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
|
DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse,
|
||||||
@@ -19,6 +20,9 @@ mod tests {
|
|||||||
Serdeable,
|
Serdeable,
|
||||||
lidarr_models::{Artist, ArtistStatistics, ArtistStatus, LidarrSerdeable, Ratings},
|
lidarr_models::{Artist, ArtistStatistics, ArtistStatus, LidarrSerdeable, Ratings},
|
||||||
};
|
};
|
||||||
|
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{
|
||||||
|
ADD_ARTIST_SEARCH_RESULT_JSON, ARTIST_JSON, download_record, task,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_artist_status_default() {
|
fn test_artist_status_default() {
|
||||||
@@ -946,6 +950,108 @@ mod tests {
|
|||||||
assert_none!(&search_result.ratings);
|
assert_none!(&search_result.ratings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_artist_status_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Artist {
|
||||||
|
status: ArtistStatus::default(),
|
||||||
|
..serde_json::from_str(ARTIST_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Artist>(ARTIST_JSON, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Artist>(ARTIST_JSON, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_artist_monitor_new_items_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Artist {
|
||||||
|
monitor_new_items: NewItemMonitorType::default(),
|
||||||
|
..serde_json::from_str(ARTIST_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Artist>(ARTIST_JSON, "monitorNewItems", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Artist>(ARTIST_JSON, "monitorNewItems", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_artist_search_result_status_deserialization_falls_back_to_default() {
|
||||||
|
let expected = AddArtistSearchResult {
|
||||||
|
status: ArtistStatus::default(),
|
||||||
|
..serde_json::from_str(ADD_ARTIST_SEARCH_RESULT_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<AddArtistSearchResult>(
|
||||||
|
ADD_ARTIST_SEARCH_RESULT_JSON,
|
||||||
|
"status",
|
||||||
|
json!(2)
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<AddArtistSearchResult>(
|
||||||
|
ADD_ARTIST_SEARCH_RESULT_JSON,
|
||||||
|
"status",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_record_status_deserialization_falls_back_to_default() {
|
||||||
|
let download_record_json = serde_json::to_string(&download_record()).unwrap();
|
||||||
|
let expected = DownloadRecord {
|
||||||
|
status: DownloadStatus::default(),
|
||||||
|
..download_record()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lidarr_task_task_name_deserialization_falls_back_to_default() {
|
||||||
|
let task_json = serde_json::to_string(&task()).unwrap();
|
||||||
|
let expected = LidarrTask {
|
||||||
|
task_name: LidarrTaskName::default(),
|
||||||
|
..task()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<LidarrTask>(&task_json, "taskName", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<LidarrTask>(&task_json, "taskName", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_with_field<T: DeserializeOwned>(json: &str, field: &str, value: Value) -> T {
|
||||||
|
let mut fixture_json: Value = serde_json::from_str(json).unwrap();
|
||||||
|
fixture_json[field] = value;
|
||||||
|
|
||||||
|
serde_json::from_value(fixture_json).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_history_event_type(event_type: Value) -> LidarrHistoryEventType {
|
fn deserialize_history_event_type(event_type: Value) -> LidarrHistoryEventType {
|
||||||
let history_item_json = json!({
|
let history_item_json = json!({
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ pub struct Collection {
|
|||||||
pub search_on_add: bool,
|
pub search_on_add: bool,
|
||||||
pub monitored: bool,
|
pub monitored: bool,
|
||||||
pub overview: Option<String>,
|
pub overview: Option<String>,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub minimum_availability: MinimumAvailability,
|
pub minimum_availability: MinimumAvailability,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub quality_profile_id: i64,
|
pub quality_profile_id: i64,
|
||||||
@@ -126,7 +127,7 @@ pub struct Credit {
|
|||||||
pub character: Option<String>,
|
pub character: Option<String>,
|
||||||
pub department: Option<String>,
|
pub department: Option<String>,
|
||||||
pub job: Option<String>,
|
pub job: Option<String>,
|
||||||
#[serde(rename(deserialize = "type"))]
|
#[serde(rename = "type", deserialize_with = "super::from_json_or_default")]
|
||||||
pub credit_type: CreditType,
|
pub credit_type: CreditType,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +151,8 @@ pub struct DeleteMovieParams {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadRecord {
|
pub struct DownloadRecord {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub status: String,
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
|
pub status: DownloadStatus,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
@@ -165,6 +167,36 @@ pub struct DownloadRecord {
|
|||||||
pub download_client: String,
|
pub download_client: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
Default,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
EnumIter,
|
||||||
|
Display,
|
||||||
|
EnumDisplayStyle,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[strum(serialize_all = "camelCase")]
|
||||||
|
pub enum DownloadStatus {
|
||||||
|
#[default]
|
||||||
|
Unknown,
|
||||||
|
Queued,
|
||||||
|
Paused,
|
||||||
|
Downloading,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
Warning,
|
||||||
|
Delay,
|
||||||
|
#[display_style(name = "Download Client Unavailable")]
|
||||||
|
DownloadClientUnavailable,
|
||||||
|
Fallback,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadsResponse {
|
pub struct DownloadsResponse {
|
||||||
@@ -316,6 +348,7 @@ pub struct Movie {
|
|||||||
pub tmdb_id: i64,
|
pub tmdb_id: i64,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub quality_profile_id: i64,
|
pub quality_profile_id: i64,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub minimum_availability: MinimumAvailability,
|
pub minimum_availability: MinimumAvailability,
|
||||||
pub certification: Option<String>,
|
pub certification: Option<String>,
|
||||||
pub tags: Vec<Number>,
|
pub tags: Vec<Number>,
|
||||||
@@ -469,6 +502,7 @@ pub struct RadarrHistoryItem {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct RadarrTask {
|
pub struct RadarrTask {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub task_name: RadarrTaskName,
|
pub task_name: RadarrTaskName,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub interval: i64,
|
pub interval: i64,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{
|
||||||
@@ -9,15 +10,18 @@ mod tests {
|
|||||||
use crate::models::{
|
use crate::models::{
|
||||||
Serdeable,
|
Serdeable,
|
||||||
radarr_models::{
|
radarr_models::{
|
||||||
AddMovieSearchResult, BlocklistItem, BlocklistResponse, Collection, Credit, DiskSpace,
|
AddMovieSearchResult, BlocklistItem, BlocklistResponse, Collection, Credit, CreditType,
|
||||||
DownloadRecord, DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult,
|
DiskSpace, DownloadRecord, DownloadStatus, DownloadsResponse, Indexer, IndexerSettings,
|
||||||
MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, QualityProfile, RadarrRelease,
|
IndexerTestResult, MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor,
|
||||||
RadarrSerdeable, RadarrTask, RadarrTaskName, Tag, Update,
|
QualityProfile, RadarrRelease, RadarrSerdeable, RadarrTask, RadarrTaskName, Tag, Update,
|
||||||
},
|
},
|
||||||
servarr_models::{
|
servarr_models::{
|
||||||
HostConfig, Log, LogResponse, QueueEvent, RootFolder, SecurityConfig, SystemStatus,
|
HostConfig, Log, LogResponse, QueueEvent, RootFolder, SecurityConfig, SystemStatus,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use crate::network::radarr_network::radarr_network_test_utils::test_utils::{
|
||||||
|
MOVIE_JSON, collection, crew_credit, download_record, task,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_task_name_display() {
|
fn test_task_name_display() {
|
||||||
@@ -66,6 +70,40 @@ mod tests {
|
|||||||
assert_str_eq!(MovieMonitor::None.to_display_str(), "None");
|
assert_str_eq!(MovieMonitor::None.to_display_str(), "None");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_status_display() {
|
||||||
|
assert_str_eq!(DownloadStatus::Unknown.to_string(), "unknown");
|
||||||
|
assert_str_eq!(DownloadStatus::Queued.to_string(), "queued");
|
||||||
|
assert_str_eq!(DownloadStatus::Paused.to_string(), "paused");
|
||||||
|
assert_str_eq!(DownloadStatus::Downloading.to_string(), "downloading");
|
||||||
|
assert_str_eq!(DownloadStatus::Completed.to_string(), "completed");
|
||||||
|
assert_str_eq!(DownloadStatus::Failed.to_string(), "failed");
|
||||||
|
assert_str_eq!(DownloadStatus::Warning.to_string(), "warning");
|
||||||
|
assert_str_eq!(DownloadStatus::Delay.to_string(), "delay");
|
||||||
|
assert_str_eq!(
|
||||||
|
DownloadStatus::DownloadClientUnavailable.to_string(),
|
||||||
|
"downloadClientUnavailable"
|
||||||
|
);
|
||||||
|
assert_str_eq!(DownloadStatus::Fallback.to_string(), "fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_status_to_display_str() {
|
||||||
|
assert_str_eq!(DownloadStatus::Unknown.to_display_str(), "Unknown");
|
||||||
|
assert_str_eq!(DownloadStatus::Queued.to_display_str(), "Queued");
|
||||||
|
assert_str_eq!(DownloadStatus::Paused.to_display_str(), "Paused");
|
||||||
|
assert_str_eq!(DownloadStatus::Downloading.to_display_str(), "Downloading");
|
||||||
|
assert_str_eq!(DownloadStatus::Completed.to_display_str(), "Completed");
|
||||||
|
assert_str_eq!(DownloadStatus::Failed.to_display_str(), "Failed");
|
||||||
|
assert_str_eq!(DownloadStatus::Warning.to_display_str(), "Warning");
|
||||||
|
assert_str_eq!(DownloadStatus::Delay.to_display_str(), "Delay");
|
||||||
|
assert_str_eq!(
|
||||||
|
DownloadStatus::DownloadClientUnavailable.to_display_str(),
|
||||||
|
"Download Client Unavailable"
|
||||||
|
);
|
||||||
|
assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_radarr_history_event_type_display() {
|
fn test_radarr_history_event_type_display() {
|
||||||
assert_str_eq!(RadarrHistoryEventType::Unknown.to_string(), "unknown");
|
assert_str_eq!(RadarrHistoryEventType::Unknown.to_string(), "unknown");
|
||||||
@@ -222,11 +260,23 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_credit_serializes_credit_type_under_the_type_key() {
|
||||||
|
let credit_json = serde_json::to_value(crew_credit()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(credit_json["type"], json!("crew"));
|
||||||
|
assert_none!(credit_json.get("creditType"));
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_value::<Credit>(credit_json).unwrap(),
|
||||||
|
crew_credit()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_download_record_default_indexer_value() {
|
fn test_download_record_default_indexer_value() {
|
||||||
let json = r#"{
|
let json = r#"{
|
||||||
"title": "test",
|
"title": "test",
|
||||||
"status": "test",
|
"status": "downloading",
|
||||||
"id": 0,
|
"id": 0,
|
||||||
"movieId": 0,
|
"movieId": 0,
|
||||||
"size": 0,
|
"size": 0,
|
||||||
@@ -235,7 +285,7 @@ mod tests {
|
|||||||
}"#;
|
}"#;
|
||||||
let expected_record = DownloadRecord {
|
let expected_record = DownloadRecord {
|
||||||
title: "test".to_owned(),
|
title: "test".to_owned(),
|
||||||
status: "test".to_owned(),
|
status: DownloadStatus::Downloading,
|
||||||
id: 0,
|
id: 0,
|
||||||
movie_id: 0,
|
movie_id: 0,
|
||||||
size: 0,
|
size: 0,
|
||||||
@@ -614,6 +664,106 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_movie_minimum_availability_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Movie {
|
||||||
|
minimum_availability: MinimumAvailability::default(),
|
||||||
|
..serde_json::from_str(MOVIE_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Movie>(MOVIE_JSON, "minimumAvailability", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Movie>(MOVIE_JSON, "minimumAvailability", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_collection_minimum_availability_deserialization_falls_back_to_default() {
|
||||||
|
let collection_json = serde_json::to_string(&collection()).unwrap();
|
||||||
|
let expected = Collection {
|
||||||
|
minimum_availability: MinimumAvailability::default(),
|
||||||
|
..collection()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Collection>(&collection_json, "minimumAvailability", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Collection>(
|
||||||
|
&collection_json,
|
||||||
|
"minimumAvailability",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_credit_credit_type_deserialization_falls_back_to_default() {
|
||||||
|
let credit_json = serde_json::to_string(&crew_credit()).unwrap();
|
||||||
|
let expected = Credit {
|
||||||
|
credit_type: CreditType::default(),
|
||||||
|
..crew_credit()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Credit>(&credit_json, "type", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Credit>(&credit_json, "type", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_radarr_task_task_name_deserialization_falls_back_to_default() {
|
||||||
|
let task_json = serde_json::to_string(&task()).unwrap();
|
||||||
|
let expected = RadarrTask {
|
||||||
|
task_name: RadarrTaskName::default(),
|
||||||
|
..task()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<RadarrTask>(&task_json, "taskName", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<RadarrTask>(&task_json, "taskName", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_record_status_deserialization_falls_back_to_default() {
|
||||||
|
let download_record_json = serde_json::to_string(&download_record()).unwrap();
|
||||||
|
let expected = DownloadRecord {
|
||||||
|
status: DownloadStatus::default(),
|
||||||
|
..download_record()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_with_field<T: DeserializeOwned>(json: &str, field: &str, value: Value) -> T {
|
||||||
|
let mut fixture_json: Value = serde_json::from_str(json).unwrap();
|
||||||
|
fixture_json[field] = value;
|
||||||
|
|
||||||
|
serde_json::from_value(fixture_json).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_history_event_type(event_type: Value) -> RadarrHistoryEventType {
|
fn deserialize_history_event_type(event_type: Value) -> RadarrHistoryEventType {
|
||||||
let history_item_json = json!({
|
let history_item_json = json!({
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ pub struct Author {
|
|||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub author_name: HorizontallyScrollableText,
|
pub author_name: HorizontallyScrollableText,
|
||||||
pub foreign_author_id: String,
|
pub foreign_author_id: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: AuthorStatus,
|
pub status: AuthorStatus,
|
||||||
pub overview: Option<String>,
|
pub overview: Option<String>,
|
||||||
pub author_type: Option<String>,
|
pub author_type: Option<String>,
|
||||||
@@ -38,6 +39,7 @@ pub struct Author {
|
|||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub metadata_profile_id: i64,
|
pub metadata_profile_id: i64,
|
||||||
pub monitored: bool,
|
pub monitored: bool,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub monitor_new_items: NewItemMonitorType,
|
pub monitor_new_items: NewItemMonitorType,
|
||||||
pub genres: Vec<String>,
|
pub genres: Vec<String>,
|
||||||
pub tags: Vec<Number>,
|
pub tags: Vec<Number>,
|
||||||
@@ -152,6 +154,7 @@ pub enum MonitorType {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadRecord {
|
pub struct DownloadRecord {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: DownloadStatus,
|
pub status: DownloadStatus,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
@@ -210,6 +213,7 @@ pub struct DownloadsResponse {
|
|||||||
pub struct AddAuthorSearchResult {
|
pub struct AddAuthorSearchResult {
|
||||||
pub foreign_author_id: String,
|
pub foreign_author_id: String,
|
||||||
pub author_name: HorizontallyScrollableText,
|
pub author_name: HorizontallyScrollableText,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: AuthorStatus,
|
pub status: AuthorStatus,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ended: bool,
|
pub ended: bool,
|
||||||
@@ -423,6 +427,7 @@ pub struct ReadarrHistoryItem {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ReadarrTask {
|
pub struct ReadarrTask {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub task_name: ReadarrTaskName,
|
pub task_name: ReadarrTaskName,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub interval: i64,
|
pub interval: i64,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
mod tests {
|
mod tests {
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::models::readarr_models::{
|
use crate::models::readarr_models::{
|
||||||
@@ -19,6 +20,9 @@ mod tests {
|
|||||||
Serdeable,
|
Serdeable,
|
||||||
readarr_models::{Author, AuthorStatistics, AuthorStatus, Ratings, ReadarrSerdeable},
|
readarr_models::{Author, AuthorStatistics, AuthorStatus, Ratings, ReadarrSerdeable},
|
||||||
};
|
};
|
||||||
|
use crate::network::readarr_network::readarr_network_test_utils::test_utils::{
|
||||||
|
ADD_AUTHOR_SEARCH_RESULT_JSON, AUTHOR_JSON, download_record,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_author_status_default() {
|
fn test_author_status_default() {
|
||||||
@@ -1140,6 +1144,113 @@ mod tests {
|
|||||||
assert_none!(&search_result.ratings);
|
assert_none!(&search_result.ratings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_author_status_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Author {
|
||||||
|
status: AuthorStatus::default(),
|
||||||
|
..serde_json::from_str(AUTHOR_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Author>(AUTHOR_JSON, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Author>(AUTHOR_JSON, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_author_monitor_new_items_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Author {
|
||||||
|
monitor_new_items: NewItemMonitorType::default(),
|
||||||
|
..serde_json::from_str(AUTHOR_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Author>(AUTHOR_JSON, "monitorNewItems", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Author>(AUTHOR_JSON, "monitorNewItems", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_author_search_result_status_deserialization_falls_back_to_default() {
|
||||||
|
let expected = AddAuthorSearchResult {
|
||||||
|
status: AuthorStatus::default(),
|
||||||
|
..serde_json::from_str(ADD_AUTHOR_SEARCH_RESULT_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<AddAuthorSearchResult>(
|
||||||
|
ADD_AUTHOR_SEARCH_RESULT_JSON,
|
||||||
|
"status",
|
||||||
|
json!(2)
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<AddAuthorSearchResult>(
|
||||||
|
ADD_AUTHOR_SEARCH_RESULT_JSON,
|
||||||
|
"status",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_record_status_deserialization_falls_back_to_default() {
|
||||||
|
let download_record_json = serde_json::to_string(&download_record()).unwrap();
|
||||||
|
let expected = DownloadRecord {
|
||||||
|
status: DownloadStatus::default(),
|
||||||
|
..download_record()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_readarr_task_task_name_deserialization_falls_back_to_default() {
|
||||||
|
let task = ReadarrTask {
|
||||||
|
name: "Backup".to_owned(),
|
||||||
|
task_name: ReadarrTaskName::Backup,
|
||||||
|
..ReadarrTask::default()
|
||||||
|
};
|
||||||
|
let task_json = serde_json::to_string(&task).unwrap();
|
||||||
|
let expected = ReadarrTask {
|
||||||
|
task_name: ReadarrTaskName::default(),
|
||||||
|
..task
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<ReadarrTask>(&task_json, "taskName", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<ReadarrTask>(&task_json, "taskName", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_with_field<T: DeserializeOwned>(json: &str, field: &str, value: Value) -> T {
|
||||||
|
let mut fixture_json: Value = serde_json::from_str(json).unwrap();
|
||||||
|
fixture_json[field] = value;
|
||||||
|
|
||||||
|
serde_json::from_value(fixture_json).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_history_event_type(event_type: Value) -> ReadarrHistoryEventType {
|
fn deserialize_history_event_type(event_type: Value) -> ReadarrHistoryEventType {
|
||||||
let history_item_json = json!({
|
let history_item_json = json!({
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ use strum::EnumIter;
|
|||||||
use {
|
use {
|
||||||
super::modals::EditionDetailsModal,
|
super::modals::EditionDetailsModal,
|
||||||
crate::models::readarr_models::{
|
crate::models::readarr_models::{
|
||||||
AuthorStatus, BookFile, DownloadStatus, Edition, MediaInfo, MonitorType, NewItemMonitorType,
|
AuthorStatus, BookFile, BookStatistics, DownloadStatus, Edition, MediaInfo, MonitorType,
|
||||||
Ratings, ReadarrHistoryEventType, ReadarrTaskName,
|
NewItemMonitorType, Ratings, ReadarrHistoryEventType, ReadarrTaskName,
|
||||||
},
|
},
|
||||||
crate::models::servarr_models::{IndexerField, Quality, QualityWrapper},
|
crate::models::servarr_models::{IndexerField, Quality, QualityWrapper},
|
||||||
crate::models::stateful_table::SortOption,
|
crate::models::stateful_table::SortOption,
|
||||||
@@ -292,6 +292,12 @@ impl ReadarrData<'_> {
|
|||||||
any_edition_ok: true,
|
any_edition_ok: true,
|
||||||
page_count: Some(288),
|
page_count: Some(288),
|
||||||
release_date: Some(date),
|
release_date: Some(date),
|
||||||
|
statistics: Some(BookStatistics {
|
||||||
|
book_file_count: 3,
|
||||||
|
total_book_count: 7,
|
||||||
|
size_on_disk: 2469606195,
|
||||||
|
percent_of_books: 42.86,
|
||||||
|
}),
|
||||||
..Book::default()
|
..Book::default()
|
||||||
};
|
};
|
||||||
let readarr_history_item = ReadarrHistoryItem {
|
let readarr_history_item = ReadarrHistoryItem {
|
||||||
|
|||||||
@@ -315,7 +315,9 @@ pub struct RootFolder {
|
|||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SecurityConfig {
|
pub struct SecurityConfig {
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub authentication_method: AuthenticationMethod,
|
pub authentication_method: AuthenticationMethod,
|
||||||
|
#[serde(default, deserialize_with = "super::from_json_or_default")]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub authentication_required: Option<AuthenticationRequired>,
|
pub authentication_required: Option<AuthenticationRequired>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -323,6 +325,7 @@ pub struct SecurityConfig {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub certificate_validation: CertificateValidation,
|
pub certificate_validation: CertificateValidation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
AuthenticationMethod, AuthenticationRequired, CertificateValidation, Indexer, QualityProfile,
|
AuthenticationMethod, AuthenticationRequired, CertificateValidation, Indexer, QualityProfile,
|
||||||
Update, UpdateChanges,
|
SecurityConfig, Update, UpdateChanges,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -90,4 +92,118 @@ mod tests {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_config_authentication_method_deserialization_falls_back_to_default() {
|
||||||
|
let security_config_json = serde_json::to_string(&security_config()).unwrap();
|
||||||
|
let expected = SecurityConfig {
|
||||||
|
authentication_method: AuthenticationMethod::default(),
|
||||||
|
..security_config()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"authenticationMethod",
|
||||||
|
json!(2)
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"authenticationMethod",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_config_authentication_required_deserialization_falls_back_to_none() {
|
||||||
|
let security_config_json = serde_json::to_string(&security_config()).unwrap();
|
||||||
|
let expected = SecurityConfig {
|
||||||
|
authentication_required: None,
|
||||||
|
..security_config()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"authenticationRequired",
|
||||||
|
json!(2)
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"authenticationRequired",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_config_deserializes_when_authentication_required_is_absent() {
|
||||||
|
let mut security_config_json = serde_json::to_value(security_config()).unwrap();
|
||||||
|
security_config_json
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.remove("authenticationRequired");
|
||||||
|
let expected = SecurityConfig {
|
||||||
|
authentication_required: None,
|
||||||
|
..security_config()
|
||||||
|
};
|
||||||
|
|
||||||
|
let deserialized = serde_json::from_value::<SecurityConfig>(security_config_json);
|
||||||
|
|
||||||
|
assert_ok!(&deserialized);
|
||||||
|
assert_eq!(deserialized.unwrap(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_config_certificate_validation_deserialization_falls_back_to_default() {
|
||||||
|
let security_config_json = serde_json::to_string(&security_config()).unwrap();
|
||||||
|
let expected = SecurityConfig {
|
||||||
|
certificate_validation: CertificateValidation::default(),
|
||||||
|
..security_config()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"certificateValidation",
|
||||||
|
json!(2)
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SecurityConfig>(
|
||||||
|
&security_config_json,
|
||||||
|
"certificateValidation",
|
||||||
|
json!("notAThing")
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_with_field<T: DeserializeOwned>(json: &str, field: &str, value: Value) -> T {
|
||||||
|
let mut fixture_json: Value = serde_json::from_str(json).unwrap();
|
||||||
|
fixture_json[field] = value;
|
||||||
|
|
||||||
|
serde_json::from_value(fixture_json).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn security_config() -> SecurityConfig {
|
||||||
|
SecurityConfig {
|
||||||
|
authentication_method: AuthenticationMethod::Forms,
|
||||||
|
authentication_required: Some(AuthenticationRequired::Enabled),
|
||||||
|
username: Some("admin".to_owned()),
|
||||||
|
password: Some("password".to_owned()),
|
||||||
|
api_key: "test-api-key".to_owned(),
|
||||||
|
certificate_validation: CertificateValidation::Disabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -112,6 +112,7 @@ pub struct DeleteSeriesParams {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadRecord {
|
pub struct DownloadRecord {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: DownloadStatus,
|
pub status: DownloadStatus,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
@@ -314,12 +315,14 @@ pub struct Series {
|
|||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub year: i64,
|
pub year: i64,
|
||||||
pub monitored: bool,
|
pub monitored: bool,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub series_type: SeriesType,
|
pub series_type: SeriesType,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub genres: Vec<String>,
|
pub genres: Vec<String>,
|
||||||
pub tags: Vec<Number>,
|
pub tags: Vec<Number>,
|
||||||
pub ratings: Rating,
|
pub ratings: Rating,
|
||||||
pub ended: bool,
|
pub ended: bool,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub status: SeriesStatus,
|
pub status: SeriesStatus,
|
||||||
pub overview: Option<String>,
|
pub overview: Option<String>,
|
||||||
pub network: Option<String>,
|
pub network: Option<String>,
|
||||||
@@ -556,6 +559,7 @@ pub struct SonarrReleaseDownloadBody {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SonarrTask {
|
pub struct SonarrTask {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(deserialize_with = "super::from_json_or_default")]
|
||||||
pub task_name: SonarrTaskName,
|
pub task_name: SonarrTaskName,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub interval: i64,
|
pub interval: i64,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
@@ -17,6 +18,9 @@ mod tests {
|
|||||||
SonarrTaskName,
|
SonarrTaskName,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
|
||||||
|
SERIES_JSON, download_record, task,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_episode_display() {
|
fn test_episode_display() {
|
||||||
@@ -698,6 +702,83 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_series_type_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Series {
|
||||||
|
series_type: SeriesType::default(),
|
||||||
|
..serde_json::from_str(SERIES_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Series>(SERIES_JSON, "seriesType", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Series>(SERIES_JSON, "seriesType", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_status_deserialization_falls_back_to_default() {
|
||||||
|
let expected = Series {
|
||||||
|
status: SeriesStatus::default(),
|
||||||
|
..serde_json::from_str(SERIES_JSON).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Series>(SERIES_JSON, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<Series>(SERIES_JSON, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_record_status_deserialization_falls_back_to_default() {
|
||||||
|
let download_record_json = serde_json::to_string(&download_record()).unwrap();
|
||||||
|
let expected = DownloadRecord {
|
||||||
|
status: DownloadStatus::default(),
|
||||||
|
..download_record()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_task_task_name_deserialization_falls_back_to_default() {
|
||||||
|
let task_json = serde_json::to_string(&task()).unwrap();
|
||||||
|
let expected = SonarrTask {
|
||||||
|
task_name: SonarrTaskName::default(),
|
||||||
|
..task()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SonarrTask>(&task_json, "taskName", json!(2)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_with_field::<SonarrTask>(&task_json, "taskName", json!("notAThing")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_with_field<T: DeserializeOwned>(json: &str, field: &str, value: Value) -> T {
|
||||||
|
let mut fixture_json: Value = serde_json::from_str(json).unwrap();
|
||||||
|
fixture_json[field] = value;
|
||||||
|
|
||||||
|
serde_json::from_value(fixture_json).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_history_event_type(event_type: Value) -> SonarrHistoryEventType {
|
fn deserialize_history_event_type(event_type: Value) -> SonarrHistoryEventType {
|
||||||
let history_item_json = json!({
|
let history_item_json = json!({
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|||||||
@@ -184,9 +184,9 @@ pub mod test_utils {
|
|||||||
DownloadRecord {
|
DownloadRecord {
|
||||||
title: "Test download title".to_owned(),
|
title: "Test download title".to_owned(),
|
||||||
status: DownloadStatus::Downloading,
|
status: DownloadStatus::Downloading,
|
||||||
id: 1,
|
id: 9,
|
||||||
album_id: Some(Number::from(1i64)),
|
album_id: Some(Number::from(1i64)),
|
||||||
artist_id: Some(Number::from(1i64)),
|
artist_id: Some(Number::from(3i64)),
|
||||||
size: 3543348019f64,
|
size: 3543348019f64,
|
||||||
sizeleft: 1771674009f64,
|
sizeleft: 1771674009f64,
|
||||||
output_path: Some(HorizontallyScrollableText::from("/nfs/music/alex/album")),
|
output_path: Some(HorizontallyScrollableText::from("/nfs/music/alex/album")),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{
|
||||||
AddMovieBody, AddMovieSearchResult, Credit, CreditType, DeleteMovieParams, DownloadRecord,
|
AddMovieBody, AddMovieSearchResult, Credit, CreditType, DeleteMovieParams, DownloadRecord,
|
||||||
EditMovieParams, Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease,
|
DownloadStatus, EditMovieParams, Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease,
|
||||||
RadarrReleaseDownloadBody,
|
RadarrReleaseDownloadBody,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_data::Notification;
|
use crate::models::servarr_data::Notification;
|
||||||
@@ -664,11 +664,11 @@ pub(in crate::network::radarr_network::library) fn get_movie_status(
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|&download| download.movie_id == movie_id)
|
.find(|&download| download.movie_id == movie_id)
|
||||||
{
|
{
|
||||||
if download.status == "downloading" {
|
if download.status == DownloadStatus::Downloading {
|
||||||
return "Downloading".to_owned();
|
return "Downloading".to_owned();
|
||||||
}
|
}
|
||||||
|
|
||||||
if download.status == "completed" {
|
if download.status == DownloadStatus::Completed {
|
||||||
return "Awaiting Import".to_owned();
|
return "Awaiting Import".to_owned();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{
|
||||||
AddMovieBody, AddMovieOptions, Credit, DeleteMovieParams, DownloadRecord, EditMovieParams,
|
AddMovieBody, AddMovieOptions, Credit, DeleteMovieParams, DownloadRecord, DownloadStatus,
|
||||||
MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, RadarrReleaseDownloadBody,
|
EditMovieParams, MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor,
|
||||||
|
RadarrReleaseDownloadBody,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_data::Notification;
|
use crate::models::servarr_data::Notification;
|
||||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||||
@@ -1199,7 +1200,7 @@ mod tests {
|
|||||||
false,
|
false,
|
||||||
&[DownloadRecord {
|
&[DownloadRecord {
|
||||||
movie_id: 1,
|
movie_id: 1,
|
||||||
status: "downloading".to_owned(),
|
status: DownloadStatus::Downloading,
|
||||||
..DownloadRecord::default()
|
..DownloadRecord::default()
|
||||||
}],
|
}],
|
||||||
1
|
1
|
||||||
@@ -1215,7 +1216,7 @@ mod tests {
|
|||||||
false,
|
false,
|
||||||
&[DownloadRecord {
|
&[DownloadRecord {
|
||||||
movie_id: 1,
|
movie_id: 1,
|
||||||
status: "completed".to_owned(),
|
status: DownloadStatus::Completed,
|
||||||
..DownloadRecord::default()
|
..DownloadRecord::default()
|
||||||
}],
|
}],
|
||||||
1
|
1
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
pub mod test_utils {
|
pub mod test_utils {
|
||||||
use crate::models::radarr_models::{
|
use crate::models::radarr_models::{
|
||||||
AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, Collection, CollectionMovie, Credit,
|
AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, Collection, CollectionMovie, Credit,
|
||||||
CreditType, DownloadRecord, DownloadsResponse, IndexerSettings, MediaInfo, MinimumAvailability,
|
CreditType, DownloadRecord, DownloadStatus, DownloadsResponse, IndexerSettings, MediaInfo,
|
||||||
Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrHistoryData, RadarrHistoryEventType,
|
MinimumAvailability, Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrHistoryData,
|
||||||
RadarrHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, Rating, RatingsList,
|
RadarrHistoryEventType, RadarrHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, Rating,
|
||||||
|
RatingsList,
|
||||||
};
|
};
|
||||||
use crate::models::servarr_models::{
|
use crate::models::servarr_models::{
|
||||||
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
||||||
@@ -339,7 +340,7 @@ pub mod test_utils {
|
|||||||
pub fn download_record() -> DownloadRecord {
|
pub fn download_record() -> DownloadRecord {
|
||||||
DownloadRecord {
|
DownloadRecord {
|
||||||
title: "Test Download Title".to_owned(),
|
title: "Test Download Title".to_owned(),
|
||||||
status: "downloading".to_owned(),
|
status: DownloadStatus::Downloading,
|
||||||
id: 1,
|
id: 1,
|
||||||
movie_id: 1,
|
movie_id: 1,
|
||||||
size: 3543348019,
|
size: 3543348019,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease, Track};
|
use crate::models::lidarr_models::{
|
||||||
|
DownloadRecord, DownloadStatus, LidarrHistoryItem, LidarrRelease, Track,
|
||||||
|
};
|
||||||
use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock};
|
use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock};
|
||||||
use crate::ui::lidarr_ui::library::track_details_ui::TrackDetailsUi;
|
use crate::ui::lidarr_ui::library::track_details_ui::TrackDetailsUi;
|
||||||
use crate::ui::lidarr_ui::lidarr_ui_utils::create_history_event_details;
|
use crate::ui::lidarr_ui::lidarr_ui_utils::create_history_event_details;
|
||||||
@@ -150,6 +152,7 @@ fn draw_tracks_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.expect("Album details modal is unpopulated")
|
.expect("Album details modal is unpopulated")
|
||||||
.tracks,
|
.tracks,
|
||||||
);
|
);
|
||||||
|
let downloads_vec = &app.data.lidarr_data.downloads.items;
|
||||||
|
|
||||||
let track_row_mapping = |track: &Track| {
|
let track_row_mapping = |track: &Track| {
|
||||||
let Track {
|
let Track {
|
||||||
@@ -187,6 +190,7 @@ fn draw_tracks_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
let duration_str = format!("{mins}:{secs:02}");
|
let duration_str = format!("{mins}:{secs:02}");
|
||||||
|
|
||||||
decorate_track_row_with_style(
|
decorate_track_row_with_style(
|
||||||
|
downloads_vec,
|
||||||
track,
|
track,
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
Cell::from(track_number.clone()),
|
Cell::from(track_number.clone()),
|
||||||
@@ -221,12 +225,35 @@ fn draw_tracks_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decorate_track_row_with_style<'a>(track: &Track, row: Row<'a>) -> Row<'a> {
|
fn decorate_track_row_with_style<'a>(
|
||||||
if track.has_file {
|
downloads_vec: &[DownloadRecord],
|
||||||
row.downloaded()
|
track: &Track,
|
||||||
} else {
|
row: Row<'a>,
|
||||||
row.missing()
|
) -> Row<'a> {
|
||||||
|
if !track.has_file {
|
||||||
|
let default_album_id = Number::from(-1i64);
|
||||||
|
if let Some(download) = downloads_vec.iter().find(|&download| {
|
||||||
|
download
|
||||||
|
.album_id
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&default_album_id)
|
||||||
|
.as_i64()
|
||||||
|
.unwrap()
|
||||||
|
== track.album_id
|
||||||
|
}) {
|
||||||
|
if download.status == DownloadStatus::Downloading {
|
||||||
|
return row.downloading();
|
||||||
|
}
|
||||||
|
|
||||||
|
if download.status == DownloadStatus::Completed {
|
||||||
|
return row.awaiting_import();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return row.missing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
row.downloaded()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_album_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
fn draw_album_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use ratatui::widgets::{Cell, Row};
|
use ratatui::widgets::{Cell, Row};
|
||||||
|
use serde_json::Number;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::lidarr_models::Track;
|
use crate::models::lidarr_models::{DownloadRecord, DownloadStatus, Track};
|
||||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||||
ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock, TRACK_DETAILS_BLOCKS,
|
ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock, TRACK_DETAILS_BLOCKS,
|
||||||
};
|
};
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
|
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
|
||||||
use crate::ui::DrawUi;
|
use crate::ui::DrawUi;
|
||||||
use crate::ui::lidarr_ui::library::album_details_ui::{
|
use crate::ui::lidarr_ui::library::album_details_ui::{
|
||||||
AlbumDetailsUi, decorate_track_row_with_style,
|
AlbumDetailsUi, decorate_track_row_with_style,
|
||||||
@@ -39,7 +41,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
let style = decorate_track_row_with_style(&track, row.clone());
|
let style = decorate_track_row_with_style(&[], &track, row.clone());
|
||||||
|
|
||||||
assert_eq!(style, row.downloaded());
|
assert_eq!(style, row.downloaded());
|
||||||
}
|
}
|
||||||
@@ -52,11 +54,89 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
let style = decorate_track_row_with_style(&track, row.clone());
|
let style = decorate_track_row_with_style(&[], &track, row.clone());
|
||||||
|
|
||||||
assert_eq!(style, row.missing());
|
assert_eq!(style, row.missing());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_track_row_with_style_downloading_when_track_has_no_file_and_album_is_downloading()
|
||||||
|
{
|
||||||
|
let downloads_vec = vec![album_download(7, DownloadStatus::Downloading)];
|
||||||
|
let track = Track {
|
||||||
|
has_file: false,
|
||||||
|
album_id: 7,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_track_row_with_style(&downloads_vec, &track, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.downloading());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_track_row_with_style_awaiting_import_when_track_has_no_file_and_album_download_is_completed()
|
||||||
|
{
|
||||||
|
let downloads_vec = vec![album_download(7, DownloadStatus::Completed)];
|
||||||
|
let track = Track {
|
||||||
|
has_file: false,
|
||||||
|
album_id: 7,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_track_row_with_style(&downloads_vec, &track, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.awaiting_import());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_track_row_with_style_missing_when_album_download_is_neither_downloading_nor_completed()
|
||||||
|
{
|
||||||
|
let downloads_vec = vec![album_download(7, DownloadStatus::Queued)];
|
||||||
|
let track = Track {
|
||||||
|
has_file: false,
|
||||||
|
album_id: 7,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_track_row_with_style(&downloads_vec, &track, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.missing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_track_row_with_style_missing_when_only_a_different_album_is_downloading() {
|
||||||
|
let downloads_vec = vec![album_download(8, DownloadStatus::Downloading)];
|
||||||
|
let track = Track {
|
||||||
|
has_file: false,
|
||||||
|
album_id: 7,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_track_row_with_style(&downloads_vec, &track, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.missing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_track_row_with_style_downloaded_when_track_has_file_and_album_is_downloading() {
|
||||||
|
let downloads_vec = vec![album_download(7, DownloadStatus::Downloading)];
|
||||||
|
let track = Track {
|
||||||
|
has_file: true,
|
||||||
|
album_id: 7,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_track_row_with_style(&downloads_vec, &track, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.downloaded());
|
||||||
|
}
|
||||||
|
|
||||||
mod snapshot_tests {
|
mod snapshot_tests {
|
||||||
use crate::ui::ui_test_utils::test_utils::TerminalSize;
|
use crate::ui::ui_test_utils::test_utils::TerminalSize;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
@@ -177,4 +257,13 @@ mod tests {
|
|||||||
insta::assert_snapshot!(output);
|
insta::assert_snapshot!(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn album_download(album_id: i64, status: DownloadStatus) -> DownloadRecord {
|
||||||
|
DownloadRecord {
|
||||||
|
status,
|
||||||
|
album_id: Some(Number::from(album_id)),
|
||||||
|
artist_id: Some(Number::from(3i64)),
|
||||||
|
..download_record()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -279,13 +279,16 @@ fn draw_albums_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.release_date
|
.release_date
|
||||||
.map_or_else(|| "N/A".to_owned(), |d| d.format("%Y-%m-%d").to_string());
|
.map_or_else(|| "N/A".to_owned(), |d| d.format("%Y-%m-%d").to_string());
|
||||||
let track_count = album.statistics.as_ref().map_or_else(
|
let track_count = album.statistics.as_ref().map_or_else(
|
||||||
|| "0/0".to_owned(),
|
|| "N/A".to_owned(),
|
||||||
|s| format!("{}/{}", s.track_file_count, s.total_track_count),
|
|s| format!("{}/{}", s.track_file_count, s.total_track_count),
|
||||||
);
|
);
|
||||||
let size = album
|
let size = album.statistics.as_ref().map_or_else(
|
||||||
.statistics
|
|| "N/A".to_owned(),
|
||||||
.as_ref()
|
|s| {
|
||||||
.map_or(0f64, |s| convert_to_gb(s.size_on_disk));
|
let size = convert_to_gb(s.size_on_disk);
|
||||||
|
format!("{size:.2} GB")
|
||||||
|
},
|
||||||
|
);
|
||||||
let duration_mins = album.duration / 60000;
|
let duration_mins = album.duration / 60000;
|
||||||
|
|
||||||
decorate_album_row_with_style(
|
decorate_album_row_with_style(
|
||||||
@@ -297,7 +300,7 @@ fn draw_albums_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
Cell::from(track_count),
|
Cell::from(track_count),
|
||||||
Cell::from(format!("{duration_mins} min")),
|
Cell::from(format!("{duration_mins} min")),
|
||||||
Cell::from(release_date),
|
Cell::from(release_date),
|
||||||
Cell::from(format!("{size:.2} GB")),
|
Cell::from(size),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -249,6 +249,19 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_artist_details_ui_renders_album_without_statistics() {
|
||||||
|
let mut app = App::test_default_fully_populated();
|
||||||
|
app.data.lidarr_data.albums.items[0].statistics = None;
|
||||||
|
app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into());
|
||||||
|
|
||||||
|
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||||
|
ArtistDetailsUi::draw(f, app, f.area());
|
||||||
|
});
|
||||||
|
|
||||||
|
insta::assert_snapshot!(output);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_artist_details_ui_renders_delete_album_prompt_over_artist_details() {
|
fn test_artist_details_ui_renders_delete_album_prompt_over_artist_details() {
|
||||||
let mut app = App::test_default_fully_populated();
|
let mut app = App::test_default_fully_populated();
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│Artist: Alex │
|
||||||
|
│Overview: some interesting description of the artist She was born in Madison, Wisconsin: a city she has never really left. Her first album took her│
|
||||||
|
│seven years to finish. │
|
||||||
|
│Disambiguation: American pianist │
|
||||||
|
│Type: Person │
|
||||||
|
│Status: Continuing │
|
||||||
|
│Genres: soundtrack │
|
||||||
|
│Rating: 84% │
|
||||||
|
│Path: /nfs/music/test-artist │
|
||||||
|
│Quality Profile: Lossless │
|
||||||
|
│Metadata Profile: Standard │
|
||||||
|
│Monitored: Yes │
|
||||||
|
│Albums: 1 │
|
||||||
|
│Tracks: 12/15 │
|
||||||
|
│╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
|
||||||
|
││ Albums │ History │ Manual Search ││
|
||||||
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
|
││ Monitored Title Type Tracks Duration Release Date Size ││
|
||||||
|
││=> 🏷 Test Album Album N/A 0 min 2023-01-01 N/A ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||||
|
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
use crate::models::lidarr_models::Track;
|
||||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS};
|
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS};
|
||||||
use crate::ui::DrawUi;
|
use crate::ui::DrawUi;
|
||||||
use crate::ui::lidarr_ui::library::track_details_ui::TrackDetailsUi;
|
use crate::ui::lidarr_ui::library::track_details_ui::{TrackDetailsUi, style_from_status};
|
||||||
|
use crate::ui::styles::{downloaded_style, missing_style};
|
||||||
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
|
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -19,6 +22,30 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_downloaded_when_track_has_file() {
|
||||||
|
let track = Track {
|
||||||
|
has_file: true,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(&track);
|
||||||
|
|
||||||
|
assert_eq!(style, downloaded_style());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_missing_when_track_has_no_file() {
|
||||||
|
let track = Track {
|
||||||
|
has_file: false,
|
||||||
|
..Track::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(&track);
|
||||||
|
|
||||||
|
assert_eq!(style, missing_style());
|
||||||
|
}
|
||||||
|
|
||||||
mod snapshot_tests {
|
mod snapshot_tests {
|
||||||
use crate::ui::ui_test_utils::test_utils::TerminalSize;
|
use crate::ui::ui_test_utils::test_utils::TerminalSize;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::logos::RADARR_LOGO;
|
use crate::logos::RADARR_LOGO;
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::models::radarr_models::{DownloadRecord, Movie};
|
use crate::models::radarr_models::{DownloadRecord, DownloadStatus, Movie};
|
||||||
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
||||||
use crate::models::servarr_models::{DiskSpace, RootFolder};
|
use crate::models::servarr_models::{DiskSpace, RootFolder};
|
||||||
use crate::ui::DrawUi;
|
use crate::ui::DrawUi;
|
||||||
@@ -230,11 +230,11 @@ fn decorate_with_row_style<'a>(
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|&download| download.movie_id == movie.id)
|
.find(|&download| download.movie_id == movie.id)
|
||||||
{
|
{
|
||||||
if download.status == "downloading" {
|
if download.status == DownloadStatus::Downloading {
|
||||||
return row.downloading();
|
return row.downloading();
|
||||||
}
|
}
|
||||||
|
|
||||||
if download.status == "completed" {
|
if download.status == DownloadStatus::Completed {
|
||||||
return row.awaiting_import();
|
return row.awaiting_import();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::models::radarr_models::{DownloadRecord, Movie};
|
use crate::models::radarr_models::{DownloadRecord, DownloadStatus, Movie};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use ratatui::widgets::{Cell, Row};
|
use ratatui::widgets::{Cell, Row};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
@@ -50,8 +50,20 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(false, Some("downloading"), false, "", RowStyle::Downloading)]
|
#[case(
|
||||||
#[case(false, Some("completed"), false, "", RowStyle::AwaitingImport)]
|
false,
|
||||||
|
Some(DownloadStatus::Downloading),
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
RowStyle::Downloading
|
||||||
|
)]
|
||||||
|
#[case(
|
||||||
|
false,
|
||||||
|
Some(DownloadStatus::Completed),
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
RowStyle::AwaitingImport
|
||||||
|
)]
|
||||||
#[case(false, None, false, "", RowStyle::UnmonitoredMissing)]
|
#[case(false, None, false, "", RowStyle::UnmonitoredMissing)]
|
||||||
#[case(false, None, true, "", RowStyle::Unreleased)]
|
#[case(false, None, true, "", RowStyle::Unreleased)]
|
||||||
#[case(false, None, true, "released", RowStyle::Missing)]
|
#[case(false, None, true, "released", RowStyle::Missing)]
|
||||||
@@ -59,7 +71,7 @@ mod tests {
|
|||||||
#[case(true, None, true, "", RowStyle::Downloaded)]
|
#[case(true, None, true, "", RowStyle::Downloaded)]
|
||||||
fn test_decorate_with_row_style(
|
fn test_decorate_with_row_style(
|
||||||
#[case] has_file: bool,
|
#[case] has_file: bool,
|
||||||
#[case] download_status: Option<&str>,
|
#[case] download_status: Option<DownloadStatus>,
|
||||||
#[case] is_monitored: bool,
|
#[case] is_monitored: bool,
|
||||||
#[case] movie_status: String,
|
#[case] movie_status: String,
|
||||||
#[case] expected_style: RowStyle,
|
#[case] expected_style: RowStyle,
|
||||||
@@ -67,7 +79,7 @@ mod tests {
|
|||||||
let downloads_vec = if let Some(download_status) = download_status {
|
let downloads_vec = if let Some(download_status) = download_status {
|
||||||
vec![DownloadRecord {
|
vec![DownloadRecord {
|
||||||
movie_id: 1,
|
movie_id: 1,
|
||||||
status: download_status.to_owned(),
|
status: download_status,
|
||||||
..DownloadRecord::default()
|
..DownloadRecord::default()
|
||||||
}]
|
}]
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -275,13 +275,16 @@ fn draw_books_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.release_date
|
.release_date
|
||||||
.map_or_else(|| "N/A".to_owned(), |d| d.format("%Y-%m-%d").to_string());
|
.map_or_else(|| "N/A".to_owned(), |d| d.format("%Y-%m-%d").to_string());
|
||||||
let book_file_count = book.statistics.as_ref().map_or_else(
|
let book_file_count = book.statistics.as_ref().map_or_else(
|
||||||
|| "0/0".to_owned(),
|
|| "N/A".to_owned(),
|
||||||
|s| format!("{}/{}", s.book_file_count, s.total_book_count),
|
|s| format!("{}/{}", s.book_file_count, s.total_book_count),
|
||||||
);
|
);
|
||||||
let size = book
|
let size = book.statistics.as_ref().map_or_else(
|
||||||
.statistics
|
|| "N/A".to_owned(),
|
||||||
.as_ref()
|
|s| {
|
||||||
.map_or(0f64, |s| convert_to_gb(s.size_on_disk));
|
let size = convert_to_gb(s.size_on_disk);
|
||||||
|
format!("{size:.2} GB")
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
decorate_book_row_with_style(
|
decorate_book_row_with_style(
|
||||||
book,
|
book,
|
||||||
@@ -290,7 +293,7 @@ fn draw_books_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
Cell::from(book.title.to_string()),
|
Cell::from(book.title.to_string()),
|
||||||
Cell::from(book_file_count),
|
Cell::from(book_file_count),
|
||||||
Cell::from(release_date),
|
Cell::from(release_date),
|
||||||
Cell::from(format!("{size:.2} GB")),
|
Cell::from(size),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -271,6 +271,19 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_author_details_ui_renders_book_without_statistics() {
|
||||||
|
let mut app = App::test_default_fully_populated();
|
||||||
|
app.data.readarr_data.books.items[0].statistics = None;
|
||||||
|
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
|
||||||
|
|
||||||
|
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||||
|
AuthorDetailsUi::draw(f, app, f.area());
|
||||||
|
});
|
||||||
|
|
||||||
|
insta::assert_snapshot!(output);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_author_details_ui_renders_update_and_scan_prompt_over_author_details() {
|
fn test_author_details_ui_renders_update_and_scan_prompt_over_author_details() {
|
||||||
let mut app = App::test_default_fully_populated();
|
let mut app = App::test_default_fully_populated();
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ expression: output
|
|||||||
││ Books │ History │ Author Search ││
|
││ Books │ History │ Author Search ││
|
||||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
││ Monitored Title Files Release Date Size ││
|
││ Monitored Title Files Release Date Size ││
|
||||||
││=> 🏷 Test Book 0/0 2023-05-20 0.00 GB ││
|
││=> 🏷 Test Book 3/7 2023-05-20 2.30 GB ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ expression: output
|
|||||||
││ Books │ History │ Author Search │Do you want to trigger an automatic search of your indexers│ ││
|
││ Books │ History │ Author Search │Do you want to trigger an automatic search of your indexers│ ││
|
||||||
││─────────────────────────────────────────│ for all monitored book(s) for the author: Test Author? │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ for all monitored book(s) for the author: Test Author? │───────────────────────────────────────────││
|
||||||
││ Monitored Title │ │Date Size ││
|
││ Monitored Title │ │Date Size ││
|
||||||
││=> 🏷 Test Book │ │20 0.00 GB ││
|
││=> 🏷 Test Book │ │20 2.30 GB ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ expression: output
|
|||||||
││ Books │ History │ Author Search ││
|
││ Books │ History │ Author Search ││
|
||||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
││ Monitored Title Files Release Date Size ││
|
││ Monitored Title Files Release Date Size ││
|
||||||
││=> 🏷 Test Book 0/0 2023-05-20 0.00 GB ││
|
││=> 🏷 Test Book 3/7 2023-05-20 2.30 GB ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ expression: output
|
|||||||
││ Books │ History │ Author Search ││
|
││ Books │ History │ Author Search ││
|
||||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
││ Monitored Title Files Release Date Size ││
|
││ Monitored Title Files Release Date Size ││
|
||||||
││=> 🏷 Test Book 0/0 2023-05-20 0.00 GB ││
|
││=> 🏷 Test Book 3/7 2023-05-20 2.30 GB ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ expression: output
|
|||||||
││ Books │ History │ Author Search │ Do you want to trigger an update and disk scan for the │ ││
|
││ Books │ History │ Author Search │ Do you want to trigger an update and disk scan for the │ ││
|
||||||
││─────────────────────────────────────────│ author: Test Author? │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ author: Test Author? │───────────────────────────────────────────││
|
||||||
││ Monitored Title │ │Date Size ││
|
││ Monitored Title │ │Date Size ││
|
||||||
││=> 🏷 Test Book │ │20 0.00 GB ││
|
││=> 🏷 Test Book │ │20 2.30 GB ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ expression: output
|
|||||||
││ Books │ History │ Author Search │Do you want to trigger an automatic search of your indexers│ ││
|
││ Books │ History │ Author Search │Do you want to trigger an automatic search of your indexers│ ││
|
||||||
││─────────────────────────────────────────│ for all monitored book(s) for the author: Test Author? │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ for all monitored book(s) for the author: Test Author? │───────────────────────────────────────────││
|
||||||
││ Monitored Title │ │Date Size ││
|
││ Monitored Title │ │Date Size ││
|
||||||
││=> 🏷 Test Book │ │20 0.00 GB ││
|
││=> 🏷 Test Book │ │20 2.30 GB ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
source: src/ui/readarr_ui/library/author_details_ui_tests.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
╭ Test Author ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│Author: Test Author │
|
||||||
|
│Status: Continuing │
|
||||||
|
│Genres: science fiction │
|
||||||
|
│Rating: N/A │
|
||||||
|
│Path: /nfs/books/Test Author │
|
||||||
|
│Quality Profile: Standard │
|
||||||
|
│Metadata Profile: Comprehensive │
|
||||||
|
│Monitored: Yes │
|
||||||
|
│Overview: some interesting description of the author He was born in Madison, Wisconsin: a city he has never really left. His first novel took him │
|
||||||
|
│seven years to finish. │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
|
||||||
|
││ Books │ History │ Author Search ││
|
||||||
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
|
││ Monitored Title Files Release Date Size ││
|
||||||
|
││=> 🏷 Test Book N/A 2023-05-20 N/A ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||||
|
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
+1
-1
@@ -22,7 +22,7 @@ expression: output
|
|||||||
││ Books │ History │ Author Search │ Do you really want to delete the book: │ ││
|
││ Books │ History │ Author Search │ Do you really want to delete the book: │ ││
|
||||||
││─────────────────────────────────────────│ Test Book? │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ Test Book? │───────────────────────────────────────────││
|
||||||
││ Monitored Title │ │Date Size ││
|
││ Monitored Title │ │Date Size ││
|
||||||
││=> 🏷 Test Book │ │20 0.00 GB ││
|
││=> 🏷 Test Book │ │20 2.30 GB ││
|
||||||
││ │ ╭───╮ │ ││
|
││ │ ╭───╮ │ ││
|
||||||
││ │ Delete Book Files: │ ✔ │ │ ││
|
││ │ Delete Book Files: │ ✔ │ │ ││
|
||||||
││ │ ╰───╯ │ ││
|
││ │ ╰───╯ │ ││
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ expression: output
|
|||||||
││ Books │ History │ Author Search │ Do you want to trigger an update and disk scan for the │ ││
|
││ Books │ History │ Author Search │ Do you want to trigger an update and disk scan for the │ ││
|
||||||
││─────────────────────────────────────────│ author: Test Author? │───────────────────────────────────────────││
|
││─────────────────────────────────────────│ author: Test Author? │───────────────────────────────────────────││
|
||||||
││ Monitored Title │ │Date Size ││
|
││ Monitored Title │ │Date Size ││
|
||||||
││=> 🏷 Test Book │ │20 0.00 GB ││
|
││=> 🏷 Test Book │ │20 2.30 GB ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
││ │ │ ││
|
││ │ │ ││
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ expression: output
|
|||||||
││ Books │ History │ Author Search ││
|
││ Books │ History │ Author Search ││
|
||||||
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
││ Monitored Title Files Release Date Size ││
|
││ Monitored Title Files Release Date Size ││
|
||||||
││=> 🏷 Test Book 0/0 2023-05-20 0.00 GB ││
|
││=> 🏷 Test Book 3/7 2023-05-20 2.30 GB ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
││ ││
|
││ ││
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||||
ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS,
|
ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS,
|
||||||
};
|
};
|
||||||
|
use crate::models::sonarr_models::{DownloadRecord, DownloadStatus, Episode};
|
||||||
use crate::ui::DrawUi;
|
use crate::ui::DrawUi;
|
||||||
use crate::ui::sonarr_ui::library::episode_details_ui::EpisodeDetailsUi;
|
use crate::ui::sonarr_ui::library::episode_details_ui::{EpisodeDetailsUi, style_from_status};
|
||||||
|
use crate::ui::styles::{
|
||||||
|
awaiting_import_style, downloaded_style, downloading_style, missing_style,
|
||||||
|
unmonitored_missing_style, unmonitored_style, unreleased_style,
|
||||||
|
};
|
||||||
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
|
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -21,6 +28,157 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_downloading_when_episode_has_no_file_and_download_is_downloading() {
|
||||||
|
let download = DownloadRecord {
|
||||||
|
status: DownloadStatus::Downloading,
|
||||||
|
..DownloadRecord::default()
|
||||||
|
};
|
||||||
|
let episode = Episode {
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(Some(&download), &episode);
|
||||||
|
|
||||||
|
assert_eq!(style, downloading_style());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_awaiting_import_when_episode_has_no_file_and_download_is_completed() {
|
||||||
|
let download = DownloadRecord {
|
||||||
|
status: DownloadStatus::Completed,
|
||||||
|
..DownloadRecord::default()
|
||||||
|
};
|
||||||
|
let episode = Episode {
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(Some(&download), &episode);
|
||||||
|
|
||||||
|
assert_eq!(style, awaiting_import_style());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_ignores_download_that_is_neither_downloading_nor_completed() {
|
||||||
|
let download = DownloadRecord {
|
||||||
|
status: DownloadStatus::Queued,
|
||||||
|
..DownloadRecord::default()
|
||||||
|
};
|
||||||
|
let episode = Episode {
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
air_date_utc: None,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(Some(&download), &episode);
|
||||||
|
|
||||||
|
assert_eq!(style, missing_style());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_unmonitored_missing_when_episode_has_no_file_and_is_unmonitored() {
|
||||||
|
let episode = Episode {
|
||||||
|
has_file: false,
|
||||||
|
monitored: false,
|
||||||
|
air_date_utc: Some(Utc::now() + Duration::days(1)),
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(None, &episode);
|
||||||
|
|
||||||
|
assert_eq!(style, unmonitored_missing_style());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_unreleased_when_episode_has_no_file_and_air_date_is_future() {
|
||||||
|
let episode = Episode {
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
air_date_utc: Some(Utc::now() + Duration::days(1)),
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(None, &episode);
|
||||||
|
|
||||||
|
assert_eq!(style, unreleased_style());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_missing_when_episode_has_no_file_and_air_date_has_passed() {
|
||||||
|
let episode = Episode {
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
air_date_utc: Some(Utc::now() - Duration::days(1)),
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(None, &episode);
|
||||||
|
|
||||||
|
assert_eq!(style, missing_style());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_missing_when_episode_has_no_file_and_no_air_date() {
|
||||||
|
let episode = Episode {
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
air_date_utc: None,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(None, &episode);
|
||||||
|
|
||||||
|
assert_eq!(style, missing_style());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_unmonitored_when_episode_has_file_and_is_unmonitored() {
|
||||||
|
let episode = Episode {
|
||||||
|
has_file: true,
|
||||||
|
monitored: false,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(None, &episode);
|
||||||
|
|
||||||
|
assert_eq!(style, unmonitored_style());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_downloaded_when_episode_has_file_and_is_monitored() {
|
||||||
|
let episode = Episode {
|
||||||
|
has_file: true,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(None, &episode);
|
||||||
|
|
||||||
|
assert_eq!(style, downloaded_style());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_from_status_ignores_download_when_episode_has_file() {
|
||||||
|
let download = DownloadRecord {
|
||||||
|
status: DownloadStatus::Downloading,
|
||||||
|
..DownloadRecord::default()
|
||||||
|
};
|
||||||
|
let episode = Episode {
|
||||||
|
has_file: true,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_status(Some(&download), &episode);
|
||||||
|
|
||||||
|
assert_eq!(style, downloaded_style());
|
||||||
|
}
|
||||||
|
|
||||||
mod snapshot_tests {
|
mod snapshot_tests {
|
||||||
use crate::ui::ui_test_utils::test_utils::TerminalSize;
|
use crate::ui::ui_test_utils::test_utils::TerminalSize;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use ratatui::widgets::{Cell, Row};
|
||||||
|
use serde_json::Number;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||||
ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS, SEASON_DETAILS_BLOCKS,
|
ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS, SEASON_DETAILS_BLOCKS,
|
||||||
};
|
};
|
||||||
|
use crate::models::sonarr_models::{DownloadRecord, DownloadStatus, Episode};
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
|
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::download_record;
|
||||||
use crate::ui::DrawUi;
|
use crate::ui::DrawUi;
|
||||||
use crate::ui::sonarr_ui::library::season_details_ui::SeasonDetailsUi;
|
use crate::ui::sonarr_ui::library::season_details_ui::{
|
||||||
|
SeasonDetailsUi, decorate_with_row_style,
|
||||||
|
};
|
||||||
|
use crate::ui::styles::ManagarrStyle;
|
||||||
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
|
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -25,6 +34,235 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_downloaded_when_episode_has_file() {
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: true,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&[], &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.downloaded());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_unmonitored_when_episode_has_file_and_is_unmonitored() {
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: true,
|
||||||
|
monitored: false,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&[], &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.unmonitored());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_missing_when_episode_has_no_file() {
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&[], &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.missing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_unmonitored_missing_when_episode_has_no_file_and_is_unmonitored()
|
||||||
|
{
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: false,
|
||||||
|
monitored: false,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&[], &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.unmonitored_missing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_unreleased_when_episode_has_no_file_and_air_date_is_in_the_future()
|
||||||
|
{
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
air_date_utc: Some(Utc::now() + Duration::days(1)),
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&[], &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.unreleased());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_missing_when_episode_has_no_file_and_air_date_is_in_the_past() {
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
air_date_utc: Some(Utc::now() - Duration::days(1)),
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&[], &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.missing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_unmonitored_missing_when_unmonitored_episode_has_no_file_and_air_date_is_in_the_future()
|
||||||
|
{
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: false,
|
||||||
|
monitored: false,
|
||||||
|
air_date_utc: Some(Utc::now() + Duration::days(1)),
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&[], &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.unmonitored_missing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_downloading_when_episode_has_no_file_and_is_downloading() {
|
||||||
|
let downloads_vec = vec![episode_download(5, DownloadStatus::Downloading)];
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&downloads_vec, &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.downloading());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_awaiting_import_when_episode_has_no_file_and_download_is_completed()
|
||||||
|
{
|
||||||
|
let downloads_vec = vec![episode_download(5, DownloadStatus::Completed)];
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&downloads_vec, &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.awaiting_import());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_missing_when_episode_download_is_neither_downloading_nor_completed()
|
||||||
|
{
|
||||||
|
let downloads_vec = vec![episode_download(5, DownloadStatus::Queued)];
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&downloads_vec, &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.missing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_missing_when_only_a_different_episode_is_downloading() {
|
||||||
|
let downloads_vec = vec![episode_download(6, DownloadStatus::Downloading)];
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&downloads_vec, &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.missing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_missing_when_download_has_no_episode_id() {
|
||||||
|
let downloads_vec = vec![DownloadRecord {
|
||||||
|
episode_id: None,
|
||||||
|
..episode_download(5, DownloadStatus::Downloading)
|
||||||
|
}];
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: false,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&downloads_vec, &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.missing());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_downloading_when_unmonitored_episode_has_no_file_and_is_downloading()
|
||||||
|
{
|
||||||
|
let downloads_vec = vec![episode_download(5, DownloadStatus::Downloading)];
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: false,
|
||||||
|
monitored: false,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&downloads_vec, &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.downloading());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decorate_with_row_style_downloaded_when_episode_has_file_and_is_downloading() {
|
||||||
|
let downloads_vec = vec![episode_download(5, DownloadStatus::Downloading)];
|
||||||
|
let episode = Episode {
|
||||||
|
id: 5,
|
||||||
|
has_file: true,
|
||||||
|
monitored: true,
|
||||||
|
..Episode::default()
|
||||||
|
};
|
||||||
|
let row = Row::new(vec![Cell::from("test".to_owned())]);
|
||||||
|
|
||||||
|
let style = decorate_with_row_style(&downloads_vec, &episode, row.clone());
|
||||||
|
|
||||||
|
assert_eq!(style, row.downloaded());
|
||||||
|
}
|
||||||
|
|
||||||
mod snapshot_tests {
|
mod snapshot_tests {
|
||||||
use crate::ui::ui_test_utils::test_utils::TerminalSize;
|
use crate::ui::ui_test_utils::test_utils::TerminalSize;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
@@ -145,4 +383,13 @@ mod tests {
|
|||||||
insta::assert_snapshot!(output);
|
insta::assert_snapshot!(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn episode_download(episode_id: i64, status: DownloadStatus) -> DownloadRecord {
|
||||||
|
DownloadRecord {
|
||||||
|
status,
|
||||||
|
id: 9,
|
||||||
|
episode_id: Some(Number::from(episode_id)),
|
||||||
|
..download_record()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SERIES_DETAILS_BLOCKS};
|
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SERIES_DETAILS_BLOCKS};
|
||||||
use crate::models::sonarr_models::{Season, SeasonStatistics, SonarrHistoryItem};
|
use crate::models::sonarr_models::{Season, SonarrHistoryItem};
|
||||||
use crate::ui::sonarr_ui::library::episode_details_ui::EpisodeDetailsUi;
|
use crate::ui::sonarr_ui::library::episode_details_ui::EpisodeDetailsUi;
|
||||||
use crate::ui::sonarr_ui::library::season_details_ui::SeasonDetailsUi;
|
use crate::ui::sonarr_ui::library::season_details_ui::SeasonDetailsUi;
|
||||||
use crate::ui::sonarr_ui::library::series_overview_ui::SeriesOverviewUi;
|
use crate::ui::sonarr_ui::library::series_overview_ui::SeriesOverviewUi;
|
||||||
@@ -229,29 +229,26 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
|
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
|
||||||
let content = Some(&mut app.data.sonarr_data.seasons);
|
let content = Some(&mut app.data.sonarr_data.seasons);
|
||||||
let season_row_mapping = |season: &Season| {
|
let season_row_mapping = |season: &Season| {
|
||||||
let Season {
|
|
||||||
title, statistics, ..
|
|
||||||
} = season;
|
|
||||||
let SeasonStatistics {
|
|
||||||
episode_file_count,
|
|
||||||
episode_count,
|
|
||||||
size_on_disk,
|
|
||||||
..
|
|
||||||
} = if let Some(stats) = statistics {
|
|
||||||
stats
|
|
||||||
} else {
|
|
||||||
&SeasonStatistics::default()
|
|
||||||
};
|
|
||||||
let season_monitored = if season.monitored { "🏷" } else { "" };
|
let season_monitored = if season.monitored { "🏷" } else { "" };
|
||||||
let size = convert_to_gb(*size_on_disk);
|
let episode_count = season.statistics.as_ref().map_or_else(
|
||||||
|
|| "N/A".to_owned(),
|
||||||
|
|s| format!("{}/{}", s.episode_file_count, s.episode_count),
|
||||||
|
);
|
||||||
|
let size = season.statistics.as_ref().map_or_else(
|
||||||
|
|| "N/A".to_owned(),
|
||||||
|
|s| {
|
||||||
|
let size = convert_to_gb(s.size_on_disk);
|
||||||
|
format!("{size:.2} GB")
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
decorate_season_row_with_style(
|
decorate_season_row_with_style(
|
||||||
season,
|
season,
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
Cell::from(season_monitored.to_owned()),
|
Cell::from(season_monitored.to_owned()),
|
||||||
Cell::from(title.clone().unwrap_or_default()),
|
Cell::from(season.title.clone().unwrap_or_default()),
|
||||||
Cell::from(format!("{episode_file_count}/{episode_count}")),
|
Cell::from(episode_count),
|
||||||
Cell::from(format!("{size:.2} GB")),
|
Cell::from(size),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -239,6 +239,19 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_series_details_ui_renders_season_without_statistics() {
|
||||||
|
let mut app = App::test_default_fully_populated();
|
||||||
|
app.data.sonarr_data.seasons.items[0].statistics = None;
|
||||||
|
app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into());
|
||||||
|
|
||||||
|
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
|
||||||
|
SeriesDetailsUi::draw(f, app, f.area());
|
||||||
|
});
|
||||||
|
|
||||||
|
insta::assert_snapshot!(output);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_series_details_ui_renders_season_details_over_series_details() {
|
fn test_series_details_ui_renders_season_details_over_series_details() {
|
||||||
let mut app = App::test_default_fully_populated();
|
let mut app = App::test_default_fully_populated();
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
source: src/ui/sonarr_ui/library/series_details_ui_tests.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
╭ Test ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│Title: Test │
|
||||||
|
│Overview: Blah blah blah It was filmed in Madison, Wisconsin: a city the crew never really left. The first season took seven years to finish. │
|
||||||
|
│Network: HBO │
|
||||||
|
│Status: Continuing │
|
||||||
|
│Genres: cool, family, fun │
|
||||||
|
│Rating: 84% │
|
||||||
|
│Year: 2022 │
|
||||||
|
│Runtime: 63 minutes │
|
||||||
|
│Path: /nfs/tv/Test │
|
||||||
|
│Quality Profile: Bluray-1080p │
|
||||||
|
│Language Profile: English │
|
||||||
|
│Monitored: Yes │
|
||||||
|
│Size on Disk: 59.51 GB │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│╭ Series Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
|
||||||
|
││ Seasons │ History ││
|
||||||
|
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
|
||||||
|
││ Monitored Season Episode Count Size on Disk ││
|
||||||
|
││=> 🏷 Season title N/A N/A ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
││ ││
|
||||||
|
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
|
||||||
|
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
Reference in new issue
Block a user