package library import "testing" func sonarrDownload(upgrade bool) SonarrWebhook { var payload SonarrWebhook payload.EventType = "Download" payload.IsUpgrade = upgrade payload.Series.ID = 12 payload.Series.Title = "Blue Bloods" payload.Series.Year = 2010 payload.EpisodeFile.ID = 8123 payload.Episodes = append(payload.Episodes, struct { ID int `json:"id"` SeasonNumber int `json:"seasonNumber"` EpisodeNumber int `json:"episodeNumber"` }{ID: 551, SeasonNumber: 6, EpisodeNumber: 7}) return payload } func TestSonarrImportBecomesOneEpisodeRefresh(t *testing.T) { requests := SonarrRequests(sonarrDownload(false)) if len(requests) != 1 { t.Fatalf("expected one request, got %d", len(requests)) } request := requests[0] if request.Action != ActionRefresh || request.Kind != KindEpisode { t.Fatalf("unexpected shape: %+v", request) } if request.Reason != ReasonImport { t.Fatalf("expected an import, got %q", request.Reason) } if request.Series != "Blue Bloods" || request.Season != 6 || request.Episode != 7 { t.Fatalf("unexpected identity: %+v", request) } } // An upgrade is silent as *news* and is still a reason to re-read the row: the file // genuinely changed. Conflating those two judgements is how a replaced file would keep a // catalogue entry describing the copy it replaced. func TestSonarrUpgradeStillRefreshes(t *testing.T) { requests := SonarrRequests(sonarrDownload(true)) if len(requests) != 1 || requests[0].Reason != ReasonUpgrade { t.Fatalf("expected one upgrade refresh, got %+v", requests) } if requests[0].Action != ActionRefresh { t.Fatalf("an upgrade must refresh, got %q", requests[0].Action) } } // The key names the file, so two deliveries of one import are one piece of work. Both // *arrs re-notify on retry and neither promises exactly-once. func TestRepeatedDeliveryKeepsOneKey(t *testing.T) { first := SonarrRequests(sonarrDownload(false)) second := SonarrRequests(sonarrDownload(false)) if first[0].Key != second[0].Key { t.Fatalf("the same import produced two keys: %q and %q", first[0].Key, second[0].Key) } // A different file for the same episode is different work, or a replacement would be // swallowed by the row its predecessor left behind. replaced := sonarrDownload(true) replaced.EpisodeFile.ID = 9001 if SonarrRequests(replaced)[0].Key == first[0].Key { t.Fatal("a replacement file must not reuse the previous file's key") } } // A multi-episode file names several episodes and each is its own row, because each may // well be delivered again on its own. func TestSonarrMultiEpisodeFileProducesOneRequestEach(t *testing.T) { payload := sonarrDownload(false) payload.Episodes = append(payload.Episodes, struct { ID int `json:"id"` SeasonNumber int `json:"seasonNumber"` EpisodeNumber int `json:"episodeNumber"` }{ID: 552, SeasonNumber: 6, EpisodeNumber: 8}) requests := SonarrRequests(payload) if len(requests) != 2 { t.Fatalf("expected two requests, got %d", len(requests)) } if requests[0].Key == requests[1].Key { t.Fatal("two episodes of one file collapsed onto one key") } } // A rename is series-wide because Sonarr does not say which episode each moved file held, // and it is a refresh rather than an invalidation: the Emby id survives a move. func TestSonarrRenameRefreshesTheSeries(t *testing.T) { var payload SonarrWebhook payload.EventType = "Rename" payload.Series.ID = 12 payload.Series.Title = "Blue Bloods" requests := SonarrRequests(payload) if len(requests) != 1 { t.Fatalf("expected one request, got %d", len(requests)) } if requests[0].Kind != KindSeries || requests[0].Action != ActionRefresh { t.Fatalf("unexpected shape: %+v", requests[0]) } if requests[0].Reason != ReasonRename { t.Fatalf("expected a rename, got %q", requests[0].Reason) } } func TestSonarrEpisodeDeleteRemovesThatEpisode(t *testing.T) { payload := sonarrDownload(false) payload.EventType = "EpisodeFileDelete" requests := SonarrRequests(payload) if len(requests) != 1 || requests[0].Action != ActionRemove { t.Fatalf("expected one removal, got %+v", requests) } if requests[0].Season != 6 || requests[0].Episode != 7 { t.Fatalf("unexpected position: %+v", requests[0]) } } // A series removed from Sonarr's list while its files stay on disk is still in the // library. Only a delete that took the media with it removes anything. func TestSonarrSeriesDeleteOnlyCountsWhenFilesWent(t *testing.T) { var payload SonarrWebhook payload.EventType = "SeriesDelete" payload.Series.ID = 12 payload.Series.Title = "Blue Bloods" if requests := SonarrRequests(payload); len(requests) != 0 { t.Fatalf("an unfollowed series must not be removed: %+v", requests) } payload.DeletedFiles = true requests := SonarrRequests(payload) if len(requests) != 1 || requests[0].Action != ActionRemove || requests[0].Kind != KindSeries { t.Fatalf("expected a series removal, got %+v", requests) } } func TestSonarrIgnoresEventsThatChangeNothing(t *testing.T) { for _, event := range []string{"Grab", "Health", "ApplicationUpdate", "", "ManualInteractionRequired"} { var payload SonarrWebhook payload.EventType = event payload.Series.Title = "Blue Bloods" if requests := SonarrRequests(payload); len(requests) != 0 { t.Fatalf("%q produced work: %+v", event, requests) } } } func radarrDownload(upgrade bool) RadarrWebhook { var payload RadarrWebhook payload.EventType = "Download" payload.IsUpgrade = upgrade payload.Movie.ID = 44 payload.Movie.Title = "Arrival" payload.Movie.Year = 2016 payload.MovieFile.ID = 441 return payload } func TestRadarrImportAndUpgradeBothRefresh(t *testing.T) { imported := RadarrRequests(radarrDownload(false)) if len(imported) != 1 || imported[0].Reason != ReasonImport || imported[0].Kind != KindMovie { t.Fatalf("unexpected import: %+v", imported) } upgraded := RadarrRequests(radarrDownload(true)) if len(upgraded) != 1 || upgraded[0].Reason != ReasonUpgrade { t.Fatalf("unexpected upgrade: %+v", upgraded) } // The two are separate work: an upgrade of a file already imported must not be // swallowed by the settled row its import left behind. if imported[0].Key == upgraded[0].Key { t.Fatal("an upgrade reused the import's key") } } func TestRadarrDeleteReadsTheTopLevelFileID(t *testing.T) { var payload RadarrWebhook payload.EventType = "MovieFileDelete" payload.Movie.ID = 44 payload.Movie.Title = "Arrival" payload.MovieFileID = 441 requests := RadarrRequests(payload) if len(requests) != 1 || requests[0].Action != ActionRemove { t.Fatalf("expected one removal, got %+v", requests) } if requests[0].Key != "radarr:moviefile:441:delete" { t.Fatalf("unexpected key: %q", requests[0].Key) } } func TestRadarrMovieDeleteOnlyCountsWhenFilesWent(t *testing.T) { var payload RadarrWebhook payload.EventType = "MovieDelete" payload.Movie.ID = 44 payload.Movie.Title = "Arrival" if requests := RadarrRequests(payload); len(requests) != 0 { t.Fatalf("an unmonitored film must not be removed: %+v", requests) } payload.DeletedFiles = true if requests := RadarrRequests(payload); len(requests) != 1 { t.Fatalf("expected a removal, got %+v", requests) } } // The Test button must be answered without recording work about something that does not // exist, which is what makes it mean "reachable". func TestTestEventIsRecognisedFromEitherArr(t *testing.T) { if !IsTestEvent("Test") || !IsTestEvent(" test ") { t.Fatal("a test event was not recognised") } if IsTestEvent("Download") { t.Fatal("an import was read as a test") } var sonarrTest SonarrWebhook sonarrTest.EventType = "Test" sonarrTest.Series.Title = "Test Title" if requests := SonarrRequests(sonarrTest); len(requests) != 0 { t.Fatalf("the test event produced work: %+v", requests) } } func TestNormalizedTitleIgnoresPunctuationAndCase(t *testing.T) { if NormalizedTitle("Marvel's Daredevil") != NormalizedTitle("Marvels Daredevil") { t.Fatal("punctuation changed the answer") } if NormalizedTitle("The Pitt") != "thepitt" { t.Fatalf("unexpected normalisation: %q", NormalizedTitle("The Pitt")) } if NormalizedTitle(" ") != "" { t.Fatal("a blank title must normalise to nothing") } }