From 0425384b49283b0a4594eee9f520c0d7f52945d5 Mon Sep 17 00:00:00 2001 From: Jim Date: Tue, 7 Jun 2022 12:23:01 -0400 Subject: [PATCH] feature (events): add TestWithoutEventing(t) (#2137) --- internal/daemon/controller/testing.go | 34 +++++++++++- internal/daemon/controller/testing_test.go | 45 ++++++++++++++++ internal/observability/event/eventer.go | 8 +-- internal/observability/event/options.go | 1 + internal/observability/event/testing.go | 33 ++++++++++++ internal/observability/event/testing_test.go | 55 ++++++++++++++++++++ 6 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 internal/observability/event/testing_test.go diff --git a/internal/daemon/controller/testing.go b/internal/daemon/controller/testing.go index 39a3663fd1..e02d4dd2f2 100644 --- a/internal/daemon/controller/testing.go +++ b/internal/daemon/controller/testing.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net" "strconv" "strings" @@ -373,6 +374,11 @@ type TestControllerOpts struct { // If true, the controller will not be started DisableAutoStart bool + // DisableEventing, if true the test controller will not create events + // You must not run the test in parallel (no calls to t.Parallel) since the + // this option relies on modifying the system wide default eventer. + DisableEventing bool + // DisableAuthorizationFailures will still cause authz checks to be // performed but they won't cause 403 Forbidden. Useful for API-level // testing to avoid a lot of faff. @@ -543,8 +549,32 @@ func TestControllerConfig(t testing.TB, ctx context.Context, tc *TestController, } opts.Config.Controller.SchedulerRunJobInterval = opts.SchedulerRunJobInterval - if err := tc.b.SetupEventing(tc.b.Logger, tc.b.StderrLock, opts.Config.Controller.Name, base.WithEventerConfig(opts.Config.Eventing)); err != nil { - t.Fatal(err) + switch { + case opts.DisableEventing: + opts.Config.Eventing = &event.EventerConfig{ + AuditEnabled: false, + ObservationsEnabled: false, + SysEventsEnabled: false, + } + testLogger := hclog.New(&hclog.LoggerOptions{ + Mutex: tc.b.StderrLock, + Output: io.Discard, + }) + e, err := event.NewEventer( + testLogger, + tc.b.StderrLock, + opts.Config.Controller.Name, + *opts.Config.Eventing, + ) + if err != nil { + t.Fatal(err) + } + tc.b.Eventer = e + event.TestWithoutEventing(t) // this ensures the sys eventer will also stop eventing + default: + if err := tc.b.SetupEventing(tc.b.Logger, tc.b.StderrLock, opts.Config.Controller.Name, base.WithEventerConfig(opts.Config.Eventing)); err != nil { + t.Fatal(err) + } } // Initialize status grace period diff --git a/internal/daemon/controller/testing_test.go b/internal/daemon/controller/testing_test.go index e0f0104ed0..b699ac88ea 100644 --- a/internal/daemon/controller/testing_test.go +++ b/internal/daemon/controller/testing_test.go @@ -1,7 +1,12 @@ package controller import ( + "bytes" + "io" + "os" "testing" + + "github.com/stretchr/testify/assert" ) func Test_TestController(t *testing.T) { @@ -17,4 +22,44 @@ func Test_TestController(t *testing.T) { defer tc1.Shutdown() defer tc2.Shutdown() }) + t.Run("controller-without-eventing", func(t *testing.T) { + const op = "Test_TestWithoutEventing" + assert := assert.New(t) + + // this isn't the best solution for capturing stdout but it works for now... + captureFn := func(fn func()) string { + old := os.Stdout + defer func() { + os.Stderr = old + }() + + r, w, _ := os.Pipe() + os.Stderr = w + + { + fn() + } + + outC := make(chan string) + // copy the output in a separate goroutine so writing to stderr can't block indefinitely + go func() { + var buf bytes.Buffer + io.Copy(&buf, r) + outC <- buf.String() + }() + + // back to normal state + w.Close() + return <-outC + } + + assert.Empty(captureFn(func() { + tc := NewTestController(t, &TestControllerOpts{DisableEventing: true}) + defer tc.Shutdown() + })) + assert.NotEmpty(captureFn(func() { + tc := NewTestController(t, nil) + defer tc.Shutdown() + })) + }) } diff --git a/internal/observability/event/eventer.go b/internal/observability/event/eventer.go index f12cd9d360..0cc6962f88 100644 --- a/internal/observability/event/eventer.go +++ b/internal/observability/event/eventer.go @@ -170,7 +170,8 @@ func NewAuditEncryptFilter(opt ...Option) (*encrypt.Filter, error) { } // NewEventer creates a new Eventer using the config. Supports options: -// WithNow, WithSerializationLock, WithBroker, WithAuditWrapper +// WithNow, WithSerializationLock, WithBroker, WithAuditWrapper, +// WithNoDefaultSink func NewEventer(log hclog.Logger, serializationLock *sync.Mutex, serverName string, c EventerConfig, opt ...Option) (*Eventer, error) { const op = "event.NewEventer" if log == nil { @@ -183,9 +184,11 @@ func NewEventer(log hclog.Logger, serializationLock *sync.Mutex, serverName stri return nil, fmt.Errorf("%s: missing server name: %w", op, ErrInvalidParameter) } + opts := getOpts(opt...) + // if there are no sinks in config, then we'll default to just one stderr // sink. - if len(c.Sinks) == 0 { + if len(c.Sinks) == 0 && !opts.withNoDefaultSink { c.Sinks = append(c.Sinks, DefaultSink()) } @@ -195,7 +198,6 @@ func NewEventer(log hclog.Logger, serializationLock *sync.Mutex, serverName stri var auditPipelines, observationPipelines, errPipelines, sysPipelines []pipeline - opts := getOpts(opt...) var b broker switch { case opts.withBroker != nil: diff --git a/internal/observability/event/options.go b/internal/observability/event/options.go index 44740c458f..6a8cd4ff76 100644 --- a/internal/observability/event/options.go +++ b/internal/observability/event/options.go @@ -54,6 +54,7 @@ type options struct { withObservationSink bool // test only option withSysSink bool // test only option withSinkFormat SinkFormat // test only option + withNoDefaultSink bool // test only option } func getDefaultOptions() options { diff --git a/internal/observability/event/testing.go b/internal/observability/event/testing.go index e4220dd675..a0922ddb09 100644 --- a/internal/observability/event/testing.go +++ b/internal/observability/event/testing.go @@ -4,16 +4,41 @@ import ( "context" "io/ioutil" "os" + "sync" "testing" "time" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/groups" "github.com/hashicorp/eventlogger" + "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/wrapperspb" ) +// TestWithoutEventing allows the caller to "disable" all eventing for a test. +// You must not run the test in parallel (no calls to t.Parallel) since the +// function relies on modifying the system wide default eventer. +func TestWithoutEventing(t testing.TB) *Eventer { + t.Helper() + require := require.New(t) + testConfig := EventerConfig{ + AuditEnabled: false, + ObservationsEnabled: false, + SysEventsEnabled: false, + } + testLock := &sync.Mutex{} + testLogger := hclog.New(&hclog.LoggerOptions{ + Mutex: testLock, + Output: ioutil.Discard, + }) + testEventer, err := NewEventer(testLogger, testLock, "TestWithoutEventing", testConfig, withNoDefaultSink(t)) + require.NoError(err) + + require.NoError(InitSysEventer(testLogger, testLock, "TestWithoutEventing", WithEventer(testEventer))) + return testEventer +} + // TestGetEventerConfig is a test accessor for the eventer's config func TestGetEventerConfig(t testing.TB, e *Eventer) EventerConfig { t.Helper() @@ -231,6 +256,14 @@ func TestWithSysSink(t testing.TB) Option { } } +// withNoDefaultSink is an unexported test option +func withNoDefaultSink(t testing.TB) Option { + t.Helper() + return func(o *options) { + o.withNoDefaultSink = true + } +} + // testWithSinkFormat is an unexported and a test option func testWithSinkFormat(t testing.TB, fmt SinkFormat) Option { t.Helper() diff --git a/internal/observability/event/testing_test.go b/internal/observability/event/testing_test.go new file mode 100644 index 0000000000..cdbed4c8c4 --- /dev/null +++ b/internal/observability/event/testing_test.go @@ -0,0 +1,55 @@ +package event_test + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "testing" + + "github.com/hashicorp/boundary/internal/observability/event" + "github.com/stretchr/testify/assert" +) + +func Test_TestWithoutEventing(t *testing.T) { + const op = "Test_TestWithoutEventing" + assert := assert.New(t) + + // this isn't the best solution for capturing stdout but it works for now... + captureFn := func(fn func()) string { + old := os.Stdout + defer func() { + os.Stderr = old + }() + + r, w, _ := os.Pipe() + os.Stderr = w + + { + fn() + } + + outC := make(chan string) + // copy the output in a separate goroutine so writing to stderr can't block indefinitely + go func() { + var buf bytes.Buffer + io.Copy(&buf, r) + outC <- buf.String() + }() + + // back to normal state + w.Close() + return <-outC + } + + assert.NotEmpty(captureFn(func() { + fmt.Fprintln(os.Stderr, "not-empty") + })) + + assert.Empty(captureFn(func() { + testCtx := context.Background() + event.TestWithoutEventing(t) + event.WriteSysEvent(testCtx, op, "test-event") + })) +}