test: refactor Spotify tests to use Ginkgo and Gomega for consistency
This commit is contained in:
+210
-154
@@ -2,165 +2,221 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"fmt"
|
||||
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/host"
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/scrobbler"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestParsePrimaryArtist(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
wantPrimary string
|
||||
wantFeat string
|
||||
}{
|
||||
{"Radiohead", "Radiohead", ""},
|
||||
{"Wretch 32 Feat. Badness & Ghetts", "Wretch 32", "Feat. Badness & Ghetts"},
|
||||
{"Artist Ft. Guest", "Artist", "Ft. Guest"},
|
||||
{"Artist Featuring Someone", "Artist", "Featuring Someone"},
|
||||
{"PinkPantheress & Ice Spice", "PinkPantheress", ""},
|
||||
{"Artist A / Artist B", "Artist A", ""},
|
||||
{"", "", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
gotPrimary, gotFeat := parsePrimaryArtist(tt.input)
|
||||
if gotPrimary != tt.wantPrimary {
|
||||
t.Errorf("parsePrimaryArtist(%q) primary = %q, want %q", tt.input, gotPrimary, tt.wantPrimary)
|
||||
}
|
||||
if gotFeat != tt.wantFeat {
|
||||
t.Errorf("parsePrimaryArtist(%q) feat = %q, want %q", tt.input, gotFeat, tt.wantFeat)
|
||||
}
|
||||
}
|
||||
}
|
||||
var _ = Describe("Spotify", func() {
|
||||
Describe("parsePrimaryArtist", func() {
|
||||
DescribeTable("extracts primary artist and feat suffix",
|
||||
func(input, expectedPrimary, expectedFeat string) {
|
||||
primary, feat := parsePrimaryArtist(input)
|
||||
Expect(primary).To(Equal(expectedPrimary))
|
||||
Expect(feat).To(Equal(expectedFeat))
|
||||
},
|
||||
Entry("simple artist", "Radiohead", "Radiohead", ""),
|
||||
Entry("Feat. separator", "Wretch 32 Feat. Badness & Ghetts", "Wretch 32", "Feat. Badness & Ghetts"),
|
||||
Entry("Ft. separator", "Artist Ft. Guest", "Artist", "Ft. Guest"),
|
||||
Entry("Featuring separator", "Artist Featuring Someone", "Artist", "Featuring Someone"),
|
||||
Entry("& co-artist", "PinkPantheress & Ice Spice", "PinkPantheress", ""),
|
||||
Entry("/ co-artist", "Artist A / Artist B", "Artist A", ""),
|
||||
Entry("empty string", "", "", ""),
|
||||
)
|
||||
})
|
||||
|
||||
func TestBuildSpotifySearchURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
title, artist string
|
||||
wantPrefix string
|
||||
wantContains string
|
||||
}{
|
||||
{"Never Gonna Give You Up", "Rick Astley", "https://open.spotify.com/search/", "Rick%20Astley"},
|
||||
{"Karma Police", "Radiohead", "https://open.spotify.com/search/", "Radiohead"},
|
||||
{"", "Solo Artist", "https://open.spotify.com/search/", "Solo%20Artist"},
|
||||
{"Only Title", "", "https://open.spotify.com/search/", "Only%20Title"},
|
||||
{"", "", "https://open.spotify.com/search/", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := buildSpotifySearchURL(tt.title, tt.artist)
|
||||
if !strings.HasPrefix(got, tt.wantPrefix) {
|
||||
t.Errorf("buildSpotifySearchURL(%q, %q) = %q, want prefix %q", tt.title, tt.artist, got, tt.wantPrefix)
|
||||
}
|
||||
if tt.wantContains != "" && !strings.Contains(got, tt.wantContains) {
|
||||
t.Errorf("buildSpotifySearchURL(%q, %q) = %q, want to contain %q", tt.title, tt.artist, got, tt.wantContains)
|
||||
}
|
||||
}
|
||||
}
|
||||
Describe("buildSpotifySearchURL", func() {
|
||||
DescribeTable("constructs Spotify search URL",
|
||||
func(title, artist, expectedSubstring string) {
|
||||
url := buildSpotifySearchURL(title, artist)
|
||||
Expect(url).To(HavePrefix("https://open.spotify.com/search/"))
|
||||
if expectedSubstring != "" {
|
||||
Expect(url).To(ContainSubstring(expectedSubstring))
|
||||
}
|
||||
},
|
||||
Entry("artist and title", "Never Gonna Give You Up", "Rick Astley", "Rick%20Astley"),
|
||||
Entry("another track", "Karma Police", "Radiohead", "Radiohead"),
|
||||
Entry("empty title", "", "Solo Artist", "Solo%20Artist"),
|
||||
Entry("empty artist", "Only Title", "", "Only%20Title"),
|
||||
Entry("both empty", "", "", ""),
|
||||
)
|
||||
})
|
||||
|
||||
func TestSpotifyCacheKey(t *testing.T) {
|
||||
key1 := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
||||
key2 := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
||||
key3 := spotifyCacheKey("Radiohead", "Karma Police", "The Bends")
|
||||
|
||||
if key1 != key2 {
|
||||
t.Error("identical inputs should produce identical cache keys")
|
||||
}
|
||||
if key1 == key3 {
|
||||
t.Error("different albums should produce different cache keys")
|
||||
}
|
||||
if !strings.HasPrefix(key1, "spotify.url.") {
|
||||
t.Errorf("cache key %q should start with 'spotify.url.'", key1)
|
||||
}
|
||||
|
||||
// Case-insensitive: "Radiohead" == "radiohead"
|
||||
keyUpper := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
||||
keyLower := spotifyCacheKey("radiohead", "karma police", "ok computer")
|
||||
if keyUpper != keyLower {
|
||||
t.Error("cache key should be case-insensitive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSpotifyID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "valid single result",
|
||||
body: `[{"spotify_track_ids":["4tIGK5G9hNDA50ZdGioZRG"]}]`,
|
||||
want: "4tIGK5G9hNDA50ZdGioZRG",
|
||||
},
|
||||
{
|
||||
name: "multiple IDs picks first",
|
||||
body: `[{"artist_name":"Lil Baby & Drake","track_name":"Yes Indeed","spotify_track_ids":["6vN77lE9LK6HP2DewaN6HZ","4wlLbLeDWbA6TzwZFp1UaK"]}]`,
|
||||
want: "6vN77lE9LK6HP2DewaN6HZ",
|
||||
},
|
||||
{
|
||||
name: "valid result with extra fields",
|
||||
body: `[{"artist_name":"Radiohead","track_name":"Karma Police","spotify_track_ids":["63OQupATfueTdZMWIV7nzz"],"release_name":"OK Computer"}]`,
|
||||
want: "63OQupATfueTdZMWIV7nzz",
|
||||
},
|
||||
{
|
||||
name: "empty spotify_track_ids array",
|
||||
body: `[{"spotify_track_ids":[]}]`,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "no spotify_track_ids field",
|
||||
body: `[{"artist_name":"Unknown"}]`,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty array",
|
||||
body: `[]`,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
body: `not json`,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "null spotify_track_ids with next result valid",
|
||||
body: `[{"spotify_track_ids":[]},{"spotify_track_ids":["abc123"]}]`,
|
||||
want: "abc123",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseSpotifyID([]byte(tt.body))
|
||||
if got != tt.want {
|
||||
t.Errorf("parseSpotifyID(%s) = %q, want %q", tt.body, got, tt.want)
|
||||
}
|
||||
Describe("spotifyCacheKey", func() {
|
||||
It("produces identical keys for identical inputs", func() {
|
||||
key1 := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
||||
key2 := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
||||
Expect(key1).To(Equal(key2))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenBrainzRequestPayloads(t *testing.T) {
|
||||
// Verify MBID request body is valid JSON
|
||||
mbid := "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
mbidBody := []byte(`[{"recording_mbid":"` + mbid + `"}]`)
|
||||
var mbidParsed []map[string]string
|
||||
if err := json.Unmarshal(mbidBody, &mbidParsed); err != nil {
|
||||
t.Fatalf("MBID request body is not valid JSON: %v", err)
|
||||
}
|
||||
if mbidParsed[0]["recording_mbid"] != mbid {
|
||||
t.Errorf("MBID body recording_mbid = %q, want %q", mbidParsed[0]["recording_mbid"], mbid)
|
||||
}
|
||||
It("produces different keys for different albums", func() {
|
||||
key1 := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
||||
key2 := spotifyCacheKey("Radiohead", "Karma Police", "The Bends")
|
||||
Expect(key1).ToNot(Equal(key2))
|
||||
})
|
||||
|
||||
// Verify metadata request body handles special characters via %q formatting
|
||||
artist := `Guns N' Roses`
|
||||
title := `Sweet Child O' Mine`
|
||||
album := `Appetite for Destruction`
|
||||
metaBody := []byte(`[{"artist_name":` + jsonQuote(artist) + `,"track_name":` + jsonQuote(title) + `,"release_name":` + jsonQuote(album) + `}]`)
|
||||
var metaParsed []map[string]string
|
||||
if err := json.Unmarshal(metaBody, &metaParsed); err != nil {
|
||||
t.Fatalf("Metadata request body is not valid JSON: %v", err)
|
||||
}
|
||||
if metaParsed[0]["artist_name"] != artist {
|
||||
t.Errorf("artist_name = %q, want %q", metaParsed[0]["artist_name"], artist)
|
||||
}
|
||||
}
|
||||
It("uses the correct prefix", func() {
|
||||
key := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
||||
Expect(key).To(HavePrefix("spotify.url."))
|
||||
})
|
||||
|
||||
func jsonQuote(s string) string {
|
||||
b, _ := json.Marshal(s)
|
||||
return string(b)
|
||||
}
|
||||
It("is case-insensitive", func() {
|
||||
keyUpper := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
||||
keyLower := spotifyCacheKey("radiohead", "karma police", "ok computer")
|
||||
Expect(keyUpper).To(Equal(keyLower))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("parseSpotifyID", func() {
|
||||
DescribeTable("extracts first Spotify track ID from ListenBrainz response",
|
||||
func(body, expectedID string) {
|
||||
Expect(parseSpotifyID([]byte(body))).To(Equal(expectedID))
|
||||
},
|
||||
Entry("valid single result",
|
||||
`[{"spotify_track_ids":["4tIGK5G9hNDA50ZdGioZRG"]}]`, "4tIGK5G9hNDA50ZdGioZRG"),
|
||||
Entry("multiple IDs picks first",
|
||||
`[{"artist_name":"Lil Baby & Drake","track_name":"Yes Indeed","spotify_track_ids":["6vN77lE9LK6HP2DewaN6HZ","4wlLbLeDWbA6TzwZFp1UaK"]}]`, "6vN77lE9LK6HP2DewaN6HZ"),
|
||||
Entry("valid result with extra fields",
|
||||
`[{"artist_name":"Radiohead","track_name":"Karma Police","spotify_track_ids":["63OQupATfueTdZMWIV7nzz"],"release_name":"OK Computer"}]`, "63OQupATfueTdZMWIV7nzz"),
|
||||
Entry("empty spotify_track_ids array",
|
||||
`[{"spotify_track_ids":[]}]`, ""),
|
||||
Entry("no spotify_track_ids field",
|
||||
`[{"artist_name":"Unknown"}]`, ""),
|
||||
Entry("empty array",
|
||||
`[]`, ""),
|
||||
Entry("invalid JSON",
|
||||
`not json`, ""),
|
||||
Entry("null first result falls through to second",
|
||||
`[{"spotify_track_ids":[]},{"spotify_track_ids":["abc123"]}]`, "abc123"),
|
||||
)
|
||||
})
|
||||
|
||||
Describe("ListenBrainz request payloads", func() {
|
||||
It("builds valid JSON for MBID requests", func() {
|
||||
mbid := "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
body := []byte(fmt.Sprintf(`[{"recording_mbid":%q}]`, mbid))
|
||||
var parsed []map[string]string
|
||||
Expect(json.Unmarshal(body, &parsed)).To(Succeed())
|
||||
Expect(parsed[0]["recording_mbid"]).To(Equal(mbid))
|
||||
})
|
||||
|
||||
It("builds valid JSON for metadata requests with special characters", func() {
|
||||
artist := `Guns N' Roses`
|
||||
title := `Sweet Child O' Mine`
|
||||
album := `Appetite for Destruction`
|
||||
payload := fmt.Sprintf(`[{"artist_name":%q,"track_name":%q,"release_name":%q}]`, artist, title, album)
|
||||
var parsed []map[string]string
|
||||
Expect(json.Unmarshal([]byte(payload), &parsed)).To(Succeed())
|
||||
Expect(parsed[0]["artist_name"]).To(Equal(artist))
|
||||
Expect(parsed[0]["track_name"]).To(Equal(title))
|
||||
Expect(parsed[0]["release_name"]).To(Equal(album))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("resolveSpotifyURL", func() {
|
||||
BeforeEach(func() {
|
||||
pdk.ResetMock()
|
||||
host.CacheMock.ExpectedCalls = nil
|
||||
host.CacheMock.Calls = nil
|
||||
pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe()
|
||||
})
|
||||
|
||||
It("returns cached URL on cache hit", func() {
|
||||
host.CacheMock.On("GetString", mock.Anything).Return("https://open.spotify.com/track/cached123", true, nil)
|
||||
|
||||
url := resolveSpotifyURL(scrobbler.TrackInfo{
|
||||
Title: "Karma Police",
|
||||
Artist: "Radiohead",
|
||||
Album: "OK Computer",
|
||||
})
|
||||
Expect(url).To(Equal("https://open.spotify.com/track/cached123"))
|
||||
})
|
||||
|
||||
It("resolves via MBID when available", func() {
|
||||
host.CacheMock.On("GetString", mock.Anything).Return("", false, nil)
|
||||
host.CacheMock.On("SetString", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
// Mock the MBID HTTP request
|
||||
mbidReq := &pdk.HTTPRequest{}
|
||||
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-mbid/json").Return(mbidReq)
|
||||
pdk.PDKMock.On("Send", mbidReq).Return(pdk.NewStubHTTPResponse(200, nil,
|
||||
[]byte(`[{"spotify_track_ids":["track123"]}]`)))
|
||||
|
||||
url := resolveSpotifyURL(scrobbler.TrackInfo{
|
||||
Title: "Karma Police",
|
||||
Artist: "Radiohead",
|
||||
Album: "OK Computer",
|
||||
MBZRecordingID: "mbid-123",
|
||||
})
|
||||
Expect(url).To(Equal("https://open.spotify.com/track/track123"))
|
||||
host.CacheMock.AssertCalled(GinkgoT(), "SetString", mock.Anything, "https://open.spotify.com/track/track123", spotifyCacheTTLHit)
|
||||
})
|
||||
|
||||
It("falls back to metadata lookup when MBID fails", func() {
|
||||
host.CacheMock.On("GetString", mock.Anything).Return("", false, nil)
|
||||
host.CacheMock.On("SetString", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
// MBID request fails
|
||||
mbidReq := &pdk.HTTPRequest{}
|
||||
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-mbid/json").Return(mbidReq)
|
||||
pdk.PDKMock.On("Send", mbidReq).Return(pdk.NewStubHTTPResponse(404, nil, []byte(`[]`)))
|
||||
|
||||
// Metadata request succeeds
|
||||
metaReq := &pdk.HTTPRequest{}
|
||||
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-metadata/json").Return(metaReq)
|
||||
pdk.PDKMock.On("Send", metaReq).Return(pdk.NewStubHTTPResponse(200, nil,
|
||||
[]byte(`[{"spotify_track_ids":["meta456"]}]`)))
|
||||
|
||||
url := resolveSpotifyURL(scrobbler.TrackInfo{
|
||||
Title: "Karma Police",
|
||||
Artist: "Radiohead",
|
||||
Album: "OK Computer",
|
||||
MBZRecordingID: "mbid-123",
|
||||
})
|
||||
Expect(url).To(Equal("https://open.spotify.com/track/meta456"))
|
||||
})
|
||||
|
||||
It("falls back to search URL when both lookups fail", func() {
|
||||
host.CacheMock.On("GetString", mock.Anything).Return("", false, nil)
|
||||
host.CacheMock.On("SetString", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
// No MBID, metadata request fails
|
||||
metaReq := &pdk.HTTPRequest{}
|
||||
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-metadata/json").Return(metaReq)
|
||||
pdk.PDKMock.On("Send", metaReq).Return(pdk.NewStubHTTPResponse(500, nil, []byte(`error`)))
|
||||
|
||||
url := resolveSpotifyURL(scrobbler.TrackInfo{
|
||||
Title: "Karma Police",
|
||||
Artist: "Radiohead",
|
||||
Album: "OK Computer",
|
||||
})
|
||||
Expect(url).To(HavePrefix("https://open.spotify.com/search/"))
|
||||
Expect(url).To(ContainSubstring("Radiohead"))
|
||||
host.CacheMock.AssertCalled(GinkgoT(), "SetString", mock.Anything, mock.Anything, spotifyCacheTTLMiss)
|
||||
})
|
||||
|
||||
It("uses Artists fallback when primary artist parse is empty", func() {
|
||||
host.CacheMock.On("GetString", mock.Anything).Return("", false, nil)
|
||||
host.CacheMock.On("SetString", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
metaReq := &pdk.HTTPRequest{}
|
||||
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-metadata/json").Return(metaReq)
|
||||
pdk.PDKMock.On("Send", metaReq).Return(pdk.NewStubHTTPResponse(200, nil,
|
||||
[]byte(`[{"spotify_track_ids":["fromArtists789"]}]`)))
|
||||
|
||||
url := resolveSpotifyURL(scrobbler.TrackInfo{
|
||||
Title: "Some Song",
|
||||
Artist: "",
|
||||
Album: "Some Album",
|
||||
Artists: []scrobbler.ArtistRef{{Name: "Fallback Artist"}},
|
||||
})
|
||||
Expect(url).To(Equal("https://open.spotify.com/track/fromArtists789"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user