VAULT-17772: audit event base (#21577)

* observability/event package, and basic error
* sink types (and validation test)
* event types (and validation test)
* options for events (and tests)
* audit event type (and tests)
This commit is contained in:
Peter Wilson
2023-07-06 11:06:27 +01:00
committed by GitHub
parent 4c1a7b53d3
commit 5c02e3f255
10 changed files with 1040 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package event
import (
"errors"
)
var ErrInvalidParameter = errors.New("invalid parameter")

View File

@@ -0,0 +1,26 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package event
import (
"fmt"
)
// EventType represents the event's type
type EventType string
const (
AuditType EventType = "audit" // AuditType represents audit events
)
// Validate ensures that EventType is one of the set of allowed event types.
func (et EventType) Validate() error {
const op = "event.(EventType).Validate"
switch et {
case AuditType:
return nil
default:
return fmt.Errorf("%s: '%s' is not a valid event type: %w", op, et, ErrInvalidParameter)
}
}

View File

@@ -0,0 +1,135 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package event
import (
"fmt"
"time"
"github.com/hashicorp/vault/sdk/logical"
)
// Audit subtypes.
const (
AuditRequest auditSubtype = "AuditRequest"
AuditResponse auditSubtype = "AuditResponse"
)
// Audit formats.
const (
AuditFormatJSON auditFormat = "json"
AuditFormatJSONX auditFormat = "jsonx"
)
// auditVersion defines the version of audit events.
const auditVersion = "v0.1"
// auditSubtype defines the type of audit event.
type auditSubtype string
// auditFormat defines types of format audit events support.
type auditFormat string
// audit is the audit event.
type audit struct {
ID string `json:"id"`
Version string `json:"version"`
Subtype auditSubtype `json:"subtype"` // the subtype of the audit event.
Timestamp time.Time `json:"timestamp"`
Data *logical.LogInput `json:"data"`
RequiredFormat auditFormat `json:"format"`
}
// newAudit should be used to create an audit event.
// auditSubtype and auditFormat are needed for audit.
// It will use the supplied options, generate an ID if required, and validate the event.
func newAudit(opt ...Option) (*audit, error) {
const op = "event.newAudit"
opts, err := getOpts(opt...)
if err != nil {
return nil, fmt.Errorf("%s: error applying options: %w", op, err)
}
if opts.withID == "" {
var err error
opts.withID, err = NewID(string(AuditType))
if err != nil {
return nil, fmt.Errorf("%s: error creating ID for event: %w", op, err)
}
}
audit := &audit{
ID: opts.withID,
Version: auditVersion,
Subtype: auditSubtype(opts.withSubtype),
Timestamp: opts.withNow,
RequiredFormat: auditFormat(opts.withFormat),
}
if err := audit.validate(); err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
return audit, nil
}
// validate attempts to ensure the event has the basic requirements of the event type configured.
func (a *audit) validate() error {
const op = "event.(audit).validate"
if a == nil {
return fmt.Errorf("%s: audit is nil: %w", op, ErrInvalidParameter)
}
if a.ID == "" {
return fmt.Errorf("%s: missing ID: %w", op, ErrInvalidParameter)
}
if a.Version != auditVersion {
return fmt.Errorf("%s: audit version unsupported: %w", op, ErrInvalidParameter)
}
if a.Timestamp.IsZero() {
return fmt.Errorf("%s: audit timestamp cannot be the zero time instant: %w", op, ErrInvalidParameter)
}
err := a.Subtype.validate()
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
err = a.RequiredFormat.validate()
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
return nil
}
// validate ensures that auditSubtype is one of the set of allowed event subtypes.
func (t auditSubtype) validate() error {
const op = "event.(audit).(subtype).validate"
switch t {
case AuditRequest, AuditResponse:
return nil
default:
return fmt.Errorf("%s: '%s' is not a valid event subtype: %w", op, t, ErrInvalidParameter)
}
}
// validate ensures that auditFormat is one of the set of allowed event formats.
func (f auditFormat) validate() error {
const op = "event.(audit).(format).validate"
switch f {
case AuditFormatJSON, AuditFormatJSONX:
return nil
default:
return fmt.Errorf("%s: '%s' is not a valid required format: %w", op, f, ErrInvalidParameter)
}
}
// String returns the string version of an auditFormat.
func (f auditFormat) String() string {
return string(f)
}

View File

@@ -0,0 +1,293 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package event
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestAuditEvent_New exercises the newAudit func to create audit events.
func TestAuditEvent_New(t *testing.T) {
tests := map[string]struct {
Options []Option
IsErrorExpected bool
ExpectedErrorMessage string
ExpectedID string
ExpectedFormat auditFormat
ExpectedSubtype auditSubtype
ExpectedTimestamp time.Time
IsNowExpected bool
}{
"nil": {
Options: nil,
IsErrorExpected: true,
ExpectedErrorMessage: "event.newAudit: event.(audit).validate: event.(audit).(subtype).validate: '' is not a valid event subtype: invalid parameter",
},
"empty-option": {
Options: []Option{},
IsErrorExpected: true,
ExpectedErrorMessage: "event.newAudit: event.(audit).validate: event.(audit).(subtype).validate: '' is not a valid event subtype: invalid parameter",
},
"bad-id": {
Options: []Option{WithID("")},
IsErrorExpected: true,
ExpectedErrorMessage: "event.newAudit: error applying options: id cannot be empty",
},
"good": {
Options: []Option{
WithID("audit_123"),
WithFormat(string(AuditFormatJSON)),
WithSubtype(string(AuditResponse)),
WithNow(time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{})),
},
IsErrorExpected: false,
ExpectedID: "audit_123",
ExpectedTimestamp: time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{}),
ExpectedSubtype: AuditResponse,
ExpectedFormat: AuditFormatJSON,
},
"good-no-time": {
Options: []Option{
WithID("audit_123"),
WithFormat(string(AuditFormatJSON)),
WithSubtype(string(AuditResponse)),
},
IsErrorExpected: false,
ExpectedID: "audit_123",
ExpectedSubtype: AuditResponse,
ExpectedFormat: AuditFormatJSON,
IsNowExpected: true,
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
audit, err := newAudit(tc.Options...)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
require.Nil(t, audit)
default:
require.NoError(t, err)
require.NotNil(t, audit)
require.Equal(t, tc.ExpectedID, audit.ID)
require.Equal(t, tc.ExpectedSubtype, audit.Subtype)
require.Equal(t, tc.ExpectedFormat, audit.RequiredFormat)
switch {
case tc.IsNowExpected:
require.True(t, time.Now().After(audit.Timestamp))
require.False(t, audit.Timestamp.IsZero())
default:
require.Equal(t, tc.ExpectedTimestamp, audit.Timestamp)
}
}
})
}
}
// TestAuditEvent_Validate exercises the validation for an audit event.
func TestAuditEvent_Validate(t *testing.T) {
tests := map[string]struct {
Value *audit
IsErrorExpected bool
ExpectedErrorMessage string
}{
"nil": {
Value: nil,
IsErrorExpected: true,
ExpectedErrorMessage: "event.(audit).validate: audit is nil: invalid parameter",
},
"default": {
Value: &audit{},
IsErrorExpected: true,
ExpectedErrorMessage: "event.(audit).validate: missing ID: invalid parameter",
},
"id-empty": {
Value: &audit{
ID: "",
Version: auditVersion,
Subtype: AuditRequest,
Timestamp: time.Now(),
Data: nil,
RequiredFormat: AuditFormatJSON,
},
IsErrorExpected: true,
ExpectedErrorMessage: "event.(audit).validate: missing ID: invalid parameter",
},
"version-fiddled": {
Value: &audit{
ID: "audit_123",
Version: "magic-v2",
Subtype: AuditRequest,
Timestamp: time.Now(),
Data: nil,
RequiredFormat: AuditFormatJSON,
},
IsErrorExpected: true,
ExpectedErrorMessage: "event.(audit).validate: audit version unsupported: invalid parameter",
},
"subtype-fiddled": {
Value: &audit{
ID: "audit_123",
Version: auditVersion,
Subtype: auditSubtype("moon"),
Timestamp: time.Now(),
Data: nil,
RequiredFormat: AuditFormatJSON,
},
IsErrorExpected: true,
ExpectedErrorMessage: "event.(audit).validate: event.(audit).(subtype).validate: 'moon' is not a valid event subtype: invalid parameter",
},
"format-fiddled": {
Value: &audit{
ID: "audit_123",
Version: auditVersion,
Subtype: AuditResponse,
Timestamp: time.Now(),
Data: nil,
RequiredFormat: auditFormat("blah"),
},
IsErrorExpected: true,
ExpectedErrorMessage: "event.(audit).validate: event.(audit).(format).validate: 'blah' is not a valid required format: invalid parameter",
},
"default-time": {
Value: &audit{
ID: "audit_123",
Version: auditVersion,
Subtype: AuditResponse,
Timestamp: time.Time{},
Data: nil,
RequiredFormat: AuditFormatJSON,
},
IsErrorExpected: true,
ExpectedErrorMessage: "event.(audit).validate: audit timestamp cannot be the zero time instant: invalid parameter",
},
"valid": {
Value: &audit{
ID: "audit_123",
Version: auditVersion,
Subtype: AuditResponse,
Timestamp: time.Now(),
Data: nil,
RequiredFormat: AuditFormatJSON,
},
IsErrorExpected: false,
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
err := tc.Value.validate()
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
default:
require.NoError(t, err)
}
})
}
}
// TestAuditEvent_Validate_Subtype exercises the validation for an audit event's subtype.
func TestAuditEvent_Validate_Subtype(t *testing.T) {
tests := map[string]struct {
Value string
IsErrorExpected bool
ExpectedErrorMessage string
}{
"empty": {
Value: "",
IsErrorExpected: true,
ExpectedErrorMessage: "event.(audit).(subtype).validate: '' is not a valid event subtype: invalid parameter",
},
"unsupported": {
Value: "foo",
IsErrorExpected: true,
ExpectedErrorMessage: "event.(audit).(subtype).validate: 'foo' is not a valid event subtype: invalid parameter",
},
"request": {
Value: "AuditRequest",
IsErrorExpected: false,
},
"response": {
Value: "AuditResponse",
IsErrorExpected: false,
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
err := auditSubtype(tc.Value).validate()
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
default:
require.NoError(t, err)
}
})
}
}
// TestAuditEvent_Validate_Format exercises the validation for an audit event's format.
func TestAuditEvent_Validate_Format(t *testing.T) {
tests := map[string]struct {
Value string
IsErrorExpected bool
ExpectedErrorMessage string
}{
"empty": {
Value: "",
IsErrorExpected: true,
ExpectedErrorMessage: "event.(audit).(format).validate: '' is not a valid required format: invalid parameter",
},
"unsupported": {
Value: "foo",
IsErrorExpected: true,
ExpectedErrorMessage: "event.(audit).(format).validate: 'foo' is not a valid required format: invalid parameter",
},
"json": {
Value: "json",
IsErrorExpected: false,
},
"jsonx": {
Value: "jsonx",
IsErrorExpected: false,
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
err := auditFormat(tc.Value).validate()
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
default:
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package event
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestEventType_Validate exercises the Validate method for EventType.
func TestEventType_Validate(t *testing.T) {
tests := map[string]struct {
Value string
IsValid bool
ExpectedError string
}{
"audit": {
Value: "audit",
IsValid: true,
},
"empty": {
Value: "",
IsValid: false,
ExpectedError: "event.(EventType).Validate: '' is not a valid event type: invalid parameter",
},
"random": {
Value: "random",
IsValid: false,
ExpectedError: "event.(EventType).Validate: 'random' is not a valid event type: invalid parameter",
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
eventType := EventType(tc.Value)
err := eventType.Validate()
switch {
case tc.IsValid:
require.NoError(t, err)
case !tc.IsValid:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedError)
}
})
}
}

View File

@@ -0,0 +1,131 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package event
import (
"errors"
"fmt"
"strings"
"time"
"github.com/hashicorp/go-uuid"
)
// Option is how Options are passed as arguments.
type Option func(*options) error
// options are used to represent configuration for an Event.
type options struct {
withID string
withNow time.Time
withSubtype string
withFormat string
}
// getDefaultOptions returns options with their default values.
func getDefaultOptions() options {
return options{
withNow: time.Now(),
}
}
// getOpts applies all the supplied Option and returns configured options.
// Each Option is applied in the order it appears in the argument list, so it is
// possible to supply the same Option numerous times and the 'last write wins'.
func getOpts(opt ...Option) (options, error) {
opts := getDefaultOptions()
for _, o := range opt {
if o == nil {
continue
}
if err := o(&opts); err != nil {
return options{}, err
}
}
return opts, nil
}
// NewID is a bit of a modified NewID has been done to stop a circular
// dependency with the errors package that is caused by importing
// boundary/internal/db
func NewID(prefix string) (string, error) {
const op = "event.NewID"
if prefix == "" {
return "", fmt.Errorf("%s: missing prefix: %w", op, ErrInvalidParameter)
}
id, err := uuid.GenerateUUID()
if err != nil {
return "", fmt.Errorf("%s: unable to generate ID: %w", op, err)
}
return fmt.Sprintf("%s_%s", prefix, id), nil
}
// WithID allows an optional ID.
func WithID(id string) Option {
return func(o *options) error {
var err error
id := strings.TrimSpace(id)
switch {
case id == "":
err = errors.New("id cannot be empty")
default:
o.withID = id
}
return err
}
}
// WithNow allows an option to represent 'now'.
func WithNow(now time.Time) Option {
return func(o *options) error {
var err error
switch {
case now.IsZero():
err = errors.New("cannot specify 'now' to be the zero time instant")
default:
o.withNow = now
}
return err
}
}
// WithSubtype allows an option to represent the subtype.
func WithSubtype(subtype string) Option {
return func(o *options) error {
var err error
subtype := strings.TrimSpace(subtype)
switch {
case subtype == "":
err = errors.New("subtype cannot be empty")
default:
o.withSubtype = subtype
}
return err
}
}
// WithFormat allows an option to represent event format.
func WithFormat(format string) Option {
return func(o *options) error {
var err error
format := strings.TrimSpace(format)
switch {
case format == "":
err = errors.New("format cannot be empty")
default:
o.withFormat = format
}
return err
}
}

View File

@@ -0,0 +1,306 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package event
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestOptions_WithFormat exercises WithFormat option to ensure it performs as expected.
func TestOptions_WithFormat(t *testing.T) {
tests := map[string]struct {
Value string
IsErrorExpected bool
ExpectedErrorMessage string
ExpectedValue string
}{
"empty": {
Value: "",
IsErrorExpected: true,
ExpectedErrorMessage: "format cannot be empty",
},
"whitespace": {
Value: " ",
IsErrorExpected: true,
ExpectedErrorMessage: "format cannot be empty",
},
"valid": {
Value: "test",
IsErrorExpected: false,
ExpectedValue: "test",
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
options := &options{}
applyOption := WithFormat(tc.Value)
err := applyOption(options)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
default:
require.NoError(t, err)
require.Equal(t, tc.ExpectedValue, options.withFormat)
}
})
}
}
// TestOptions_WithSubtype exercises WithSubtype option to ensure it performs as expected.
func TestOptions_WithSubtype(t *testing.T) {
tests := map[string]struct {
Value string
IsErrorExpected bool
ExpectedErrorMessage string
ExpectedValue string
}{
"empty": {
Value: "",
IsErrorExpected: true,
ExpectedErrorMessage: "subtype cannot be empty",
},
"whitespace": {
Value: " ",
IsErrorExpected: true,
ExpectedErrorMessage: "subtype cannot be empty",
},
"valid": {
Value: "test",
IsErrorExpected: false,
ExpectedValue: "test",
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
options := &options{}
applyOption := WithSubtype(tc.Value)
err := applyOption(options)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
default:
require.NoError(t, err)
require.Equal(t, tc.ExpectedValue, options.withSubtype)
}
})
}
}
// TestOptions_WithNow exercises WithNow option to ensure it performs as expected.
func TestOptions_WithNow(t *testing.T) {
tests := map[string]struct {
Value time.Time
IsErrorExpected bool
ExpectedErrorMessage string
ExpectedValue time.Time
}{
"default-time": {
Value: time.Time{},
IsErrorExpected: true,
ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant",
},
"valid-time": {
Value: time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{}),
IsErrorExpected: false,
ExpectedValue: time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{}),
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
options := &options{}
applyOption := WithNow(tc.Value)
err := applyOption(options)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
default:
require.NoError(t, err)
require.Equal(t, tc.ExpectedValue, options.withNow)
}
})
}
}
// TestOptions_WithID exercises WithID option to ensure it performs as expected.
func TestOptions_WithID(t *testing.T) {
tests := map[string]struct {
Value string
IsErrorExpected bool
ExpectedErrorMessage string
ExpectedValue string
}{
"empty": {
Value: "",
IsErrorExpected: true,
ExpectedErrorMessage: "id cannot be empty",
},
"whitespace": {
Value: " ",
IsErrorExpected: true,
ExpectedErrorMessage: "id cannot be empty",
},
"valid": {
Value: "test",
IsErrorExpected: false,
ExpectedValue: "test",
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
options := &options{}
applyOption := WithID(tc.Value)
err := applyOption(options)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
default:
require.NoError(t, err)
require.Equal(t, tc.ExpectedValue, options.withID)
}
})
}
}
// TestOptions_Default exercises getDefaultOptions to assert the default values.
func TestOptions_Default(t *testing.T) {
opts := getDefaultOptions()
require.NotNil(t, opts)
require.True(t, time.Now().After(opts.withNow))
require.False(t, opts.withNow.IsZero())
}
// TestOptions_Opts exercises getOpts with various Option values.
func TestOptions_Opts(t *testing.T) {
tests := map[string]struct {
opts []Option
IsErrorExpected bool
ExpectedErrorMessage string
ExpectedID string
ExpectedSubtype string
ExpectedFormat string
IsNowExpected bool
ExpectedNow time.Time
}{
"nil-options": {
opts: nil,
IsErrorExpected: false,
IsNowExpected: true,
},
"empty-options": {
opts: []Option{},
IsErrorExpected: false,
IsNowExpected: true,
},
"with-multiple-valid-id": {
opts: []Option{
WithID("qwerty"),
WithID("juan"),
},
IsErrorExpected: false,
ExpectedID: "juan",
IsNowExpected: true,
},
"with-multiple-valid-subtype": {
opts: []Option{
WithSubtype("qwerty"),
WithSubtype("juan"),
},
IsErrorExpected: false,
ExpectedSubtype: "juan",
IsNowExpected: true,
},
"with-multiple-valid-format": {
opts: []Option{
WithFormat("qwerty"),
WithFormat("juan"),
},
IsErrorExpected: false,
ExpectedFormat: "juan",
IsNowExpected: true,
},
"with-multiple-valid-now": {
opts: []Option{
WithNow(time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{})),
WithNow(time.Date(2023, time.July, 4, 13, 0o3, 0o0, 0o0, &time.Location{})),
},
IsErrorExpected: false,
ExpectedNow: time.Date(2023, time.July, 4, 13, 0o3, 0o0, 0o0, &time.Location{}),
IsNowExpected: false,
},
"with-multiple-valid-then-invalid-now": {
opts: []Option{
WithNow(time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{})),
WithNow(time.Time{}),
},
IsErrorExpected: true,
ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant",
},
"with-multiple-valid-options": {
opts: []Option{
WithID("qwerty"),
WithSubtype("typey2"),
WithFormat("json"),
WithNow(time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{})),
},
IsErrorExpected: false,
ExpectedID: "qwerty",
ExpectedSubtype: "typey2",
ExpectedFormat: "json",
ExpectedNow: time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{}),
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
opts, err := getOpts(tc.opts...)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
default:
require.NotNil(t, opts)
require.NoError(t, err)
require.Equal(t, tc.ExpectedID, opts.withID)
require.Equal(t, tc.ExpectedSubtype, opts.withSubtype)
require.Equal(t, tc.ExpectedFormat, opts.withFormat)
switch {
case tc.IsNowExpected:
require.True(t, time.Now().After(opts.withNow))
require.False(t, opts.withNow.IsZero())
default:
require.Equal(t, tc.ExpectedNow, opts.withNow)
}
}
})
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package event
import (
"fmt"
)
const (
FileSink SinkType = "file"
SocketSink SinkType = "socket"
SyslogSink SinkType = "syslog"
)
// SinkType defines the type of sink
type SinkType string
// Validate ensures that SinkType is one of the set of allowed sink types.
func (t SinkType) Validate() error {
const op = "event.(SinkType).Validate"
switch t {
case FileSink, SocketSink, SyslogSink:
return nil
default:
return fmt.Errorf("%s: '%s' is not a valid sink type: %w", op, t, ErrInvalidParameter)
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package event
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestSinkType_Validate exercises the validation for a sink type.
func TestSinkType_Validate(t *testing.T) {
tests := map[string]struct {
Value string
IsValid bool
ExpectedError string
}{
"file": {
Value: "file",
IsValid: true,
},
"syslog": {
Value: "syslog",
IsValid: true,
},
"socket": {
Value: "socket",
IsValid: true,
},
"empty": {
Value: "",
IsValid: false,
ExpectedError: "event.(SinkType).Validate: '' is not a valid sink type: invalid parameter",
},
"random": {
Value: "random",
IsValid: false,
ExpectedError: "event.(SinkType).Validate: 'random' is not a valid sink type: invalid parameter",
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
sinkType := SinkType(tc.Value)
err := sinkType.Validate()
switch {
case tc.IsValid:
require.NoError(t, err)
case !tc.IsValid:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedError)
}
})
}
}