Files
silo-server/internal/requests/discover_logo_cache_test.go
T

116 lines
3.0 KiB
Go

package requests
import (
"context"
"errors"
"sync"
"sync/atomic"
"testing"
"time"
)
type fakeLogoLookup struct {
calls atomic.Int64
logoFor map[int]string
err error
}
func (f *fakeLogoLookup) Lookup(_ context.Context, id int) (string, error) {
f.calls.Add(1)
if f.err != nil {
return "", f.err
}
return f.logoFor[id], nil
}
func TestLogoCacheReturnsCachedValueAfterFirstCall(t *testing.T) {
lookup := &fakeLogoLookup{logoFor: map[int]string{420: "/hUze.png"}}
cache := newLogoCache(lookup.Lookup, time.Hour)
for i := 0; i < 3; i++ {
got, err := cache.Get(context.Background(), 420)
if err != nil {
t.Fatalf("Get: %v", err)
}
if got != "/hUze.png" {
t.Errorf("got %q, want /hUze.png", got)
}
}
if calls := lookup.calls.Load(); calls != 1 {
t.Errorf("upstream calls = %d, want 1", calls)
}
}
func TestLogoCacheExpiresAfterTTL(t *testing.T) {
lookup := &fakeLogoLookup{logoFor: map[int]string{1: "/a.png"}}
cache := newLogoCache(lookup.Lookup, 10*time.Millisecond)
if _, err := cache.Get(context.Background(), 1); err != nil {
t.Fatalf("first Get: %v", err)
}
time.Sleep(25 * time.Millisecond)
if _, err := cache.Get(context.Background(), 1); err != nil {
t.Fatalf("second Get: %v", err)
}
if calls := lookup.calls.Load(); calls != 2 {
t.Errorf("calls = %d, want 2", calls)
}
}
func TestLogoCacheSingleflightDeduplicatesParallelMisses(t *testing.T) {
lookup := &fakeLogoLookup{logoFor: map[int]string{1: "/a.png"}}
cache := newLogoCache(func(ctx context.Context, id int) (string, error) {
time.Sleep(20 * time.Millisecond)
return lookup.Lookup(ctx, id)
}, time.Hour)
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if _, err := cache.Get(context.Background(), 1); err != nil {
t.Errorf("Get: %v", err)
}
}()
}
wg.Wait()
if calls := lookup.calls.Load(); calls != 1 {
t.Errorf("calls = %d, want 1 (singleflight should dedupe)", calls)
}
}
func TestLogoCacheReturnsErrorAndDoesNotCacheFailure(t *testing.T) {
failure := errors.New("upstream boom")
lookup := &fakeLogoLookup{err: failure}
cache := newLogoCache(lookup.Lookup, time.Hour)
if _, err := cache.Get(context.Background(), 1); !errors.Is(err, failure) {
t.Fatalf("first err = %v, want %v", err, failure)
}
if _, err := cache.Get(context.Background(), 1); !errors.Is(err, failure) {
t.Fatalf("second err = %v, want %v", err, failure)
}
if calls := lookup.calls.Load(); calls != 2 {
t.Errorf("calls = %d, want 2 (errors must not be cached)", calls)
}
}
func TestLogoCacheEmptyPathIsCachedAsMiss(t *testing.T) {
lookup := &fakeLogoLookup{logoFor: map[int]string{1: ""}}
cache := newLogoCache(lookup.Lookup, time.Hour)
for i := 0; i < 3; i++ {
got, err := cache.Get(context.Background(), 1)
if err != nil {
t.Fatalf("Get: %v", err)
}
if got != "" {
t.Errorf("got %q, want empty", got)
}
}
if calls := lookup.calls.Load(); calls != 1 {
t.Errorf("calls = %d, want 1 (empty strings should still be cached)", calls)
}
}