156 lines
5.8 KiB
Go
156 lines
5.8 KiB
Go
package api
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/ponzischeme89/memby/server/internal/library"
|
||
|
|
)
|
||
|
|
|
||
|
|
func episodeResult(season, episode int, title string) library.IngestResult {
|
||
|
|
return library.IngestResult{
|
||
|
|
Source: "sonarr",
|
||
|
|
Kind: library.KindEpisode,
|
||
|
|
Reason: library.ReasonImport,
|
||
|
|
ItemID: "emby-" + title,
|
||
|
|
Name: title,
|
||
|
|
SeriesName: "The Bear",
|
||
|
|
Season: season,
|
||
|
|
Episode: episode,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestEpisodeSummaryNamesTheEpisodeBothWays(t *testing.T) {
|
||
|
|
if got := episodeSummary(episodeResult(3, 5, "Children")); got != "S03E05 — Children" {
|
||
|
|
t.Errorf("summary = %q, want the code and the title", got)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Emby records plenty of episodes under the show's own name, and "S03E05 — The Bear"
|
||
|
|
// reads as a mistake where the code alone reads as an episode.
|
||
|
|
same := episodeResult(3, 5, "The Bear")
|
||
|
|
if got := episodeSummary(same); got != "S03E05" {
|
||
|
|
t.Errorf("summary = %q, want the code alone when the title repeats the series", got)
|
||
|
|
}
|
||
|
|
|
||
|
|
untitled := episodeResult(3, 5, "")
|
||
|
|
if got := episodeSummary(untitled); got != "S03E05" {
|
||
|
|
t.Errorf("summary = %q, want the code alone", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// A season pack is one piece of news. Every arrival replaces the same banner, which is
|
||
|
|
// what the shared anchor is for, and the wording moves from the episode to the count.
|
||
|
|
func TestIngestRunsCollapseASeasonPack(t *testing.T) {
|
||
|
|
var runs ingestRuns
|
||
|
|
now := time.Date(2026, 8, 18, 19, 4, 0, 0, time.UTC)
|
||
|
|
|
||
|
|
first := runs.record("bear|3", "emby-1", "S03E01", now, ingestRunWindow)
|
||
|
|
if first.Count != 1 || first.Anchor != "emby-1" {
|
||
|
|
t.Fatalf("first = %+v, want a run of one anchored on it", first)
|
||
|
|
}
|
||
|
|
if got := episodeRunMessage(first); got != "S03E01 is ready to watch." {
|
||
|
|
t.Errorf("message = %q, want the episode named", got)
|
||
|
|
}
|
||
|
|
|
||
|
|
second := runs.record("bear|3", "emby-2", "S03E02", now.Add(30*time.Second), ingestRunWindow)
|
||
|
|
if second.Anchor != "emby-1" {
|
||
|
|
t.Errorf("anchor = %q, want the run's first episode so the banner is replaced", second.Anchor)
|
||
|
|
}
|
||
|
|
if second.Count != 2 {
|
||
|
|
t.Errorf("count = %d, want 2", second.Count)
|
||
|
|
}
|
||
|
|
if got := episodeRunMessage(second); got != "2 new episodes are ready to watch." {
|
||
|
|
t.Errorf("message = %q, want the count once there is more than one", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Next week's episode is its own news. Televisions dedupe on the alert id forever, so a
|
||
|
|
// run that reused last week's anchor would be silently swallowed on every set in the house.
|
||
|
|
func TestIngestRunsStartAfreshOnceTheWindowHasClosed(t *testing.T) {
|
||
|
|
var runs ingestRuns
|
||
|
|
now := time.Date(2026, 8, 18, 19, 4, 0, 0, time.UTC)
|
||
|
|
|
||
|
|
runs.record("bear|3", "emby-1", "S03E01", now, ingestRunWindow)
|
||
|
|
later := runs.record("bear|3", "emby-2", "S03E02", now.Add(ingestRunWindow+time.Minute), ingestRunWindow)
|
||
|
|
|
||
|
|
if later.Anchor != "emby-2" {
|
||
|
|
t.Errorf("anchor = %q, want a new run", later.Anchor)
|
||
|
|
}
|
||
|
|
if later.Count != 1 {
|
||
|
|
t.Errorf("count = %d, want a run of one", later.Count)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Two shows importing at once are two pieces of news, not one run of four episodes.
|
||
|
|
func TestIngestRunsAreKeptPerSeason(t *testing.T) {
|
||
|
|
var runs ingestRuns
|
||
|
|
now := time.Date(2026, 8, 18, 19, 4, 0, 0, time.UTC)
|
||
|
|
|
||
|
|
runs.record(seasonRunKey("The Bear", 3), "bear-1", "S03E01", now, ingestRunWindow)
|
||
|
|
other := runs.record(seasonRunKey("Slow Horses", 4), "horses-1", "S04E01", now, ingestRunWindow)
|
||
|
|
|
||
|
|
if other.Count != 1 || other.Anchor != "horses-1" {
|
||
|
|
t.Fatalf("other show = %+v, want a run of its own", other)
|
||
|
|
}
|
||
|
|
// And the same show under a different spelling is still the same show, the rule the
|
||
|
|
// schedule row and the ingest worker already match titles by.
|
||
|
|
same := runs.record(seasonRunKey("the bear!", 3), "bear-2", "S03E02", now, ingestRunWindow)
|
||
|
|
if same.Anchor != "bear-1" || same.Count != 2 {
|
||
|
|
t.Fatalf("same season = %+v, want it folded into the first run", same)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// The tally is memory the gateway can afford to lose, so it must also be memory it cannot
|
||
|
|
// grow without bound.
|
||
|
|
func TestIngestRunsAreBounded(t *testing.T) {
|
||
|
|
var runs ingestRuns
|
||
|
|
now := time.Date(2026, 8, 18, 19, 4, 0, 0, time.UTC)
|
||
|
|
for i := 0; i < trackedIngestRuns*2; i++ {
|
||
|
|
runs.record(time.Duration(i).String(), "item", "S01E01", now, ingestRunWindow)
|
||
|
|
}
|
||
|
|
if len(runs.runs) > trackedIngestRuns {
|
||
|
|
t.Fatalf("tracking %d runs, want a cap of %d", len(runs.runs), trackedIngestRuns)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Only an import is news. An upgrade replaced a file that was already watchable, and a
|
||
|
|
// rename or a delete is housekeeping — announcing any of them trains viewers to look away.
|
||
|
|
func TestOnlyAnImportIsAnnounced(t *testing.T) {
|
||
|
|
s := &Server{log: discardLogger()}
|
||
|
|
for _, reason := range []string{
|
||
|
|
library.ReasonUpgrade, library.ReasonRename, library.ReasonDelete,
|
||
|
|
} {
|
||
|
|
result := episodeResult(3, 5, "Children")
|
||
|
|
result.Reason = reason
|
||
|
|
// A nil cache would be reached by publishAlert if this announced anything; it
|
||
|
|
// returns early on one, so the assertion is that nothing is recorded either.
|
||
|
|
s.AnnounceLibraryIngest(t.Context(), result)
|
||
|
|
if len(s.ingestRuns.runs) != 0 {
|
||
|
|
t.Fatalf("%s was treated as news", reason)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// A series-level result is a rename settling or a show written ahead of its first episode.
|
||
|
|
// Neither is something anybody can press Play on.
|
||
|
|
func TestASeriesRefreshIsNotAnnounced(t *testing.T) {
|
||
|
|
s := &Server{log: discardLogger()}
|
||
|
|
s.AnnounceLibraryIngest(t.Context(), library.IngestResult{
|
||
|
|
Source: "sonarr", Kind: library.KindSeries, Reason: library.ReasonImport,
|
||
|
|
ItemID: "series-1", Name: "The Bear", SeriesName: "The Bear",
|
||
|
|
})
|
||
|
|
if len(s.ingestRuns.runs) != 0 {
|
||
|
|
t.Fatal("a series refresh was announced")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMovieRunMessageSaysTheFilmIsThere(t *testing.T) {
|
||
|
|
// The wording is the whole point of moving the announcement behind the scan: the
|
||
|
|
// banner published from the webhook could only ever promise the film was coming.
|
||
|
|
run := ingestRun{Anchor: "a", Count: 1, Latest: "S01E01"}
|
||
|
|
if strings.Contains(episodeRunMessage(run), "shortly") {
|
||
|
|
t.Error("the message still promises rather than states")
|
||
|
|
}
|
||
|
|
}
|