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.
This commit is contained in:
Dark-Alex-17 committed 2026-09-11 12:27:53 -06:00
1 parent 9dbd985506
commit 86b91ce6dd
10 files changed
+578 -6

No files matched your search

+126 -1
View File
@@ -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::<SonarrHistoryItem>(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()
}
}