mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
313 lines
8.6 KiB
Go
313 lines
8.6 KiB
Go
//go:build !windows
|
|
|
|
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/tests"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("ConfigService", func() {
|
|
var service *configServiceImpl
|
|
var ctx context.Context
|
|
|
|
BeforeEach(func() {
|
|
ctx = context.Background()
|
|
})
|
|
|
|
Describe("newConfigService", func() {
|
|
It("creates service with provided config", func() {
|
|
config := map[string]string{"key1": "value1", "key2": "value2"}
|
|
service = newConfigService("test_plugin", config)
|
|
Expect(service.pluginName).To(Equal("test_plugin"))
|
|
Expect(service.config).To(Equal(config))
|
|
})
|
|
|
|
It("creates service with empty config when nil", func() {
|
|
service = newConfigService("test_plugin", nil)
|
|
Expect(service.config).ToNot(BeNil())
|
|
Expect(service.config).To(BeEmpty())
|
|
})
|
|
})
|
|
|
|
Describe("Get", func() {
|
|
BeforeEach(func() {
|
|
service = newConfigService("test_plugin", map[string]string{
|
|
"api_key": "secret123",
|
|
"debug_mode": "true",
|
|
"max_items": "100",
|
|
})
|
|
})
|
|
|
|
It("returns value for existing key", func() {
|
|
value, exists := service.Get(ctx, "api_key")
|
|
Expect(exists).To(BeTrue())
|
|
Expect(value).To(Equal("secret123"))
|
|
})
|
|
|
|
It("returns not exists for missing key", func() {
|
|
value, exists := service.Get(ctx, "missing_key")
|
|
Expect(exists).To(BeFalse())
|
|
Expect(value).To(Equal(""))
|
|
})
|
|
})
|
|
|
|
Describe("GetInt", func() {
|
|
BeforeEach(func() {
|
|
service = newConfigService("test_plugin", map[string]string{
|
|
"max_items": "100",
|
|
"timeout": "30",
|
|
"negative": "-50",
|
|
"not_a_number": "abc",
|
|
"float": "3.14",
|
|
})
|
|
})
|
|
|
|
It("returns integer for valid numeric value", func() {
|
|
value, exists := service.GetInt(ctx, "max_items")
|
|
Expect(exists).To(BeTrue())
|
|
Expect(value).To(Equal(int64(100)))
|
|
})
|
|
|
|
It("returns negative integer", func() {
|
|
value, exists := service.GetInt(ctx, "negative")
|
|
Expect(exists).To(BeTrue())
|
|
Expect(value).To(Equal(int64(-50)))
|
|
})
|
|
|
|
It("returns not exists for non-numeric value", func() {
|
|
value, exists := service.GetInt(ctx, "not_a_number")
|
|
Expect(exists).To(BeFalse())
|
|
Expect(value).To(Equal(int64(0)))
|
|
})
|
|
|
|
It("returns not exists for float value", func() {
|
|
value, exists := service.GetInt(ctx, "float")
|
|
Expect(exists).To(BeFalse())
|
|
Expect(value).To(Equal(int64(0)))
|
|
})
|
|
|
|
It("returns not exists for missing key", func() {
|
|
value, exists := service.GetInt(ctx, "missing_key")
|
|
Expect(exists).To(BeFalse())
|
|
Expect(value).To(Equal(int64(0)))
|
|
})
|
|
})
|
|
|
|
Describe("List", func() {
|
|
BeforeEach(func() {
|
|
service = newConfigService("test_plugin", map[string]string{
|
|
"zebra": "z",
|
|
"apple": "a",
|
|
"banana": "b",
|
|
"user_alice": "token1",
|
|
"user_bob": "token2",
|
|
"user_charlie": "token3",
|
|
})
|
|
})
|
|
|
|
It("returns all keys in sorted order when prefix is empty", func() {
|
|
keys := service.List(ctx, "")
|
|
Expect(keys).To(Equal([]string{"apple", "banana", "user_alice", "user_bob", "user_charlie", "zebra"}))
|
|
})
|
|
|
|
It("returns only keys matching prefix", func() {
|
|
keys := service.List(ctx, "user_")
|
|
Expect(keys).To(Equal([]string{"user_alice", "user_bob", "user_charlie"}))
|
|
})
|
|
|
|
It("returns empty slice when no keys match prefix", func() {
|
|
keys := service.List(ctx, "nonexistent_")
|
|
Expect(keys).To(BeEmpty())
|
|
})
|
|
|
|
It("returns empty slice for empty config", func() {
|
|
service = newConfigService("test_plugin", nil)
|
|
keys := service.List(ctx, "")
|
|
Expect(keys).To(BeEmpty())
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("ConfigService Integration", Ordered, func() {
|
|
var (
|
|
manager *Manager
|
|
tmpDir string
|
|
)
|
|
|
|
BeforeAll(func() {
|
|
var err error
|
|
tmpDir, err = os.MkdirTemp("", "config-test-*")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Copy the test-config plugin
|
|
srcPath := filepath.Join(testdataDir, "test-config"+PackageExtension)
|
|
destPath := filepath.Join(tmpDir, "test-config"+PackageExtension)
|
|
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[:])
|
|
|
|
// Setup config
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.Plugins.Enabled = true
|
|
conf.Server.Plugins.Folder = tmpDir
|
|
conf.Server.Plugins.AutoReload = false
|
|
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
|
|
|
|
// Setup mock DataStore with pre-enabled plugin and config
|
|
mockPluginRepo := tests.CreateMockPluginRepo()
|
|
mockPluginRepo.Permitted = true
|
|
mockPluginRepo.SetData(model.Plugins{{
|
|
ID: "test-config",
|
|
Path: destPath,
|
|
SHA256: hashHex,
|
|
Enabled: true,
|
|
Config: `{"api_key":"test_secret","max_retries":"5","timeout":"30"}`,
|
|
}})
|
|
dataStore := &tests.MockDataStore{MockedPlugin: mockPluginRepo}
|
|
|
|
// Create and start manager
|
|
manager = &Manager{
|
|
plugins: make(map[string]*plugin),
|
|
ds: dataStore,
|
|
subsonicRouter: http.NotFoundHandler(),
|
|
}
|
|
err = manager.Start(GinkgoT().Context())
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
DeferCleanup(func() {
|
|
_ = manager.Stop()
|
|
_ = os.RemoveAll(tmpDir)
|
|
})
|
|
})
|
|
|
|
Describe("Plugin Loading", func() {
|
|
It("should load plugin without config permission", func() {
|
|
manager.mu.RLock()
|
|
p, ok := manager.plugins["test-config"]
|
|
manager.mu.RUnlock()
|
|
Expect(ok).To(BeTrue())
|
|
// Config service doesn't require permission, so Permissions can be nil
|
|
// Just verify the plugin loaded
|
|
Expect(p.manifest.Name).To(Equal("Test Config Plugin"))
|
|
})
|
|
})
|
|
|
|
Describe("Config Operations via Plugin", func() {
|
|
type testConfigInput struct {
|
|
Operation string `json:"operation"`
|
|
Key string `json:"key,omitempty"`
|
|
Prefix string `json:"prefix,omitempty"`
|
|
}
|
|
type testConfigOutput struct {
|
|
StringVal string `json:"string_val,omitempty"`
|
|
IntVal int64 `json:"int_val,omitempty"`
|
|
Keys []string `json:"keys,omitempty"`
|
|
Exists bool `json:"exists,omitempty"`
|
|
Error *string `json:"error,omitempty"`
|
|
}
|
|
|
|
// Helper to call test plugin's exported function
|
|
callTestConfig := func(ctx context.Context, input testConfigInput) (*testConfigOutput, error) {
|
|
manager.mu.RLock()
|
|
p := manager.plugins["test-config"]
|
|
manager.mu.RUnlock()
|
|
|
|
instance, err := p.instance(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer instance.Close(ctx)
|
|
|
|
inputBytes, _ := json.Marshal(input)
|
|
_, outputBytes, err := instance.Call("nd_test_config", inputBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var output testConfigOutput
|
|
if err := json.Unmarshal(outputBytes, &output); err != nil {
|
|
return nil, err
|
|
}
|
|
if output.Error != nil {
|
|
return nil, errors.New(*output.Error)
|
|
}
|
|
return &output, nil
|
|
}
|
|
|
|
It("should get string value", func() {
|
|
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
|
Operation: "get",
|
|
Key: "api_key",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.StringVal).To(Equal("test_secret"))
|
|
Expect(output.Exists).To(BeTrue())
|
|
})
|
|
|
|
It("should return not exists for missing key", func() {
|
|
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
|
Operation: "get",
|
|
Key: "nonexistent",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Exists).To(BeFalse())
|
|
})
|
|
|
|
It("should get integer value", func() {
|
|
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
|
Operation: "get_int",
|
|
Key: "max_retries",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.IntVal).To(Equal(int64(5)))
|
|
Expect(output.Exists).To(BeTrue())
|
|
})
|
|
|
|
It("should return not exists for non-integer value", func() {
|
|
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
|
Operation: "get_int",
|
|
Key: "api_key", // This is a string, not an integer
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Exists).To(BeFalse())
|
|
})
|
|
|
|
It("should list all config keys with empty prefix", func() {
|
|
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
|
Operation: "list",
|
|
Prefix: "",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Keys).To(ConsistOf("api_key", "max_retries", "timeout"))
|
|
})
|
|
|
|
It("should list config keys with prefix filter", func() {
|
|
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
|
Operation: "list",
|
|
Prefix: "max",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Keys).To(ConsistOf("max_retries"))
|
|
})
|
|
})
|
|
})
|