mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
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.
This commit is contained in:
parent
dedaf8e64a
commit
188584e3fb
138
plugins/playlist_generator_test.go
Normal file
138
plugins/playlist_generator_test.go
Normal file
@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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())
|
||||
|
||||
16
plugins/testdata/test-playlist-generator/go.mod
vendored
Normal file
16
plugins/testdata/test-playlist-generator/go.mod
vendored
Normal file
@ -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
|
||||
14
plugins/testdata/test-playlist-generator/go.sum
vendored
Normal file
14
plugins/testdata/test-playlist-generator/go.sum
vendored
Normal file
@ -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=
|
||||
71
plugins/testdata/test-playlist-generator/main.go
vendored
Normal file
71
plugins/testdata/test-playlist-generator/main.go
vendored
Normal file
@ -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() {}
|
||||
6
plugins/testdata/test-playlist-generator/manifest.json
vendored
Normal file
6
plugins/testdata/test-playlist-generator/manifest.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Test Playlist Generator",
|
||||
"author": "Navidrome Test",
|
||||
"version": "1.0.0",
|
||||
"description": "A test playlist generator plugin for integration testing"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user