🏡 index : ironmagma/quintodrome.git

//go:build !windows

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
}