mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
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:
10
internal/observability/event/errors.go
Normal file
10
internal/observability/event/errors.go
Normal 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")
|
||||
26
internal/observability/event/event_type.go
Normal file
26
internal/observability/event/event_type.go
Normal 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)
|
||||
}
|
||||
}
|
||||
135
internal/observability/event/event_type_audit.go
Normal file
135
internal/observability/event/event_type_audit.go
Normal 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)
|
||||
}
|
||||
293
internal/observability/event/event_type_audit_test.go
Normal file
293
internal/observability/event/event_type_audit_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
51
internal/observability/event/event_type_test.go
Normal file
51
internal/observability/event/event_type_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
131
internal/observability/event/options.go
Normal file
131
internal/observability/event/options.go
Normal 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
|
||||
}
|
||||
}
|
||||
306
internal/observability/event/options_test.go
Normal file
306
internal/observability/event/options_test.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
28
internal/observability/event/sink_type.go
Normal file
28
internal/observability/event/sink_type.go
Normal 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)
|
||||
}
|
||||
}
|
||||
59
internal/observability/event/sink_type_test.go
Normal file
59
internal/observability/event/sink_type_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user