Files
silo-server/internal/pluginhost/runtime_host_server_test.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)
}
}