Files
SpotiFLAC-Mobile/go_backend/cover_test.go
T

260 lines
6.5 KiB
Go

package gobackend
import (
"bytes"
"fmt"
"image"
"image/color"
"image/jpeg"
"image/png"
"net/http"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
func encodedTestCover(t *testing.T, width, height int, format string) []byte {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
img.SetRGBA(x, y, color.RGBA{
R: uint8(x % 256),
G: uint8(y % 256),
B: uint8((x + y) % 256),
A: uint8(128 + (x+y)%128),
})
}
}
var encoded bytes.Buffer
var err error
if format == "png" {
err = png.Encode(&encoded, img)
} else {
err = jpeg.Encode(&encoded, img, &jpeg.Options{Quality: 95})
}
if err != nil {
t.Fatalf("encode test cover: %v", err)
}
return encoded.Bytes()
}
func resetCoverCache() {
coverMu.Lock()
coverCache = map[string]*coverCacheEntry{}
coverInflight = map[string]*coverInflightCall{}
coverCacheBytes = 0
coverMu.Unlock()
}
func TestFetchCoverCachedSingleflight(t *testing.T) {
orig := coverFetch
defer func() { coverFetch = orig }()
resetCoverCache()
var calls int32
entered := make(chan struct{})
release := make(chan struct{})
coverFetch = func(string) ([]byte, error) {
if atomic.AddInt32(&calls, 1) == 1 {
close(entered)
}
<-release
return []byte("coverbytes"), nil
}
const url = "https://cdn.example/cover_max.jpg"
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
if _, err := fetchCoverCached(url); err != nil {
t.Errorf("leader fetch error: %v", err)
}
}()
<-entered // leader has registered inflight and is blocked in coverFetch
wg.Add(1)
go func() {
defer wg.Done()
if _, err := fetchCoverCached(url); err != nil {
t.Errorf("follower fetch error: %v", err)
}
}()
close(release)
wg.Wait()
if got := atomic.LoadInt32(&calls); got != 1 {
t.Fatalf("expected 1 fetch for concurrent requests, got %d", got)
}
}
func TestFetchCoverCachedTTLExpiry(t *testing.T) {
orig := coverFetch
defer func() { coverFetch = orig }()
resetCoverCache()
var calls int32
coverFetch = func(string) ([]byte, error) {
atomic.AddInt32(&calls, 1)
return []byte("data"), nil
}
const url = "https://cdn.example/ttl.jpg"
if _, err := fetchCoverCached(url); err != nil {
t.Fatalf("first fetch error: %v", err)
}
// second call served from cache
if _, err := fetchCoverCached(url); err != nil {
t.Fatalf("second fetch error: %v", err)
}
if got := atomic.LoadInt32(&calls); got != 1 {
t.Fatalf("expected cache hit, got %d fetches", got)
}
// expire the entry and confirm a refetch
coverMu.Lock()
coverCache[url].expiresAt = time.Now().Add(-time.Minute)
coverMu.Unlock()
if _, err := fetchCoverCached(url); err != nil {
t.Fatalf("third fetch error: %v", err)
}
if got := atomic.LoadInt32(&calls); got != 2 {
t.Fatalf("expected refetch after TTL expiry, got %d fetches", got)
}
}
func TestDownloadCoverUsesProviderURLUnchanged(t *testing.T) {
orig := coverFetch
defer func() { coverFetch = orig }()
resetCoverCache()
const providerURL = "https://i.scdn.co/image/ab67616d00001e02example"
var requestedURL string
coverFetch = func(url string) ([]byte, error) {
requestedURL = url
return []byte("provider-cover"), nil
}
got, err := downloadCoverToMemory(providerURL)
if err != nil {
t.Fatalf("download provider cover: %v", err)
}
if requestedURL != providerURL {
t.Fatalf("requested URL = %q, want provider URL %q", requestedURL, providerURL)
}
if string(got) != "provider-cover" {
t.Fatalf("downloaded cover = %q", got)
}
}
func TestFetchCoverBytesRejectsOversizedResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
writer.Header().Set("Content-Length", fmt.Sprintf("%d", maxCoverDownloadBytes+1))
writer.WriteHeader(http.StatusOK)
}))
defer server.Close()
SetAllowPrivateNetwork(true)
defer SetAllowPrivateNetwork(false)
if _, err := fetchCoverBytes(server.URL); err == nil || !strings.Contains(err.Error(), "exceeds") {
t.Fatalf("expected oversized cover rejection, got %v", err)
}
}
func TestResizeCoverForEmbeddingPreservesAspectRatio(t *testing.T) {
original := encodedTestCover(t, 1200, 600, "jpeg")
resized, changed, err := resizeCoverForEmbedding(original, 500)
if err != nil {
t.Fatalf("resize cover: %v", err)
}
if !changed {
t.Fatal("expected oversized artwork to be resized")
}
config, format, err := image.DecodeConfig(bytes.NewReader(resized))
if err != nil {
t.Fatalf("decode resized cover config: %v", err)
}
if config.Width != 500 || config.Height != 250 {
t.Fatalf("resized dimensions = %dx%d, want 500x250", config.Width, config.Height)
}
if format != "jpeg" {
t.Fatalf("resized format = %q, want jpeg", format)
}
}
func TestResizeCoverForEmbeddingKeepsSmallArtworkByteForByte(t *testing.T) {
original := encodedTestCover(t, 320, 320, "jpeg")
resized, changed, err := resizeCoverForEmbedding(original, 500)
if err != nil {
t.Fatalf("resize cover: %v", err)
}
if changed {
t.Fatal("artwork within the limit should not be re-encoded")
}
if !bytes.Equal(resized, original) {
t.Fatal("artwork within the limit was modified")
}
}
func TestResizeCoverForEmbeddingPreservesPNG(t *testing.T) {
original := encodedTestCover(t, 600, 1200, "png")
resized, changed, err := resizeCoverForEmbedding(original, 500)
if err != nil {
t.Fatalf("resize PNG cover: %v", err)
}
if !changed {
t.Fatal("expected oversized PNG artwork to be resized")
}
config, format, err := image.DecodeConfig(bytes.NewReader(resized))
if err != nil {
t.Fatalf("decode resized PNG config: %v", err)
}
if config.Width != 250 || config.Height != 500 {
t.Fatalf("resized dimensions = %dx%d, want 250x500", config.Width, config.Height)
}
if format != "png" {
t.Fatalf("resized format = %q, want png", format)
}
}
func TestDownloadCoverToMemorySizedCachesDerivedVariant(t *testing.T) {
originalFetch := coverFetch
defer func() { coverFetch = originalFetch }()
resetCoverCache()
original := encodedTestCover(t, 1200, 600, "jpeg")
var calls int32
coverFetch = func(string) ([]byte, error) {
atomic.AddInt32(&calls, 1)
return original, nil
}
for range 2 {
resized, err := downloadCoverToMemorySized(
"https://cdn.example/album.jpg",
500,
)
if err != nil {
t.Fatalf("download sized cover: %v", err)
}
width, height := coverDimensions(resized)
if width != 500 || height != 250 {
t.Fatalf("sized cover = %dx%d, want 500x250", width, height)
}
}
if got := atomic.LoadInt32(&calls); got != 1 {
t.Fatalf("provider cover fetched %d times, want once", got)
}
}