navidrome/core/extdata/provider_topsongs_test.go
Deluan e03cc1abeb UpdateAlbumInfo tests - wip
Signed-off-by: Deluan <deluan@navidrome.org>
2025-03-30 15:21:23 -04:00

251 lines
7.7 KiB
Go

package extdata
import (
"context"
"errors"
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core/agents"
_ "github.com/navidrome/navidrome/core/agents/lastfm"
_ "github.com/navidrome/navidrome/core/agents/listenbrainz"
_ "github.com/navidrome/navidrome/core/agents/spotify"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
)
var _ = Describe("Provider - TopSongs", func() {
var ds model.DataStore
var provider Provider
var artistRepo *mockArtistRepo
var mediaFileRepo *mockMediaFileRepo
var mockTopSongsAgent *mockArtistTopSongsAgent
var agentsCombined Agents
var ctx context.Context
var originalAgentsConfig string
BeforeEach(func() {
ctx = context.Background()
originalAgentsConfig = conf.Server.Agents
artistRepo = newMockArtistRepo()
mediaFileRepo = newMockMediaFileRepo()
ds = &tests.MockDataStore{
MockedArtist: artistRepo,
MockedMediaFile: mediaFileRepo,
}
mockTopSongsAgent = &mockArtistTopSongsAgent{}
agentsCombined = &mockAgents{
topSongsAgent: mockTopSongsAgent,
similarAgent: nil,
}
provider = NewProvider(ds, agentsCombined)
})
AfterEach(func() {
conf.Server.Agents = originalAgentsConfig
})
Describe("TopSongs", func() {
BeforeEach(func() {
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
artist2 := model.Artist{ID: "artist-2", Name: "Artist Two"}
artistRepo.SetData(model.Artists{artist1, artist2})
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzReleaseTrackID: "mbid-1"}
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1", MbzReleaseTrackID: "mbid-2"}
song3 := model.MediaFile{ID: "song-3", Title: "Song Three", ArtistID: "artist-2", MbzReleaseTrackID: "mbid-3"}
mediaFileRepo.SetData(model.MediaFiles{song1, song2, song3})
mockTopSongsAgent.SetTopSongs([]agents.Song{
{Name: "Song One", MBID: "mbid-1"},
{Name: "Song Two", MBID: "mbid-2"},
})
})
It("returns top songs for a known artist", func() {
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
artistRepo.FindByName("Artist One", artist1)
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzReleaseTrackID: "mbid-1"}
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1", MbzReleaseTrackID: "mbid-2"}
mediaFileRepo.FindByMBID("mbid-1", song1)
mediaFileRepo.FindByMBID("mbid-2", song2)
songs, err := provider.TopSongs(ctx, "Artist One", 2)
Expect(err).ToNot(HaveOccurred())
Expect(songs).To(HaveLen(2))
Expect(songs[0].ID).To(Equal("song-1"))
Expect(songs[1].ID).To(Equal("song-2"))
})
It("returns nil for an unknown artist", func() {
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
if opt.Max != 1 || opt.Filters == nil {
return false
}
_, ok := opt.Filters.(squirrel.Like)
return ok
})).Return(model.Artists{}, model.ErrNotFound).Once()
songs, err := provider.TopSongs(ctx, "Unknown Artist", 5)
Expect(err).ToNot(HaveOccurred())
Expect(songs).To(BeNil())
})
It("returns nil when the agent returns an error", func() {
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
artistRepo.FindByName("Artist One", artist1)
mockTopSongsAgent.SetError(errors.New("agent error"))
song1 := model.MediaFile{ID: "song-1"}
song2 := model.MediaFile{ID: "song-2"}
mediaFileRepo.FindByMBID("mbid-1", song1)
mediaFileRepo.FindByMBID("mbid-2", song2)
songs, err := provider.TopSongs(ctx, "Artist One", 5)
Expect(err).To(MatchError("agent error"))
Expect(songs).To(BeNil())
})
It("returns nil when the agent returns ErrNotFound", func() {
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
artistRepo.FindByName("Artist One", artist1)
mockTopSongsAgent.SetError(agents.ErrNotFound)
song1 := model.MediaFile{ID: "song-1"}
song2 := model.MediaFile{ID: "song-2"}
mediaFileRepo.FindByMBID("mbid-1", song1)
mediaFileRepo.FindByMBID("mbid-2", song2)
songs, err := provider.TopSongs(ctx, "Artist One", 5)
Expect(err).To(MatchError(model.ErrNotFound))
Expect(songs).To(BeNil())
})
It("returns fewer songs if count is less than available top songs", func() {
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
artistRepo.FindByName("Artist One", artist1)
song1 := model.MediaFile{ID: "song-1"}
mediaFileRepo.FindByMBID("mbid-1", song1)
songs, err := provider.TopSongs(ctx, "Artist One", 1)
Expect(err).ToNot(HaveOccurred())
Expect(songs).To(HaveLen(1))
Expect(songs[0].ID).To(Equal("song-1"))
})
It("returns fewer songs if fewer matching tracks are found", func() {
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
artistRepo.FindByName("Artist One", artist1)
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzReleaseTrackID: "mbid-1"}
matcherForMBID := func(expectedMBID string) func(opt model.QueryOptions) bool {
return func(opt model.QueryOptions) bool {
if opt.Filters == nil {
return false
}
andClause, ok := opt.Filters.(squirrel.And)
if !ok {
return false
}
for _, condition := range andClause {
if eqClause, ok := condition.(squirrel.Eq); ok {
if mbid, exists := eqClause["mbz_recording_id"]; exists {
return mbid == expectedMBID
}
}
}
return false
}
}
matcherForTitleArtistFallback := func(artistID, title string) func(opt model.QueryOptions) bool {
return func(opt model.QueryOptions) bool {
if opt.Filters == nil {
return false
}
andClause, ok := opt.Filters.(squirrel.And)
if !ok || len(andClause) < 3 {
return false
}
foundLike := false
for _, condition := range andClause {
if likeClause, ok := condition.(squirrel.Like); ok {
if _, exists := likeClause["order_title"]; exists {
foundLike = true
break
}
}
}
return foundLike
}
}
mediaFileRepo.On("GetAll", mock.MatchedBy(matcherForMBID("mbid-1"))).Return(model.MediaFiles{song1}, nil).Once()
mediaFileRepo.On("GetAll", mock.MatchedBy(matcherForMBID("mbid-2"))).Return(model.MediaFiles{}, nil).Once()
mediaFileRepo.On("GetAll", mock.MatchedBy(matcherForTitleArtistFallback("artist-1", "Song Two"))).Return(model.MediaFiles{}, nil).Once()
songs, err := provider.TopSongs(ctx, "Artist One", 2)
Expect(err).ToNot(HaveOccurred())
Expect(songs).To(HaveLen(1))
Expect(songs[0].ID).To(Equal("song-1"))
})
It("returns nil when context is canceled", func() {
// This test case is not provided in the original file or the new code block
// It's assumed to exist as it's called in the new code block
})
})
})
// Mock implementation for ArtistTopSongsRetriever
// This remains here as it's specific to TopSongs tests and simpler than mockSimilarArtistAgent
type mockArtistTopSongsAgent struct {
mock.Mock
topSongs []agents.Song
err error
}
func (m *mockArtistTopSongsAgent) AgentName() string {
return "mockTopSongs"
}
func (m *mockArtistTopSongsAgent) SetTopSongs(songs []agents.Song) {
m.topSongs = songs
m.err = nil
}
func (m *mockArtistTopSongsAgent) SetError(err error) {
m.err = err
m.topSongs = nil
}
func (m *mockArtistTopSongsAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) {
if m.err != nil {
return nil, m.err
}
if len(m.topSongs) > count {
return m.topSongs[:count], nil
}
return m.topSongs, nil
}
var _ agents.ArtistTopSongsRetriever = (*mockArtistTopSongsAgent)(nil)