Files
memby/server/internal/api/requests_status_test.go
T

206 lines
8.1 KiB
Go
Raw Normal View History

2026-08-12 14:13:19 +12:00
package api
import (
2026-08-19 14:25:44 +12:00
"strings"
2026-08-12 14:13:19 +12:00
"testing"
"time"
)
func TestRequestStatusPrefersAvailabilityOverEverything(t *testing.T) {
// A title the household can watch is available whether or not the *arr still tracks it
// and whether or not anything thinks it has been released. Being watchable is the
// strongest fact there is about a request, so nothing below may override it.
cases := []struct {
name string
subject RequestSubject
}{
{"in library, untracked, unreleased", RequestSubject{InLibrary: true}},
{"has file but not imported", RequestSubject{Tracked: true, HasFile: true}},
{"in library and still being worked on", RequestSubject{
Tracked: true, InLibrary: true, Released: true,
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := requestStatusFor(tc.subject); got != RequestStatusAvailable {
t.Fatalf("expected %q, got %q", RequestStatusAvailable, got)
}
})
}
}
func TestRequestStatusReportsRemovalBeforeReleaseState(t *testing.T) {
// An untracked title's release date says nothing: nobody is working on it either way,
// so "unavailable" must win over "pending". Getting this the wrong way round would park
// a removed request on "waiting for release" for ever.
got := requestStatusFor(RequestSubject{Tracked: false, Released: false})
if got != RequestStatusUnavailable {
t.Fatalf("expected %q for an untracked title, got %q", RequestStatusUnavailable, got)
}
}
2026-08-19 14:25:44 +12:00
func TestRequestStatusSeparatesPendingFromSearching(t *testing.T) {
2026-08-12 14:13:19 +12:00
pending := requestStatusFor(RequestSubject{Tracked: true, Released: false})
if pending != RequestStatusPending {
t.Fatalf("expected %q, got %q", RequestStatusPending, pending)
}
2026-08-19 14:25:44 +12:00
// Tracked, released, and the download client has never heard of it: nothing has been
// found. This is the distinction the whole feature rests on — before it, this case and
// a download two minutes from finishing were the same word.
searching := requestStatusFor(RequestSubject{Tracked: true, Released: true})
if searching != RequestStatusSearching {
t.Fatalf("expected %q, got %q", RequestStatusSearching, searching)
}
}
func TestQueueOutranksTheReleaseState(t *testing.T) {
// Radarr's minimum-availability setting decides when to start looking, not whether bytes
// are moving. A household that fetches on the cinema date has films that are "not
// released" and half downloaded at once, and the honest answer is the half.
got := requestStatusFor(RequestSubject{
Tracked: true,
Released: false,
Progress: requestProgress{Status: RequestStatusDownloading, Progress: 50},
})
if got != RequestStatusDownloading {
t.Fatalf("expected %q, got %q", RequestStatusDownloading, got)
}
// But nothing outranks having it. A queue row against a title already on the shelf is an
// upgrade, and a card somebody can press Play on must not read as downloading.
got = requestStatusFor(RequestSubject{
Tracked: true,
InLibrary: true,
Progress: requestProgress{Status: RequestStatusDownloading, Progress: 50},
})
if got != RequestStatusAvailable {
t.Fatalf("expected %q, got %q", RequestStatusAvailable, got)
}
}
func TestCollapseNarrowsOnlyTheDownloadStates(t *testing.T) {
// An older television is told "processing" for the whole span it used to cover, and
// nothing else it understands is disturbed.
for _, state := range []string{
RequestStatusSearching, RequestStatusFound,
RequestStatusDownloading, RequestStatusFailed,
} {
if got := collapseRequestStatus(state); got != RequestStatusProcessing {
t.Fatalf("%q should collapse to %q, got %q", state, RequestStatusProcessing, got)
}
}
for _, state := range []string{
RequestStatusAvailable, RequestStatusPending, RequestStatusRequested,
RequestStatusUnavailable, RequestStatusRequestable, RequestStatusProcessing,
} {
if got := collapseRequestStatus(state); got != state {
t.Fatalf("%q must survive collapsing, got %q", state, got)
}
2026-08-12 14:13:19 +12:00
}
}
func TestLookupStatusOffersRequestableOnlyWhenNothingHasIt(t *testing.T) {
got := lookupStatusFor(RequestSubject{}, false)
if got != RequestStatusRequestable {
t.Fatalf("expected %q, got %q", RequestStatusRequestable, got)
}
// The request list rule must never produce it: there, an untracked title is one somebody
// removed rather than one nobody has asked for.
if requestStatusFor(RequestSubject{}) == RequestStatusRequestable {
t.Fatal("requestStatusFor must not return requestable")
}
}
func TestLookupStatusPutsOwnRequestAboveHouseholdButBelowAvailability(t *testing.T) {
// Somebody needs to be told they already asked, before being told the household happens
// to track it — that is what stops them pressing the button again.
mine := lookupStatusFor(RequestSubject{Tracked: true, Released: true}, true)
if mine != RequestStatusRequested {
t.Fatalf("expected %q for the viewer's own ask, got %q", RequestStatusRequested, mine)
}
// But being watchable now is better news than having asked.
available := lookupStatusFor(RequestSubject{InLibrary: true}, true)
if available != RequestStatusAvailable {
t.Fatalf("expected %q, got %q", RequestStatusAvailable, available)
}
}
func TestMovieReleasedTreatsUnknownStatusAsReleased(t *testing.T) {
// A Radarr vocabulary this build has never seen must degrade to "processing", not park
// the request on "pending" for ever.
for _, status := range []string{"released", "deleted", "somethingNew", ""} {
if !movieReleased(status) {
t.Fatalf("expected %q to count as released", status)
}
}
for _, status := range []string{"announced", "inCinemas", "INCINEMAS", " announced "} {
if movieReleased(status) {
t.Fatalf("expected %q to count as unreleased", status)
}
}
}
func TestSeriesReleasedReadsUpcomingAndFutureAirings(t *testing.T) {
now := time.Date(2026, 8, 12, 0, 0, 0, 0, time.UTC)
later := now.Add(48 * time.Hour)
earlier := now.Add(-48 * time.Hour)
if seriesReleased("upcoming", nil, now) {
t.Fatal("an upcoming series has nothing to fetch yet")
}
if seriesReleased("", &later, now) {
t.Fatal("a series whose only airing is in the future has nothing to fetch yet")
}
if !seriesReleased("continuing", &later, now) {
t.Fatal("a continuing series with a future episode is still fetchable now")
}
if !seriesReleased("", &earlier, now) {
t.Fatal("a series that has already aired is fetchable")
}
}
func TestRequestStatusLabelAndDetailCoverEveryState(t *testing.T) {
// Every state must carry its own wording: the television renders what it is handed, so
// a state that fell through to the default would show "Requested" on a card that is
// actually available.
states := []string{
2026-08-19 14:25:44 +12:00
RequestStatusAvailable, RequestStatusSearching, RequestStatusFound,
RequestStatusDownloading, RequestStatusProcessing, RequestStatusPending,
RequestStatusFailed, RequestStatusUnavailable, RequestStatusRequestable,
RequestStatusRequested,
2026-08-12 14:13:19 +12:00
}
seen := map[string]bool{}
for _, state := range states {
label := requestStatusLabel(state)
if label == "" {
t.Fatalf("state %q has no label", state)
}
if seen[label] {
t.Fatalf("state %q reuses the label %q", state, label)
}
seen[label] = true
}
2026-08-19 14:25:44 +12:00
for _, state := range states {
if requestStatusDetail(state, "movie", 0) == "" {
t.Fatalf("state %q has no detail line", state)
}
}
if requestStatusDetail(RequestStatusPending, "series", 0) ==
requestStatusDetail(RequestStatusPending, "movie", 0) {
2026-08-12 14:13:19 +12:00
t.Fatal("a pending series and a pending film are waiting for different things")
}
2026-08-19 14:25:44 +12:00
// The one rule this feature is judged on: an estimate is offered when the download
// client gave one and never otherwise. Both sentences are legitimate; saying the second
// while holding a number, or the first while holding none, is not.
withEstimate := requestStatusDetail(RequestStatusDownloading, "movie", 840)
withoutEstimate := requestStatusDetail(RequestStatusDownloading, "movie", 0)
if withEstimate == withoutEstimate {
t.Fatal("a download with an estimate must not read the same as one without")
}
if !strings.Contains(withEstimate, "14 minutes") {
t.Fatalf("840 seconds should read as about 14 minutes, got %q", withEstimate)
}
if strings.Contains(withoutEstimate, "~") {
t.Fatalf("a download with no estimate must not print one, got %q", withoutEstimate)
}
2026-08-12 14:13:19 +12:00
}