Files
threshold/internal/observability/event/context_test.go
T
Jeff MitchellandJim 7ab7c8eaf4 Have fallback eventing use json format (#2216)
* Have fallback eventing use json format

The server will initialize a logger via InitFallbackLogger(...)
after setting up the server's logging (and honoring the related
cmd flags).  This fallback logger will be used if when no eventer
is initialized.

If no fallback logger is initialized, then a event.defaultLogger() will
be used if no eventer is defined.

Co-authored-by: Jim <jlambert@hashicorp.com>
2022-07-11 16:12:02 -04:00

1280 lines
36 KiB
Go

package event_test
import (
"context"
"encoding/json"
stderrors "errors"
"fmt"
"io/ioutil"
"os"
"sync"
"testing"
"time"
pbs "github.com/hashicorp/boundary/internal/gen/testing/event"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/eventlogger/filters/encrypt"
"github.com/hashicorp/eventlogger/formatter_filters/cloudevents"
"github.com/hashicorp/go-hclog"
"github.com/mitchellh/copystructure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
)
const apiRequest = "APIRequest"
const (
testAuditVersion = "v0.1"
testObservationVersion = "v0.1"
)
type testAudit struct {
Id string `json:"id"` // std audit/boundary field
Version string `json:"version"` // std audit/boundary field
Type string `json:"type"` // std audit field
Timestamp time.Time `json:"timestamp"` // std audit field
RequestInfo *event.RequestInfo `json:"request_info,omitempty"` // boundary field
Auth *event.Auth `json:"auth,omitempty"` // std audit field
Request *event.Request `json:"request,omitempty"` // std audit field
Response *event.Response `json:"response,omitempty"` // std audit field
Flush bool `json:"-"`
}
func Test_NewRequestInfoContext(t *testing.T) {
testInfo := event.TestRequestInfo(t)
testInfoMissingId := event.TestRequestInfo(t)
testInfoMissingId.Id = ""
testInfoMissingEventId := event.TestRequestInfo(t)
testInfoMissingEventId.EventId = ""
tests := []struct {
name string
ctx context.Context
requestInfo *event.RequestInfo
wantErrIs error
wantErrContains string
}{
{
name: "missing-ctx",
requestInfo: testInfo,
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing context",
},
{
name: "missing-request-info",
ctx: context.Background(),
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing request info",
},
{
name: "missing-request-info-id",
ctx: context.Background(),
requestInfo: testInfoMissingId,
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing request info id",
},
{
name: "missing-request-info-event-id",
ctx: context.Background(),
requestInfo: testInfoMissingEventId,
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing request info event id",
},
{
name: "valid",
ctx: context.Background(),
requestInfo: testInfo,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
ctx, err := event.NewRequestInfoContext(tt.ctx, tt.requestInfo)
if tt.wantErrIs != nil {
require.Errorf(err, "should have gotten an error")
assert.Nilf(ctx, "context should be nil")
assert.ErrorIs(err, tt.wantErrIs)
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
require.NoErrorf(err, "should not have been a problem getting the request info")
require.NotNilf(ctx, "cxt returned shouldn't be nil")
got, ok := event.RequestInfoFromContext(ctx)
require.Truef(ok, "should be ok to get the request info")
assert.Equal(tt.requestInfo, got)
})
}
}
func Test_RequestInfoFromContext(t *testing.T) {
testInfo := event.TestRequestInfo(t)
testCtx, err := event.NewRequestInfoContext(context.Background(), testInfo)
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
wantInfo *event.RequestInfo
wantNotOk bool
}{
{
name: "missing-ctx",
wantNotOk: true,
},
{
name: "no-request-info",
ctx: context.Background(),
wantNotOk: true,
},
{
name: "valid",
ctx: testCtx,
wantInfo: testInfo,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, ok := event.RequestInfoFromContext(tt.ctx)
if tt.wantNotOk {
require.Falsef(ok, "should not have returned ok for the request info")
assert.Nilf(got, "should not have returned %q request info", got)
return
}
require.Truef(ok, "should have been okay for getting the request info")
require.NotNilf(got, "request info should not be nil")
assert.Equal(tt.wantInfo, got)
})
}
}
func Test_NewEventerContext(t *testing.T) {
testSetup := event.TestEventerConfig(t, "Test_NewEventerContext")
testLock := &sync.Mutex{}
testLogger := testLogger(t, testLock)
testEventer, err := event.NewEventer(testLogger, testLock, "Test_NewEventerContext", testSetup.EventerConfig)
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
eventer *event.Eventer
wantErrIs error
wantErrContains string
}{
{
name: "missing-ctx",
eventer: testEventer,
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing context",
},
{
name: "missing-eventer",
ctx: context.Background(),
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing eventer",
},
{
name: "valid",
ctx: context.Background(),
eventer: testEventer,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
ctx, err := event.NewEventerContext(tt.ctx, tt.eventer)
if tt.wantErrIs != nil {
require.Errorf(err, "should have gotten an error")
assert.Nilf(ctx, "context should be nil")
assert.ErrorIs(err, tt.wantErrIs)
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
require.NoErrorf(err, "should not have been a problem getting the eventer")
require.NotNilf(ctx, "cxt returned shouldn't be nil")
got, ok := event.EventerFromContext(ctx)
require.Truef(ok, "should be ok to get the eventer")
assert.Equal(tt.eventer, got)
})
}
}
func Test_EventerFromContext(t *testing.T) {
testSetup := event.TestEventerConfig(t, "Test_EventerFromContext")
testLock := &sync.Mutex{}
testLogger := testLogger(t, testLock)
testEventer, err := event.NewEventer(testLogger, testLock, "Test_EventerFromContext", testSetup.EventerConfig)
require.NoError(t, err)
testEventerCtx, err := event.NewEventerContext(context.Background(), testEventer)
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
wantEventer *event.Eventer
wantNotOk bool
}{
{
name: "missing-ctx",
wantNotOk: true,
},
{
name: "no-eventer",
ctx: context.Background(),
wantNotOk: true,
},
{
name: "valid",
ctx: testEventerCtx,
wantEventer: testEventer,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, ok := event.EventerFromContext(tt.ctx)
if tt.wantNotOk {
require.Falsef(ok, "should not have returned ok for the eventer")
assert.Nilf(got, "should not have returned %q eventer", got)
return
}
require.Truef(ok, "should have been okay for getting an eventer")
require.NotNilf(got, "eventer should not be nil")
assert.Equal(tt.wantEventer, got)
})
}
}
func Test_WriteObservation(t *testing.T) {
// this test and its subtests cannot be run in parallel because of it's
// dependency on the sysEventer
c := event.TestEventerConfig(t, "WriteObservation")
testLock := &sync.Mutex{}
testLogger := testLogger(t, testLock)
e, err := event.NewEventer(testLogger, testLock, "Test_WriteObservation", c.EventerConfig)
require.NoError(t, err)
info := &event.RequestInfo{Id: "867-5309", EventId: "411"}
testCtx, err := event.NewEventerContext(context.Background(), e)
require.NoError(t, err)
testCtx, err = event.NewRequestInfoContext(testCtx, info)
require.NoError(t, err)
testCtxNoEventInfoId, err := event.NewEventerContext(context.Background(), e)
require.NoError(t, err)
noEventId := &event.RequestInfo{Id: "867-5309", EventId: "411"}
testCtxNoEventInfoId, err = event.NewRequestInfoContext(testCtxNoEventInfoId, noEventId)
require.NoError(t, err)
noEventId.EventId = ""
noEventId.Id = ""
type observationPayload struct {
header []interface{}
details []interface{}
}
testPayloads := []observationPayload{
{
header: []interface{}{"name", "bar"},
},
{
header: []interface{}{"list", []string{"1", "2"}},
},
{
details: []interface{}{"file", "temp-file.txt"},
},
}
testWantHeader := map[string]interface{}{
"name": "bar",
"list": []string{"1", "2"},
}
testWantDetails := map[string]interface{}{
"file": "temp-file.txt",
}
tests := []struct {
name string
noOperation bool
noFlush bool
observationPayload []observationPayload
header map[string]interface{}
details map[string]interface{}
ctx context.Context
observationSinkFileName string
setup func() error
cleanup func()
wantErrIs error
wantErrContains string
}{
{
name: "no-info-event-id",
noFlush: true,
ctx: testCtxNoEventInfoId,
observationPayload: []observationPayload{
{
header: []interface{}{"name", "bar"},
},
},
header: map[string]interface{}{
"name": "bar",
},
observationSinkFileName: c.AllEvents.Name(),
setup: func() error {
return event.InitSysEventer(testLogger, testLock, "no-info-event-id", event.WithEventerConfig(&c.EventerConfig))
},
cleanup: func() { event.TestResetSystEventer(t) },
},
{
name: "missing-ctx",
observationPayload: testPayloads,
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing context",
},
{
name: "missing-op",
ctx: testCtx,
noOperation: true,
observationPayload: testPayloads,
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing operation",
},
{
name: "no-header-or-details-in-payload",
noFlush: true,
ctx: testCtx,
observationPayload: []observationPayload{
{},
},
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "specify either header or details options",
},
{
name: "no-ctx-eventer-and-syseventer-not-initialized",
ctx: context.Background(),
observationPayload: testPayloads,
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing both context and system eventer",
},
{
name: "use-syseventer",
noFlush: true,
ctx: context.Background(),
observationPayload: []observationPayload{
{
header: []interface{}{"name", "bar"},
},
},
header: map[string]interface{}{
"name": "bar",
},
observationSinkFileName: c.AllEvents.Name(),
setup: func() error {
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
},
cleanup: func() { event.TestResetSystEventer(t) },
},
{
name: "use-syseventer-with-cancelled-ctx",
noFlush: true,
ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
return ctx
}(),
observationPayload: []observationPayload{
{
header: []interface{}{"name", "bar"},
},
},
header: map[string]interface{}{
"name": "bar",
},
observationSinkFileName: c.AllEvents.Name(),
setup: func() error {
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
},
cleanup: func() { event.TestResetSystEventer(t) },
},
{
name: "simple",
ctx: testCtx,
observationPayload: testPayloads,
header: testWantHeader,
details: testWantDetails,
observationSinkFileName: c.AllEvents.Name(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
if tt.setup != nil {
require.NoError(tt.setup())
}
if tt.cleanup != nil {
defer tt.cleanup()
}
op := tt.name
if tt.noOperation {
op = ""
}
require.Greater(len(tt.observationPayload), 0)
for _, p := range tt.observationPayload {
err := event.WriteObservation(tt.ctx, event.Op(op), event.WithHeader(p.header...), event.WithDetails(p.details...))
if tt.wantErrIs != nil {
assert.ErrorIs(err, tt.wantErrIs)
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
require.NoError(err)
}
if !tt.noFlush {
require.NoError(event.WriteObservation(tt.ctx, event.Op(tt.name), event.WithFlush()))
}
if tt.observationSinkFileName != "" {
defer func() { _ = os.WriteFile(tt.observationSinkFileName, nil, 0o666) }()
b, err := ioutil.ReadFile(tt.observationSinkFileName)
assert.NoError(err)
gotObservation := &cloudevents.Event{}
err = json.Unmarshal(b, gotObservation)
require.NoErrorf(err, "json: %s", string(b))
actualJson, err := json.Marshal(gotObservation)
require.NoError(err)
wantJson := testObservationJsonFromCtx(t, tt.ctx, event.Op(tt.name), gotObservation, tt.header, tt.details)
assert.JSONEq(string(wantJson), string(actualJson))
}
})
}
t.Run("not-enabled", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
c := event.TestEventerConfig(t, "WriteObservation")
c.EventerConfig.ObservationsEnabled = false
testLock := &sync.Mutex{}
e, err := event.NewEventer(testLogger, testLock, "not-enabled", c.EventerConfig)
require.NoError(err)
testCtx, err := event.NewEventerContext(context.Background(), e)
require.NoError(err)
testCtx, err = event.NewRequestInfoContext(testCtx, info)
require.NoError(err)
hdr := map[string]interface{}{
"list": []string{"1", "2"},
}
require.NoError(event.WriteObservation(testCtx, "not-enabled", event.WithHeader(hdr), event.WithFlush()))
b, err := ioutil.ReadFile(c.AllEvents.Name())
assert.NoError(err)
assert.Len(b, 0)
})
}
func Test_Filtering(t *testing.T) {
testLock := &sync.Mutex{}
testLogger := testLogger(t, testLock)
tests := []struct {
name string
allow []string
deny []string
hdr []interface{}
found bool
}{
{
name: "allowed",
allow: []string{`"/data/list" contains "1"`},
hdr: []interface{}{"list", []string{"1", "2"}},
found: true,
},
{
name: "not-allowed",
allow: []string{`"/data/list" contains "22"`},
hdr: []interface{}{"list", []string{"1", "2"}},
found: false,
},
{
name: "deny",
deny: []string{`"/data/list" contains "1"`},
hdr: []interface{}{"list", []string{"1", "2"}},
found: false,
},
{
name: "not-deny",
deny: []string{`"/data/list" contains "22"`},
hdr: []interface{}{"list", []string{"1", "2"}},
found: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
info := &event.RequestInfo{Id: "867-5309", EventId: "411"}
c := event.TestEventerConfig(t, "WriteObservation-filtering")
c.EventerConfig.Sinks[0].AllowFilters = tt.allow
c.EventerConfig.Sinks[0].DenyFilters = tt.deny
e, err := event.NewEventer(testLogger, testLock, "filtering", c.EventerConfig)
require.NoError(err)
testCtx, err := event.NewEventerContext(context.Background(), e)
require.NoError(err)
testCtx, err = event.NewRequestInfoContext(testCtx, info)
require.NoError(err)
require.NoError(event.WriteObservation(testCtx, "not-enabled", event.WithHeader(tt.hdr...), event.WithFlush()))
b, err := ioutil.ReadFile(c.AllEvents.Name())
assert.NoError(err)
switch tt.found {
case true:
assert.NotEmpty(b)
case false:
assert.Empty(b)
}
})
}
}
func Test_DefaultEventerConfig(t *testing.T) {
t.Run("assert-default", func(t *testing.T) {
assert.Equal(t, &event.EventerConfig{
AuditEnabled: false,
ObservationsEnabled: true,
SysEventsEnabled: true,
Sinks: []*event.SinkConfig{event.DefaultSink()},
}, event.DefaultEventerConfig())
})
}
func testObservationJsonFromCtx(t *testing.T, ctx context.Context, caller event.Op, got *cloudevents.Event, hdr, details map[string]interface{}) []byte {
t.Helper()
require := require.New(t)
reqInfo, _ := event.RequestInfoFromContext(ctx)
// require.Truef(ok, "missing reqInfo in ctx")
j := cloudevents.Event{
ID: got.ID,
Time: got.Time,
Source: got.Source,
SpecVersion: got.SpecVersion,
Type: got.Type,
DataContentType: got.DataContentType,
Data: map[string]interface{}{
event.RequestInfoField: reqInfo,
event.VersionField: testObservationVersion,
},
}
if hdr != nil {
h := j.Data.(map[string]interface{})
for k, v := range hdr {
h[k] = v
}
}
if details != nil {
details[event.OpField] = string(caller)
d := got.Data.(map[string]interface{})[event.DetailsField].([]interface{})[0].(map[string]interface{})
j.Data.(map[string]interface{})[event.DetailsField] = []struct {
CreatedAt string `json:"created_at"`
Type string `json:"type"`
Payload map[string]interface{} `json:"payload"`
}{
{
CreatedAt: d[event.CreatedAtField].(string),
Type: d[event.TypeField].(string),
Payload: details,
},
}
}
b, err := json.Marshal(j)
require.NoError(err)
return b
}
func Test_WriteAudit(t *testing.T) {
// this test and its subtests cannot be run in parallel because of it's
// dependency on the sysEventer
now := time.Now()
c := event.TestEventerConfig(t, "WriteAudit")
testLock := &sync.Mutex{}
testLogger := testLogger(t, testLock)
e, err := event.NewEventer(testLogger, testLock, "Test_WriteAudit", c.EventerConfig)
require.NoError(t, err)
info := &event.RequestInfo{Id: "867-5309", EventId: "411"}
ctx, err := event.NewEventerContext(context.Background(), e)
require.NoError(t, err)
ctx, err = event.NewRequestInfoContext(ctx, info)
require.NoError(t, err)
testAuth := &event.Auth{
AuthTokenId: "test_auth_token_id",
UserEmail: "test_user_email",
UserName: "test_user_name",
UserInfo: &event.UserInfo{
UserId: "test_user_id",
AuthAccountId: "test_auth_account_id",
},
GrantsInfo: &event.GrantsInfo{
Grants: []event.Grant{
{
Grant: "test_grant",
ScopeId: "test_grant_scope_id",
},
},
},
}
testReq := &event.Request{
Operation: "POST",
Endpoint: "/v1/hosts",
Details: &pbs.TestAuthenticateRequest{
AuthMethodId: "test_1234567890",
TokenType: "test-cookie",
Command: "test-command",
Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{
"password": structpb.NewStringValue("fido"),
}},
},
}
testResp := &event.Response{
StatusCode: 200,
Details: &pbs.TestAuthenticateResponse{
Command: "test-command",
Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{
"token": structpb.NewStringValue("test-token"),
}},
},
}
tests := []struct {
name string
auditOpts [][]event.Option
wantAudit *testAudit
ctx context.Context
auditSinkFileName string
setup func() error
cleanup func()
noOperation bool
noFlush bool
wantErrIs error
wantErrContains string
}{
{
name: "missing-ctx",
auditOpts: [][]event.Option{
{
event.WithAuth(testAuth),
event.WithRequest(testReq),
},
},
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing context",
},
{
name: "missing-op",
ctx: ctx,
auditOpts: [][]event.Option{
{
event.WithAuth(testAuth),
event.WithRequest(testReq),
},
},
noOperation: true,
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing operation",
},
{
name: "no-ctx-eventer-and-syseventer-not-initialized",
ctx: context.Background(),
auditOpts: [][]event.Option{
{
event.WithAuth(testAuth),
event.WithRequest(testReq),
},
},
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing both context and system eventer",
},
{
name: "use-syseventer",
noFlush: true,
ctx: context.Background(),
auditOpts: [][]event.Option{
{
event.WithAuth(
func() *event.Auth {
dup, err := copystructure.Copy(testAuth)
require.NoError(t, err)
return dup.(*event.Auth)
}(),
),
event.WithRequest(
func() *event.Request {
dup, err := copystructure.Copy(testReq)
require.NoError(t, err)
return dup.(*event.Request)
}(),
),
},
},
wantAudit: &testAudit{
Auth: func() *event.Auth {
dup, err := copystructure.Copy(testAuth)
require.NoError(t, err)
dup.(*event.Auth).UserEmail = encrypt.RedactedData
dup.(*event.Auth).UserName = encrypt.RedactedData
return dup.(*event.Auth)
}(),
Request: func() *event.Request {
dup, err := copystructure.Copy(testReq)
require.NoError(t, err)
dup.(*event.Request).Details.(*pbs.TestAuthenticateRequest).Attributes = &structpb.Struct{
Fields: map[string]*structpb.Value{
"password": structpb.NewStringValue(encrypt.RedactedData),
},
}
return dup.(*event.Request)
}(),
},
setup: func() error {
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
},
cleanup: func() { event.TestResetSystEventer(t) },
auditSinkFileName: c.AllEvents.Name(),
},
{
name: "use-syseventer-with-cancelled-ctx",
noFlush: true,
ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
return ctx
}(),
auditOpts: [][]event.Option{
{
event.WithAuth(testAuth),
event.WithRequest(testReq),
},
},
wantAudit: &testAudit{
Auth: func() *event.Auth {
dup, err := copystructure.Copy(testAuth)
require.NoError(t, err)
dup.(*event.Auth).UserEmail = encrypt.RedactedData
dup.(*event.Auth).UserName = encrypt.RedactedData
return dup.(*event.Auth)
}(),
Request: func() *event.Request {
dup, err := copystructure.Copy(testReq)
require.NoError(t, err)
dup.(*event.Request).Details.(*pbs.TestAuthenticateRequest).Attributes = &structpb.Struct{
Fields: map[string]*structpb.Value{
"password": structpb.NewStringValue(encrypt.RedactedData),
},
}
return dup.(*event.Request)
}(),
},
setup: func() error {
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
},
cleanup: func() { event.TestResetSystEventer(t) },
auditSinkFileName: c.AllEvents.Name(),
},
{
name: "simple",
ctx: ctx,
auditOpts: [][]event.Option{
{
event.WithAuth(testAuth),
event.WithRequest(testReq),
},
{
event.WithResponse(testResp),
},
},
wantAudit: &testAudit{
Id: "411",
Auth: func() *event.Auth {
dup, err := copystructure.Copy(testAuth)
require.NoError(t, err)
dup.(*event.Auth).UserEmail = encrypt.RedactedData
dup.(*event.Auth).UserName = encrypt.RedactedData
return dup.(*event.Auth)
}(),
Request: func() *event.Request {
dup, err := copystructure.Copy(testReq)
require.NoError(t, err)
dup.(*event.Request).Details.(*pbs.TestAuthenticateRequest).Attributes = &structpb.Struct{
Fields: map[string]*structpb.Value{
"password": structpb.NewStringValue(encrypt.RedactedData),
},
}
return dup.(*event.Request)
}(),
Response: func() *event.Response {
dup, err := copystructure.Copy(testResp)
require.NoError(t, err)
dup.(*event.Response).Details.(*pbs.TestAuthenticateResponse).Attributes = &structpb.Struct{
Fields: map[string]*structpb.Value{
"token": structpb.NewStringValue(encrypt.RedactedData),
},
}
return dup.(*event.Response)
}(),
},
auditSinkFileName: c.AllEvents.Name(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
if tt.setup != nil {
require.NoError(tt.setup())
}
if tt.cleanup != nil {
defer tt.cleanup()
}
op := tt.name
if tt.noOperation {
op = ""
}
require.Greater(len(tt.auditOpts), 0)
for _, opts := range tt.auditOpts {
opts := append(opts, event.WithNow(now))
err := event.WriteAudit(tt.ctx, event.Op(op), opts...)
if tt.wantErrIs != nil {
assert.ErrorIs(err, tt.wantErrIs)
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
require.NoError(err)
}
if !tt.noFlush {
require.NoError(event.WriteAudit(tt.ctx, event.Op(op), event.WithFlush(), event.WithNow(now)))
}
if tt.auditSinkFileName != "" {
defer func() { _ = os.WriteFile(tt.auditSinkFileName, nil, 0o666) }()
b, err := ioutil.ReadFile(tt.auditSinkFileName)
require.NoError(err)
gotAudit := &cloudevents.Event{}
err = json.Unmarshal(b, gotAudit)
require.NoErrorf(err, "json: %s", string(b))
actualJson, err := json.Marshal(gotAudit)
require.NoError(err)
wantEvent := cloudevents.Event{
ID: gotAudit.ID,
Source: gotAudit.Source,
SpecVersion: gotAudit.SpecVersion,
DataContentType: gotAudit.DataContentType,
Time: gotAudit.Time,
Type: "audit",
Data: map[string]interface{}{
"auth": tt.wantAudit.Auth,
"id": gotAudit.Data.(map[string]interface{})["id"],
"timestamp": now,
"request": tt.wantAudit.Request,
"type": apiRequest,
"version": testAuditVersion,
},
}
if tt.wantAudit.Id != "" {
wantEvent.Data.(map[string]interface{})["id"] = tt.wantAudit.Id
wantEvent.Data.(map[string]interface{})["request_info"] = event.RequestInfo{
Id: gotAudit.Data.(map[string]interface{})["request_info"].(map[string]interface{})["id"].(string),
}
}
if tt.wantAudit.Response != nil {
wantEvent.Data.(map[string]interface{})["response"] = tt.wantAudit.Response
}
wantJson, err := json.Marshal(wantEvent)
require.NoError(err)
assert.JSONEq(string(wantJson), string(actualJson))
}
})
}
t.Run("not-enabled", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
c := event.TestEventerConfig(t, "WriteAudit")
c.EventerConfig.AuditEnabled = false
testLock := &sync.Mutex{}
e, err := event.NewEventer(testLogger, testLock, "not-enabled", c.EventerConfig)
require.NoError(err)
testCtx, err := event.NewEventerContext(context.Background(), e)
require.NoError(err)
testCtx, err = event.NewRequestInfoContext(testCtx, info)
require.NoError(err)
require.NoError(event.WriteAudit(testCtx, "not-enabled", event.WithRequest(testReq), event.WithFlush()))
b, err := ioutil.ReadFile(c.AllEvents.Name())
assert.NoError(err)
assert.Len(b, 0)
})
}
func Test_WriteError(t *testing.T) {
// this test and its subtests cannot be run in parallel because of it's
// dependency on the sysEventer
now := time.Now()
c := event.TestEventerConfig(t, "WriteAudit")
testLock := &sync.Mutex{}
testLogger := testLogger(t, testLock)
e, err := event.NewEventer(testLogger, testLock, "Test_WriteError", c.EventerConfig, event.WithNow(now))
require.NoError(t, err)
info := &event.RequestInfo{Id: "867-5309", EventId: "411"}
testCtx, err := event.NewEventerContext(context.Background(), e)
require.NoError(t, err)
testCtx, err = event.NewRequestInfoContext(testCtx, info)
require.NoError(t, err)
testCtxNoInfoId, err := event.NewEventerContext(context.Background(), e)
require.NoError(t, err)
noId := &event.RequestInfo{Id: "867-5309", EventId: "411"}
testCtxNoInfoId, err = event.NewRequestInfoContext(testCtxNoInfoId, noId)
require.NoError(t, err)
noId.Id = ""
noId.EventId = ""
testError := fakeError{
Msg: "test",
Code: "code",
}
tests := []struct {
name string
ctx context.Context
e error
opt []event.Option
info *event.RequestInfo
setup func() error
cleanup func()
noOperation bool
errSinkFileName string
noOutput bool
}{
{
name: "missing-caller",
ctx: testCtx,
e: &testError,
noOperation: true,
noOutput: true,
},
{
name: "no-ctx-eventer-and-syseventer-not-initialized",
ctx: context.Background(),
e: &testError,
errSinkFileName: c.ErrorEvents.Name(),
noOutput: true,
},
{
name: "use-syseventer",
ctx: context.Background(),
e: &testError,
setup: func() error {
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
},
cleanup: func() { event.TestResetSystEventer(t) },
errSinkFileName: c.ErrorEvents.Name(),
},
{
name: "use-syseventer-with-cancelled-ctx",
ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
return ctx
}(),
e: &testError,
setup: func() error {
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
},
cleanup: func() { event.TestResetSystEventer(t) },
errSinkFileName: c.ErrorEvents.Name(),
},
{
name: "no-info-id",
ctx: testCtxNoInfoId,
e: &testError,
info: &event.RequestInfo{},
setup: func() error {
return event.InitSysEventer(testLogger, testLock, "no-info-id", event.WithEventerConfig(&c.EventerConfig))
},
cleanup: func() { event.TestResetSystEventer(t) },
errSinkFileName: c.ErrorEvents.Name(),
},
{
name: "simple",
ctx: testCtx,
e: &testError,
info: info,
errSinkFileName: c.ErrorEvents.Name(),
},
{
name: "simple-with-opt",
ctx: testCtx,
e: &testError,
opt: []event.Option{event.WithInfo("test", "info")},
info: info,
errSinkFileName: c.ErrorEvents.Name(),
},
{
name: "stderrors",
ctx: testCtx,
e: stderrors.New("test std errors"),
opt: []event.Option{event.WithInfo("test", "info")},
info: info,
errSinkFileName: c.ErrorEvents.Name(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
if tt.setup != nil {
require.NoError(tt.setup())
}
if tt.cleanup != nil {
defer tt.cleanup()
}
op := tt.name
if tt.noOperation {
op = ""
}
event.WriteError(tt.ctx, event.Op(op), tt.e, tt.opt...)
if tt.errSinkFileName != "" {
defer func() { _ = os.WriteFile(tt.errSinkFileName, nil, 0o666) }()
b, err := ioutil.ReadFile(tt.errSinkFileName)
require.NoError(err)
if tt.noOutput {
assert.Lenf(b, 0, "should be an empty file: %s", string(b))
return
}
gotError := &cloudevents.Event{}
err = json.Unmarshal(b, gotError)
require.NoErrorf(err, "json: %s", string(b))
if _, ok := gotError.Data.(map[string]interface{})["error_fields"].(map[string]interface{})["Msg"]; ok {
actualError := fakeError{
Msg: gotError.Data.(map[string]interface{})["error_fields"].(map[string]interface{})["Msg"].(string),
Code: gotError.Data.(map[string]interface{})["error_fields"].(map[string]interface{})["Code"].(string),
}
assert.Equal(tt.e, &actualError)
}
}
})
}
}
type fakeError struct {
Code string
Msg string
}
func (f *fakeError) Error() string {
return f.Msg
}
func Test_WriteSysEvent(t *testing.T) {
// this test and its subtests cannot be run in parallel because of it's
// dependency on the sysEventer
ctx := context.Background()
c := event.TestEventerConfig(t, "Test_WriteSysEvent")
testLock := &sync.Mutex{}
testLogger := testLogger(t, testLock)
tests := []struct {
name string
ctx context.Context
data []interface{}
msg string
info *event.RequestInfo
setup func() error
cleanup func()
noOperation bool
sinkFileName string
noOutput bool
}{
{
name: "no-data",
ctx: ctx,
noOutput: true,
},
{
name: "missing-caller",
ctx: ctx,
msg: "hello",
data: []interface{}{"data", "test-data"},
noOperation: true,
},
{
name: "syseventer-not-initialized",
ctx: context.Background(),
msg: "hello",
data: []interface{}{"data", "test-data"},
sinkFileName: c.AllEvents.Name(),
noOutput: true,
},
{
name: "use-syseventer",
ctx: context.Background(),
msg: "hello",
data: []interface{}{"data", "test-data", event.ServerName, "test-server", event.ServerAddress, "localhost"},
setup: func() error {
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
},
cleanup: func() { event.TestResetSystEventer(t) },
sinkFileName: c.AllEvents.Name(),
},
{
name: "use-syseventer-with-cancelled-ctx",
ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
return ctx
}(),
msg: "hello",
data: []interface{}{"data", "test-data", event.ServerName, "test-server", event.ServerAddress, "localhost"},
setup: func() error {
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
},
cleanup: func() { event.TestResetSystEventer(t) },
sinkFileName: c.AllEvents.Name(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
if tt.setup != nil {
require.NoError(tt.setup())
}
if tt.cleanup != nil {
defer tt.cleanup()
}
op := tt.name
if tt.noOperation {
op = ""
}
event.WriteSysEvent(tt.ctx, event.Op(op), tt.msg, tt.data...)
if tt.sinkFileName != "" {
defer func() { _ = os.WriteFile(tt.sinkFileName, nil, 0o666) }()
b, err := ioutil.ReadFile(tt.sinkFileName)
require.NoError(err)
if tt.noOutput {
assert.Lenf(b, 0, "should be an empty file: %s", string(b))
return
}
gotSysEvent := &cloudevents.Event{}
err = json.Unmarshal(b, gotSysEvent)
require.NoErrorf(err, "json: %s", string(b))
expected := event.ConvertArgs(tt.data...)
expected["msg"] = tt.msg
assert.Equal(expected, gotSysEvent.Data.(map[string]interface{})["data"].(map[string]interface{}))
}
})
}
}
func TestConvertArgs(t *testing.T) {
tests := []struct {
name string
args []interface{}
want map[string]interface{}
}{
{
name: "no-args",
args: []interface{}{},
want: nil,
},
{
name: "nil-first-arg",
args: []interface{}{nil},
want: map[string]interface{}{
event.MissingKey: nil,
},
},
{
name: "odd-number-of-args",
args: []interface{}{1, 2, 3},
want: map[string]interface{}{
"1": 2,
event.MissingKey: 3,
},
},
{
name: "struct-key",
args: []interface{}{[]struct{ name string }{{name: "alice"}}, 1},
want: map[string]interface{}{
"[{alice}]": 1,
},
},
{
name: "test-key-with-stringer",
args: []interface{}{testIntKeyWithStringer(11), "eleven"},
want: map[string]interface{}{
"*11*": "eleven",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
got := event.ConvertArgs(tt.args...)
assert.Equal(tt.want, got)
})
}
}
type testIntKeyWithStringer int
func (ti testIntKeyWithStringer) String() string {
return fmt.Sprint("*", int(ti), "*")
}
func testLogger(t *testing.T, testLock hclog.Locker) hclog.Logger {
t.Helper()
return hclog.New(&hclog.LoggerOptions{
Mutex: testLock,
Name: "test",
JSONFormat: true,
})
}