302 lines
9.5 KiB
Go
302 lines
9.5 KiB
Go
package pluginhost_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
|
|
pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/events"
|
|
"github.com/Silo-Server/silo-server/internal/pluginhost"
|
|
)
|
|
|
|
type fakeHub struct {
|
|
calls []events.Envelope
|
|
}
|
|
|
|
func (f *fakeHub) Publish(_ context.Context, env events.Envelope) error {
|
|
f.calls = append(f.calls, env)
|
|
return nil
|
|
}
|
|
|
|
type fakeLibLister struct{ libs []pluginhost.LibraryRecord }
|
|
|
|
func (f *fakeLibLister) ListLibraries(_ context.Context, _ string) ([]pluginhost.LibraryRecord, error) {
|
|
return f.libs, nil
|
|
}
|
|
|
|
func TestRuntimeHostServer_PublishEvent_AutoPrefixesAndPublishes(t *testing.T) {
|
|
hub := &fakeHub{}
|
|
srv := pluginhost.NewRuntimeHostServer(hub, &fakeLibLister{}, "silo.example")
|
|
|
|
payload, _ := structpb.NewStruct(map[string]any{"foo": "bar"})
|
|
_, err := srv.PublishEvent(context.Background(), &pluginv1.PublishEventRequest{
|
|
EventName: "approved",
|
|
Payload: payload,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PublishEvent: %v", err)
|
|
}
|
|
if len(hub.calls) != 1 {
|
|
t.Fatalf("hub calls = %d, want 1", len(hub.calls))
|
|
}
|
|
got := hub.calls[0]
|
|
if got.Channel != events.ChannelPlugins {
|
|
t.Errorf("channel = %q, want %q", got.Channel, events.ChannelPlugins)
|
|
}
|
|
if got.Event != "plugin.silo.example.approved" {
|
|
t.Errorf("event = %q, want %q", got.Event, "plugin.silo.example.approved")
|
|
}
|
|
}
|
|
|
|
func TestRuntimeHostServer_PublishEvent_RejectsEmptyName(t *testing.T) {
|
|
hub := &fakeHub{}
|
|
srv := pluginhost.NewRuntimeHostServer(hub, &fakeLibLister{}, "silo.example")
|
|
|
|
_, err := srv.PublishEvent(context.Background(), &pluginv1.PublishEventRequest{EventName: ""})
|
|
if err == nil {
|
|
t.Fatal("expected error for empty event name")
|
|
}
|
|
if len(hub.calls) != 0 {
|
|
t.Errorf("hub.calls = %d, want 0", len(hub.calls))
|
|
}
|
|
}
|
|
|
|
func TestRuntimeHostServer_PublishEventTo_AutoPrefixesAndTargets(t *testing.T) {
|
|
hub := &fakeHub{}
|
|
srv := pluginhost.NewRuntimeHostServer(hub, &fakeLibLister{}, "silo.example")
|
|
|
|
payload, _ := structpb.NewStruct(map[string]any{"foo": "bar"})
|
|
_, err := srv.PublishEventTo(context.Background(), &pluginv1.PublishEventToRequest{
|
|
TargetPluginId: "silo.requests",
|
|
EventName: "approved",
|
|
Payload: payload,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PublishEventTo: %v", err)
|
|
}
|
|
if len(hub.calls) != 1 {
|
|
t.Fatalf("hub calls = %d, want 1", len(hub.calls))
|
|
}
|
|
got := hub.calls[0]
|
|
if got.Event != "plugin.silo.example.approved" {
|
|
t.Errorf("event = %q, want plugin.silo.example.approved", got.Event)
|
|
}
|
|
if got.TargetPluginID != "silo.requests" {
|
|
t.Errorf("target_plugin_id = %q, want silo.requests", got.TargetPluginID)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeHostServer_PublishEventTo_RejectsEmptyTarget(t *testing.T) {
|
|
hub := &fakeHub{}
|
|
srv := pluginhost.NewRuntimeHostServer(hub, &fakeLibLister{}, "silo.example")
|
|
|
|
_, err := srv.PublishEventTo(context.Background(), &pluginv1.PublishEventToRequest{EventName: "approved"})
|
|
if err == nil {
|
|
t.Fatal("expected error for empty target_plugin_id")
|
|
}
|
|
if len(hub.calls) != 0 {
|
|
t.Errorf("hub.calls = %d, want 0", len(hub.calls))
|
|
}
|
|
}
|
|
|
|
func TestRuntimeHostServer_ListLibraries_PassesUserID(t *testing.T) {
|
|
libs := &fakeLibLister{libs: []pluginhost.LibraryRecord{
|
|
{ID: "lib-1", Name: "Movies", MediaType: "movie"},
|
|
{ID: "lib-2", Name: "Shows", MediaType: "tv"},
|
|
}}
|
|
srv := pluginhost.NewRuntimeHostServer(&fakeHub{}, libs, "silo.example")
|
|
|
|
resp, err := srv.ListLibraries(context.Background(), &pluginv1.ListLibrariesRequest{UserId: "u1"})
|
|
if err != nil {
|
|
t.Fatalf("ListLibraries: %v", err)
|
|
}
|
|
if len(resp.GetLibraries()) != 2 {
|
|
t.Errorf("got %d libraries, want 2", len(resp.GetLibraries()))
|
|
}
|
|
if resp.GetLibraries()[0].GetId() != "lib-1" {
|
|
t.Errorf("first lib id = %q", resp.GetLibraries()[0].GetId())
|
|
}
|
|
}
|
|
|
|
type fakeInstalledPluginLister struct {
|
|
rows []pluginhost.InstalledPluginRecord
|
|
}
|
|
|
|
func (f *fakeInstalledPluginLister) ListInstalledPlugins(context.Context) ([]pluginhost.InstalledPluginRecord, error) {
|
|
return f.rows, nil
|
|
}
|
|
|
|
func TestRuntimeHostServer_ListInstalledPlugins_ReturnsPlugins(t *testing.T) {
|
|
lister := &fakeInstalledPluginLister{rows: []pluginhost.InstalledPluginRecord{
|
|
{
|
|
InstallationID: 42,
|
|
PluginID: "silo.requests",
|
|
Version: "0.1.0",
|
|
Enabled: true,
|
|
Capabilities: []*pluginv1.CapabilityDescriptor{
|
|
{Type: "request_router.v1", Id: "default"},
|
|
},
|
|
},
|
|
}}
|
|
srv := pluginhost.NewRuntimeHostServerWithServices(
|
|
&fakeHub{}, &fakeLibLister{}, nil, lister, nil, "silo.example", 7,
|
|
)
|
|
|
|
resp, err := srv.ListInstalledPlugins(context.Background(), &pluginv1.ListInstalledPluginsRequest{})
|
|
if err != nil {
|
|
t.Fatalf("ListInstalledPlugins: %v", err)
|
|
}
|
|
if len(resp.GetPlugins()) != 1 {
|
|
t.Fatalf("plugins = %d, want 1", len(resp.GetPlugins()))
|
|
}
|
|
got := resp.GetPlugins()[0]
|
|
if got.GetInstallationId() != 42 || got.GetPluginId() != "silo.requests" || !got.GetEnabled() {
|
|
t.Errorf("plugin = %+v", got)
|
|
}
|
|
if got.GetCapabilities()[0].GetType() != "request_router.v1" {
|
|
t.Errorf("capability = %+v", got.GetCapabilities()[0])
|
|
}
|
|
}
|
|
|
|
type fakeConfigSetter struct {
|
|
installationID int
|
|
key string
|
|
value map[string]any
|
|
}
|
|
|
|
func (f *fakeConfigSetter) SetGlobalConfigEntry(_ context.Context, installationID int, key string, value map[string]any) error {
|
|
f.installationID = installationID
|
|
f.key = key
|
|
f.value = value
|
|
return nil
|
|
}
|
|
|
|
func TestRuntimeHostServer_SetGlobalConfigEntry_PassesInstallationKeyAndValue(t *testing.T) {
|
|
setter := &fakeConfigSetter{}
|
|
srv := pluginhost.NewRuntimeHostServerWithServices(
|
|
&fakeHub{}, &fakeLibLister{}, nil, nil, setter, "silo.example", 42,
|
|
)
|
|
value, _ := structpb.NewStruct(map[string]any{"baseUrl": "https://example.test"})
|
|
|
|
_, err := srv.SetGlobalConfigEntry(context.Background(), &pluginv1.SetGlobalConfigEntryRequest{
|
|
Key: "connection",
|
|
Value: value,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SetGlobalConfigEntry: %v", err)
|
|
}
|
|
if setter.installationID != 42 {
|
|
t.Errorf("installationID = %d, want 42", setter.installationID)
|
|
}
|
|
if setter.key != "connection" {
|
|
t.Errorf("key = %q, want connection", setter.key)
|
|
}
|
|
if got := setter.value["baseUrl"]; got != "https://example.test" {
|
|
t.Errorf("baseUrl = %#v, want https://example.test", got)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeHostServer_SetGlobalConfigEntry_RejectsEmptyKey(t *testing.T) {
|
|
setter := &fakeConfigSetter{}
|
|
srv := pluginhost.NewRuntimeHostServerWithServices(
|
|
&fakeHub{}, &fakeLibLister{}, nil, nil, setter, "silo.example", 42,
|
|
)
|
|
|
|
_, err := srv.SetGlobalConfigEntry(context.Background(), &pluginv1.SetGlobalConfigEntryRequest{})
|
|
if err == nil {
|
|
t.Fatal("expected error for empty key")
|
|
}
|
|
if setter.installationID != 0 {
|
|
t.Errorf("setter was called for empty key")
|
|
}
|
|
}
|
|
|
|
func TestRuntimeHostServer_PublishEvent_RateLimited(t *testing.T) {
|
|
hub := &fakeHub{}
|
|
srv := pluginhost.NewRuntimeHostServerWithRate(hub, &fakeLibLister{}, "silo.example", 2)
|
|
|
|
payload, _ := structpb.NewStruct(map[string]any{"i": float64(1)})
|
|
for i := 0; i < 5; i++ {
|
|
_, _ = srv.PublishEvent(context.Background(), &pluginv1.PublishEventRequest{
|
|
EventName: "ping",
|
|
Payload: payload,
|
|
})
|
|
}
|
|
// rate.NewLimiter(2, 2) → burst=2 + ~0 tokens replenished within
|
|
// microsecond test runtime → 2-3 calls succeed, rest are rate-limited.
|
|
if got := len(hub.calls); got > 3 {
|
|
t.Errorf("hub.calls = %d, expected at most 3 with rate=2", got)
|
|
}
|
|
if len(hub.calls) == 0 {
|
|
t.Errorf("expected at least the burst to succeed")
|
|
}
|
|
}
|
|
|
|
type fakeCatalog struct {
|
|
matches []pluginhost.LibraryPresenceRecord
|
|
gotIDs []string
|
|
gotType string
|
|
gotProv string
|
|
}
|
|
|
|
func (f *fakeCatalog) LookupByExternalIDs(_ context.Context, provider, mediaType string, ids []string) ([]pluginhost.LibraryPresenceRecord, error) {
|
|
f.gotProv = provider
|
|
f.gotType = mediaType
|
|
f.gotIDs = append(f.gotIDs, ids...)
|
|
return f.matches, nil
|
|
}
|
|
|
|
func TestRuntimeHostServer_CheckMediaPresence_Returns(t *testing.T) {
|
|
cat := &fakeCatalog{matches: []pluginhost.LibraryPresenceRecord{
|
|
{ExternalID: "603", MediaID: "m-1", LibraryID: "lib-1", Title: "The Matrix"},
|
|
}}
|
|
srv := pluginhost.NewRuntimeHostServerWithCatalog(&fakeHub{}, &fakeLibLister{}, cat, "silo.example")
|
|
|
|
resp, err := srv.CheckMediaPresence(context.Background(), &pluginv1.CheckMediaPresenceRequest{
|
|
Provider: "tmdb",
|
|
MediaType: "movie",
|
|
Ids: []string{"603", "550"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CheckMediaPresence: %v", err)
|
|
}
|
|
if len(resp.GetPresent()) != 1 {
|
|
t.Fatalf("got %d, want 1", len(resp.GetPresent()))
|
|
}
|
|
if resp.GetPresent()[0].GetExternalId() != "603" {
|
|
t.Errorf("got %+v", resp.GetPresent()[0])
|
|
}
|
|
if cat.gotProv != "tmdb" || cat.gotType != "movie" {
|
|
t.Errorf("delegate: prov=%q type=%q", cat.gotProv, cat.gotType)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeHostServer_CheckMediaPresence_RejectsTooMany(t *testing.T) {
|
|
srv := pluginhost.NewRuntimeHostServerWithCatalog(&fakeHub{}, &fakeLibLister{}, &fakeCatalog{}, "p")
|
|
ids := make([]string, 101)
|
|
for i := range ids {
|
|
ids[i] = "x"
|
|
}
|
|
_, err := srv.CheckMediaPresence(context.Background(), &pluginv1.CheckMediaPresenceRequest{
|
|
Provider: "tmdb",
|
|
MediaType: "movie",
|
|
Ids: ids,
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for >100 ids")
|
|
}
|
|
}
|
|
|
|
func TestRuntimeHostServer_CheckMediaPresence_NoCatalogReturnsEmpty(t *testing.T) {
|
|
srv := pluginhost.NewRuntimeHostServer(&fakeHub{}, &fakeLibLister{}, "p")
|
|
resp, err := srv.CheckMediaPresence(context.Background(), &pluginv1.CheckMediaPresenceRequest{
|
|
Provider: "tmdb", MediaType: "movie", Ids: []string{"1"},
|
|
})
|
|
if err != nil || len(resp.GetPresent()) != 0 {
|
|
t.Errorf("expected empty no-catalog response, got %+v err=%v", resp, err)
|
|
}
|
|
}
|