Watchlist and favorites sections keep their stored list order by default (provider-synced positions first, then newest-added). An optional sort/order config now supports title, release date, IMDb rating, and date-added-to-list ordering, applied consistently on the home rail and the catalog "see all" page. added_at is resolved from the list entries via a shared OrderPersonalListIDs helper since it is distinct from the catalog's library added_at. The section editor gains a Sort dropdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
137 lines
4.5 KiB
Go
137 lines
4.5 KiB
Go
package sections
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/catalog"
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
|
)
|
|
|
|
// Watchlist and favorites sections are profile-scoped: with no user store (or
|
|
// no authenticated profile) they must degrade to an empty rail instead of the
|
|
// "unsupported section type" error they returned before being wired up.
|
|
func TestFetchSectionPersonalListWithoutStoreReturnsEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := &Fetcher{}
|
|
for _, sectionType := range []SectionType{SectionWatchlist, SectionFavorites} {
|
|
s := ResolvedSection{ID: "s1", SectionType: sectionType, ItemLimit: 20}
|
|
items, total, err := f.fetchSection(context.Background(), s, nil, nil, 0, "", catalog.AccessFilter{})
|
|
if err != nil {
|
|
t.Fatalf("fetchSection(%s) error = %v, want nil", sectionType, err)
|
|
}
|
|
if len(items) != 0 || total != 0 {
|
|
t.Fatalf("fetchSection(%s) = %d items, total %d; want empty", sectionType, len(items), total)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSortPersonalListItems(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f64 := func(v float64) *float64 { return &v }
|
|
str := func(v string) *string { return &v }
|
|
build := func() []*models.MediaItem {
|
|
return []*models.MediaItem{
|
|
{ContentID: "b", Title: "Beta", ReleaseDate: str("2020-01-01"), RatingIMDB: f64(6.1)},
|
|
{ContentID: "a", Title: "alpha", ReleaseDate: str("2024-05-05"), RatingIMDB: nil},
|
|
{ContentID: "c", Title: "Gamma", FirstAirDate: str("2022-03-03"), RatingIMDB: f64(8.4)},
|
|
{ContentID: "d", Title: "Delta", RatingIMDB: f64(7.0)},
|
|
}
|
|
}
|
|
|
|
cases := []struct {
|
|
field, order string
|
|
want []string
|
|
}{
|
|
{"title", "asc", []string{"a", "b", "d", "c"}},
|
|
{"title", "desc", []string{"c", "d", "b", "a"}},
|
|
// FirstAirDate is used when ReleaseDate is missing; unknown dates last.
|
|
{"release_date", "desc", []string{"a", "c", "b", "d"}},
|
|
{"release_date", "asc", []string{"b", "c", "a", "d"}},
|
|
// Missing ratings sort last regardless of direction.
|
|
{"rating_imdb", "desc", []string{"c", "d", "b", "a"}},
|
|
{"rating_imdb", "asc", []string{"b", "d", "c", "a"}},
|
|
}
|
|
for _, tc := range cases {
|
|
qs, ok := catalog.NormalizePersonalListSort(tc.field, tc.order)
|
|
if !ok {
|
|
t.Fatalf("NormalizePersonalListSort(%s, %s) not ok", tc.field, tc.order)
|
|
}
|
|
items := build()
|
|
sortPersonalListItems(items, qs)
|
|
got := make([]string, len(items))
|
|
for i, item := range items {
|
|
got[i] = item.ContentID
|
|
}
|
|
for i := range tc.want {
|
|
if got[i] != tc.want[i] {
|
|
t.Fatalf("sort %s %s = %v, want %v", tc.field, tc.order, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizePersonalListSortDefaultsAndRejects(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if qs, ok := catalog.NormalizePersonalListSort("title", ""); !ok || qs.Order != "asc" {
|
|
t.Fatalf("title default = %+v, %v; want asc", qs, ok)
|
|
}
|
|
if qs, ok := catalog.NormalizePersonalListSort("release_date", ""); !ok || qs.Order != "desc" {
|
|
t.Fatalf("release_date default = %+v, %v; want desc", qs, ok)
|
|
}
|
|
if _, ok := catalog.NormalizePersonalListSort("", "asc"); ok {
|
|
t.Fatal("empty field must keep list order")
|
|
}
|
|
if _, ok := catalog.NormalizePersonalListSort("progress", ""); ok {
|
|
t.Fatal("personalized/unsupported fields must be rejected")
|
|
}
|
|
}
|
|
|
|
// added_at sorts by when the item was added to the list, from the entry
|
|
// timestamps rather than item metadata; missing timestamps sort last and any
|
|
// other sort keeps the stored list order.
|
|
func TestOrderPersonalListIDsByAddedAt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
build := func() []catalog.PersonalListEntry {
|
|
return []catalog.PersonalListEntry{
|
|
{ID: "synced", AddedAt: "2024-02-02T00:00:00Z"},
|
|
{ID: "old", AddedAt: "2023-01-01T00:00:00Z"},
|
|
{ID: "new", AddedAt: "2025-06-06T00:00:00Z"},
|
|
{ID: "unknown", AddedAt: ""},
|
|
}
|
|
}
|
|
|
|
qs, ok := catalog.NormalizePersonalListSort("added_at", "")
|
|
if !ok || qs.Order != "desc" {
|
|
t.Fatalf("added_at default = %+v, %v; want desc", qs, ok)
|
|
}
|
|
|
|
got := catalog.OrderPersonalListIDs(build(), qs)
|
|
want := []string{"new", "synced", "old", "unknown"}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("added_at desc = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
got = catalog.OrderPersonalListIDs(build(), catalog.QuerySort{Field: "added_at", Order: "asc"})
|
|
want = []string{"old", "synced", "new", "unknown"}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("added_at asc = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
got = catalog.OrderPersonalListIDs(build(), catalog.QuerySort{})
|
|
want = []string{"synced", "old", "new", "unknown"}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("no sort = %v, want stored order %v", got, want)
|
|
}
|
|
}
|
|
}
|