package event import ( "context" "fmt" "strings" "sync" "testing" "github.com/hashicorp/eventlogger" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_InitSysEventer(t *testing.T) { // this test and its subtests cannot be run in parallel because of it's // dependency on the sysEventer testConfig := TestEventerConfig(t, "InitSysEventer") testLock := &sync.Mutex{} testLogger := hclog.New(&hclog.LoggerOptions{ Mutex: testLock, }) testEventer, err := NewEventer(testLogger, testLock, "Test_InitSysEventer", testConfig.EventerConfig) require.NoError(t, err) tests := []struct { name string log hclog.Logger lock *sync.Mutex serverName string opt []Option want *Eventer wantErrIs error }{ { name: "missing-both-eventer-and-config", serverName: "missing-both-eventer-and-config", wantErrIs: ErrInvalidParameter, }, { name: "missing-hclog", opt: []Option{WithEventerConfig(&testConfig.EventerConfig)}, lock: testLock, serverName: "missing-hclog", wantErrIs: ErrInvalidParameter, }, { name: "missing-lock", opt: []Option{WithEventerConfig(&testConfig.EventerConfig)}, log: testLogger, serverName: "missing-lock", wantErrIs: ErrInvalidParameter, }, { name: "missing-serverName", opt: []Option{WithEventerConfig(&testConfig.EventerConfig)}, log: testLogger, lock: testLock, wantErrIs: ErrInvalidParameter, }, { name: "missing-eventer-and-config", log: testLogger, lock: testLock, serverName: "success-with-config", wantErrIs: ErrInvalidParameter, }, { name: "both-eventer-and-config", opt: []Option{WithEventerConfig(&testConfig.EventerConfig), WithEventer(testEventer)}, log: testLogger, lock: testLock, serverName: "success-with-config", wantErrIs: ErrInvalidParameter, }, { name: "bad-config", opt: []Option{WithEventerConfig(&EventerConfig{Sinks: []SinkConfig{{Format: "bad-format"}}})}, log: testLogger, lock: testLock, serverName: "success-with-config", wantErrIs: ErrInvalidParameter, }, { name: "success-with-config", opt: []Option{WithEventerConfig(&testConfig.EventerConfig)}, log: testLogger, lock: testLock, serverName: "success-with-config", want: &Eventer{ logger: testLogger, conf: testConfig.EventerConfig, }, }, { name: "success-with-default-config", opt: []Option{WithEventerConfig(&EventerConfig{})}, log: testLogger, lock: testLock, serverName: "success-with-default-config", want: &Eventer{ logger: testLogger, conf: EventerConfig{ Sinks: []SinkConfig{ { Name: "default", EventTypes: []Type{EveryType}, Format: JSONSinkFormat, SinkType: StderrSink, }, }, }, }, }, { name: "success-with-eventer", opt: []Option{WithEventer(testEventer)}, log: testLogger, lock: testLock, serverName: "success-with-eventer", want: testEventer, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer TestResetSystEventer(t) assert, require := assert.New(t), require.New(t) err := InitSysEventer(tt.log, tt.lock, tt.serverName, tt.opt...) got := SysEventer() if tt.wantErrIs != nil { require.Nil(got) require.Error(err) assert.ErrorIs(err, tt.wantErrIs) return } require.NoError(err) require.NotNil(got) tt.want.broker = got.broker tt.want.flushableNodes = got.flushableNodes tt.want.auditPipelines = got.auditPipelines tt.want.errPipelines = got.errPipelines tt.want.observationPipelines = got.observationPipelines assert.Equal(tt.want, got) }) } } func TestEventer_writeObservation(t *testing.T) { t.Parallel() ctx := context.Background() testSetup := TestEventerConfig(t, "TestEventer_writeObservation") testLock := &sync.Mutex{} testLogger := hclog.New(&hclog.LoggerOptions{ Mutex: testLock, }) eventer, err := NewEventer(testLogger, testLock, "TestEventer_writeObservation", testSetup.EventerConfig) require.NoError(t, err) testHeader := map[string]interface{}{"name": "header"} testDetail := map[string]interface{}{"name": "details"} testObservation, err := newObservation("Test_NewEventer", WithHeader(testHeader), WithDetails(testDetail)) require.NoError(t, err) tests := []struct { name string broker broker observation *observation wantErrIs error }{ { name: "missing-observation", broker: &testMockBroker{}, wantErrIs: ErrInvalidParameter, }, { name: "send-fails", broker: &testMockBroker{errorOnSend: fmt.Errorf("%s: no msg: %w", "test", ErrIo)}, observation: testObservation, wantErrIs: ErrMaxRetries, }, { name: "success", broker: &testMockBroker{}, observation: testObservation, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) eventer.broker = tt.broker err = eventer.writeObservation(ctx, tt.observation) if tt.wantErrIs != nil { require.Error(err) assert.ErrorIs(err, tt.wantErrIs) return } require.NoError(err) }) } t.Run("e2e", func(t *testing.T) { require := require.New(t) c := EventerConfig{ ObservationsEnabled: true, } testLock := &sync.Mutex{} testLogger := hclog.New(&hclog.LoggerOptions{ Mutex: testLock, Name: "test", }) // with no defined config, it will default to a stderr sink e, err := NewEventer(testLogger, testLock, "e2e-test", c) require.NoError(err) m := map[string]interface{}{ "name": "bar", "list": []string{"1", "2"}, } observationEvent, err := newObservation("Test_NewEventer", WithHeader(m)) require.NoError(err) require.NoError(e.writeObservation(context.Background(), observationEvent)) }) } func TestEventer_writeAudit(t *testing.T) { t.Parallel() ctx := context.Background() testSetup := TestEventerConfig(t, "Test_NewEventer") testLock := &sync.Mutex{} testLogger := hclog.New(&hclog.LoggerOptions{ Mutex: testLock, Name: "test", }) eventer, err := NewEventer(testLogger, testLock, "TestEventer_writeAudit", testSetup.EventerConfig) require.NoError(t, err) testAudit, err := newAudit( "TestEventer_writeAudit", WithRequestInfo(TestRequestInfo(t)), WithAuth(testAuth(t)), WithRequest(testRequest(t)), WithResponse(testResponse(t))) require.NoError(t, err) tests := []struct { name string broker broker audit *audit wantErrIs error }{ { name: "missing-audit", broker: &testMockBroker{}, wantErrIs: ErrInvalidParameter, }, { name: "send-fails", broker: &testMockBroker{errorOnSend: fmt.Errorf("%s: no msg: %w", "test", ErrIo)}, audit: testAudit, wantErrIs: ErrIo, }, { name: "success", broker: &testMockBroker{}, audit: testAudit, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) eventer.broker = tt.broker err = eventer.writeAudit(ctx, tt.audit) if tt.wantErrIs != nil { require.Error(err) assert.ErrorIs(err, tt.wantErrIs) return } require.NoError(err) }) } } func TestEventer_writeError(t *testing.T) { t.Parallel() ctx := context.Background() testSetup := TestEventerConfig(t, "Test_NewEventer") testLock := &sync.Mutex{} testLogger := hclog.New(&hclog.LoggerOptions{ Mutex: testLock, Name: "test", }) eventer, er := NewEventer(testLogger, testLock, "TestEventer_writeError", testSetup.EventerConfig) require.NoError(t, er) testError, er := newError("TestEventer_writeError", fmt.Errorf("%s: no msg: test", ErrIo)) require.NoError(t, er) tests := []struct { name string broker broker err *err wantErrIs error }{ { name: "missing-error", broker: &testMockBroker{}, wantErrIs: ErrInvalidParameter, }, { name: "send-fails", broker: &testMockBroker{errorOnSend: fmt.Errorf("%s: no msg: test", ErrIo)}, err: testError, wantErrIs: ErrMaxRetries, }, { name: "success", broker: &testMockBroker{}, err: testError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) eventer.broker = tt.broker err := eventer.writeError(ctx, tt.err) if tt.wantErrIs != nil { require.Error(err) assert.ErrorIs(err, tt.wantErrIs) return } require.NoError(err) }) } } func Test_NewEventer(t *testing.T) { t.Parallel() testSetup := TestEventerConfig(t, "Test_NewEventer") testSetupWithOpts := TestEventerConfig(t, "Test_NewEventer", TestWithAuditSink(t), TestWithObservationSink(t), testWithSysSink(t)) testLock := &sync.Mutex{} testLogger := hclog.New(&hclog.LoggerOptions{ Mutex: testLock, Name: "test", }) tests := []struct { name string config EventerConfig opts []Option logger hclog.Logger lock *sync.Mutex serverName string want *Eventer wantRegistered []string wantPipelines []string wantThresholds map[eventlogger.EventType]int wantErrIs error }{ { name: "missing-logger", config: testSetup.EventerConfig, lock: testLock, serverName: "missing-logger", wantErrIs: ErrInvalidParameter, }, { name: "missing-lock", config: testSetup.EventerConfig, logger: testLogger, serverName: "missing-lock", wantErrIs: ErrInvalidParameter, }, { name: "missing-server-name", config: testSetup.EventerConfig, logger: testLogger, lock: testLock, wantErrIs: ErrInvalidParameter, }, { name: "success-with-default-config", config: EventerConfig{}, logger: testLogger, lock: testLock, serverName: "success-with-default-config", want: &Eventer{ logger: testLogger, conf: EventerConfig{ Sinks: []SinkConfig{ { Name: "default", EventTypes: []Type{EveryType}, Format: JSONSinkFormat, SinkType: StderrSink, }, }, }, }, wantRegistered: []string{ "cloudevents", // fmt for everything "stderr", // stderr "gated-observation", // stderr "gated-audit", // stderr }, wantPipelines: []string{ "audit", // stderr "observation", // stderr "error", // stderr "system", // stderr }, wantThresholds: map[eventlogger.EventType]int{ "error": 1, "system": 1, "observation": 1, "audit": 1, }, }, { name: "testSetup", config: testSetup.EventerConfig, logger: testLogger, lock: testLock, serverName: "testSetup", want: &Eventer{ logger: testLogger, conf: testSetup.EventerConfig, }, wantRegistered: []string{ "cloudevents", // stderr "stderr", // stderr "gated-observation", // stderr "gated-audit", // stderr "cloudevents", // every-type-file-sync "tmp-all-events", // every-type-file-sync "gated-observation", // every-type-file-sync "gated-audit", // every-type-file-sync "cloudevents", // error-file-sink "tmp-errors", // error-file-sink }, wantPipelines: []string{ "audit", // every-type-file-sync "audit", // stderr "observation", // every-type-file-sync "observation", // stderr "error", // every-type-file-sync "error", // stderr "error", // error-file-sink "system", // stderr "system", // stderr }, wantThresholds: map[eventlogger.EventType]int{ "error": 3, "system": 2, "observation": 2, "audit": 2, }, }, { name: "testSetup-with-all-opts", config: testSetupWithOpts.EventerConfig, logger: testLogger, lock: testLock, serverName: "testSetup-with-all-opts", want: &Eventer{ logger: testLogger, conf: testSetupWithOpts.EventerConfig, }, wantRegistered: []string{ "cloudevents", // stderr "stderr", // stderr "gated-observation", // stderr "gated-audit", // stderr "cloudevents", // every-type-file-sync "tmp-all-events", // every-type-file-sync "gated-observation", // every-type-file-sync "gated-audit", // every-type-file-sync "cloudevents", // error-file-sink "tmp-errors", // error-file-sink "cloudevents", // observation-file-sink "gated-observation", // observation-file-sink "tmp-observation", // observations-file-sink "cloudevents", // audit-file-sink "gated-audit", // audit-file-sink "tmp-audit", // audit-file-sink "cloudevents", // sys-file-sink "tmp-sysevents", // sys-file-sink }, wantPipelines: []string{ "audit", // every-type-file-sync "audit", // stderr "observation", // every-type-file-sync "observation", // stderr "error", // every-type-file-sync "error", // stderr "error", // error-file-sink "audit", // audit-file-sink "observation", // observation-file-sink "system", // stderr "system", // every-type-file-sync "system", // sys-file-sink }, wantThresholds: map[eventlogger.EventType]int{ "error": 3, "system": 3, "observation": 3, "audit": 3, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) testBroker := &testMockBroker{} got, err := NewEventer(tt.logger, tt.lock, tt.serverName, tt.config, TestWithBroker(t, testBroker)) if tt.wantErrIs != nil { require.Error(err) require.Nil(got) assert.ErrorIs(err, tt.wantErrIs) return } require.NoError(err) require.NotNil(got) tt.want.broker = got.broker tt.want.flushableNodes = got.flushableNodes tt.want.auditPipelines = got.auditPipelines tt.want.errPipelines = got.errPipelines tt.want.observationPipelines = got.observationPipelines assert.Equal(tt.want, got) assert.Lenf(testBroker.registeredNodeIds, len(tt.wantRegistered), "got nodes: %q", testBroker.registeredNodeIds) registeredNodeIds := map[string]bool{} for _, id := range testBroker.registeredNodeIds { registeredNodeIds[string(id)] = true } for _, want := range tt.wantRegistered { found := false for got := range registeredNodeIds { if strings.Contains(got, want) { found = true delete(registeredNodeIds, got) break } } assert.Truef(found, "did not find %s in the registered nodes: %s", want, testBroker.registeredNodeIds) } assert.Lenf(testBroker.pipelines, len(tt.wantPipelines), "got pipelines: %q", testBroker.pipelines) registeredPipelines := map[string]eventlogger.Pipeline{} gotAuditCnt := 0 gotErrCnt := 0 gotObservationCnt := 0 for _, got := range testBroker.pipelines { registeredPipelines[string(got.PipelineID)] = got switch got.EventType { case eventlogger.EventType(AuditType): gotAuditCnt += 1 case eventlogger.EventType(ErrorType): gotErrCnt += 1 case eventlogger.EventType(ObservationType): gotObservationCnt += 1 } } wantAuditCnt := 0 wantErrCnt := 0 wantObservationCnt := 0 for _, want := range tt.wantPipelines { switch want { case string(AuditType): wantAuditCnt += 1 case string(ErrorType): wantErrCnt += 1 case string(ObservationType): wantObservationCnt += 1 } found := false for id, got := range registeredPipelines { if strings.Contains(string(got.EventType), want) { found = true delete(registeredPipelines, id) break } } assert.Truef(found, "did not find %s in the registered pipelines: %s", want, testBroker.pipelines) } assert.Equal(wantAuditCnt, gotAuditCnt) assert.Equal(wantErrCnt, gotErrCnt) assert.Equal(wantObservationCnt, gotObservationCnt) assert.Equal(tt.wantThresholds, testBroker.successThresholds) }) } } func TestEventer_Reopen(t *testing.T) { t.Parallel() t.Run("simple", func(t *testing.T) { assert, require := assert.New(t), require.New(t) testLock := &sync.Mutex{} testLogger := hclog.New(&hclog.LoggerOptions{ Mutex: testLock, Name: "test", }) e, err := NewEventer(testLogger, testLock, "TestEventer_Reopen", EventerConfig{}) require.NoError(err) e.broker = nil require.NoError(e.Reopen()) e.broker = &testMockBroker{} require.NoError(e.Reopen()) assert.True(e.broker.(*testMockBroker).reopened) }) } func TestEventer_FlushNodes(t *testing.T) { t.Parallel() t.Run("simple", func(t *testing.T) { assert, require := assert.New(t), require.New(t) testLock := &sync.Mutex{} testLogger := hclog.New(&hclog.LoggerOptions{ Mutex: testLock, Name: "test", }) e, err := NewEventer(testLogger, testLock, "TestEventer_FlushNodes", EventerConfig{}) require.NoError(err) node := &testFlushNode{} e.flushableNodes = append(e.flushableNodes, node) require.NoError(e.FlushNodes(context.Background())) node.raiseError = true require.Error(e.FlushNodes(context.Background())) assert.True(node.flushed) }) } type testFlushNode struct { flushed bool raiseError bool } func (t *testFlushNode) FlushAll(_ context.Context) error { t.flushed = true if t.raiseError { return fmt.Errorf("%s: test error: flush-all", ErrInvalidParameter) } return nil }