// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package event import ( "os" "path/filepath" "testing" "time" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/eventlogger" "github.com/stretchr/testify/require" ) // TestFileSink_Type ensures that the node is a 'sink' type. func TestFileSink_Type(t *testing.T) { f, err := NewFileSink(filepath.Join(t.TempDir(), "vault.log"), "json") require.NoError(t, err) require.NotNil(t, f) require.Equal(t, eventlogger.NodeTypeSink, f.Type()) } // TestNewFileSink tests creation of an AuditFileSink. func TestNewFileSink(t *testing.T) { tests := map[string]struct { ShouldUseAbsolutePath bool // Path should contain the filename if temp dir is true Path string Format string Options []Option IsErrorExpected bool ExpectedErrorMessage string // Expected values of AuditFileSink ExpectedFileMode os.FileMode ExpectedFormat string ExpectedPath string ExpectedPrefix string }{ "default-values": { ShouldUseAbsolutePath: true, IsErrorExpected: true, ExpectedErrorMessage: "event.NewFileSink: path is required", }, "spacey-path": { ShouldUseAbsolutePath: true, Path: " ", Format: "json", IsErrorExpected: true, ExpectedErrorMessage: "event.NewFileSink: path is required", }, "valid-path-and-format": { Path: "vault.log", Format: "json", IsErrorExpected: false, ExpectedFileMode: defaultFileMode, ExpectedFormat: "json", ExpectedPrefix: "", }, "file-mode-not-default-or-zero": { Path: "vault.log", Format: "json", Options: []Option{WithFileMode("0007")}, IsErrorExpected: false, ExpectedFormat: "json", ExpectedPrefix: "", ExpectedFileMode: 0o007, }, "prefix": { Path: "vault.log", Format: "json", Options: []Option{WithFileMode("0007")}, IsErrorExpected: false, ExpectedPrefix: "bleep", ExpectedFormat: "json", ExpectedFileMode: 0o007, }, } for name, tc := range tests { name := name tc := tc t.Run(name, func(t *testing.T) { // t.Parallel() // If we need a real directory as a path we can use a temp dir. // but we should keep track of it for comparison in the new sink. var tempDir string tempPath := tc.Path if !tc.ShouldUseAbsolutePath { tempDir = t.TempDir() tempPath = filepath.Join(tempDir, tempPath) } sink, err := NewFileSink(tempPath, tc.Format, tc.Options...) switch { case tc.IsErrorExpected: require.Error(t, err) require.EqualError(t, err, tc.ExpectedErrorMessage) require.Nil(t, sink) default: require.NoError(t, err) require.NotNil(t, sink) // Assert properties are correct. require.Equal(t, tc.ExpectedFormat, sink.requiredFormat) require.Equal(t, tc.ExpectedFileMode, sink.fileMode) switch { case tc.ShouldUseAbsolutePath: require.Equal(t, tc.ExpectedPath, sink.path) default: require.Equal(t, tempPath, sink.path) } } }) } } // TestFileSink_Reopen tests that the sink reopens files as expected when requested to. // stdout and discard paths are ignored. // see: https://developer.hashicorp.com/vault/docs/audit/file#file_path func TestFileSink_Reopen(t *testing.T) { tests := map[string]struct { Path string ShouldUseAbsolutePath bool ShouldCreateFile bool ShouldIgnoreFileMode bool Options []Option IsErrorExpected bool ExpectedErrorMessage string ExpectedFileMode os.FileMode }{ // Should be ignored by Reopen "devnull": { Path: "/dev/null", ShouldUseAbsolutePath: true, ShouldIgnoreFileMode: true, }, "happy": { Path: "vault.log", ExpectedFileMode: os.FileMode(defaultFileMode), }, "filemode-existing": { Path: "vault.log", ShouldCreateFile: true, Options: []Option{WithFileMode("0000")}, ExpectedFileMode: os.FileMode(defaultFileMode), }, } for name, tc := range tests { name := name tc := tc t.Run(name, func(t *testing.T) { t.Parallel() // If we need a real directory as a path we can use a temp dir. // but we should keep track of it for comparison in the new sink. var tempDir string tempPath := tc.Path if !tc.ShouldUseAbsolutePath { tempDir = t.TempDir() tempPath = filepath.Join(tempDir, tc.Path) } // If the file mode is 0 then we will need a pre-created file to stat. // Only do this for paths that are not 'special keywords' if tc.ShouldCreateFile && tc.Path != devnull { f, err := os.OpenFile(tempPath, os.O_CREATE, defaultFileMode) require.NoError(t, err) defer func() { err = os.Remove(f.Name()) require.NoError(t, err) }() } sink, err := NewFileSink(tempPath, "json", tc.Options...) require.NoError(t, err) require.NotNil(t, sink) err = sink.Reopen() switch { case tc.IsErrorExpected: require.Error(t, err) require.EqualError(t, err, tc.ExpectedErrorMessage) default: require.NoError(t, err) info, err := os.Stat(tempPath) require.NoError(t, err) require.NotNil(t, info) if !tc.ShouldIgnoreFileMode { require.Equal(t, tc.ExpectedFileMode, info.Mode()) } } }) } } // TestFileSink_Process ensures that Process behaves as expected. func TestFileSink_Process(t *testing.T) { tests := map[string]struct { ShouldUseAbsolutePath bool Path string ShouldCreateFile bool Format string ShouldIgnoreFormat bool Data string ShouldUseNilEvent bool IsErrorExpected bool ExpectedErrorMessage string }{ "devnull": { ShouldUseAbsolutePath: true, Path: devnull, Format: "json", Data: "foo", IsErrorExpected: false, }, "no-formatted-data": { ShouldCreateFile: true, Path: "juan.log", Format: "json", Data: "foo", ShouldIgnoreFormat: true, IsErrorExpected: true, ExpectedErrorMessage: "event.(FileSink).Process: unable to retrieve event formatted as \"json\"", }, "nil": { Path: "foo.log", Format: "json", Data: "foo", ShouldUseNilEvent: true, IsErrorExpected: true, ExpectedErrorMessage: "event.(FileSink).Process: event is nil: invalid parameter", }, } for name, tc := range tests { name := name tc := tc t.Run(name, func(t *testing.T) { // Temp dir for most testing unless we're trying to test an error var tempDir string tempPath := tc.Path if !tc.ShouldUseAbsolutePath { tempDir = t.TempDir() tempPath = filepath.Join(tempDir, tc.Path) } // Create a file if we will need it there before Process kicks off. if tc.ShouldCreateFile && tc.Path != devnull { f, err := os.OpenFile(tempPath, os.O_CREATE, defaultFileMode) require.NoError(t, err) defer func() { err = os.Remove(f.Name()) require.NoError(t, err) }() } // Set up a sink sink, err := NewFileSink(tempPath, tc.Format) require.NoError(t, err) require.NotNil(t, sink) // Generate a fake event ctx := namespace.RootContext(nil) event := &eventlogger.Event{ Type: "audit", CreatedAt: time.Now(), Formatted: make(map[string][]byte), Payload: struct{ ID string }{ID: "123"}, } if !tc.ShouldIgnoreFormat { event.FormattedAs(tc.Format, []byte(tc.Data)) } if tc.ShouldUseNilEvent { event = nil } // The actual exercising of the sink. event, err = sink.Process(ctx, event) switch { case tc.IsErrorExpected: require.Error(t, err) require.EqualError(t, err, tc.ExpectedErrorMessage) require.Nil(t, event) default: require.NoError(t, err) require.Nil(t, event) } }) } }