mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-13 03:42:35 -06:00
3368497a96
The `test.WithTempFS` helper is used _extensively_ throughout our tests. Since `t.TempDir()` became a thing (Go 1.16), it probably shouldn't be, as that function does all the same things but in a more idiomatic manner. Main issues with `test.WithTempFS`: - It doesn't take a `*testing.T`, making failures reported without correct/helpful location. - It creates a new scope for no particular reason, where it could just have returned the root directory instead. An additional scope == an additionl level of indentation. This change adds the new `test.TempDir` and `test.TempDirOf` functions, which tries to address these issues. There are way too many places where `test.WithTempFS` is used for me to fix in a single PR, so more will have to come later. Most of the changes here don't even use the new functions, but replace the use of `test.WithTempFS` with `t.TempDir()` directly, as no files were passed to the function there. Also: - Replace a number of `reflect.DeepEqual` calls with better alternatives (not using reflection) Recommended reviewing with whitespace diffs hidden! Signed-off-by: Anders Eknert <anders.eknert@apple.com>
772 lines
19 KiB
Go
772 lines
19 KiB
Go
// Copyright 2020 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"maps"
|
|
"net/http"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
internal_tracing "github.com/open-policy-agent/opa/internal/distributedtracing"
|
|
"github.com/open-policy-agent/opa/internal/storage/mock"
|
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
"github.com/open-policy-agent/opa/v1/logging"
|
|
"github.com/open-policy-agent/opa/v1/logging/test"
|
|
"github.com/open-policy-agent/opa/v1/plugins/rest"
|
|
inmem "github.com/open-policy-agent/opa/v1/storage/inmem/test"
|
|
"github.com/open-policy-agent/opa/v1/topdown/cache"
|
|
prom "github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
func TestManagerCacheTriggers(t *testing.T) {
|
|
m, err := New([]byte{}, "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
l1Called := false
|
|
m.RegisterCacheTrigger(func(*cache.Config) {
|
|
l1Called = true
|
|
})
|
|
|
|
if m.registeredCacheTriggers[0] == nil {
|
|
t.Fatal("First listener failed to register")
|
|
}
|
|
|
|
l2Called := false
|
|
m.RegisterCacheTrigger(func(*cache.Config) {
|
|
l2Called = true
|
|
})
|
|
|
|
if m.registeredCacheTriggers[0] == nil || m.registeredCacheTriggers[1] == nil {
|
|
t.Fatal("Second listener failed to register")
|
|
}
|
|
|
|
if l1Called == true || l2Called == true {
|
|
t.Fatal("Listeners should not be called yet")
|
|
}
|
|
|
|
err = m.Reconfigure(m.GetConfig())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
if l1Called == false || l2Called == false {
|
|
t.Fatal("Listeners should hav been called")
|
|
}
|
|
}
|
|
|
|
func TestManagerNDCacheTriggers(t *testing.T) {
|
|
m, err := New([]byte{}, "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
l1Called := false
|
|
m.RegisterNDCacheTrigger(func(bool) {
|
|
l1Called = true
|
|
})
|
|
|
|
if m.registeredNDCacheTriggers[0] == nil {
|
|
t.Fatal("First listener failed to register")
|
|
}
|
|
|
|
l2Called := false
|
|
m.RegisterNDCacheTrigger(func(bool) {
|
|
l2Called = true
|
|
})
|
|
|
|
if m.registeredNDCacheTriggers[0] == nil || m.registeredNDCacheTriggers[1] == nil {
|
|
t.Fatal("Second listener failed to register")
|
|
}
|
|
|
|
if l1Called == true || l2Called == true {
|
|
t.Fatal("Listeners should not be called yet")
|
|
}
|
|
|
|
err = m.Reconfigure(m.GetConfig())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
if l1Called == false || l2Called == false {
|
|
t.Fatal("Listeners should hav been called")
|
|
}
|
|
}
|
|
|
|
func TestManagerPluginStatusListener(t *testing.T) {
|
|
m, err := New([]byte{}, "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
defer m.Stop(context.Background())
|
|
|
|
// Register two listeners
|
|
var l1Status map[string]*Status
|
|
m.RegisterPluginStatusListener("l1", func(status map[string]*Status) {
|
|
l1Status = status
|
|
})
|
|
|
|
var l2Status map[string]*Status
|
|
m.RegisterPluginStatusListener("l2", func(status map[string]*Status) {
|
|
l2Status = status
|
|
})
|
|
|
|
// Ensure starting statuses are empty by default
|
|
currentStatus := m.PluginStatus()
|
|
if len(currentStatus) != 0 {
|
|
t.Fatalf("Expected 0 statuses in current plugin status map, got: %+v", currentStatus)
|
|
}
|
|
|
|
// Push an update to a plugin, ensure current status is reflected and listeners were called
|
|
const message = "foo"
|
|
m.UpdatePluginStatus("p1", &Status{State: StateOK, Message: message})
|
|
currentStatus = m.PluginStatus()
|
|
if len(currentStatus) != 1 || currentStatus["p1"].State != StateOK || currentStatus["p1"].Message != message {
|
|
t.Fatalf("Expected 1 statuses in current plugin status map with state OK and message 'foo', got: %+v", currentStatus)
|
|
}
|
|
if !maps.EqualFunc(currentStatus, l1Status, (*Status).Equal) || !maps.EqualFunc(l1Status, l2Status, (*Status).Equal) {
|
|
t.Fatalf("Unexpected status in updates:\n\n\texpecting: %+v\n\n\tgot: l1: %+v l2: %+v\n", currentStatus, l1Status, l2Status)
|
|
}
|
|
|
|
// Unregister the first listener
|
|
m.UnregisterPluginStatusListener("l1")
|
|
l1Status = nil
|
|
|
|
// Send another update, ensure the status is ok and the remaining listener is still called
|
|
m.UpdatePluginStatus("p2", &Status{State: StateErr})
|
|
currentStatus = m.PluginStatus()
|
|
if len(currentStatus) != 2 || currentStatus["p1"].State != StateOK || currentStatus["p1"].Message != message || currentStatus["p2"].State != StateErr {
|
|
t.Fatalf("Unexpected current plugin status, got: %+v", currentStatus)
|
|
}
|
|
if !maps.EqualFunc(currentStatus, l2Status, (*Status).Equal) {
|
|
t.Fatalf("Unexpected status in updates:\n\n\texpecting: %+v\n\n\tgot: %+v\n", currentStatus, l2Status)
|
|
}
|
|
if l1Status != nil {
|
|
t.Fatalf("Expected unregistered listener l1 to not be called, got: %+v", l1Status)
|
|
}
|
|
|
|
// Unregister the last listener
|
|
m.UnregisterPluginStatusListener("l2")
|
|
l2Status = nil
|
|
|
|
// Ensure updates can still be sent with no listeners
|
|
m.UpdatePluginStatus("p2", &Status{State: StateOK})
|
|
currentStatus = m.PluginStatus()
|
|
if len(currentStatus) != 2 || currentStatus["p1"].State != StateOK || currentStatus["p1"].Message != message || currentStatus["p2"].State != StateOK {
|
|
t.Fatalf("Unexpected current plugin status, got: %+v", currentStatus)
|
|
}
|
|
if l2Status != nil {
|
|
t.Fatalf("Expected unregistered listener l2 to not be called, got: %+v", l2Status)
|
|
}
|
|
}
|
|
|
|
func TestPluginStatusUpdateOnStartAndStop(t *testing.T) {
|
|
m, err := New([]byte{}, "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
m.Register("p1", &testPlugin{m})
|
|
|
|
err = m.Start(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
m.Stop(t.Context())
|
|
}
|
|
|
|
type testPlugin struct {
|
|
m *Manager
|
|
}
|
|
|
|
func (p *testPlugin) Start(context.Context) error {
|
|
p.m.UpdatePluginStatus("p1", &Status{State: StateOK})
|
|
return nil
|
|
}
|
|
|
|
func (p *testPlugin) Stop(context.Context) {
|
|
p.m.UpdatePluginStatus("p1", &Status{State: StateNotReady})
|
|
}
|
|
|
|
func (p *testPlugin) Reconfigure(context.Context, any) {
|
|
p.m.UpdatePluginStatus("p1", &Status{State: StateNotReady})
|
|
}
|
|
|
|
func TestPluginManagerLazyInitBeforePluginStart(t *testing.T) {
|
|
m, err := New([]byte(`{"plugins": {"someplugin": {"enabled": true}}}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mock := &mockForInitStartOrdering{Manager: m}
|
|
|
|
m.Register("someplugin", mock)
|
|
|
|
if err := m.Start(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !mock.Started {
|
|
t.Fatal("expected plugin to be started")
|
|
}
|
|
}
|
|
|
|
func TestPluginManagerInitBeforePluginStart(t *testing.T) {
|
|
m, err := New([]byte(`{"plugins": {"someplugin": {}}}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := m.Init(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mock := &mockForInitStartOrdering{Manager: m}
|
|
|
|
m.Register("someplugin", mock)
|
|
|
|
if err := m.Start(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !mock.Started {
|
|
t.Fatal("expected plugin to be started")
|
|
}
|
|
}
|
|
|
|
func TestPluginManagerInitIdempotence(t *testing.T) {
|
|
mockStore := mock.New()
|
|
|
|
m, err := New([]byte(`{"plugins": {"someplugin": {}}}`), "test", mockStore)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx := t.Context()
|
|
|
|
if err := m.Init(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
exp := len(mockStore.Transactions)
|
|
|
|
if err := m.Init(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(mockStore.Transactions) != exp {
|
|
t.Fatal("expected num txns to be:", exp, "but got:", len(mockStore.Transactions))
|
|
}
|
|
}
|
|
|
|
func TestManagerWithCachingConfig(t *testing.T) {
|
|
m, err := New([]byte(`{"caching": {"inter_query_builtin_cache": {"max_size_bytes": 100}, "inter_query_builtin_value_cache": {"max_num_entries": 100}}}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected, _ := cache.ParseCachingConfig(nil)
|
|
limit := int64(100)
|
|
expected.InterQueryBuiltinCache.MaxSizeBytes = &limit
|
|
maxNumEntriesInterQueryValueCache := int(100)
|
|
expected.InterQueryBuiltinValueCache.MaxNumEntries = &maxNumEntriesInterQueryValueCache
|
|
|
|
if !reflect.DeepEqual(m.InterQueryBuiltinCacheConfig(), expected) {
|
|
t.Fatalf("want %+v got %+v", expected, m.interQueryBuiltinCacheConfig)
|
|
}
|
|
|
|
// config error
|
|
_, err = New([]byte(`{"caching": {"inter_query_builtin_cache": {"max_size_bytes": "100"}}}`), "test", inmem.New())
|
|
if err == nil {
|
|
t.Fatal("expected error but got nil")
|
|
}
|
|
|
|
// config error
|
|
_, err = New([]byte(`{"caching": {"inter_query_builtin_value_cache": {"max_num_entries": "100"}}}`), "test", inmem.New())
|
|
if err == nil {
|
|
t.Fatal("expected error but got nil")
|
|
}
|
|
}
|
|
|
|
func TestManagerWithNDCachingConfig(t *testing.T) {
|
|
m, err := New([]byte(`{"nd_builtin_cache": true}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := true
|
|
if cfg := m.GetConfig(); !cfg.NDBuiltinCache == expected {
|
|
t.Fatalf("want %+v got %+v", expected, cfg.NDBuiltinCache)
|
|
}
|
|
|
|
// config error
|
|
_, err = New([]byte(`{"nd_builtin_cache": "x"}`), "test", inmem.New())
|
|
if err == nil {
|
|
t.Fatal("expected error but got nil")
|
|
}
|
|
}
|
|
|
|
type mockForInitStartOrdering struct {
|
|
Manager *Manager
|
|
Started bool
|
|
}
|
|
|
|
func (m *mockForInitStartOrdering) Start(_ context.Context) error {
|
|
m.Started = true
|
|
if m.Manager.initialized {
|
|
return nil
|
|
}
|
|
return errors.New("expected manager to be initialized")
|
|
}
|
|
|
|
func (*mockForInitStartOrdering) Stop(context.Context) {}
|
|
func (*mockForInitStartOrdering) Reconfigure(context.Context, any) {}
|
|
|
|
func TestPluginManagerAuthPlugin(t *testing.T) {
|
|
m, err := New([]byte(`{"plugins": {"someplugin": {}}}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := m.Init(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mock := &myAuthPluginMock{}
|
|
|
|
m.Register("someplugin", mock)
|
|
|
|
authPlugin := m.AuthPlugin("someplugin")
|
|
|
|
if authPlugin == nil {
|
|
t.Fatal("expected to receive HTTPAuthPlugin")
|
|
}
|
|
|
|
switch authPlugin.(type) {
|
|
case *myAuthPluginMock:
|
|
return
|
|
default:
|
|
t.Fatal("expected HTTPAuthPlugin to be myAuthPluginMock")
|
|
}
|
|
}
|
|
|
|
func TestPluginManagerLogger(t *testing.T) {
|
|
logger := logging.Get().WithFields(map[string]any{"context": "myloggincontext"})
|
|
|
|
m, err := New([]byte(`{}`), "test", inmem.New(), Logger(logger))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if m.Logger() != logger {
|
|
t.Fatal("Logger was not configured on plugin manager")
|
|
}
|
|
}
|
|
|
|
func TestPluginManagerConsoleLogger(t *testing.T) {
|
|
consoleLogger := test.New()
|
|
|
|
mgr, err := New([]byte(`{}`), "", inmem.New(), ConsoleLogger(consoleLogger))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
const fieldKey = "foo"
|
|
const fieldValue = "bar"
|
|
mgr.ConsoleLogger().WithFields(map[string]any{fieldKey: fieldValue}).Info("Some message")
|
|
|
|
entries := consoleLogger.Entries()
|
|
|
|
exp := []test.LogEntry{
|
|
{
|
|
Level: logging.Info,
|
|
Fields: map[string]any{fieldKey: fieldValue},
|
|
Message: "Some message",
|
|
},
|
|
}
|
|
|
|
if !reflect.DeepEqual(exp, entries) {
|
|
t.Fatalf("want %v but got %v", exp, entries)
|
|
}
|
|
}
|
|
|
|
func TestPluginManagerPrometheusRegister(t *testing.T) {
|
|
register := prometheusRegisterMock{Collectors: map[prom.Collector]bool{}}
|
|
mgr, err := New([]byte(`{}`), "", inmem.New(), WithPrometheusRegister(register))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
counter := prom.NewCounter(prom.CounterOpts{})
|
|
if err := mgr.PrometheusRegister().Register(counter); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if register.Collectors[counter] != true {
|
|
t.Fatalf("Counter metric was not registered on prometheus")
|
|
}
|
|
}
|
|
|
|
func TestPluginManagerTracerProvider(t *testing.T) {
|
|
_, tracerProvider, _, err := internal_tracing.Init(t.Context(), []byte(`{ "distributed_tracing": { "type": "grpc" } }`), "test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, err := New([]byte(`{}`), "test", inmem.New(), WithTracerProvider(tracerProvider))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if m.TracerProvider() != tracerProvider {
|
|
t.Fatal("TracerProvider was not configured on plugin manager")
|
|
}
|
|
}
|
|
|
|
func TestPluginManagerServerInitialized(t *testing.T) {
|
|
// Verify that ServerInitializedChannel is closed when
|
|
// ServerInitialized is called.
|
|
m1, err := New([]byte{}, "test1", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
initChannel1 := m1.ServerInitializedChannel()
|
|
m1.ServerInitialized()
|
|
// Verify that ServerInitialized is idempotent and will not panic
|
|
m1.ServerInitialized()
|
|
select {
|
|
case <-initChannel1:
|
|
break
|
|
default:
|
|
t.Fatal("expected ServerInitializedChannel to be closed")
|
|
}
|
|
|
|
// Verify that ServerInitializedChannel is open when
|
|
// ServerInitialized is not called.
|
|
m2, err := New([]byte{}, "test2", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
initChannel2 := m2.ServerInitializedChannel()
|
|
select {
|
|
case <-initChannel2:
|
|
t.Fatal("expected ServerInitializedChannel to be open and have no messages")
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func TestUpdatePluginStatusAfterStop(t *testing.T) {
|
|
m, err := New([]byte{}, "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
m.Register("p1", &testPlugin{m})
|
|
|
|
if err := m.Start(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.Stop(t.Context())
|
|
|
|
// Must not hang.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
m.UpdatePluginStatus("p1", &Status{State: StateNotReady})
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal("UpdatePluginStatus hung after Stop")
|
|
}
|
|
}
|
|
|
|
func TestPluginStatusAfterStop(t *testing.T) {
|
|
m, err := New([]byte{}, "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
m.Register("p1", &testPlugin{m})
|
|
|
|
if err := m.Start(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.Stop(t.Context())
|
|
|
|
done := make(chan struct{})
|
|
var status map[string]*Status
|
|
go func() {
|
|
status = m.PluginStatus()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal("PluginStatus hung after Stop")
|
|
}
|
|
|
|
// testPlugin.Stop() reports StateNotReady
|
|
if s, ok := status["p1"]; !ok {
|
|
t.Fatal("expected status for p1")
|
|
} else if s.State != StateNotReady {
|
|
t.Fatalf("expected StateNotReady, got %v", s.State)
|
|
}
|
|
}
|
|
|
|
func TestRegisterAfterStop(t *testing.T) {
|
|
m, err := New([]byte{}, "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Start(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.Stop(t.Context())
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
m.Register("late", &testPlugin{m})
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal("Register hung after Stop")
|
|
}
|
|
}
|
|
|
|
func TestRegisterListenerAfterStop(t *testing.T) {
|
|
m, err := New([]byte{}, "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Start(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.Stop(t.Context())
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
m.RegisterPluginStatusListener("l1", func(map[string]*Status) {})
|
|
m.UnregisterPluginStatusListener("l1")
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal("RegisterPluginStatusListener hung after Stop")
|
|
}
|
|
}
|
|
|
|
type myAuthPluginMock struct{}
|
|
|
|
func (*myAuthPluginMock) NewClient(c rest.Config) (*http.Client, error) {
|
|
tlsConfig, err := rest.DefaultTLSConfig(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rest.DefaultRoundTripperClient(
|
|
tlsConfig,
|
|
10,
|
|
), nil
|
|
}
|
|
|
|
func (*myAuthPluginMock) Prepare(*http.Request) error {
|
|
return nil
|
|
}
|
|
|
|
func (*myAuthPluginMock) Start(context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (*myAuthPluginMock) Stop(context.Context) {
|
|
}
|
|
|
|
func (*myAuthPluginMock) Reconfigure(context.Context, any) {
|
|
}
|
|
|
|
type prometheusRegisterMock struct {
|
|
Collectors map[prom.Collector]bool
|
|
}
|
|
|
|
func (p prometheusRegisterMock) Register(collector prom.Collector) error {
|
|
p.Collectors[collector] = true
|
|
return nil
|
|
}
|
|
|
|
func (p prometheusRegisterMock) MustRegister(collector ...prom.Collector) {
|
|
for _, c := range collector {
|
|
p.Collectors[c] = true
|
|
}
|
|
}
|
|
|
|
func (p prometheusRegisterMock) Unregister(collector prom.Collector) bool {
|
|
delete(p.Collectors, collector)
|
|
return true
|
|
}
|
|
|
|
// mockExternalSource is a simple implementation for testing
|
|
type mockExternalSource struct {
|
|
refs []ast.Ref
|
|
rules []*ast.Rule
|
|
}
|
|
|
|
func (m *mockExternalSource) Refs() []ast.Ref {
|
|
return m.refs
|
|
}
|
|
|
|
func (m *mockExternalSource) Init(context.Context, ast.Ref) (ast.ExternalRuleIndex, error) {
|
|
return &mockExternalIndex{rules: m.rules}, nil
|
|
}
|
|
|
|
type mockExternalIndex struct {
|
|
rules []*ast.Rule
|
|
}
|
|
|
|
func (*mockExternalIndex) Opts() *ast.ExternalSourceOptions {
|
|
return nil
|
|
}
|
|
|
|
func (m *mockExternalIndex) Lookup(context.Context, ...ast.LookupOption) ([]*ast.Rule, ast.ExternalRuleIndex, error) {
|
|
return m.rules, nil, nil
|
|
}
|
|
|
|
// testExternalSourcePlugin registers an external source during construction
|
|
type testExternalSourcePlugin struct {
|
|
manager *Manager
|
|
started bool
|
|
}
|
|
|
|
func (p *testExternalSourcePlugin) Start(context.Context) error {
|
|
p.started = true
|
|
return nil
|
|
}
|
|
|
|
func (*testExternalSourcePlugin) Stop(context.Context) {}
|
|
|
|
func (*testExternalSourcePlugin) Reconfigure(context.Context, any) {}
|
|
|
|
// TestExternalSourceIntegration verifies external source behavior during plugin lifecycle
|
|
func TestExternalSourceIntegration(t *testing.T) {
|
|
t.Run("sources wired after plugin start", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
m, err := New([]byte(`{}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatalf("Failed to create manager: %v", err)
|
|
}
|
|
|
|
if err := m.Init(ctx); err != nil {
|
|
t.Fatalf("Failed to initialize manager: %v", err)
|
|
}
|
|
|
|
module := ast.MustParseModule(`package external.test
|
|
test_rule := true`)
|
|
pkgRef := ast.MustParseRef("data.external.test")
|
|
|
|
source := &mockExternalSource{
|
|
refs: []ast.Ref{pkgRef},
|
|
rules: module.Rules,
|
|
}
|
|
|
|
plugin := &testExternalSourcePlugin{manager: m}
|
|
m.Register("test_external_source", plugin)
|
|
m.RegisterExternalSource(pkgRef, source)
|
|
|
|
if m.GetExternalSources() == nil || m.GetExternalSources().Len() != 1 {
|
|
t.Fatalf("Expected 1 external source, got %d", m.GetExternalSources().Len())
|
|
}
|
|
|
|
if err := m.Start(ctx); err != nil {
|
|
t.Fatalf("Failed to start manager: %v", err)
|
|
}
|
|
|
|
if !plugin.started {
|
|
t.Fatal("Expected plugin to be started")
|
|
}
|
|
|
|
compiler := m.GetCompiler()
|
|
if compiler == nil || compiler.RuleTree == nil {
|
|
t.Fatal("Expected compiler with rule tree after Start()")
|
|
}
|
|
})
|
|
|
|
t.Run("stop cleans up external source plugins", func(t *testing.T) {
|
|
ctx := t.Context()
|
|
m, err := New([]byte(`{}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatalf("Failed to create manager: %v", err)
|
|
}
|
|
|
|
if err := m.Init(ctx); err != nil {
|
|
t.Fatalf("Failed to initialize manager: %v", err)
|
|
}
|
|
|
|
module := ast.MustParseModule(`package external.test
|
|
test_rule := true`)
|
|
pkgRef := ast.MustParseRef("data.external.test")
|
|
|
|
source := &mockExternalSource{
|
|
refs: []ast.Ref{pkgRef},
|
|
rules: module.Rules,
|
|
}
|
|
|
|
plugin := &testExternalSourcePlugin{manager: m}
|
|
m.Register("test_external_source", plugin)
|
|
m.RegisterExternalSource(pkgRef, source)
|
|
|
|
if err := m.Start(ctx); err != nil {
|
|
t.Fatalf("Failed to start manager: %v", err)
|
|
}
|
|
|
|
if !plugin.started {
|
|
t.Fatal("Expected plugin to be started")
|
|
}
|
|
|
|
m.Stop(ctx)
|
|
|
|
if m.GetExternalSources() == nil || m.GetExternalSources().Len() != 1 {
|
|
t.Fatalf("Expected external sources to still be registered after stop, got %d", m.GetExternalSources().Len())
|
|
}
|
|
})
|
|
|
|
t.Run("no recompilation when no sources registered", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
m, err := New([]byte(`{}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatalf("Failed to create manager: %v", err)
|
|
}
|
|
|
|
if err := m.Init(ctx); err != nil {
|
|
t.Fatalf("Failed to initialize manager: %v", err)
|
|
}
|
|
|
|
compilerBeforeStart := m.GetCompiler()
|
|
if compilerBeforeStart == nil {
|
|
t.Fatal("Expected compiler to be initialized after Init()")
|
|
}
|
|
|
|
plugin := &testPlugin{m: m}
|
|
m.Register("test_plugin", plugin)
|
|
|
|
if err := m.Start(ctx); err != nil {
|
|
t.Fatalf("Failed to start manager: %v", err)
|
|
}
|
|
|
|
if m.GetCompiler() != compilerBeforeStart {
|
|
t.Fatal("Expected compiler to remain the same when no external sources registered")
|
|
}
|
|
})
|
|
}
|