mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
PlaylistProvider capability now requires 'users' permission in the manifest (matching existing Scrobbler behavior) and validates that the resolved owner user ID is in the plugin's allowed users list before creating playlists.
210 lines
7.1 KiB
Go
210 lines
7.1 KiB
Go
//go:build !windows
|
|
|
|
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
|
"github.com/navidrome/navidrome/core/matcher"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/tests"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
const testDataDir = "plugins/testdata"
|
|
|
|
// Shared test state initialized in BeforeSuite
|
|
var (
|
|
testdataDir string // Path to testdata folder with test plugin .ndp packages
|
|
tmpPluginsDir string // Temp directory for plugin tests that modify files
|
|
testManager *Manager
|
|
)
|
|
|
|
func TestPlugins(t *testing.T) {
|
|
tests.Init(t, false)
|
|
buildTestPlugins(t, testDataDir)
|
|
|
|
// Create a shared wazero compilation cache directory.
|
|
// All test managers will point CacheFolder here so that WASM compilation
|
|
// is done once per binary and then reused from disk cache.
|
|
sharedCacheDir, err := os.MkdirTemp("", "plugins-shared-cache-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create shared cache dir: %v", err)
|
|
}
|
|
t.Cleanup(func() { os.RemoveAll(sharedCacheDir) })
|
|
|
|
// Set CacheFolder globally so all tests (including those using
|
|
// configtest.SetupConfig) inherit it without needing to set it manually.
|
|
conf.Server.CacheFolder = sharedCacheDir
|
|
|
|
log.SetLevel(log.LevelFatal)
|
|
RegisterFailHandler(Fail)
|
|
RunSpecs(t, "Plugins Suite")
|
|
}
|
|
|
|
func buildTestPlugins(t *testing.T, path string) {
|
|
t.Helper()
|
|
start := time.Now()
|
|
t.Logf("[BeforeSuite] Current working directory: %s", path)
|
|
cmd := exec.Command("make", "-C", path)
|
|
out, err := cmd.CombinedOutput()
|
|
t.Logf("[BeforeSuite] Make output: %s elapsed: %s", string(out), time.Since(start))
|
|
if err != nil {
|
|
t.Fatalf("Failed to build test plugins: %v", err)
|
|
}
|
|
}
|
|
|
|
// createTestManager creates a new plugin Manager with the given plugin config.
|
|
// It creates a temp directory, copies the test-metadata-agent plugin, and starts the manager.
|
|
// Returns the manager, temp directory path, and a cleanup function.
|
|
func createTestManager(pluginConfig map[string]map[string]string) (*Manager, string) {
|
|
return createTestManagerWithPlugins(pluginConfig, "test-metadata-agent"+PackageExtension)
|
|
}
|
|
|
|
// createTestManagerWithPlugins creates a new plugin Manager with the given plugin config
|
|
// and specified plugins. It creates a temp directory, copies the specified plugins, and starts the manager.
|
|
// Returns the manager and temp directory path.
|
|
func createTestManagerWithPlugins(pluginConfig map[string]map[string]string, plugins ...string) (*Manager, string) {
|
|
return createTestManagerWithPluginsAndMetrics(pluginConfig, noopMetricsRecorder{}, plugins...)
|
|
}
|
|
|
|
// pluginOverride allows tests to override model.Plugin fields for specific plugins.
|
|
type pluginOverride struct {
|
|
AllUsers bool
|
|
Users string // JSON array of user IDs, e.g. `["user-1"]`
|
|
}
|
|
|
|
// createTestManagerWithPluginOverrides creates a new plugin Manager with the given plugin config,
|
|
// per-plugin overrides, and specified plugins.
|
|
func createTestManagerWithPluginOverrides(pluginConfig map[string]map[string]string, overrides map[string]pluginOverride, plugins ...string) (*Manager, string) {
|
|
return createTestManagerFull(pluginConfig, overrides, noopMetricsRecorder{}, plugins...)
|
|
}
|
|
|
|
// createTestManagerWithPluginsAndMetrics creates a new plugin Manager with the given plugin config,
|
|
// metrics recorder, and specified plugins. It creates a temp directory, copies the specified plugins, and starts the manager.
|
|
// Returns the manager and temp directory path.
|
|
func createTestManagerWithPluginsAndMetrics(pluginConfig map[string]map[string]string, metrics PluginMetricsRecorder, plugins ...string) (*Manager, string) {
|
|
return createTestManagerFull(pluginConfig, nil, metrics, plugins...)
|
|
}
|
|
|
|
func createTestManagerFull(pluginConfig map[string]map[string]string, overrides map[string]pluginOverride, metrics PluginMetricsRecorder, plugins ...string) (*Manager, string) {
|
|
// Create temp directory
|
|
tmpDir, err := os.MkdirTemp("", "plugins-test-*")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Copy test plugins to temp dir and build plugin list with SHA256
|
|
var enabledPlugins model.Plugins
|
|
for _, plugin := range plugins {
|
|
srcPath := filepath.Join(testdataDir, plugin)
|
|
destPath := filepath.Join(tmpDir, plugin)
|
|
data, err := os.ReadFile(srcPath)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
err = os.WriteFile(destPath, data, 0600)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Compute SHA256 for the plugin
|
|
hash := sha256.Sum256(data)
|
|
hashHex := hex.EncodeToString(hash[:])
|
|
pluginName := plugin[:len(plugin)-len(PackageExtension)] // Remove .ndp extension
|
|
|
|
// Build config JSON if provided
|
|
configJSON := ""
|
|
if pluginConfig != nil && pluginConfig[pluginName] != nil {
|
|
// Encode config to JSON
|
|
configBytes, err := json.Marshal(pluginConfig[pluginName])
|
|
Expect(err).ToNot(HaveOccurred())
|
|
configJSON = string(configBytes)
|
|
}
|
|
|
|
p := model.Plugin{
|
|
ID: pluginName,
|
|
Path: destPath,
|
|
SHA256: hashHex,
|
|
Enabled: true,
|
|
Config: configJSON,
|
|
AllUsers: true, // Allow all users by default in tests
|
|
}
|
|
if overrides != nil {
|
|
if o, ok := overrides[pluginName]; ok {
|
|
p.AllUsers = o.AllUsers
|
|
p.Users = o.Users
|
|
}
|
|
}
|
|
enabledPlugins = append(enabledPlugins, p)
|
|
}
|
|
|
|
// Setup config
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.Plugins.Enabled = true
|
|
conf.Server.Plugins.Folder = tmpDir
|
|
conf.Server.Plugins.AutoReload = false
|
|
|
|
// Setup mock DataStore with pre-enabled plugins
|
|
mockPluginRepo := tests.CreateMockPluginRepo()
|
|
mockPluginRepo.Permitted = true
|
|
mockPluginRepo.SetData(enabledPlugins)
|
|
|
|
// Pre-seed a mock user repo with a default user so that
|
|
// PlaylistProvider's discoverAndSync can resolve usernames.
|
|
mockUserRepo := tests.CreateMockUserRepo()
|
|
_ = mockUserRepo.Put(&model.User{ID: "user-1", UserName: "admin"})
|
|
|
|
dataStore := &tests.MockDataStore{MockedPlugin: mockPluginRepo, MockedUser: mockUserRepo, MockedPlaylist: tests.CreateMockPlaylistRepo()}
|
|
|
|
// Create and start manager
|
|
manager := &Manager{
|
|
plugins: make(map[string]*plugin),
|
|
ds: dataStore,
|
|
matcher: matcher.New(dataStore),
|
|
metrics: metrics,
|
|
subsonicRouter: http.NotFoundHandler(), // Stub router for tests
|
|
}
|
|
err = manager.Start(GinkgoT().Context())
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
DeferCleanup(func() {
|
|
_ = manager.Stop()
|
|
_ = os.RemoveAll(tmpDir)
|
|
})
|
|
|
|
return manager, tmpDir
|
|
}
|
|
|
|
var _ = BeforeSuite(func() {
|
|
// Get testdata directory (where test plugin .ndp packages live)
|
|
_, currentFile, _, ok := runtime.Caller(0)
|
|
Expect(ok).To(BeTrue())
|
|
testdataDir = filepath.Join(filepath.Dir(currentFile), "testdata")
|
|
|
|
// Create shared manager for most tests
|
|
testManager, tmpPluginsDir = createTestManager(nil)
|
|
})
|
|
|
|
var _ = AfterSuite(func() {
|
|
if testManager != nil {
|
|
_ = testManager.Stop()
|
|
}
|
|
if tmpPluginsDir != "" {
|
|
_ = os.RemoveAll(tmpPluginsDir)
|
|
}
|
|
})
|
|
|
|
// noopMetricsRecorder is a no-op implementation of PluginMetricsRecorder for tests
|
|
type noopMetricsRecorder struct{}
|
|
|
|
func (noopMetricsRecorder) RecordPluginRequest(context.Context, string, string, bool, int64) {}
|