Files

230 lines
8.0 KiB
Go
Raw Permalink Normal View History

2026-08-18 14:59:29 +12:00
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")
}
}