mirror of
https://github.com/navidrome/navidrome.git
synced 2026-01-03 06:15:22 +00:00
197 lines
5.6 KiB
Go
197 lines
5.6 KiB
Go
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/navidrome/navidrome/core/agents"
|
|
"github.com/navidrome/navidrome/server/events"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Manager", Ordered, func() {
|
|
var ctx context.Context
|
|
|
|
BeforeAll(func() {
|
|
ctx = GinkgoT().Context()
|
|
})
|
|
|
|
Describe("Plugin Loading", func() {
|
|
It("loads enabled plugins from DB on Start", func() {
|
|
// Plugin is already loaded by testManager.Start() via loadEnabledPlugins
|
|
names := testManager.PluginNames(string(CapabilityMetadataAgent))
|
|
Expect(names).To(ContainElement("test-metadata-agent"))
|
|
})
|
|
})
|
|
|
|
Describe("unloadPlugin", func() {
|
|
It("removes a loaded plugin", func() {
|
|
// Plugin is already loaded from Start
|
|
err := testManager.unloadPlugin("test-metadata-agent")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
names := testManager.PluginNames(string(CapabilityMetadataAgent))
|
|
Expect(names).ToNot(ContainElement("test-metadata-agent"))
|
|
})
|
|
|
|
It("returns error when plugin not found", func() {
|
|
err := testManager.unloadPlugin("nonexistent")
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.Error()).To(ContainSubstring("not found"))
|
|
})
|
|
})
|
|
|
|
Describe("EnablePlugin", func() {
|
|
It("enables and loads a disabled plugin", func() {
|
|
// First disable the plugin (which also unloads it)
|
|
err := testManager.DisablePlugin(ctx, "test-metadata-agent")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(testManager.PluginNames(string(CapabilityMetadataAgent))).ToNot(ContainElement("test-metadata-agent"))
|
|
|
|
// Enable it
|
|
err = testManager.EnablePlugin(ctx, "test-metadata-agent")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
names := testManager.PluginNames(string(CapabilityMetadataAgent))
|
|
Expect(names).To(ContainElement("test-metadata-agent"))
|
|
})
|
|
})
|
|
|
|
Describe("DisablePlugin", func() {
|
|
It("disables and unloads an enabled plugin", func() {
|
|
// Ensure the plugin is loaded first
|
|
_ = testManager.EnablePlugin(ctx, "test-metadata-agent")
|
|
|
|
err := testManager.DisablePlugin(ctx, "test-metadata-agent")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
names := testManager.PluginNames(string(CapabilityMetadataAgent))
|
|
Expect(names).ToNot(ContainElement("test-metadata-agent"))
|
|
})
|
|
})
|
|
|
|
Describe("GetPluginInfo", func() {
|
|
BeforeEach(func() {
|
|
// Ensure plugin is loaded for this test
|
|
_ = testManager.EnablePlugin(ctx, "test-metadata-agent")
|
|
})
|
|
|
|
It("returns information about all loaded plugins", func() {
|
|
info := testManager.GetPluginInfo()
|
|
Expect(info).To(HaveKey("test-metadata-agent"))
|
|
Expect(info["test-metadata-agent"].Name).To(Equal("Test Plugin"))
|
|
Expect(info["test-metadata-agent"].Version).To(Equal("1.0.0"))
|
|
})
|
|
})
|
|
|
|
It("can call the plugin concurrently", func() {
|
|
// Ensure plugin is loaded
|
|
_ = testManager.EnablePlugin(ctx, "test-metadata-agent")
|
|
|
|
const concurrency = 30
|
|
errs := make(chan error, concurrency)
|
|
bios := make(chan string, concurrency)
|
|
|
|
g := sync.WaitGroup{}
|
|
g.Add(concurrency)
|
|
for i := range concurrency {
|
|
go func(i int) {
|
|
defer g.Done()
|
|
a, ok := testManager.LoadMediaAgent("test-metadata-agent")
|
|
Expect(ok).To(BeTrue())
|
|
agent := a.(agents.ArtistBiographyRetriever)
|
|
bio, err := agent.GetArtistBiography(ctx, fmt.Sprintf("artist-%d", i), fmt.Sprintf("Artist %d", i), "")
|
|
if err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
bios <- bio
|
|
}(i)
|
|
}
|
|
g.Wait()
|
|
|
|
// Collect results
|
|
for range concurrency {
|
|
select {
|
|
case err := <-errs:
|
|
Expect(err).ToNot(HaveOccurred())
|
|
case bio := <-bios:
|
|
Expect(bio).To(ContainSubstring("Biography for Artist"))
|
|
}
|
|
}
|
|
})
|
|
|
|
Describe("sendPluginRefreshEvent", func() {
|
|
var broker *testBroker
|
|
var manager *Manager
|
|
|
|
BeforeEach(func() {
|
|
broker = &testBroker{}
|
|
manager = &Manager{
|
|
broker: broker,
|
|
}
|
|
})
|
|
|
|
It("sends refresh event with single plugin ID", func() {
|
|
manager.sendPluginRefreshEvent(ctx, "test-plugin")
|
|
|
|
Expect(broker.broadcastCalled).To(BeTrue())
|
|
Expect(broker.lastEvent).ToNot(BeNil())
|
|
Expect(broker.lastEventCtx).To(Equal(ctx))
|
|
|
|
refreshEvent, ok := broker.lastEvent.(*events.RefreshResource)
|
|
Expect(ok).To(BeTrue(), "event should be a RefreshResource")
|
|
Expect(refreshEvent.Data(refreshEvent)).To(Equal(`{"plugin":["test-plugin"]}`))
|
|
})
|
|
|
|
It("sends refresh event with multiple plugin IDs", func() {
|
|
manager.sendPluginRefreshEvent(ctx, "plugin-1", "plugin-2", "plugin-3")
|
|
|
|
Expect(broker.broadcastCalled).To(BeTrue())
|
|
refreshEvent, ok := broker.lastEvent.(*events.RefreshResource)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(refreshEvent.Data(refreshEvent)).To(Equal(`{"plugin":["plugin-1","plugin-2","plugin-3"]}`))
|
|
})
|
|
|
|
It("sends refresh event with wildcard when using events.Any", func() {
|
|
manager.sendPluginRefreshEvent(ctx, events.Any)
|
|
|
|
Expect(broker.broadcastCalled).To(BeTrue())
|
|
refreshEvent, ok := broker.lastEvent.(*events.RefreshResource)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(refreshEvent.Data(refreshEvent)).To(Equal(`{"plugin":["*"]}`))
|
|
})
|
|
|
|
It("does not panic when broker is nil", func() {
|
|
manager.broker = nil
|
|
Expect(func() {
|
|
manager.sendPluginRefreshEvent(ctx, "test-plugin")
|
|
}).ToNot(Panic())
|
|
})
|
|
})
|
|
})
|
|
|
|
// testBroker is a simple mock implementation of events.Broker for testing
|
|
type testBroker struct {
|
|
lastEvent events.Event
|
|
lastEventCtx context.Context
|
|
broadcastCalled bool
|
|
}
|
|
|
|
func (m *testBroker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Not used in tests
|
|
}
|
|
|
|
func (m *testBroker) SendMessage(ctx context.Context, event events.Event) {
|
|
// Not used in tests
|
|
}
|
|
|
|
func (m *testBroker) SendBroadcastMessage(ctx context.Context, event events.Event) {
|
|
m.lastEvent = event
|
|
m.lastEventCtx = ctx
|
|
m.broadcastCalled = true
|
|
}
|