You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/event/context_test.go

1430 lines
40 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package event_test
import (
"context"
"encoding/json"
stderrors "errors"
"fmt"
"os"
"sync"
"testing"
"time"
"github.com/hashicorp/boundary/internal/event"
pbs "github.com/hashicorp/boundary/internal/gen/testing/event"
"github.com/hashicorp/eventlogger/filters/encrypt"
"github.com/hashicorp/eventlogger/formatter_filters/cloudevents"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-uuid"
"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:"-"`
CorrelationId string `json:"correlation_id,omitempty"`
}
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 []any
details []any
}
testPayloads := []observationPayload{
{
header: []any{"name", "bar"},
},
{
header: []any{"list", []string{"1", "2"}},
},
{
details: []any{"file", "temp-file.txt"},
},
}
testWantHeader := map[string]any{
"name": "bar",
"list": []string{"1", "2"},
}
testWantDetails := map[string]any{
"file": "temp-file.txt",
}
tests := []struct {
name string
noOperation bool
noFlush bool
telemetryFlag bool
observationPayload []observationPayload
header map[string]any
details map[string]any
Request *event.Request
Response *event.Response
ctx context.Context
observationSinkFileName string
setup func() error
cleanup func()
wantErrIs error
wantErrContains string
}{
{
name: "no-info-event-id",
noFlush: true,
telemetryFlag: true,
ctx: testCtxNoEventInfoId,
observationPayload: []observationPayload{
{
header: []any{"name", "bar"},
},
},
header: map[string]any{
"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-header-or-details-in-payload-no-request-no-response",
ctx: testCtx,
noFlush: true,
observationPayload: []observationPayload{
{},
},
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "specify either header or details options or request or response for an event payload",
},
{
name: "telemetry-not-enabled-but-request-or-response-available",
ctx: testCtx,
noFlush: true,
telemetryFlag: false,
Request: &event.Request{
Operation: "create-test",
Endpoint: "0.0.0.0",
},
observationPayload: testPayloads,
},
{
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,
telemetryFlag: true,
ctx: context.Background(),
observationPayload: []observationPayload{
{
header: []any{"name", "bar"},
},
},
header: map[string]any{
"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,
telemetryFlag: true,
ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
return ctx
}(),
observationPayload: []observationPayload{
{
header: []any{"name", "bar"},
},
},
header: map[string]any{
"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-header",
ctx: testCtx,
telemetryFlag: true,
observationPayload: testPayloads,
header: testWantHeader,
},
{
name: "simple-details",
ctx: testCtx,
telemetryFlag: true,
observationPayload: testPayloads,
details: testWantDetails,
},
}
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...),
event.WithRequest(tt.Request),
event.WithResponse(tt.Response))
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.telemetryFlag {
require.Nil(event.WriteObservation(tt.ctx, event.Op(tt.name), event.WithRequest(tt.Request),
event.WithResponse(tt.Response)))
require.Nil(event.WriteObservation(tt.ctx, event.Op(tt.name), event.WithDetails(tt.details)))
}
if tt.observationSinkFileName != "" {
defer func() { _ = os.WriteFile(tt.observationSinkFileName, nil, 0o666) }()
b, err := os.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]any{
"list": []string{"1", "2"},
}
require.NoError(event.WriteObservation(testCtx, "not-enabled", event.WithHeader(hdr), event.WithFlush()))
b, err := os.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 []any
found bool
}{
{
name: "allowed",
allow: []string{`"/data/list" contains "1"`},
hdr: []any{"list", []string{"1", "2"}},
found: true,
},
{
name: "not-allowed",
allow: []string{`"/data/list" contains "22"`},
hdr: []any{"list", []string{"1", "2"}},
found: false,
},
{
name: "deny",
deny: []string{`"/data/list" contains "1"`},
hdr: []any{"list", []string{"1", "2"}},
found: false,
},
{
name: "not-deny",
deny: []string{`"/data/list" contains "22"`},
hdr: []any{"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 := os.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]any) []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]any{
event.RequestInfoField: reqInfo,
event.VersionField: testObservationVersion,
},
}
if hdr != nil {
h := j.Data.(map[string]any)
for k, v := range hdr {
h[k] = v
}
}
if details != nil {
details[event.OpField] = string(caller)
d := got.Data.(map[string]any)[event.DetailsField].([]any)[0].(map[string]any)
j.Data.(map[string]any)[event.DetailsField] = []struct {
CreatedAt string `json:"created_at"`
Type string `json:"type"`
Payload map[string]any `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)
corId, err := uuid.GenerateUUID()
require.NoError(t, err)
ctx, err = event.NewCorrelationIdContext(ctx, corId)
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: func() context.Context {
ctx, err := event.NewCorrelationIdContext(context.Background(), corId)
require.NoError(t, err)
return ctx
}(),
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)
}(),
CorrelationId: corId,
},
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())
ctx, err = event.NewCorrelationIdContext(ctx, corId)
require.NoError(t, err)
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)
}(),
CorrelationId: corId,
},
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",
CorrelationId: corId,
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 := os.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]any{
"auth": tt.wantAudit.Auth,
"id": gotAudit.Data.(map[string]any)["id"],
"timestamp": now,
"request": tt.wantAudit.Request,
"type": apiRequest,
"version": testAuditVersion,
"correlation_id": tt.wantAudit.CorrelationId,
},
}
if tt.wantAudit.Id != "" {
wantEvent.Data.(map[string]any)["id"] = tt.wantAudit.Id
wantEvent.Data.(map[string]any)["request_info"] = event.RequestInfo{
Id: gotAudit.Data.(map[string]any)["request_info"].(map[string]any)["id"].(string),
}
}
if tt.wantAudit.Response != nil {
wantEvent.Data.(map[string]any)["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 := os.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 := os.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]any)["error_fields"].(map[string]any)["Msg"]; ok {
actualError := fakeError{
Msg: gotError.Data.(map[string]any)["error_fields"].(map[string]any)["Msg"].(string),
Code: gotError.Data.(map[string]any)["error_fields"].(map[string]any)["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 []any
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: []any{"data", "test-data"},
noOperation: true,
},
{
name: "syseventer-not-initialized",
ctx: context.Background(),
msg: "hello",
data: []any{"data", "test-data"},
sinkFileName: c.AllEvents.Name(),
noOutput: true,
},
{
name: "use-syseventer",
ctx: context.Background(),
msg: "hello",
data: []any{"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: []any{"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 := os.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]any)["data"].(map[string]any))
}
})
}
}
func TestConvertArgs(t *testing.T) {
tests := []struct {
name string
args []any
want map[string]any
}{
{
name: "no-args",
args: []any{},
want: nil,
},
{
name: "nil-first-arg",
args: []any{nil},
want: map[string]any{
event.MissingKey: nil,
},
},
{
name: "odd-number-of-args",
args: []any{1, 2, 3},
want: map[string]any{
"1": 2,
event.MissingKey: 3,
},
},
{
name: "struct-key",
args: []any{[]struct{ name string }{{name: "alice"}}, 1},
want: map[string]any{
"[{alice}]": 1,
},
},
{
name: "test-key-with-stringer",
args: []any{testIntKeyWithStringer(11), "eleven"},
want: map[string]any{
"*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,
})
}
func Test_NewCorrelationIdContext(t *testing.T) {
corId, err := uuid.GenerateUUID()
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
correlationId string
wantErrIs error
wantErrContains string
}{
{
name: "missing-ctx",
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing context",
},
{
name: "missing-correlation-id",
ctx: context.Background(),
wantErrIs: event.ErrInvalidParameter,
wantErrContains: "missing correlation id",
},
{
name: "valid",
ctx: context.Background(),
correlationId: corId,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
ctx, err := event.NewCorrelationIdContext(tt.ctx, tt.correlationId)
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.NoError(err)
require.NotNil(ctx)
got, ok := event.CorrelationIdFromContext(ctx)
require.True(ok)
assert.Equal(tt.correlationId, got)
})
}
}
func Test_CorrelationIdFromContext(t *testing.T) {
corId, err := uuid.GenerateUUID()
require.NoError(t, err)
testCtx, err := event.NewCorrelationIdContext(context.Background(), corId)
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
wantCorId string
wantNotOk bool
}{
{
name: "missing-ctx",
wantNotOk: true,
},
{
name: "no-correlation-id",
ctx: context.Background(),
wantNotOk: true,
},
{
name: "valid",
ctx: testCtx,
wantCorId: corId,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, ok := event.CorrelationIdFromContext(tt.ctx)
if tt.wantNotOk {
require.False(ok)
assert.Empty(got)
return
}
require.True(ok)
assert.Equal(tt.wantCorId, got)
})
}
}