package plugins
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"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"
wazeroCacheDir = ".wazero-cache"
)
var (
testdataDir string
tmpPluginsDir string
testManager *Manager
)
func TestPlugins(t *testing.T) {
tests.Init(t, false)
conf.Server.CacheFolder = conf.NewDir(filepath.Join(testDataDir, wazeroCacheDir))
conf.Server.Plugins.CacheSize = "1GB"
log.SetLevel(log.LevelFatal)
RegisterFailHandler(Fail)
RunSpecs(t, "Plugins Suite")
}
func stubLocalhostDNS() {
dialResolver = tests.StubResolver(map[string]string{"localhost.": "127.0.0.1"})
DeferCleanup(func() { dialResolver = nil })
}
func createTestManager(pluginConfig map[string]map[string]string) (*Manager, string) {
return createTestManagerWithPlugins(pluginConfig, "test-metadata-agent"+PackageExtension)
}
func createTestManagerWithPlugins(pluginConfig map[string]map[string]string, plugins ...string) (*Manager, string) {
return createTestManagerWithPluginsAndMetrics(pluginConfig, noopMetricsRecorder{}, plugins...)
}
func installTestPlugins(dir string, plugins ...string) model.Plugins {
var rows model.Plugins
for _, plugin := range plugins {
data, err := os.ReadFile(filepath.Join(testdataDir, plugin))
Expect(err).ToNot(HaveOccurred())
destPath := filepath.Join(dir, plugin)
Expect(os.WriteFile(destPath, data, 0600)).To(Succeed())
hash := sha256.Sum256(data)
rows = append(rows, model.Plugin{
ID: strings.TrimSuffix(plugin, PackageExtension),
Path: destPath,
SHA256: hex.EncodeToString(hash[:]),
Enabled: true,
})
}
return rows
}
func createTestManagerWithPluginsAndMetrics(pluginConfig map[string]map[string]string, metrics PluginMetricsRecorder, plugins ...string) (*Manager, string) {
tmpDir, err := os.MkdirTemp("", "plugins-test-*")
Expect(err).ToNot(HaveOccurred())
enabledPlugins := installTestPlugins(tmpDir, plugins...)
for i, p := range enabledPlugins {
enabledPlugins[i].AllUsers = true
if pluginConfig[p.ID] != nil {
configBytes, err := json.Marshal(pluginConfig[p.ID])
Expect(err).ToNot(HaveOccurred())
enabledPlugins[i].Config = string(configBytes)
}
}
DeferCleanup(configtest.SetupConfig())
conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = conf.NewDir(tmpDir)
conf.Server.Plugins.AutoReload = false
mockPluginRepo := tests.CreateMockPluginRepo()
mockPluginRepo.Permitted = true
mockPluginRepo.SetData(enabledPlugins)
dataStore := &tests.MockDataStore{MockedPlugin: mockPluginRepo}
manager := &Manager{
plugins: make(map[string]*plugin),
ds: dataStore,
metrics: metrics,
subsonicRouter: http.NotFoundHandler(),
}
err = manager.Start(GinkgoT().Context())
Expect(err).ToNot(HaveOccurred())
DeferCleanup(func() {
_ = manager.Stop()
_ = os.RemoveAll(tmpDir)
})
return manager, tmpDir
}
var _ = SynchronizedBeforeSuite(func() {
buildTestPlugins(testDataDir)
}, func() {
_, currentFile, _, ok := runtime.Caller(0)
Expect(ok).To(BeTrue())
testdataDir = filepath.Join(filepath.Dir(currentFile), "testdata")
testManager, tmpPluginsDir = createTestManager(nil)
})
var _ = AfterSuite(func() {
if testManager != nil {
_ = testManager.Stop()
}
if tmpPluginsDir != "" {
_ = os.RemoveAll(tmpPluginsDir)
}
})
type noopMetricsRecorder struct{}
func (noopMetricsRecorder) RecordPluginRequest(context.Context, string, string, bool, int64) {}