154 lines
4.2 KiB
Go
154 lines
4.2 KiB
Go
package sections
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/catalog"
|
|
"github.com/Silo-Server/silo-server/internal/sections/recipes"
|
|
)
|
|
|
|
func TestCachedEditorialCandidatesReusesCandidateListForSameScope(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := &Fetcher{
|
|
Clock: recipes.FixedClock(time.Date(2026, 5, 2, 12, 0, 0, 0, time.UTC)),
|
|
}
|
|
calls := 0
|
|
loader := func(context.Context, string, *int, []int, catalog.AccessFilter) ([]string, error) {
|
|
calls++
|
|
return []string{"first", "second"}, nil
|
|
}
|
|
|
|
first, err := f.cachedEditorialCandidates(context.Background(), "actor", nil, []int{2, 1}, catalog.AccessFilter{
|
|
MaxContentRating: "PG-13",
|
|
}, time.Hour, loader)
|
|
if err != nil {
|
|
t.Fatalf("first cachedEditorialCandidates: %v", err)
|
|
}
|
|
first[0] = "mutated"
|
|
|
|
second, err := f.cachedEditorialCandidates(context.Background(), "actor", nil, []int{1, 2}, catalog.AccessFilter{
|
|
MaxContentRating: "PG-13",
|
|
}, time.Hour, loader)
|
|
if err != nil {
|
|
t.Fatalf("second cachedEditorialCandidates: %v", err)
|
|
}
|
|
|
|
if calls != 1 {
|
|
t.Fatalf("loader calls = %d, want 1", calls)
|
|
}
|
|
if got, want := second[0], "first"; got != want {
|
|
t.Fatalf("cached candidates were mutated through returned slice: got %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestCachedEditorialCandidatesSeparatesAccessScopeAndExpires(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Date(2026, 5, 2, 12, 0, 0, 0, time.UTC)
|
|
f := &Fetcher{
|
|
Clock: recipes.FixedClock(now),
|
|
}
|
|
calls := 0
|
|
loader := func(context.Context, string, *int, []int, catalog.AccessFilter) ([]string, error) {
|
|
calls++
|
|
return []string{time.Unix(int64(calls), 0).UTC().Format(time.RFC3339)}, nil
|
|
}
|
|
|
|
filter := catalog.AccessFilter{MaxContentRating: "PG-13"}
|
|
if _, err := f.cachedEditorialCandidates(context.Background(), "actor", nil, nil, filter, time.Hour, loader); err != nil {
|
|
t.Fatalf("first cachedEditorialCandidates: %v", err)
|
|
}
|
|
if _, err := f.cachedEditorialCandidates(context.Background(), "actor", nil, nil, catalog.AccessFilter{
|
|
MaxContentRating: "R",
|
|
}, time.Hour, loader); err != nil {
|
|
t.Fatalf("different filter cachedEditorialCandidates: %v", err)
|
|
}
|
|
|
|
f.Clock = recipes.FixedClock(now.Add(2 * time.Hour))
|
|
if _, err := f.cachedEditorialCandidates(context.Background(), "actor", nil, nil, filter, time.Hour, loader); err != nil {
|
|
t.Fatalf("expired cachedEditorialCandidates: %v", err)
|
|
}
|
|
|
|
if calls != 3 {
|
|
t.Fatalf("loader calls = %d, want 3", calls)
|
|
}
|
|
}
|
|
|
|
func TestCachedEditorialCandidatesSeparatesNilAndEmptyLibraryScope(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := &Fetcher{
|
|
Clock: recipes.FixedClock(time.Date(2026, 5, 2, 12, 0, 0, 0, time.UTC)),
|
|
}
|
|
calls := 0
|
|
loader := func(context.Context, string, *int, []int, catalog.AccessFilter) ([]string, error) {
|
|
calls++
|
|
return []string{"ok"}, nil
|
|
}
|
|
|
|
if _, err := f.cachedEditorialCandidates(context.Background(), "actor", nil, nil, catalog.AccessFilter{}, time.Hour, loader); err != nil {
|
|
t.Fatalf("nil scope cachedEditorialCandidates: %v", err)
|
|
}
|
|
if _, err := f.cachedEditorialCandidates(context.Background(), "actor", nil, []int{}, catalog.AccessFilter{}, time.Hour, loader); err != nil {
|
|
t.Fatalf("empty scope cachedEditorialCandidates: %v", err)
|
|
}
|
|
|
|
if calls != 2 {
|
|
t.Fatalf("loader calls = %d, want 2", calls)
|
|
}
|
|
}
|
|
|
|
func TestCachedEditorialCandidatesCoalescesConcurrentMisses(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := &Fetcher{
|
|
Clock: recipes.FixedClock(time.Date(2026, 5, 2, 12, 0, 0, 0, time.UTC)),
|
|
}
|
|
var (
|
|
mu sync.Mutex
|
|
calls int
|
|
)
|
|
started := make(chan struct{})
|
|
release := make(chan struct{})
|
|
loader := func(context.Context, string, *int, []int, catalog.AccessFilter) ([]string, error) {
|
|
mu.Lock()
|
|
calls++
|
|
if calls == 1 {
|
|
close(started)
|
|
}
|
|
mu.Unlock()
|
|
<-release
|
|
return []string{"shared"}, nil
|
|
}
|
|
|
|
const workers = 8
|
|
var wg sync.WaitGroup
|
|
wg.Add(workers)
|
|
errs := make(chan error, workers)
|
|
for range workers {
|
|
go func() {
|
|
defer wg.Done()
|
|
_, err := f.cachedEditorialCandidates(context.Background(), "actor", nil, nil, catalog.AccessFilter{}, time.Hour, loader)
|
|
errs <- err
|
|
}()
|
|
}
|
|
|
|
<-started
|
|
close(release)
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Fatalf("cachedEditorialCandidates: %v", err)
|
|
}
|
|
}
|
|
if calls != 1 {
|
|
t.Fatalf("loader calls = %d, want 1", calls)
|
|
}
|
|
}
|