mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 10:27:08 +02:00
The dial-time SSRF guard runs on the resolved IP, so the tests that prove a symbolic hostname cannot reach loopback used "localhost." — a trailing dot never matches /etc/hosts, so Go queries real DNS. Machines whose resolver does not answer "localhost." (a VPN DNS, for example) got "no such host" before the dial guard ever ran, failing three specs. Add tests.StubResolver, a net.Resolver backed by an in-memory DNS responder over net.Pipe, and let the plugin dialers take a resolver so tests can inject it. Name resolution in those specs no longer depends on the machine's DNS.
163 lines
5.5 KiB
Go
163 lines
5.5 KiB
Go
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"
|
|
)
|
|
|
|
// 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)
|
|
|
|
// Set globally so tests using configtest.SetupConfig inherit it. The cache
|
|
// persists between runs; entries are content-addressed, so a stale one only misses.
|
|
conf.Server.CacheFolder = conf.NewDir(filepath.Join(testDataDir, wazeroCacheDir))
|
|
conf.Server.Plugins.CacheSize = "1GB" // the default evicts the cache mid-run
|
|
|
|
log.SetLevel(log.LevelFatal)
|
|
RegisterFailHandler(Fail)
|
|
RunSpecs(t, "Plugins Suite")
|
|
}
|
|
|
|
// stubLocalhostDNS resolves "localhost." without real DNS: the trailing dot never matches /etc/hosts.
|
|
func stubLocalhostDNS() {
|
|
dialResolver = tests.StubResolver(map[string]string{"localhost.": "127.0.0.1"})
|
|
DeferCleanup(func() { dialResolver = nil })
|
|
}
|
|
|
|
// 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...)
|
|
}
|
|
|
|
// installTestPlugins copies the given .ndp packages into dir and returns their
|
|
// enabled DB rows, so callers can grant whatever access the test needs.
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
// Create temp directory
|
|
tmpDir, err := os.MkdirTemp("", "plugins-test-*")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
enabledPlugins := installTestPlugins(tmpDir, plugins...)
|
|
for i, p := range enabledPlugins {
|
|
enabledPlugins[i].AllUsers = true // Allow all users by default in tests
|
|
if pluginConfig[p.ID] != nil {
|
|
configBytes, err := json.Marshal(pluginConfig[p.ID])
|
|
Expect(err).ToNot(HaveOccurred())
|
|
enabledPlugins[i].Config = string(configBytes)
|
|
}
|
|
}
|
|
|
|
// Setup config
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.Plugins.Enabled = true
|
|
conf.Server.Plugins.Folder = conf.NewDir(tmpDir)
|
|
conf.Server.Plugins.AutoReload = false
|
|
|
|
// Setup mock DataStore with pre-enabled plugins
|
|
mockPluginRepo := tests.CreateMockPluginRepo()
|
|
mockPluginRepo.Permitted = true
|
|
mockPluginRepo.SetData(enabledPlugins)
|
|
dataStore := &tests.MockDataStore{MockedPlugin: mockPluginRepo}
|
|
|
|
// Create and start manager
|
|
manager := &Manager{
|
|
plugins: make(map[string]*plugin),
|
|
ds: 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 _ = SynchronizedBeforeSuite(func() {
|
|
// Build once: the testdata Makefile is not safe to run concurrently.
|
|
buildTestPlugins(testDataDir)
|
|
}, 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) {}
|