navidrome/plugins/scrobbler_adapter_test.go
2025-12-31 17:06:33 -05:00

199 lines
6.2 KiB
Go

//go:build !windows
package plugins
import (
"context"
"errors"
"time"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// ctxWithUser returns a fresh context with the test user.
// Must be called within each test, not in BeforeAll, because the context
// from BeforeAll gets cancelled before tests run.
func ctxWithUser() context.Context {
return request.WithUser(GinkgoT().Context(), model.User{ID: "user-1", UserName: "testuser"})
}
var _ = Describe("ScrobblerPlugin", Ordered, func() {
var (
scrobblerManager *Manager
s scrobbler.Scrobbler
)
BeforeAll(func() {
// Load the scrobbler via a new manager with the test-scrobbler plugin
scrobblerManager, _ = createTestManagerWithPlugins(nil, "test-scrobbler"+PackageExtension)
var ok bool
s, ok = scrobblerManager.LoadScrobbler("test-scrobbler")
Expect(ok).To(BeTrue())
})
Describe("LoadScrobbler", func() {
It("returns a scrobbler for a plugin with Scrobbler capability", func() {
Expect(s).ToNot(BeNil())
})
It("returns false for a plugin without Scrobbler capability", func() {
_, ok := testManager.LoadScrobbler("test-metadata-agent")
Expect(ok).To(BeFalse())
})
It("returns false for non-existent plugin", func() {
_, ok := scrobblerManager.LoadScrobbler("non-existent")
Expect(ok).To(BeFalse())
})
})
Describe("IsAuthorized", func() {
It("returns true when plugin is configured to authorize", func() {
result := s.IsAuthorized(ctxWithUser(), "user-1")
Expect(result).To(BeTrue())
})
It("returns false when plugin is configured to not authorize", func() {
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-scrobbler": {"authorized": "false"},
}, "test-scrobbler"+PackageExtension)
sc, ok := manager.LoadScrobbler("test-scrobbler")
Expect(ok).To(BeTrue())
result := sc.IsAuthorized(ctxWithUser(), "user-1")
Expect(result).To(BeFalse())
})
})
Describe("NowPlaying", func() {
It("successfully calls the plugin", func() {
track := &model.MediaFile{
ID: "track-1",
Title: "Test Song",
Album: "Test Album",
Artist: "Test Artist",
AlbumArtist: "Test Album Artist",
Duration: 180,
TrackNumber: 1,
DiscNumber: 1,
}
err := s.NowPlaying(ctxWithUser(), "user-1", track, 30)
Expect(err).ToNot(HaveOccurred())
})
It("returns error when plugin returns error", func() {
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-scrobbler": {"error": "service unavailable", "error_type": "scrobbler(retry_later)"},
}, "test-scrobbler"+PackageExtension)
sc, ok := manager.LoadScrobbler("test-scrobbler")
Expect(ok).To(BeTrue())
track := &model.MediaFile{ID: "track-1", Title: "Test Song"}
err := sc.NowPlaying(ctxWithUser(), "user-1", track, 30)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(scrobbler.ErrRetryLater))
})
})
Describe("Scrobble", func() {
It("successfully calls the plugin", func() {
sc := scrobbler.Scrobble{
MediaFile: model.MediaFile{
ID: "track-1",
Title: "Test Song",
Album: "Test Album",
Artist: "Test Artist",
AlbumArtist: "Test Album Artist",
Duration: 180,
TrackNumber: 1,
DiscNumber: 1,
},
TimeStamp: time.Now(),
}
err := s.Scrobble(ctxWithUser(), "user-1", sc)
Expect(err).ToNot(HaveOccurred())
})
It("returns error when plugin returns not_authorized error", func() {
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-scrobbler": {"error": "user not linked", "error_type": "scrobbler(not_authorized)"},
}, "test-scrobbler"+PackageExtension)
sc, ok := manager.LoadScrobbler("test-scrobbler")
Expect(ok).To(BeTrue())
scrobble := scrobbler.Scrobble{
MediaFile: model.MediaFile{ID: "track-1", Title: "Test Song"},
TimeStamp: time.Now(),
}
err := sc.Scrobble(ctxWithUser(), "user-1", scrobble)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(scrobbler.ErrNotAuthorized))
})
It("returns error when plugin returns unrecoverable error", func() {
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-scrobbler": {"error": "track rejected", "error_type": "scrobbler(unrecoverable)"},
}, "test-scrobbler"+PackageExtension)
sc, ok := manager.LoadScrobbler("test-scrobbler")
Expect(ok).To(BeTrue())
scrobble := scrobbler.Scrobble{
MediaFile: model.MediaFile{ID: "track-1", Title: "Test Song"},
TimeStamp: time.Now(),
}
err := sc.Scrobble(ctxWithUser(), "user-1", scrobble)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(scrobbler.ErrUnrecoverable))
})
})
Describe("PluginNames", func() {
It("returns plugin names with Scrobbler capability", func() {
names := scrobblerManager.PluginNames("Scrobbler")
Expect(names).To(ContainElement("test-scrobbler"))
})
It("does not return metadata agent plugins for Scrobbler capability", func() {
names := testManager.PluginNames("Scrobbler")
Expect(names).ToNot(ContainElement("test-metadata-agent"))
})
})
})
var _ = Describe("mapScrobblerError", func() {
It("returns nil for nil error", func() {
Expect(mapScrobblerError(nil)).ToNot(HaveOccurred())
})
It("returns ErrNotAuthorized for error containing 'not_authorized'", func() {
err := mapScrobblerError(errors.New("plugin error: scrobbler(not_authorized)"))
Expect(err).To(MatchError(scrobbler.ErrNotAuthorized))
})
It("returns ErrRetryLater for error containing 'retry_later'", func() {
err := mapScrobblerError(errors.New("temporary failure: scrobbler(retry_later)"))
Expect(err).To(MatchError(scrobbler.ErrRetryLater))
})
It("returns ErrUnrecoverable for error containing 'unrecoverable'", func() {
err := mapScrobblerError(errors.New("fatal error: scrobbler(unrecoverable)"))
Expect(err).To(MatchError(scrobbler.ErrUnrecoverable))
})
It("returns ErrUnrecoverable for unknown error", func() {
err := mapScrobblerError(errors.New("some unknown error"))
Expect(err).To(MatchError(scrobbler.ErrUnrecoverable))
})
})