mirror of
https://github.com/navidrome/navidrome.git
synced 2025-05-15 09:36:38 +03:00
518 lines
15 KiB
Go
518 lines
15 KiB
Go
package extdata
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"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/log"
|
|
"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", func() {
|
|
var ds model.DataStore
|
|
var provider Provider
|
|
var mockAgent *mockArtistTopSongsAgent
|
|
var mockAgents *mockAllAgents
|
|
var artistRepo *mockArtistRepo
|
|
var mediaFileRepo *mockMediaFileRepo
|
|
var ctx context.Context
|
|
var originalAgentsConfig string
|
|
|
|
BeforeEach(func() {
|
|
ctx = context.Background()
|
|
|
|
// Store the original agents config to restore it later
|
|
originalAgentsConfig = conf.Server.Agents
|
|
|
|
// Setup mocks
|
|
artistRepo = newMockArtistRepo()
|
|
mediaFileRepo = newMockMediaFileRepo()
|
|
|
|
ds = &tests.MockDataStore{
|
|
MockedArtist: artistRepo,
|
|
MockedMediaFile: mediaFileRepo,
|
|
}
|
|
|
|
// Clear the agents map to prevent interference from previous tests
|
|
agents.Map = nil
|
|
|
|
// Create a mock agent
|
|
mockAgent = &mockArtistTopSongsAgent{}
|
|
log.Debug(ctx, "Creating mock agent", "agent", mockAgent)
|
|
|
|
// Create a mock for the Agents interface that Provider depends on
|
|
mockAgents = newMockAllAgents()
|
|
mockAgents.topSongsRetriever = mockAgent
|
|
|
|
// Create the provider instance with our mock Agents implementation
|
|
provider = NewProvider(ds, mockAgents)
|
|
})
|
|
|
|
AfterEach(func() {
|
|
// Restore original config
|
|
conf.Server.Agents = originalAgentsConfig
|
|
})
|
|
|
|
Describe("TopSongs with direct agent injection", func() {
|
|
BeforeEach(func() {
|
|
// Set up test data
|
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
|
|
artist2 := model.Artist{ID: "artist-2", Name: "Artist Two"}
|
|
|
|
song1 := model.MediaFile{
|
|
ID: "song-1",
|
|
Title: "Song One",
|
|
Artist: "Artist One",
|
|
ArtistID: "artist-1",
|
|
AlbumArtistID: "artist-1",
|
|
MbzReleaseTrackID: "mbid-1",
|
|
Missing: false,
|
|
}
|
|
|
|
song2 := model.MediaFile{
|
|
ID: "song-2",
|
|
Title: "Song Two",
|
|
Artist: "Artist One",
|
|
ArtistID: "artist-1",
|
|
AlbumArtistID: "artist-1",
|
|
MbzReleaseTrackID: "mbid-2",
|
|
Missing: false,
|
|
}
|
|
|
|
song3 := model.MediaFile{
|
|
ID: "song-3",
|
|
Title: "Song Three",
|
|
Artist: "Artist Two",
|
|
ArtistID: "artist-2",
|
|
AlbumArtistID: "artist-2",
|
|
MbzReleaseTrackID: "mbid-3",
|
|
Missing: false,
|
|
}
|
|
|
|
// Set up basic data for the repos
|
|
artistRepo.SetData(model.Artists{artist1, artist2})
|
|
mediaFileRepo.SetData(model.MediaFiles{song1, song2, song3})
|
|
|
|
})
|
|
|
|
It("returns matching songs from the agent results", func() {
|
|
// Setup data needed for this specific test
|
|
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",
|
|
Missing: false,
|
|
}
|
|
|
|
song2 := model.MediaFile{
|
|
ID: "song-2",
|
|
Title: "Song Two",
|
|
ArtistID: "artist-1",
|
|
MbzReleaseTrackID: "mbid-2",
|
|
Missing: false,
|
|
}
|
|
|
|
mediaFileRepo.FindByMBID("mbid-1", song1)
|
|
mediaFileRepo.FindByMBID("mbid-2", song2)
|
|
|
|
// Configure the mockAgent to return some top songs
|
|
mockAgent.topSongs = []agents.Song{
|
|
{Name: "Song One", MBID: "mbid-1"},
|
|
{Name: "Song Two", MBID: "mbid-2"},
|
|
}
|
|
|
|
songs, err := provider.TopSongs(ctx, "Artist One", 5)
|
|
|
|
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"))
|
|
|
|
// Verify the agent was called with the right parameters
|
|
Expect(mockAgent.lastArtistID).To(Equal("artist-1"))
|
|
Expect(mockAgent.lastArtistName).To(Equal("Artist One"))
|
|
Expect(mockAgent.lastCount).To(Equal(5))
|
|
})
|
|
|
|
It("returns nil when artist is not found", func() {
|
|
// Clear any previous mock setup to avoid conflicts
|
|
artistRepo = newMockArtistRepo()
|
|
|
|
// Setup for artist not found scenario - return empty list
|
|
artistRepo.On("GetAll", mock.Anything).Return(model.Artists{}, nil).Once()
|
|
|
|
// We need to recreate the datastore with the new mocks
|
|
ds = &tests.MockDataStore{
|
|
MockedArtist: artistRepo,
|
|
MockedMediaFile: mediaFileRepo,
|
|
}
|
|
|
|
// Create a new provider with the updated datastore
|
|
provider = NewProvider(ds, mockAgents)
|
|
|
|
songs, err := provider.TopSongs(ctx, "Unknown Artist", 5)
|
|
|
|
Expect(err).To(BeNil())
|
|
Expect(songs).To(BeNil())
|
|
})
|
|
|
|
It("returns empty list when no matching songs are found", func() {
|
|
// Set up artist data
|
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
|
|
artistRepo.FindByName("Artist One", artist1)
|
|
|
|
// Configure the agent to return songs that don't match our repo
|
|
mockAgent.topSongs = []agents.Song{
|
|
{Name: "Nonexistent Song", MBID: "unknown-mbid"},
|
|
}
|
|
|
|
// Default to empty response for any queries
|
|
mediaFileRepo.On("GetAll", mock.Anything).Return(model.MediaFiles{}, nil).Maybe()
|
|
|
|
songs, err := provider.TopSongs(ctx, "Artist One", 5)
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(songs).To(HaveLen(0))
|
|
})
|
|
|
|
It("returns nil when agent returns errors", func() {
|
|
// Set up artist data
|
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
|
|
artistRepo.SetData(model.Artists{artist1})
|
|
artistRepo.FindByName("Artist One", artist1)
|
|
|
|
// Set the error
|
|
testError := errors.New("some agent error")
|
|
mockAgent.err = testError
|
|
|
|
songs, err := provider.TopSongs(ctx, "Artist One", 5)
|
|
|
|
// Current behavior returns nil for both error and songs
|
|
Expect(err).To(BeNil())
|
|
Expect(songs).To(BeNil())
|
|
})
|
|
|
|
It("respects count parameter", func() {
|
|
// Set up test data
|
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
|
|
song1 := model.MediaFile{
|
|
ID: "song-1",
|
|
Title: "Song One",
|
|
ArtistID: "artist-1",
|
|
MbzReleaseTrackID: "mbid-1",
|
|
Missing: false,
|
|
}
|
|
|
|
// Set up mocks
|
|
artistRepo.SetData(model.Artists{artist1})
|
|
artistRepo.FindByName("Artist One", artist1)
|
|
mediaFileRepo.FindByMBID("mbid-1", song1)
|
|
|
|
// Configure the mockAgent
|
|
mockAgent.topSongs = []agents.Song{
|
|
{Name: "Song One", MBID: "mbid-1"},
|
|
{Name: "Song Two", MBID: "mbid-2"},
|
|
{Name: "Song Three", MBID: "mbid-3"},
|
|
}
|
|
|
|
// Default to empty response for any queries
|
|
mediaFileRepo.On("GetAll", mock.Anything).Return(model.MediaFiles{}, nil).Maybe()
|
|
|
|
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"))
|
|
})
|
|
})
|
|
|
|
Describe("TopSongs with agent registration", func() {
|
|
BeforeEach(func() {
|
|
// Set our mock agent as the only agent
|
|
conf.Server.Agents = "mock"
|
|
|
|
// Set up test data
|
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
|
|
|
|
song1 := model.MediaFile{
|
|
ID: "song-1",
|
|
Title: "Song One",
|
|
Artist: "Artist One",
|
|
ArtistID: "artist-1",
|
|
MbzReleaseTrackID: "mbid-1",
|
|
Missing: false,
|
|
}
|
|
|
|
song2 := model.MediaFile{
|
|
ID: "song-2",
|
|
Title: "Song Two",
|
|
Artist: "Artist One",
|
|
ArtistID: "artist-1",
|
|
MbzReleaseTrackID: "mbid-2",
|
|
Missing: false,
|
|
}
|
|
|
|
// Set up basic data for the repos
|
|
artistRepo.SetData(model.Artists{artist1})
|
|
mediaFileRepo.SetData(model.MediaFiles{song1, song2})
|
|
|
|
// Set up the specific mock responses needed for the TopSongs method
|
|
artistRepo.FindByName("Artist One", artist1)
|
|
mediaFileRepo.FindByMBID("mbid-1", song1)
|
|
mediaFileRepo.FindByMBID("mbid-2", song2)
|
|
|
|
// Setup default behavior for empty searches
|
|
mediaFileRepo.On("GetAll", mock.Anything).Return(model.MediaFiles{}, nil).Maybe()
|
|
|
|
// Configure and register the agent
|
|
mockAgent.topSongs = []agents.Song{
|
|
{Name: "Song One", MBID: "mbid-1"},
|
|
{Name: "Song Two", MBID: "mbid-2"},
|
|
}
|
|
|
|
// Register our mock agent
|
|
agents.Register("mock", func(model.DataStore) agents.Interface { return mockAgent })
|
|
|
|
// Create the provider instance with registered agents
|
|
provider = NewProvider(ds, agents.GetAgents(ds))
|
|
})
|
|
|
|
It("returns matching songs from the registered agent", func() {
|
|
songs, err := provider.TopSongs(ctx, "Artist One", 5)
|
|
|
|
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"))
|
|
})
|
|
})
|
|
|
|
Describe("Error propagation from agents", func() {
|
|
BeforeEach(func() {
|
|
// Set up test data
|
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
|
|
|
|
// Set up basic data for the repos
|
|
artistRepo.SetData(model.Artists{artist1})
|
|
artistRepo.FindByName("Artist One", artist1)
|
|
|
|
// Setup default behavior for empty searches
|
|
mediaFileRepo.On("GetAll", mock.Anything).Return(model.MediaFiles{}, nil).Maybe()
|
|
|
|
// Create a mock with a custom GetArtistTopSongs implementation that returns an error
|
|
testError := errors.New("direct agent error")
|
|
mockAgent.getArtistTopSongsFn = func(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) {
|
|
return nil, testError
|
|
}
|
|
})
|
|
|
|
It("handles errors from the agent according to current behavior", func() {
|
|
songs, err := provider.TopSongs(ctx, "Artist One", 5)
|
|
|
|
// Current behavior returns nil for both error and songs
|
|
Expect(err).To(BeNil())
|
|
Expect(songs).To(BeNil())
|
|
})
|
|
})
|
|
})
|
|
|
|
// MockAllAgents implements the Agents interface that Provider depends on
|
|
type mockAllAgents struct {
|
|
mock.Mock
|
|
topSongsRetriever agents.ArtistTopSongsRetriever
|
|
}
|
|
|
|
func newMockAllAgents() *mockAllAgents {
|
|
return &mockAllAgents{}
|
|
}
|
|
|
|
func (m *mockAllAgents) AgentName() string {
|
|
return "mockAllAgents"
|
|
}
|
|
|
|
func (m *mockAllAgents) GetArtistMBID(ctx context.Context, id string, name string) (string, error) {
|
|
args := m.Called(ctx, id, name)
|
|
return args.String(0), args.Error(1)
|
|
}
|
|
|
|
func (m *mockAllAgents) GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) {
|
|
args := m.Called(ctx, id, name, mbid)
|
|
return args.String(0), args.Error(1)
|
|
}
|
|
|
|
func (m *mockAllAgents) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) {
|
|
args := m.Called(ctx, id, name, mbid)
|
|
return args.String(0), args.Error(1)
|
|
}
|
|
|
|
func (m *mockAllAgents) GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) {
|
|
args := m.Called(ctx, id, name, mbid, limit)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).([]agents.Artist), args.Error(1)
|
|
}
|
|
|
|
func (m *mockAllAgents) GetArtistImages(ctx context.Context, id, name, mbid string) ([]agents.ExternalImage, error) {
|
|
args := m.Called(ctx, id, name, mbid)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).([]agents.ExternalImage), args.Error(1)
|
|
}
|
|
|
|
func (m *mockAllAgents) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) {
|
|
// Delegate to the top songs retriever if it's set
|
|
if m.topSongsRetriever != nil {
|
|
return m.topSongsRetriever.GetArtistTopSongs(ctx, id, artistName, mbid, count)
|
|
}
|
|
|
|
args := m.Called(ctx, id, artistName, mbid, count)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).([]agents.Song), args.Error(1)
|
|
}
|
|
|
|
func (m *mockAllAgents) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*agents.AlbumInfo, error) {
|
|
args := m.Called(ctx, name, artist, mbid)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*agents.AlbumInfo), args.Error(1)
|
|
}
|
|
|
|
// Make sure mockAllAgents implements the Agents interface
|
|
var _ Agents = (*mockAllAgents)(nil)
|
|
|
|
// Mock agent implementation for testing
|
|
type mockArtistTopSongsAgent struct {
|
|
agents.Interface
|
|
err error
|
|
topSongs []agents.Song
|
|
lastArtistID string
|
|
lastArtistName string
|
|
lastMBID string
|
|
lastCount int
|
|
getArtistTopSongsFn func(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error)
|
|
}
|
|
|
|
func (m *mockArtistTopSongsAgent) AgentName() string {
|
|
return "mock"
|
|
}
|
|
|
|
func (m *mockArtistTopSongsAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) {
|
|
m.lastCount = count
|
|
m.lastArtistID = id
|
|
m.lastArtistName = artistName
|
|
m.lastMBID = mbid
|
|
|
|
log.Debug(ctx, "MockAgent.GetArtistTopSongs called", "id", id, "name", artistName, "mbid", mbid, "count", count)
|
|
|
|
// Use the custom function if available
|
|
if m.getArtistTopSongsFn != nil {
|
|
return m.getArtistTopSongsFn(ctx, id, artistName, mbid, count)
|
|
}
|
|
|
|
if m.err != nil {
|
|
log.Debug(ctx, "MockAgent.GetArtistTopSongs returning error", "err", m.err)
|
|
return nil, m.err
|
|
}
|
|
|
|
log.Debug(ctx, "MockAgent.GetArtistTopSongs returning songs", "count", len(m.topSongs))
|
|
return m.topSongs, nil
|
|
}
|
|
|
|
// Make sure the mock agent implements the necessary interface
|
|
var _ agents.ArtistTopSongsRetriever = (*mockArtistTopSongsAgent)(nil)
|
|
|
|
// Mocked ArtistRepo that uses testify's mock
|
|
type mockArtistRepo struct {
|
|
mock.Mock
|
|
model.ArtistRepository
|
|
}
|
|
|
|
func newMockArtistRepo() *mockArtistRepo {
|
|
return &mockArtistRepo{}
|
|
}
|
|
|
|
func (m *mockArtistRepo) SetData(artists model.Artists) {
|
|
// Store the data for Get queries
|
|
for _, a := range artists {
|
|
m.On("Get", a.ID).Return(&a, nil)
|
|
}
|
|
}
|
|
|
|
func (m *mockArtistRepo) SetError(hasError bool) {
|
|
if hasError {
|
|
m.On("GetAll", mock.Anything).Return(nil, errors.New("error"))
|
|
}
|
|
}
|
|
|
|
func (m *mockArtistRepo) FindByName(name string, artist model.Artist) {
|
|
// Set up a mock for finding an artist by name with LIKE filter, using Anything matcher for flexibility
|
|
m.On("GetAll", mock.Anything).Return(model.Artists{artist}, nil).Once()
|
|
}
|
|
|
|
func (m *mockArtistRepo) GetAll(options ...model.QueryOptions) (model.Artists, error) {
|
|
args := m.Called(mock.Anything)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(model.Artists), args.Error(1)
|
|
}
|
|
|
|
// Mocked MediaFileRepo that uses testify's mock
|
|
type mockMediaFileRepo struct {
|
|
mock.Mock
|
|
model.MediaFileRepository
|
|
}
|
|
|
|
func newMockMediaFileRepo() *mockMediaFileRepo {
|
|
return &mockMediaFileRepo{}
|
|
}
|
|
|
|
func (m *mockMediaFileRepo) SetData(mediaFiles model.MediaFiles) {
|
|
// Store the data for Get queries
|
|
for _, mf := range mediaFiles {
|
|
m.On("Get", mf.ID).Return(&mf, nil)
|
|
}
|
|
}
|
|
|
|
func (m *mockMediaFileRepo) SetError(hasError bool) {
|
|
if hasError {
|
|
m.On("GetAll", mock.Anything).Return(nil, errors.New("error"))
|
|
}
|
|
}
|
|
|
|
func (m *mockMediaFileRepo) FindByMBID(mbid string, mediaFile model.MediaFile) {
|
|
// Set up a mock for finding a media file by MBID using Anything matcher for flexibility
|
|
m.On("GetAll", mock.Anything).Return(model.MediaFiles{mediaFile}, nil).Once()
|
|
}
|
|
|
|
func (m *mockMediaFileRepo) FindByArtistAndTitle(artistID string, title string, mediaFile model.MediaFile) {
|
|
// Set up a mock for finding a media file by artist ID and title
|
|
m.On("GetAll", mock.Anything).Return(model.MediaFiles{mediaFile}, nil).Once()
|
|
}
|
|
|
|
func (m *mockMediaFileRepo) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
|
|
args := m.Called(mock.Anything)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(model.MediaFiles), args.Error(1)
|
|
}
|