diff --git a/plugins/playlist_generator_test.go b/plugins/playlist_generator_test.go new file mode 100644 index 000000000..5f474352b --- /dev/null +++ b/plugins/playlist_generator_test.go @@ -0,0 +1,138 @@ +//go:build !windows + +package plugins + +import ( + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/model/id" + "github.com/navidrome/navidrome/tests" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("PlaylistGenerator", Ordered, func() { + var ( + pgManager *Manager + mockPlsRepo *tests.MockPlaylistRepo + ) + + BeforeAll(func() { + pgManager, _ = createTestManagerWithPlugins(nil, + "test-playlist-generator"+PackageExtension, + ) + + // Pre-initialize the mock playlist repo to avoid a race with the + // discoverAndSync goroutine that is launched during Start(). + mockDS := pgManager.ds.(*tests.MockDataStore) + if mockDS.MockedPlaylist == nil { + mockDS.MockedPlaylist = tests.CreateMockPlaylistRepo() + } + mockPlsRepo = mockDS.MockedPlaylist.(*tests.MockPlaylistRepo) + }) + + Describe("capability detection", func() { + It("detects the PlaylistGenerator capability", func() { + names := pgManager.PluginNames(string(CapabilityPlaylistGenerator)) + Expect(names).To(ContainElement("test-playlist-generator")) + }) + }) + + Describe("startPlaylistGenerators", func() { + It("creates an orchestrator for the plugin", func() { + Expect(pgManager.playlistGenerators).To(HaveKey("test-playlist-generator")) + }) + + It("discovers and syncs playlists from the plugin", func() { + // The orchestrator runs discoverAndSync in a goroutine on Start(). + // Give it a moment to complete. + Eventually(func() int { + return len(mockPlsRepo.Data) + }).Should(BeNumerically(">=", 2)) + }) + + It("creates playlists with correct fields", func() { + // Check that playlists have the correct plugin fields + Eventually(func() bool { + for _, pls := range mockPlsRepo.Data { + if pls.PluginID == "test-playlist-generator" && pls.PluginPlaylistID == "daily-mix-1" { + return true + } + } + return false + }).Should(BeTrue()) + + // Find the daily-mix-1 playlist and verify its fields + var dailyMix1 *model.Playlist + for _, pls := range mockPlsRepo.Data { + if pls.PluginPlaylistID == "daily-mix-1" { + dailyMix1 = pls + break + } + } + Expect(dailyMix1).ToNot(BeNil()) + Expect(dailyMix1.Name).To(Equal("Daily Mix 1")) + Expect(dailyMix1.Comment).To(Equal("Your personalized daily mix")) + Expect(dailyMix1.ExternalImageURL).To(Equal("https://example.com/cover1.jpg")) + Expect(dailyMix1.OwnerID).To(Equal("user-1")) + Expect(dailyMix1.PluginID).To(Equal("test-playlist-generator")) + Expect(dailyMix1.PluginPlaylistID).To(Equal("daily-mix-1")) + Expect(dailyMix1.Public).To(BeFalse()) + }) + + It("generates deterministic playlist IDs", func() { + expectedID := id.NewHash("test-playlist-generator", "daily-mix-1", "user-1") + Eventually(func() bool { + _, exists := mockPlsRepo.Data[expectedID] + return exists + }).Should(BeTrue()) + }) + + It("creates distinct IDs for different playlists", func() { + id1 := id.NewHash("test-playlist-generator", "daily-mix-1", "user-1") + id2 := id.NewHash("test-playlist-generator", "daily-mix-2", "user-1") + Expect(id1).ToNot(Equal(id2)) + + Eventually(func() bool { + _, exists1 := mockPlsRepo.Data[id1] + _, exists2 := mockPlsRepo.Data[id2] + return exists1 && exists2 + }).Should(BeTrue()) + }) + }) + + Describe("GetPlaylists error handling", func() { + It("handles plugin errors gracefully", func() { + errManager, _ := createTestManagerWithPlugins(map[string]map[string]string{ + "test-playlist-generator": {"error": "service unavailable"}, + }, "test-playlist-generator"+PackageExtension) + + // Should still have the orchestrator (error is logged, not fatal) + Expect(errManager.playlistGenerators).To(HaveKey("test-playlist-generator")) + + // But no playlists created + errDS := errManager.ds.(*tests.MockDataStore) + if errDS.MockedPlaylist == nil { + errDS.MockedPlaylist = tests.CreateMockPlaylistRepo() + } + errPlsRepo := errDS.MockedPlaylist.(*tests.MockPlaylistRepo) + // The orchestrator was started but GetPlaylists returned error, + // so no playlists should be created + Consistently(func() int { + return len(errPlsRepo.Data) + }, "500ms").Should(Equal(0)) + }) + }) + + Describe("stop", func() { + It("stops the orchestrator when the manager stops", func() { + stopManager, _ := createTestManagerWithPlugins(nil, + "test-playlist-generator"+PackageExtension, + ) + Expect(stopManager.playlistGenerators).To(HaveKey("test-playlist-generator")) + + err := stopManager.Stop() + Expect(err).ToNot(HaveOccurred()) + Expect(stopManager.playlistGenerators).To(BeEmpty()) + }) + }) +}) diff --git a/plugins/plugins_suite_test.go b/plugins/plugins_suite_test.go index 1799ba3ce..62022fd95 100644 --- a/plugins/plugins_suite_test.go +++ b/plugins/plugins_suite_test.go @@ -137,10 +137,11 @@ func createTestManagerWithPluginsAndMetrics(pluginConfig map[string]map[string]s // Create and start manager manager := &Manager{ - plugins: make(map[string]*plugin), - ds: dataStore, - metrics: metrics, - subsonicRouter: http.NotFoundHandler(), // Stub router for tests + plugins: make(map[string]*plugin), + playlistGenerators: make(map[string]*playlistGeneratorOrchestrator), + ds: dataStore, + metrics: metrics, + subsonicRouter: http.NotFoundHandler(), // Stub router for tests } err = manager.Start(GinkgoT().Context()) Expect(err).ToNot(HaveOccurred()) diff --git a/plugins/testdata/test-playlist-generator/go.mod b/plugins/testdata/test-playlist-generator/go.mod new file mode 100644 index 000000000..7539672b5 --- /dev/null +++ b/plugins/testdata/test-playlist-generator/go.mod @@ -0,0 +1,16 @@ +module test-playlist-generator + +go 1.25 + +require github.com/navidrome/navidrome/plugins/pdk/go v0.0.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/extism/go-pdk v1.1.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.11.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/navidrome/navidrome/plugins/pdk/go => ../../pdk/go diff --git a/plugins/testdata/test-playlist-generator/go.sum b/plugins/testdata/test-playlist-generator/go.sum new file mode 100644 index 000000000..af880eb51 --- /dev/null +++ b/plugins/testdata/test-playlist-generator/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ= +github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/testdata/test-playlist-generator/main.go b/plugins/testdata/test-playlist-generator/main.go new file mode 100644 index 000000000..f4960a051 --- /dev/null +++ b/plugins/testdata/test-playlist-generator/main.go @@ -0,0 +1,71 @@ +// Test playlist generator plugin for Navidrome plugin system integration tests. +package main + +import ( + "fmt" + + "github.com/navidrome/navidrome/plugins/pdk/go/pdk" + pg "github.com/navidrome/navidrome/plugins/pdk/go/playlistgenerator" +) + +func init() { + pg.Register(&testPlaylistGenerator{}) +} + +type testPlaylistGenerator struct{} + +func (t *testPlaylistGenerator) GetPlaylists(_ pg.GetPlaylistsRequest) (pg.GetPlaylistsResponse, error) { + // Check for configured error + errMsg, hasErr := pdk.GetConfig("error") + if hasErr && errMsg != "" { + return pg.GetPlaylistsResponse{}, fmt.Errorf("%s", errMsg) + } + + // Get the owner user ID from config (defaults to "user-1") + ownerID := "user-1" + if id, ok := pdk.GetConfig("owner_id"); ok && id != "" { + ownerID = id + } + + return pg.GetPlaylistsResponse{ + Playlists: []pg.PlaylistInfo{ + {ID: "daily-mix-1", OwnerUserID: ownerID}, + {ID: "daily-mix-2", OwnerUserID: ownerID}, + }, + RefreshInterval: 0, // No re-discovery in tests + }, nil +} + +func (t *testPlaylistGenerator) GetPlaylist(req pg.GetPlaylistRequest) (pg.GetPlaylistResponse, error) { + // Check for configured error + errMsg, hasErr := pdk.GetConfig("get_playlist_error") + if hasErr && errMsg != "" { + return pg.GetPlaylistResponse{}, fmt.Errorf("%s", errMsg) + } + + switch req.ID { + case "daily-mix-1": + return pg.GetPlaylistResponse{ + Name: "Daily Mix 1", + Description: "Your personalized daily mix", + CoverArtURL: "https://example.com/cover1.jpg", + Tracks: []pg.SongRef{ + {Name: "Song A", Artist: "Artist One"}, + {Name: "Song B", Artist: "Artist Two"}, + }, + ValidUntil: 0, // Static, no refresh + }, nil + case "daily-mix-2": + return pg.GetPlaylistResponse{ + Name: "Daily Mix 2", + Tracks: []pg.SongRef{ + {Name: "Song C", Artist: "Artist Three"}, + }, + ValidUntil: 0, + }, nil + default: + return pg.GetPlaylistResponse{}, fmt.Errorf("unknown playlist: %s", req.ID) + } +} + +func main() {} diff --git a/plugins/testdata/test-playlist-generator/manifest.json b/plugins/testdata/test-playlist-generator/manifest.json new file mode 100644 index 000000000..6874d089f --- /dev/null +++ b/plugins/testdata/test-playlist-generator/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "Test Playlist Generator", + "author": "Navidrome Test", + "version": "1.0.0", + "description": "A test playlist generator plugin for integration testing" +}