mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-03 00:30:54 +02:00
260 lines
6.5 KiB
Go
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)
|
|
}
|
|
}
|