From 188584e3fb6182c20664b8037d7c10c55ab56675 Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 5 Mar 2026 08:12:12 -0500 Subject: [PATCH] test(plugins): add PlaylistGenerator integration tests with test WASM plugin Add a test playlist generator plugin and integration tests for the PlaylistGenerator capability. The test plugin exercises GetPlaylists and GetPlaylist WASM functions, and the tests verify orchestration flow including capability detection, playlist discovery/sync, deterministic ID generation, error handling, and graceful stop. Also initialize playlistGenerators map in the test helper to prevent nil map panics when loading plugins with PlaylistGenerator capability. --- plugins/playlist_generator_test.go | 138 ++++++++++++++++++ plugins/plugins_suite_test.go | 9 +- .../testdata/test-playlist-generator/go.mod | 16 ++ .../testdata/test-playlist-generator/go.sum | 14 ++ .../testdata/test-playlist-generator/main.go | 71 +++++++++ .../test-playlist-generator/manifest.json | 6 + 6 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 plugins/playlist_generator_test.go create mode 100644 plugins/testdata/test-playlist-generator/go.mod create mode 100644 plugins/testdata/test-playlist-generator/go.sum create mode 100644 plugins/testdata/test-playlist-generator/main.go create mode 100644 plugins/testdata/test-playlist-generator/manifest.json 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" +}