Files
threshold/internal/observability/event/context_test.go
T
hashicorp-copywrite[bot]andJohan Brandhorst-Satzkorn 29da0bcb92 [COMPLIANCE] License changes (#3567)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* Update copyright file headers to BUS-1.1

* Rerun make gen

This will pick up the last of the license changes

* Revert "Rerun make gen"

This reverts commit dbe0968bffd92745d8663c6940debae098395a6b.

* Remove copywrite from generation script

The current version of copywrite is not sophisticated enough
to only apply the copywrite changes to certain subdirectories,
so to avoid accidentally relicensing our api and sdk modules,
we will stop running the copywrite script until such time
that it can do the right thing.

* License protobuf API as MPL

The protobuf API files are used to generate the SDK,
since we want the SDK to be MPL, the protobuf files
must also be MPL. This extends to all protobuf files
whose sources end up in the SDK directory, including:

- internal/proto/controller/api
- internal/proto/controller/custom_options
- internal/proto/plugin

It also includes the generated service directory in
internal/gen/controller/api/services, since they are
generated from the protobuf sources used for the SDK.

* Change website license file name

This makes the naming scheme consistent with the rest
of the repository.

* Add note on licensing to README

* Set copywrite license to BUSL

* Revert "Add note on licensing to README"

This reverts commit 4d865a91a7a4f11605f0dc4609355d7e3c3cfffe.

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
2023-08-11 10:31:53 -07:00

1283 lines
36 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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 []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
observationPayload []observationPayload
header map[string]any
details map[string]any
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: []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-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: []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,
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",
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]any{
"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 []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 := 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]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)
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]any{
"auth": tt.wantAudit.Auth,
"id": gotAudit.Data.(map[string]any)["id"],
"timestamp": now,
"request": tt.wantAudit.Request,
"type": apiRequest,
"version": testAuditVersion,
},
}
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 := 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]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 := 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]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,
})
}