From 86b91ce6dd2e51b5106c5799e7a24013828dfdaa Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 11 Sep 2026 12:27:53 -0600 Subject: [PATCH] fix: keep history readable when an event type is unrecognised A single history record whose event type the Servarr build has no name for made the entire history response fail to decode, so the view died with "Failed to parse response!" and showed nothing at all. The event type is stored as an integer and is only serialized as a camelCase string when the build can name it. For values it cannot name, it is emitted as a bare JSON number instead, which is a type mismatch rather than merely an unknown name. Serde's other attribute does not help: it is rejected outright unless it sits on the last variant, and even there it only catches unrecognised strings, leaving a number to fail exactly as before. from_json_or_default buffers the field and falls back to the type's default for any input the enum does not recognise, whether that is an unknown string, a number, or any other JSON value. It is applied to the event type of all four history item types. Known values still map exactly as they did, and serialization is untouched. Verified against a live Readarr carrying such a record: the history listing failed to decode before the change, and afterwards lists every record, rendering the unnamed one as Unknown. Verified by mutation. Seven mutations all fail the new suite: dropping the fallback, discarding the decoded value, removing the attribute from each of the four history items, and renaming a variant on serialize only. The five per-tree and serialize-only mutations were each invisible to every pre-existing test. --- src/models/lidarr_models.rs | 1 + src/models/lidarr_models_tests.rs | 150 ++++++++++++++++++++++++++++- src/models/mod.rs | 11 ++- src/models/model_tests.rs | 17 +++- src/models/radarr_models.rs | 1 + src/models/radarr_models_tests.rs | 126 +++++++++++++++++++++++- src/models/readarr_models.rs | 1 + src/models/readarr_models_tests.rs | 149 +++++++++++++++++++++++++++- src/models/sonarr_models.rs | 1 + src/models/sonarr_models_tests.rs | 127 +++++++++++++++++++++++- 10 files changed, 578 insertions(+), 6 deletions(-) diff --git a/src/models/lidarr_models.rs b/src/models/lidarr_models.rs index 333d51d9..74fe95b1 100644 --- a/src/models/lidarr_models.rs +++ b/src/models/lidarr_models.rs @@ -404,6 +404,7 @@ pub struct LidarrHistoryItem { #[serde(default)] pub quality: QualityWrapper, pub date: DateTime, + #[serde(deserialize_with = "super::from_json_or_default")] pub event_type: LidarrHistoryEventType, #[serde(default)] pub data: LidarrHistoryData, diff --git a/src/models/lidarr_models_tests.rs b/src/models/lidarr_models_tests.rs index f3fd6ad4..618d6f5f 100644 --- a/src/models/lidarr_models_tests.rs +++ b/src/models/lidarr_models_tests.rs @@ -2,7 +2,7 @@ mod tests { use chrono::Utc; use pretty_assertions::{assert_eq, assert_str_eq}; - use serde_json::json; + use serde_json::{Value, json}; use crate::models::lidarr_models::{ AddArtistSearchResult, Album, AudioTags, BlocklistItem, BlocklistResponse, DownloadRecord, @@ -773,6 +773,126 @@ mod tests { ); } + #[test] + fn test_lidarr_history_event_type_deserialization() { + assert_eq!( + deserialize_history_event_type(json!("unknown")), + LidarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!("grabbed")), + LidarrHistoryEventType::Grabbed + ); + assert_eq!( + deserialize_history_event_type(json!("artistFolderImported")), + LidarrHistoryEventType::ArtistFolderImported + ); + assert_eq!( + deserialize_history_event_type(json!("albumImportIncomplete")), + LidarrHistoryEventType::AlbumImportIncomplete + ); + assert_eq!( + deserialize_history_event_type(json!("downloadIgnored")), + LidarrHistoryEventType::DownloadIgnored + ); + assert_eq!( + deserialize_history_event_type(json!("downloadImported")), + LidarrHistoryEventType::DownloadImported + ); + assert_eq!( + deserialize_history_event_type(json!("downloadFailed")), + LidarrHistoryEventType::DownloadFailed + ); + assert_eq!( + deserialize_history_event_type(json!("trackFileDeleted")), + LidarrHistoryEventType::TrackFileDeleted + ); + assert_eq!( + deserialize_history_event_type(json!("trackFileImported")), + LidarrHistoryEventType::TrackFileImported + ); + assert_eq!( + deserialize_history_event_type(json!("trackFileRenamed")), + LidarrHistoryEventType::TrackFileRenamed + ); + assert_eq!( + deserialize_history_event_type(json!("trackFileRetagged")), + LidarrHistoryEventType::TrackFileRetagged + ); + } + + #[test] + fn test_lidarr_history_event_type_deserialization_falls_back_to_unknown() { + assert_eq!( + deserialize_history_event_type(json!("albumImportPartiallyComplete")), + LidarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(2)), + LidarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(11)), + LidarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(null)), + LidarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!({ "id": 2 })), + LidarrHistoryEventType::Unknown + ); + } + + #[test] + fn test_lidarr_history_event_type_serialization() { + assert_str_eq!( + serialize_history_event_type(LidarrHistoryEventType::Unknown), + "unknown" + ); + assert_str_eq!( + serialize_history_event_type(LidarrHistoryEventType::Grabbed), + "grabbed" + ); + assert_str_eq!( + serialize_history_event_type(LidarrHistoryEventType::ArtistFolderImported), + "artistFolderImported" + ); + assert_str_eq!( + serialize_history_event_type(LidarrHistoryEventType::AlbumImportIncomplete), + "albumImportIncomplete" + ); + assert_str_eq!( + serialize_history_event_type(LidarrHistoryEventType::DownloadIgnored), + "downloadIgnored" + ); + assert_str_eq!( + serialize_history_event_type(LidarrHistoryEventType::DownloadImported), + "downloadImported" + ); + assert_str_eq!( + serialize_history_event_type(LidarrHistoryEventType::DownloadFailed), + "downloadFailed" + ); + assert_str_eq!( + serialize_history_event_type(LidarrHistoryEventType::TrackFileDeleted), + "trackFileDeleted" + ); + assert_str_eq!( + serialize_history_event_type(LidarrHistoryEventType::TrackFileImported), + "trackFileImported" + ); + assert_str_eq!( + serialize_history_event_type(LidarrHistoryEventType::TrackFileRenamed), + "trackFileRenamed" + ); + assert_str_eq!( + serialize_history_event_type(LidarrHistoryEventType::TrackFileRetagged), + "trackFileRetagged" + ); + } + #[test] fn test_add_artist_search_result_deserialization() { let search_result_json = json!({ @@ -825,4 +945,32 @@ mod tests { assert!(search_result.genres.is_empty()); assert_none!(&search_result.ratings); } + + fn deserialize_history_event_type(event_type: Value) -> LidarrHistoryEventType { + let history_item_json = json!({ + "id": 1, + "sourceTitle": "Test Source Title", + "albumId": 1, + "artistId": 1, + "trackId": 1, + "date": "2024-01-01T00:00:00Z", + "eventType": event_type + }); + + serde_json::from_value::(history_item_json) + .unwrap() + .event_type + } + + fn serialize_history_event_type(event_type: LidarrHistoryEventType) -> String { + let history_item = LidarrHistoryItem { + event_type, + ..LidarrHistoryItem::default() + }; + + serde_json::to_value(history_item).unwrap()["eventType"] + .as_str() + .unwrap() + .to_owned() + } } diff --git a/src/models/mod.rs b/src/models/mod.rs index 867da8b1..870be97d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -11,7 +11,7 @@ use radarr_models::RadarrSerdeable; use readarr_models::ReadarrSerdeable; use regex::Regex; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; -use serde_json::Number; +use serde_json::{Number, Value}; use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use sonarr_models::SonarrSerdeable; @@ -442,6 +442,15 @@ where ))) } +pub fn from_json_or_default<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: Deserialize<'de> + Default, +{ + let value: Value = Deserialize::deserialize(deserializer)?; + Ok(T::deserialize(value).unwrap_or_default()) +} + pub fn strip_non_search_characters(input: &str) -> String { Regex::new(r"[^a-zA-Z0-9.,/'\-:\s]") .unwrap() diff --git a/src/models/model_tests.rs b/src/models/model_tests.rs index 1b8dc3e7..3f572369 100644 --- a/src/models/model_tests.rs +++ b/src/models/model_tests.rs @@ -11,12 +11,13 @@ mod tests { use crate::models::{ BlockSelectionState, HorizontallyScrollableText, Scrollable, ScrollableText, TabRoute, TabState, }; - use crate::models::{from_i64, strip_non_search_characters}; + use crate::models::{from_i64, from_json_or_default, strip_non_search_characters}; use pretty_assertions::{assert_eq, assert_str_eq}; use serde::de::IntoDeserializer; use serde::de::value::Error as ValueError; use serde::de::value::F64Deserializer; use serde::de::value::I64Deserializer; + use serde::de::value::StrDeserializer; use serde_json::to_string; const BLOCKS: &[&[i32]] = &[&[11, 12], &[21, 22], &[31, 32]]; @@ -803,6 +804,20 @@ mod tests { assert_ok_eq_x!(from_f64(deserializer), 1.0); } + #[test] + fn test_from_json_or_default() { + let deserializer: I64Deserializer = 1i64.into_deserializer(); + + assert_ok_eq_x!(from_json_or_default::<_, i64>(deserializer), 1); + } + + #[test] + fn test_from_json_or_default_falls_back_to_default() { + let deserializer: StrDeserializer<'_, ValueError> = "unexpected".into_deserializer(); + + assert_ok_eq_x!(from_json_or_default::<_, i64>(deserializer), 0); + } + #[test] fn test_horizontally_scrollable_serialize() { let text = HorizontallyScrollableText::from("Test"); diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 8b59fd3d..c71fe71f 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -459,6 +459,7 @@ pub struct RadarrHistoryItem { pub quality: QualityWrapper, pub languages: Vec, pub date: DateTime, + #[serde(deserialize_with = "super::from_json_or_default")] pub event_type: RadarrHistoryEventType, #[serde(default)] pub data: RadarrHistoryData, diff --git a/src/models/radarr_models_tests.rs b/src/models/radarr_models_tests.rs index b9ed2485..a114a17d 100644 --- a/src/models/radarr_models_tests.rs +++ b/src/models/radarr_models_tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; - use serde_json::json; + use serde_json::{Value, json}; use crate::models::radarr_models::{ RadarrHistoryEventType, RadarrHistoryItem, RadarrHistoryWrapper, @@ -126,6 +126,102 @@ mod tests { ); } + #[test] + fn test_radarr_history_event_type_deserialization() { + assert_eq!( + deserialize_history_event_type(json!("unknown")), + RadarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!("grabbed")), + RadarrHistoryEventType::Grabbed + ); + assert_eq!( + deserialize_history_event_type(json!("downloadFolderImported")), + RadarrHistoryEventType::DownloadFolderImported + ); + assert_eq!( + deserialize_history_event_type(json!("downloadFailed")), + RadarrHistoryEventType::DownloadFailed + ); + assert_eq!( + deserialize_history_event_type(json!("movieFileDeleted")), + RadarrHistoryEventType::MovieFileDeleted + ); + assert_eq!( + deserialize_history_event_type(json!("movieFolderImported")), + RadarrHistoryEventType::MovieFolderImported + ); + assert_eq!( + deserialize_history_event_type(json!("movieFileRenamed")), + RadarrHistoryEventType::MovieFileRenamed + ); + assert_eq!( + deserialize_history_event_type(json!("downloadIgnored")), + RadarrHistoryEventType::DownloadIgnored + ); + } + + #[test] + fn test_radarr_history_event_type_deserialization_falls_back_to_unknown() { + assert_eq!( + deserialize_history_event_type(json!("movieFileImportFailed")), + RadarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(2)), + RadarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(11)), + RadarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(null)), + RadarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!({ "id": 2 })), + RadarrHistoryEventType::Unknown + ); + } + + #[test] + fn test_radarr_history_event_type_serialization() { + assert_str_eq!( + serialize_history_event_type(RadarrHistoryEventType::Unknown), + "unknown" + ); + assert_str_eq!( + serialize_history_event_type(RadarrHistoryEventType::Grabbed), + "grabbed" + ); + assert_str_eq!( + serialize_history_event_type(RadarrHistoryEventType::DownloadFolderImported), + "downloadFolderImported" + ); + assert_str_eq!( + serialize_history_event_type(RadarrHistoryEventType::DownloadFailed), + "downloadFailed" + ); + assert_str_eq!( + serialize_history_event_type(RadarrHistoryEventType::MovieFileDeleted), + "movieFileDeleted" + ); + assert_str_eq!( + serialize_history_event_type(RadarrHistoryEventType::MovieFolderImported), + "movieFolderImported" + ); + assert_str_eq!( + serialize_history_event_type(RadarrHistoryEventType::MovieFileRenamed), + "movieFileRenamed" + ); + assert_str_eq!( + serialize_history_event_type(RadarrHistoryEventType::DownloadIgnored), + "downloadIgnored" + ); + } + #[test] fn test_download_record_default_indexer_value() { let json = r#"{ @@ -517,4 +613,32 @@ mod tests { RadarrSerdeable::IndexerTestResults(indexer_test_results) ); } + + fn deserialize_history_event_type(event_type: Value) -> RadarrHistoryEventType { + let history_item_json = json!({ + "id": 1, + "sourceTitle": "Test Source Title", + "movieId": 1, + "quality": { "quality": { "name": "HD - 1080p" } }, + "languages": [], + "date": "2024-01-01T00:00:00Z", + "eventType": event_type + }); + + serde_json::from_value::(history_item_json) + .unwrap() + .event_type + } + + fn serialize_history_event_type(event_type: RadarrHistoryEventType) -> String { + let history_item = RadarrHistoryItem { + event_type, + ..RadarrHistoryItem::default() + }; + + serde_json::to_value(history_item).unwrap()["eventType"] + .as_str() + .unwrap() + .to_owned() + } } diff --git a/src/models/readarr_models.rs b/src/models/readarr_models.rs index bb2f10e6..17dc3257 100644 --- a/src/models/readarr_models.rs +++ b/src/models/readarr_models.rs @@ -413,6 +413,7 @@ pub struct ReadarrHistoryItem { #[serde(default)] pub quality: QualityWrapper, pub date: DateTime, + #[serde(deserialize_with = "super::from_json_or_default")] pub event_type: ReadarrHistoryEventType, #[serde(default)] pub data: ReadarrHistoryData, diff --git a/src/models/readarr_models_tests.rs b/src/models/readarr_models_tests.rs index fe163400..20ffb5b5 100644 --- a/src/models/readarr_models_tests.rs +++ b/src/models/readarr_models_tests.rs @@ -2,7 +2,7 @@ mod tests { use chrono::Utc; use pretty_assertions::{assert_eq, assert_str_eq}; - use serde_json::json; + use serde_json::{Value, json}; use crate::models::readarr_models::{ AddAuthorSearchResult, BlocklistItem, BlocklistResponse, Book, BookFile, DownloadRecord, @@ -758,6 +758,126 @@ mod tests { ); } + #[test] + fn test_readarr_history_event_type_deserialization() { + assert_eq!( + deserialize_history_event_type(json!("unknown")), + ReadarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!("grabbed")), + ReadarrHistoryEventType::Grabbed + ); + assert_eq!( + deserialize_history_event_type(json!("authorFolderImported")), + ReadarrHistoryEventType::AuthorFolderImported + ); + assert_eq!( + deserialize_history_event_type(json!("bookImportIncomplete")), + ReadarrHistoryEventType::BookImportIncomplete + ); + assert_eq!( + deserialize_history_event_type(json!("downloadIgnored")), + ReadarrHistoryEventType::DownloadIgnored + ); + assert_eq!( + deserialize_history_event_type(json!("downloadImported")), + ReadarrHistoryEventType::DownloadImported + ); + assert_eq!( + deserialize_history_event_type(json!("downloadFailed")), + ReadarrHistoryEventType::DownloadFailed + ); + assert_eq!( + deserialize_history_event_type(json!("bookFileDeleted")), + ReadarrHistoryEventType::BookFileDeleted + ); + assert_eq!( + deserialize_history_event_type(json!("bookFileImported")), + ReadarrHistoryEventType::BookFileImported + ); + assert_eq!( + deserialize_history_event_type(json!("bookFileRenamed")), + ReadarrHistoryEventType::BookFileRenamed + ); + assert_eq!( + deserialize_history_event_type(json!("bookFileRetagged")), + ReadarrHistoryEventType::BookFileRetagged + ); + } + + #[test] + fn test_readarr_history_event_type_deserialization_falls_back_to_unknown() { + assert_eq!( + deserialize_history_event_type(json!("bookImportPartiallyComplete")), + ReadarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(2)), + ReadarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(11)), + ReadarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(null)), + ReadarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!({ "id": 2 })), + ReadarrHistoryEventType::Unknown + ); + } + + #[test] + fn test_readarr_history_event_type_serialization() { + assert_str_eq!( + serialize_history_event_type(ReadarrHistoryEventType::Unknown), + "unknown" + ); + assert_str_eq!( + serialize_history_event_type(ReadarrHistoryEventType::Grabbed), + "grabbed" + ); + assert_str_eq!( + serialize_history_event_type(ReadarrHistoryEventType::AuthorFolderImported), + "authorFolderImported" + ); + assert_str_eq!( + serialize_history_event_type(ReadarrHistoryEventType::BookImportIncomplete), + "bookImportIncomplete" + ); + assert_str_eq!( + serialize_history_event_type(ReadarrHistoryEventType::DownloadIgnored), + "downloadIgnored" + ); + assert_str_eq!( + serialize_history_event_type(ReadarrHistoryEventType::DownloadImported), + "downloadImported" + ); + assert_str_eq!( + serialize_history_event_type(ReadarrHistoryEventType::DownloadFailed), + "downloadFailed" + ); + assert_str_eq!( + serialize_history_event_type(ReadarrHistoryEventType::BookFileDeleted), + "bookFileDeleted" + ); + assert_str_eq!( + serialize_history_event_type(ReadarrHistoryEventType::BookFileImported), + "bookFileImported" + ); + assert_str_eq!( + serialize_history_event_type(ReadarrHistoryEventType::BookFileRenamed), + "bookFileRenamed" + ); + assert_str_eq!( + serialize_history_event_type(ReadarrHistoryEventType::BookFileRetagged), + "bookFileRetagged" + ); + } + #[test] fn test_readarr_task_name_display() { assert_str_eq!( @@ -1019,4 +1139,31 @@ mod tests { assert!(search_result.genres.is_empty()); assert_none!(&search_result.ratings); } + + fn deserialize_history_event_type(event_type: Value) -> ReadarrHistoryEventType { + let history_item_json = json!({ + "id": 1, + "authorId": 1, + "bookId": 1, + "sourceTitle": "Test Source Title", + "date": "2024-01-01T00:00:00Z", + "eventType": event_type + }); + + serde_json::from_value::(history_item_json) + .unwrap() + .event_type + } + + fn serialize_history_event_type(event_type: ReadarrHistoryEventType) -> String { + let history_item = ReadarrHistoryItem { + event_type, + ..ReadarrHistoryItem::default() + }; + + serde_json::to_value(history_item).unwrap()["eventType"] + .as_str() + .unwrap() + .to_owned() + } } diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index 1b139543..11ee02d2 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -499,6 +499,7 @@ pub struct SonarrHistoryItem { pub quality: QualityWrapper, pub languages: Vec>, pub date: DateTime, + #[serde(deserialize_with = "super::from_json_or_default")] pub event_type: SonarrHistoryEventType, pub data: SonarrHistoryData, } diff --git a/src/models/sonarr_models_tests.rs b/src/models/sonarr_models_tests.rs index d7ea2026..8e82559a 100644 --- a/src/models/sonarr_models_tests.rs +++ b/src/models/sonarr_models_tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; - use serde_json::json; + use serde_json::{Value, json}; use crate::models::{ Serdeable, @@ -213,6 +213,102 @@ mod tests { ); } + #[test] + fn test_sonarr_history_event_type_deserialization() { + assert_eq!( + deserialize_history_event_type(json!("unknown")), + SonarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!("grabbed")), + SonarrHistoryEventType::Grabbed + ); + assert_eq!( + deserialize_history_event_type(json!("seriesFolderImported")), + SonarrHistoryEventType::SeriesFolderImported + ); + assert_eq!( + deserialize_history_event_type(json!("downloadFolderImported")), + SonarrHistoryEventType::DownloadFolderImported + ); + assert_eq!( + deserialize_history_event_type(json!("downloadFailed")), + SonarrHistoryEventType::DownloadFailed + ); + assert_eq!( + deserialize_history_event_type(json!("episodeFileDeleted")), + SonarrHistoryEventType::EpisodeFileDeleted + ); + assert_eq!( + deserialize_history_event_type(json!("episodeFileRenamed")), + SonarrHistoryEventType::EpisodeFileRenamed + ); + assert_eq!( + deserialize_history_event_type(json!("downloadIgnored")), + SonarrHistoryEventType::DownloadIgnored + ); + } + + #[test] + fn test_sonarr_history_event_type_deserialization_falls_back_to_unknown() { + assert_eq!( + deserialize_history_event_type(json!("episodeFileImportFailed")), + SonarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(2)), + SonarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(11)), + SonarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!(null)), + SonarrHistoryEventType::Unknown + ); + assert_eq!( + deserialize_history_event_type(json!({ "id": 2 })), + SonarrHistoryEventType::Unknown + ); + } + + #[test] + fn test_sonarr_history_event_type_serialization() { + assert_str_eq!( + serialize_history_event_type(SonarrHistoryEventType::Unknown), + "unknown" + ); + assert_str_eq!( + serialize_history_event_type(SonarrHistoryEventType::Grabbed), + "grabbed" + ); + assert_str_eq!( + serialize_history_event_type(SonarrHistoryEventType::SeriesFolderImported), + "seriesFolderImported" + ); + assert_str_eq!( + serialize_history_event_type(SonarrHistoryEventType::DownloadFolderImported), + "downloadFolderImported" + ); + assert_str_eq!( + serialize_history_event_type(SonarrHistoryEventType::DownloadFailed), + "downloadFailed" + ); + assert_str_eq!( + serialize_history_event_type(SonarrHistoryEventType::EpisodeFileDeleted), + "episodeFileDeleted" + ); + assert_str_eq!( + serialize_history_event_type(SonarrHistoryEventType::EpisodeFileRenamed), + "episodeFileRenamed" + ); + assert_str_eq!( + serialize_history_event_type(SonarrHistoryEventType::DownloadIgnored), + "downloadIgnored" + ); + } + #[test] fn test_task_name_display() { assert_str_eq!( @@ -601,4 +697,33 @@ mod tests { SonarrSerdeable::IndexerTestResults(indexer_test_results) ); } + + fn deserialize_history_event_type(event_type: Value) -> SonarrHistoryEventType { + let history_item_json = json!({ + "id": 1, + "sourceTitle": "Test Source Title", + "episodeId": 1, + "quality": { "quality": { "name": "HDTV - 1080p" } }, + "languages": [], + "date": "2024-01-01T00:00:00Z", + "eventType": event_type, + "data": {} + }); + + serde_json::from_value::(history_item_json) + .unwrap() + .event_type + } + + fn serialize_history_event_type(event_type: SonarrHistoryEventType) -> String { + let history_item = SonarrHistoryItem { + event_type, + ..SonarrHistoryItem::default() + }; + + serde_json::to_value(history_item).unwrap()["eventType"] + .as_str() + .unwrap() + .to_owned() + } }