Files
silo-server/internal/sections/fetcher_personal_list_test.go
QuickandClaude Fable 5 69dde62fcd feat(sections): configurable sort for watchlist/favorites sections
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>
2026-07-05 18:45:48 -04:00

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)
}
}
}