* feat(collections): add profile-scoped display filters
* refactor(collections): dedup display-filter helpers per review
Address code-review feedback on the profile-scoped display filters
without changing behavior:
- Widen CompletedHistoryItemMap to accept ProgressCompletionStore and
drop the duplicate completedHistoryItemMapForProgress copy.
- Extract the duplicated MDBList candidate retry loop into a generic
collectionutil.FetchMDBListWithFallback helper, used by both the user
and library collection syncers, and cover it with unit tests.
- Reuse validateOptionalLibraryIDs in HandleUpdateCollection instead of
an inline positive-ID loop.
- Import the shared COLLECTION_{WATCH,MEDIA}_FILTER_OPTIONS in the
template config form rather than redefining them locally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(collections): sanitize query_definition library_ids fallback
readSourceConfigLibraryIDs validated source_config.library_ids (finite,
positive, truncated, deduplicated) but returned the query_definition
fallback raw, so legacy rows could surface zero/negative/duplicate IDs
that the backend now rejects on save. Extract a shared sanitizer and
apply it to both paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(docs): This makes the agents annoying to work with
* Improve playback session handling
* Support collection source order in catalog filters
* fix(collections): address display filter review feedback
* refactor(catalog): remove duplicate collection query params
* Hide episode media scope for collection overlays
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
104 lines
3.6 KiB
Go
104 lines
3.6 KiB
Go
package collectionutil
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestNormalizeMDBListURL(t *testing.T) {
|
|
cases := []struct {
|
|
in, want string
|
|
}{
|
|
{"", ""},
|
|
{" ", ""},
|
|
{"https://mdblist.com/lists/example-user/watchlist", "https://mdblist.com/lists/example-user/watchlist/json"},
|
|
{"https://mdblist.com/lists/example-user/watchlist/", "https://mdblist.com/lists/example-user/watchlist/json"},
|
|
{"https://mdblist.com/lists/example-user/watchlist/json", "https://mdblist.com/lists/example-user/watchlist/json"},
|
|
{"https://mdblist.com/lists/example-user/watchlist/json/", "https://mdblist.com/lists/example-user/watchlist/json"},
|
|
{"https://mdblist.com/lists/example-user/external/1234/json", "https://mdblist.com/lists/example-user/external/1234/json"},
|
|
{"https://mdblist.com/lists/example-user/external/1234/json/json", "https://mdblist.com/lists/example-user/external/1234/json"},
|
|
{" https://mdblist.com/lists/example-user/watchlist ", "https://mdblist.com/lists/example-user/watchlist/json"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := NormalizeMDBListURL(tc.in); got != tc.want {
|
|
t.Errorf("NormalizeMDBListURL(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMDBListURLCandidatesNormalizesAndDeduplicates(t *testing.T) {
|
|
got := MDBListURLCandidates(
|
|
"https://mdblist.com/lists/example-user/external/1234/json/json",
|
|
"https://mdblist.com/lists/example-user/external/1234/json",
|
|
"https://mdblist.com/lists/example-user/other",
|
|
)
|
|
want := []string{
|
|
"https://mdblist.com/lists/example-user/external/1234/json",
|
|
"https://mdblist.com/lists/example-user/other/json",
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("MDBListURLCandidates = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestFetchMDBListWithFallback(t *testing.T) {
|
|
errFetch := errors.New("fetch failed")
|
|
|
|
t.Run("returns first success without trying later candidates", func(t *testing.T) {
|
|
var tried []string
|
|
got, err := FetchMDBListWithFallback([]string{"a", "b"}, func(url string) ([]string, error) {
|
|
tried = append(tried, url)
|
|
return []string{url + "-entry"}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, []string{"a-entry"}) {
|
|
t.Fatalf("entries = %#v, want first candidate's result", got)
|
|
}
|
|
if !reflect.DeepEqual(tried, []string{"a"}) {
|
|
t.Fatalf("tried = %#v, want to stop after first success", tried)
|
|
}
|
|
})
|
|
|
|
t.Run("falls back past a failing candidate", func(t *testing.T) {
|
|
var tried []string
|
|
got, err := FetchMDBListWithFallback([]string{"a", "b"}, func(url string) ([]string, error) {
|
|
tried = append(tried, url)
|
|
if url == "a" {
|
|
return nil, errFetch
|
|
}
|
|
return []string{url + "-entry"}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, []string{"b-entry"}) {
|
|
t.Fatalf("entries = %#v, want fallback candidate's result", got)
|
|
}
|
|
if !reflect.DeepEqual(tried, []string{"a", "b"}) {
|
|
t.Fatalf("tried = %#v, want both candidates attempted", tried)
|
|
}
|
|
})
|
|
|
|
t.Run("returns the last error when every candidate fails", func(t *testing.T) {
|
|
_, err := FetchMDBListWithFallback([]string{"a", "b"}, func(string) ([]string, error) {
|
|
return nil, errFetch
|
|
})
|
|
if !errors.Is(err, errFetch) {
|
|
t.Fatalf("err = %v, want %v", err, errFetch)
|
|
}
|
|
})
|
|
|
|
t.Run("empty candidate list yields nil result and nil error", func(t *testing.T) {
|
|
got, err := FetchMDBListWithFallback(nil, func(string) ([]string, error) {
|
|
t.Fatal("fetch should not be called for an empty list")
|
|
return nil, nil
|
|
})
|
|
if err != nil || got != nil {
|
|
t.Fatalf("got %#v, %v; want nil, nil", got, err)
|
|
}
|
|
})
|
|
}
|