Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
363 lines
14 KiB
Go
363 lines
14 KiB
Go
package external_test
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"net/url"
|
||
|
||
"github.com/navidrome/navidrome/conf"
|
||
"github.com/navidrome/navidrome/conf/configtest"
|
||
"github.com/navidrome/navidrome/core/agents"
|
||
. "github.com/navidrome/navidrome/core/external"
|
||
"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 - ArtistImage", func() {
|
||
var ds *tests.MockDataStore
|
||
var provider Provider
|
||
var mockArtistRepo *mockArtistRepo
|
||
var mockAlbumRepo *mockAlbumRepo
|
||
var mockMediaFileRepo *mockMediaFileRepo
|
||
var mockImageAgent *mockArtistImageAgent
|
||
var agentsCombined *mockAgents
|
||
var ctx context.Context
|
||
|
||
BeforeEach(func() {
|
||
DeferCleanup(configtest.SetupConfig())
|
||
conf.Server.Agents = "mockImage" // Configure only the mock agent
|
||
ctx = GinkgoT().Context()
|
||
|
||
mockArtistRepo = newMockArtistRepo()
|
||
mockAlbumRepo = newMockAlbumRepo()
|
||
mockMediaFileRepo = newMockMediaFileRepo()
|
||
|
||
ds = &tests.MockDataStore{
|
||
MockedArtist: mockArtistRepo,
|
||
MockedAlbum: mockAlbumRepo,
|
||
MockedMediaFile: mockMediaFileRepo,
|
||
}
|
||
|
||
mockImageAgent = newMockArtistImageAgent()
|
||
|
||
// Use the mockAgents from helper, setting the specific agent
|
||
agentsCombined = &mockAgents{
|
||
imageAgent: mockImageAgent,
|
||
}
|
||
|
||
provider = NewProvider(ds, agentsCombined)
|
||
|
||
// Default mocks for successful Get calls
|
||
mockArtistRepo.On("Get", "artist-1").Return(&model.Artist{ID: "artist-1", Name: "Artist One"}, nil).Maybe()
|
||
mockAlbumRepo.On("Get", "album-1").Return(&model.Album{ID: "album-1", Name: "Album One", AlbumArtistID: "artist-1"}, nil).Maybe()
|
||
mockMediaFileRepo.On("Get", "mf-1").Return(&model.MediaFile{ID: "mf-1", Title: "Track One", ArtistID: "artist-1"}, nil).Maybe()
|
||
// Default mock for non-existent entities
|
||
mockArtistRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Maybe()
|
||
mockAlbumRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Maybe()
|
||
mockMediaFileRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Maybe()
|
||
|
||
// Default successful image agent response
|
||
mockImageAgent.On("GetArtistImages", mock.Anything, "artist-1", "Artist One", "").
|
||
Return([]agents.ExternalImage{
|
||
{URL: "http://example.com/large.jpg", Size: 1000},
|
||
{URL: "http://example.com/medium.jpg", Size: 500},
|
||
{URL: "http://example.com/small.jpg", Size: 200},
|
||
}, nil).Maybe()
|
||
})
|
||
|
||
AfterEach(func() {
|
||
mockArtistRepo.AssertExpectations(GinkgoT())
|
||
mockAlbumRepo.AssertExpectations(GinkgoT())
|
||
mockMediaFileRepo.AssertExpectations(GinkgoT())
|
||
mockImageAgent.AssertExpectations(GinkgoT())
|
||
})
|
||
|
||
It("returns the largest image URL when successful", func() {
|
||
// Arrange
|
||
expectedURL, _ := url.Parse("http://example.com/large.jpg")
|
||
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||
|
||
// Assert
|
||
Expect(err).ToNot(HaveOccurred())
|
||
Expect(imgURL).To(Equal(expectedURL))
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||
})
|
||
|
||
It("returns ErrNotFound if the artist is not found in the DB", func() {
|
||
// Arrange
|
||
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "not-found")
|
||
|
||
// Assert
|
||
Expect(err).To(MatchError(model.ErrNotFound))
|
||
Expect(imgURL).To(BeNil())
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "not-found")
|
||
mockImageAgent.AssertNotCalled(GinkgoT(), "GetArtistImages", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||
})
|
||
|
||
It("returns the agent error if the agent fails", func() {
|
||
// Arrange
|
||
agentErr := errors.New("agent failure")
|
||
mockImageAgent.Mock = mock.Mock{} // Reset default expectation
|
||
mockImageAgent.On("GetArtistImages", ctx, "artist-1", "Artist One", "").Return(nil, agentErr).Once()
|
||
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||
|
||
// Assert
|
||
Expect(err).To(MatchError(model.ErrNotFound)) // Corrected Expectation: The provider maps agent errors (other than canceled) to ErrNotFound if no image was found/populated
|
||
Expect(imgURL).To(BeNil())
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||
})
|
||
|
||
It("returns ErrNotFound if the agent returns ErrNotFound", func() {
|
||
// Arrange
|
||
mockImageAgent.Mock = mock.Mock{} // Reset default expectation
|
||
mockImageAgent.On("GetArtistImages", ctx, "artist-1", "Artist One", "").Return(nil, agents.ErrNotFound).Once()
|
||
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||
|
||
// Assert
|
||
Expect(err).To(MatchError(model.ErrNotFound))
|
||
Expect(imgURL).To(BeNil())
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||
})
|
||
|
||
It("returns ErrNotFound if the agent returns no images", func() {
|
||
// Arrange
|
||
mockImageAgent.Mock = mock.Mock{} // Reset default expectation
|
||
mockImageAgent.On("GetArtistImages", ctx, "artist-1", "Artist One", "").Return([]agents.ExternalImage{}, nil).Once()
|
||
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||
|
||
// Assert
|
||
Expect(err).To(MatchError(model.ErrNotFound)) // Implementation maps empty result to ErrNotFound
|
||
Expect(imgURL).To(BeNil())
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||
})
|
||
|
||
It("returns context error if context is canceled before agent call", func() {
|
||
// Arrange
|
||
cctx, cancelCtx := context.WithCancel(context.Background())
|
||
mockArtistRepo.Mock = mock.Mock{} // Reset default expectation for artist repo as well
|
||
mockArtistRepo.On("Get", "artist-1").Return(&model.Artist{ID: "artist-1", Name: "Artist One"}, nil).Run(func(args mock.Arguments) {
|
||
cancelCtx() // Cancel context *during* the DB call simulation
|
||
}).Once()
|
||
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(cctx, "artist-1")
|
||
|
||
// Assert
|
||
Expect(err).To(MatchError(context.Canceled))
|
||
Expect(imgURL).To(BeNil())
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||
})
|
||
|
||
It("derives artist ID from MediaFile ID", func() {
|
||
// Arrange: Add mocks for the initial GetEntityByID lookups
|
||
mockArtistRepo.On("Get", "mf-1").Return(nil, model.ErrNotFound).Once()
|
||
mockAlbumRepo.On("Get", "mf-1").Return(nil, model.ErrNotFound).Once()
|
||
// Default mocks for MediaFileRepo.Get("mf-1") and ArtistRepo.Get("artist-1") handle the rest
|
||
expectedURL, _ := url.Parse("http://example.com/large.jpg")
|
||
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "mf-1")
|
||
|
||
// Assert
|
||
Expect(err).ToNot(HaveOccurred())
|
||
Expect(imgURL).To(Equal(expectedURL))
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "mf-1") // GetEntityByID sequence
|
||
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "mf-1") // GetEntityByID sequence
|
||
mockMediaFileRepo.AssertCalled(GinkgoT(), "Get", "mf-1")
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1") // Should be called after getting MF
|
||
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||
})
|
||
|
||
It("derives artist ID from Album ID", func() {
|
||
// Arrange: Add mock for the initial GetEntityByID lookup
|
||
mockArtistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once()
|
||
// Default mocks for AlbumRepo.Get("album-1") and ArtistRepo.Get("artist-1") handle the rest
|
||
expectedURL, _ := url.Parse("http://example.com/large.jpg")
|
||
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "album-1")
|
||
|
||
// Assert
|
||
Expect(err).ToNot(HaveOccurred())
|
||
Expect(imgURL).To(Equal(expectedURL))
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "album-1") // GetEntityByID sequence
|
||
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1") // Should be called after getting Album
|
||
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||
})
|
||
|
||
It("returns ErrNotFound if derived artist is not found", func() {
|
||
// Arrange
|
||
// Add mocks for the initial GetEntityByID lookups
|
||
mockArtistRepo.On("Get", "mf-bad-artist").Return(nil, model.ErrNotFound).Once()
|
||
mockAlbumRepo.On("Get", "mf-bad-artist").Return(nil, model.ErrNotFound).Once()
|
||
mockMediaFileRepo.On("Get", "mf-bad-artist").Return(&model.MediaFile{ID: "mf-bad-artist", ArtistID: "not-found"}, nil).Once()
|
||
// Add expectation for the recursive GetEntityByID call for the MediaFileRepo
|
||
mockMediaFileRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Maybe()
|
||
// The default mocks for ArtistRepo/AlbumRepo handle the final "not-found" lookups
|
||
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "mf-bad-artist")
|
||
|
||
// Assert
|
||
Expect(err).To(MatchError(model.ErrNotFound))
|
||
Expect(imgURL).To(BeNil())
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "mf-bad-artist") // GetEntityByID sequence
|
||
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "mf-bad-artist") // GetEntityByID sequence
|
||
mockMediaFileRepo.AssertCalled(GinkgoT(), "Get", "mf-bad-artist")
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "not-found")
|
||
mockImageAgent.AssertNotCalled(GinkgoT(), "GetArtistImages", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||
})
|
||
|
||
It("handles different image orders from agent", func() {
|
||
// Arrange
|
||
mockImageAgent.Mock = mock.Mock{} // Reset default expectation
|
||
mockImageAgent.On("GetArtistImages", ctx, "artist-1", "Artist One", "").
|
||
Return([]agents.ExternalImage{
|
||
{URL: "http://example.com/small.jpg", Size: 200},
|
||
{URL: "http://example.com/large.jpg", Size: 1000},
|
||
{URL: "http://example.com/medium.jpg", Size: 500},
|
||
}, nil).Once()
|
||
expectedURL, _ := url.Parse("http://example.com/large.jpg")
|
||
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||
|
||
// Assert
|
||
Expect(err).ToNot(HaveOccurred())
|
||
Expect(imgURL).To(Equal(expectedURL)) // Still picks the largest
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||
})
|
||
|
||
It("handles agent returning only one image", func() {
|
||
// Arrange
|
||
mockImageAgent.Mock = mock.Mock{} // Reset default expectation
|
||
mockImageAgent.On("GetArtistImages", ctx, "artist-1", "Artist One", "").
|
||
Return([]agents.ExternalImage{
|
||
{URL: "http://example.com/medium.jpg", Size: 500},
|
||
}, nil).Once()
|
||
expectedURL, _ := url.Parse("http://example.com/medium.jpg")
|
||
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||
|
||
// Assert
|
||
Expect(err).ToNot(HaveOccurred())
|
||
Expect(imgURL).To(Equal(expectedURL))
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||
})
|
||
|
||
Context("Unicode handling in artist names", func() {
|
||
var artistWithEnDash *model.Artist
|
||
var expectedURL *url.URL
|
||
|
||
const (
|
||
originalArtistName = "Run–D.M.C." // Artist name with en dash
|
||
normalizedArtistName = "Run-D.M.C." // Normalized version with hyphen
|
||
)
|
||
|
||
BeforeEach(func() {
|
||
// Test with en dash (–) in artist name like "Run–D.M.C."
|
||
artistWithEnDash = &model.Artist{ID: "artist-endash", Name: originalArtistName}
|
||
mockArtistRepo.Mock = mock.Mock{} // Reset default expectations
|
||
mockArtistRepo.On("Get", "artist-endash").Return(artistWithEnDash, nil).Once()
|
||
|
||
expectedURL, _ = url.Parse("http://example.com/rundmc.jpg")
|
||
|
||
// Mock the image agent to return an image for the artist
|
||
mockImageAgent.On("GetArtistImages", ctx, "artist-endash", mock.AnythingOfType("string"), "").
|
||
Return([]agents.ExternalImage{
|
||
{URL: "http://example.com/rundmc.jpg", Size: 1000},
|
||
}, nil).Once()
|
||
|
||
})
|
||
|
||
When("DevPreserveUnicodeInExternalCalls is true", func() {
|
||
BeforeEach(func() {
|
||
conf.Server.DevPreserveUnicodeInExternalCalls = true
|
||
})
|
||
It("preserves Unicode characters in artist names", func() {
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "artist-endash")
|
||
|
||
// Assert
|
||
Expect(err).ToNot(HaveOccurred())
|
||
Expect(imgURL).To(Equal(expectedURL))
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-endash")
|
||
// This is the key assertion: ensure the original Unicode name is used
|
||
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-endash", originalArtistName, "")
|
||
})
|
||
})
|
||
|
||
When("DevPreserveUnicodeInExternalCalls is false", func() {
|
||
BeforeEach(func() {
|
||
conf.Server.DevPreserveUnicodeInExternalCalls = false
|
||
})
|
||
|
||
It("normalizes Unicode characters", func() {
|
||
// Act
|
||
imgURL, err := provider.ArtistImage(ctx, "artist-endash")
|
||
|
||
// Assert
|
||
Expect(err).ToNot(HaveOccurred())
|
||
Expect(imgURL).To(Equal(expectedURL))
|
||
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-endash")
|
||
// This assertion ensures the normalized name is used (en dash → hyphen)
|
||
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-endash", normalizedArtistName, "")
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
// mockArtistImageAgent implementation using testify/mock
|
||
// This remains local as it's specific to testing the ArtistImage functionality
|
||
type mockArtistImageAgent struct {
|
||
mock.Mock
|
||
agents.ArtistImageRetriever // Embed interface
|
||
}
|
||
|
||
// Constructor for the mock agent
|
||
func newMockArtistImageAgent() *mockArtistImageAgent {
|
||
mock := new(mockArtistImageAgent)
|
||
// Set default AgentName if needed, although usually called via mockAgents
|
||
mock.On("AgentName").Return("mockImage").Maybe()
|
||
return mock
|
||
}
|
||
|
||
func (m *mockArtistImageAgent) AgentName() string {
|
||
args := m.Called()
|
||
return args.String(0)
|
||
}
|
||
|
||
func (m *mockArtistImageAgent) GetArtistImages(ctx context.Context, id, artistName, mbid string) ([]agents.ExternalImage, error) {
|
||
args := m.Called(ctx, id, artistName, mbid)
|
||
// Need careful type assertion for potentially nil slice
|
||
var res []agents.ExternalImage
|
||
if args.Get(0) != nil {
|
||
res = args.Get(0).([]agents.ExternalImage)
|
||
}
|
||
return res, args.Error(1)
|
||
}
|
||
|
||
// Ensure mockAgent implements the interface
|
||
var _ agents.ArtistImageRetriever = (*mockArtistImageAgent)(nil)
|