Files
SpotiFLAC-Mobile/go_backend/songlink_test.go
T

198 lines
6.0 KiB
Go

package gobackend
import (
"context"
"io"
"net/http"
"sync"
"sync/atomic"
"testing"
"time"
)
type roundTripFunc func(*http.Request) (*http.Response, error)
func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return fn(req)
}
type blockingPlatformResolver struct {
calls atomic.Int32
started chan struct{}
release chan struct{}
result resolverResult
}
func (r *blockingPlatformResolver) Resolve(context.Context, string, resolverMetadata) (resolverResult, error) {
if r.calls.Add(1) == 1 {
close(r.started)
}
<-r.release
return r.result, nil
}
func TestIdenticalPlatformResolverRequestsAreCoalesced(t *testing.T) {
resolver := &blockingPlatformResolver{
started: make(chan struct{}),
release: make(chan struct{}),
result: resolverResult{Links: map[string]songLinkPlatformLink{
"deezer": {URL: "https://www.deezer.com/track/123"},
}},
}
client := &SongLinkClient{fallbackResolver: resolver}
const workers = 16
start := make(chan struct{})
errs := make(chan error, workers)
var wg sync.WaitGroup
wg.Add(workers)
for range workers {
go func() {
defer wg.Done()
<-start
links, err := client.resolveTrackPlatforms("https://open.spotify.com/track/coalesced")
if err == nil && links["deezer"].URL == "" {
err = io.ErrUnexpectedEOF
}
errs <- err
}()
}
close(start)
select {
case <-resolver.started:
case <-time.After(time.Second):
t.Fatal("resolver did not start")
}
time.Sleep(20 * time.Millisecond)
close(resolver.release)
wg.Wait()
close(errs)
for err := range errs {
if err != nil {
t.Fatalf("coalesced resolution failed: %v", err)
}
}
if got := resolver.calls.Load(); got != 1 {
t.Fatalf("resolver calls = %d, want 1", got)
}
}
func TestGetRetryAfterDurationMissingHeaderReturnsZero(t *testing.T) {
resp := &http.Response{Header: make(http.Header)}
if got := getRetryAfterDuration(resp); got != 0 {
t.Fatalf("getRetryAfterDuration() = %v, want 0", got)
}
}
func resetTrackAvailabilityCache() {
trackAvailabilityCacheMu.Lock()
trackAvailabilityCache = map[string]trackAvailabilityCacheEntry{}
trackAvailabilityCacheMu.Unlock()
}
// testResolverResult is an in-memory fixture; none of these URLs are fetched.
func testResolverResult() resolverResult {
return resolverResult{Links: map[string]songLinkPlatformLink{
"spotify": {URL: "https://open.spotify.com/track/testspotifyid"},
"deezer": {URL: "https://www.deezer.com/track/101"},
"amazonMusic": {URL: "https://music.amazon.com/tracks/TESTASIN"},
"tidal": {URL: "https://listen.tidal.com/track/202"},
"qobuz": {URL: "https://open.qobuz.com/track/303"},
"youtubeMusic": {URL: "https://music.youtube.com/watch?v=testvideoid1"},
}}
}
func TestCheckTrackAvailabilityFromSpotifyUsesActiveResolverChain(t *testing.T) {
resetTrackAvailabilityCache()
client := &SongLinkClient{fallbackResolver: &stubPlatformResolver{result: testResolverResult()}}
availability, err := client.CheckTrackAvailability("testspotifyid", "")
if err != nil {
t.Fatalf("CheckTrackAvailability() error = %v", err)
}
if availability.SpotifyID != "testspotifyid" || availability.DeezerID != "101" {
t.Fatalf("availability IDs = %+v", availability)
}
if !availability.Deezer || !availability.Amazon || !availability.Tidal || !availability.Qobuz || !availability.YouTube {
t.Fatalf("availability flags = %+v", availability)
}
if availability.YouTubeID != "testvideoid1" {
t.Fatalf("YouTubeID = %q", availability.YouTubeID)
}
}
type inputCapturingResolver struct {
input string
result resolverResult
}
func (r *inputCapturingResolver) Resolve(_ context.Context, input string, _ resolverMetadata) (resolverResult, error) {
r.input = input
return r.result, nil
}
func TestResolveTrackPlatformsByPlatformBuildsDirectURL(t *testing.T) {
resolver := &inputCapturingResolver{result: testResolverResult()}
client := &SongLinkClient{fallbackResolver: resolver}
links, err := client.resolveTrackPlatformsByPlatform("spotify", "song", "testspotifyid")
if err != nil {
t.Fatalf("resolveTrackPlatformsByPlatform() error = %v", err)
}
if resolver.input != "https://open.spotify.com/track/testspotifyid" {
t.Fatalf("resolver input = %q", resolver.input)
}
if links["deezer"].URL == "" {
t.Fatalf("resolver links = %#v", links)
}
}
func TestCheckTrackAvailabilityCachesResult(t *testing.T) {
resetTrackAvailabilityCache()
resolver := &stubPlatformResolver{result: resolverResult{Links: map[string]songLinkPlatformLink{
"spotify": {URL: "https://open.spotify.com/track/cachedid"},
"deezer": {URL: "https://www.deezer.com/track/111"},
}}}
client := &SongLinkClient{fallbackResolver: resolver}
first, err := client.CheckTrackAvailability("cachedid", "")
if err != nil {
t.Fatalf("first CheckTrackAvailability() error = %v", err)
}
second, err := client.CheckTrackAvailability("cachedid", "")
if err != nil {
t.Fatalf("second CheckTrackAvailability() error = %v", err)
}
if resolver.calls != 1 {
t.Fatalf("resolver calls = %d, want 1", resolver.calls)
}
if first == second || second.DeezerID != "111" {
t.Fatalf("cached result = %+v", second)
}
}
func TestCheckTrackAvailabilityNegativeCacheTTL(t *testing.T) {
resetTrackAvailabilityCache()
entry := trackAvailabilityCacheEntry{err: true, expiresAt: time.Now().Add(-time.Second)}
key := GetSongLinkRegion() + "|spotify:expiredneg"
trackAvailabilityCacheMu.Lock()
trackAvailabilityCache[key] = entry
trackAvailabilityCacheMu.Unlock()
if _, hit, _ := trackAvailabilityCacheLookup(key); hit {
t.Fatal("expired negative entry should not be a cache hit")
}
}
func TestCheckAvailabilityFromDeezerUsesActiveResolverChain(t *testing.T) {
client := &SongLinkClient{fallbackResolver: &stubPlatformResolver{result: testResolverResult()}}
availability, err := client.CheckAvailabilityFromDeezer("908604612")
if err != nil {
t.Fatalf("CheckAvailabilityFromDeezer() error = %v", err)
}
if !availability.Deezer || availability.DeezerID != "908604612" || availability.SpotifyID != "testspotifyid" {
t.Fatalf("availability = %+v", availability)
}
}