fleet/server/contexts/ctxerr/ctxerr_test.go
Roberto Dip b2d07e56b5
report error analytics (#6341)
Related to #5898, this reports an anonymized summary of errors stored in Redis into the analytics payload.

For each error stored, this includes:

- A `count` attribute with the number of occurrences of the error
- A `loc` attribute with the 3 topmost lines in the stack trace. Note that stack traces only contain package name + line number (example: github.com/fleetdm/fleet/server.go:12

This also includes a minor refactor around error types.
2022-06-28 16:31:14 -03:00

359 lines
9.5 KiB
Go

package ctxerr
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"regexp"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/host"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
pkgerrors "github.com/pkg/errors" //nolint:depguard
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setup() (context.Context, func()) {
ctx := context.Background()
eh := MockHandler{}
ctx = NewContext(ctx, eh)
nowFn = func() time.Time {
now, _ := time.Parse(time.RFC3339, "1969-06-19T21:44:05Z")
return now
}
return ctx, func() { nowFn = time.Now }
}
func TestCause(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
errNew := errors.New("new")
fmtWrap := fmt.Errorf("fmt: %w", errNew)
pkgWrap := pkgerrors.Wrap(errNew, "pkg")
pkgFmtWrap := pkgerrors.Wrap(fmtWrap, "pkg")
fmtPkgWrap := fmt.Errorf("fmt: %w", pkgWrap)
ctxNew := New(ctx, "ctxerr")
ctxWrap := Wrap(ctx, ctxNew, "wrap")
ctxDoubleWrap := Wrap(ctx, ctxWrap, "re-wrap")
pkgFmtCtxWrap := pkgerrors.Wrap(fmt.Errorf("fmt: %w", ctxWrap), "pkg")
fmtPkgCtxWrap := fmt.Errorf("fmt: %w", pkgerrors.Wrap(ctxWrap, "pkg"))
cases := []struct {
in, out error
}{
{nil, nil},
{io.EOF, io.EOF},
{errNew, errNew},
{fmtWrap, errNew},
{pkgWrap, errNew},
{pkgFmtWrap, errNew},
{fmtPkgWrap, errNew},
{ctxNew, ctxNew},
{ctxWrap, ctxNew},
{ctxDoubleWrap, ctxNew},
{pkgFmtCtxWrap, ctxNew},
{fmtPkgCtxWrap, ctxNew},
}
for _, c := range cases {
t.Run(fmt.Sprintf("%T: %[1]v", c.in), func(t *testing.T) {
got := Cause(c.in)
require.Equal(t, c.out, got)
})
}
}
func TestNew(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
err := New(ctx, "new").(*FleetError)
require.Equal(t, err.msg, "new")
require.NotEmpty(t, err.stack.List())
require.Nil(t, err.cause)
}
func TestNewWithData(t *testing.T) {
t.Run("with valid data", func(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
data := map[string]interface{}{"foo": "bar"}
err := NewWithData(ctx, "new", data).(*FleetError)
require.Equal(t, err.msg, "new")
require.NotEmpty(t, err.stack.List())
require.Nil(t, err.cause)
require.Equal(t, err.data, json.RawMessage(`{"foo":"bar","timestamp":"1969-06-19T21:44:05Z"}`))
})
t.Run("with invalid data", func(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
data := map[string]interface{}{"foo": make(chan int)}
err := NewWithData(ctx, "new", data).(*FleetError)
require.Equal(t, err.msg, "new")
require.NotEmpty(t, err.stack.List())
require.Nil(t, err.cause)
assert.Regexp(t, regexp.MustCompile(`{"error": ".+"}`), string(err.data))
})
}
func TestErrorf(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
err := Errorf(ctx, "%s %d", "new", 1).(*FleetError)
require.Equal(t, err.msg, "new 1")
require.NotEmpty(t, err.stack.List())
require.Nil(t, err.cause)
}
func TestWrap(t *testing.T) {
t.Run("with message provided", func(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
cause := errors.New("cause")
err := Wrap(ctx, cause, "new").(*FleetError)
require.Equal(t, err.msg, "new")
require.NotEmpty(t, err.stack.List())
require.NotNil(t, err.cause)
})
t.Run("without message provided", func(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
cause := errors.New("cause")
err := Wrap(ctx, cause).(*FleetError)
require.Equal(t, err.msg, "")
require.NotEmpty(t, err.stack.List())
require.NotNil(t, err.cause)
})
t.Run("with nil error provided", func(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
err := Wrap(ctx, nil)
require.Equal(t, err, nil)
})
}
func TestWrapNewWithData(t *testing.T) {
t.Run("with valid data", func(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
cause := errors.New("cause")
data := map[string]interface{}{"foo": "bar"}
err := WrapWithData(ctx, cause, "new", data).(*FleetError)
require.Equal(t, err.msg, "new")
require.NotEmpty(t, err.stack.List())
require.NotNil(t, err.cause)
require.Equal(t, err.data, json.RawMessage(`{"foo":"bar","timestamp":"1969-06-19T21:44:05Z"}`))
})
t.Run("with invalid data", func(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
cause := errors.New("cause")
data := map[string]interface{}{"foo": make(chan int)}
err := WrapWithData(ctx, cause, "new", data).(*FleetError)
require.Equal(t, err.msg, "new")
require.NotEmpty(t, err.stack.List())
require.NotNil(t, err.cause)
assert.Regexp(t, regexp.MustCompile(`{"error": ".+"}`), string(err.data))
})
t.Run("without message provided", func(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
data := map[string]interface{}{"foo": make(chan int)}
cause := errors.New("cause")
err := WrapWithData(ctx, cause, "", data).(*FleetError)
require.Equal(t, err.msg, "")
require.NotEmpty(t, err.stack.List())
require.NotNil(t, err.cause)
assert.Regexp(t, regexp.MustCompile(`{"error": ".+"}`), string(err.data))
})
t.Run("with nil error provided", func(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
err := WrapWithData(ctx, nil, "msg", map[string]interface{}{"foo": "bar"})
require.Equal(t, err, nil)
})
}
func TestWrapf(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
cause := errors.New("cause")
err := Wrapf(ctx, cause, "%s %d", "new", 1).(*FleetError)
require.Equal(t, err.msg, "new 1")
require.NotEmpty(t, err.stack.List())
require.NotNil(t, err.cause)
}
func TestUnwrap(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
cause := errors.New("cause")
err := Wrap(ctx, cause, "new")
require.Equal(t, Unwrap(err), cause)
}
func TestMarshalJSON(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
errNew := errors.New("a")
errWrap := Wrap(ctx, errNew, "b").(*FleetError)
errWrap.stack = mockStack{[]string{"sb"}}
errNewWithData := NewWithData(ctx, "c", map[string]interface{}{"f": "c"}).(*FleetError)
errNewWithData.stack = mockStack{[]string{"sc"}}
cases := []struct {
msg string
in error
out string
}{
{
"non-wrapped errors",
errNew,
`[{"message": "a"}]`,
},
{
"wrapped error",
errWrap,
`[{"message": "a"}, {"message": "b", "data": {"timestamp": "1969-06-19T21:44:05Z"}, "stack": ["sb"]}]`,
},
{
"wrapped error with data",
errNewWithData,
`[{"message": "c", "stack": ["sc"], "data": {"f": "c", "timestamp": "1969-06-19T21:44:05Z"}}]`,
},
}
for _, c := range cases {
t.Run(c.msg, func(t *testing.T) {
json, err := MarshalJSON(c.in)
require.NoError(t, err)
require.JSONEq(t, c.out, string(json))
})
}
}
func TestStackMethod(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
errNew := errors.New("a")
errWrap := Wrap(ctx, errNew, "b").(*FleetError)
errWrap.stack = mockStack{[]string{"sb"}}
require.Equal(t, []string{"sb"}, errWrap.Stack())
}
func TestFleetCause(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
var nilErr *FleetError
errNew := errors.New("a")
errWrapRoot := Wrap(ctx, errNew, "wrapRoot")
errWrap1 := Wrap(ctx, errWrapRoot, "wrap1")
errWrap2 := Wrap(ctx, errWrap1, "wrap2")
cases := []struct {
msg string
in error
out error
}{
{"non-fleet, unwrapped errors returns nil", errNew, nilErr},
{"fleet unwrapped errors returns the error itself", errWrapRoot, errWrapRoot},
{"deeply nested errors return the root fleet error", errWrap1, errWrapRoot},
{"deeply nested errors return the root fleet error", errWrap2, errWrapRoot},
}
for _, c := range cases {
t.Run(c.msg, func(t *testing.T) {
actual := FleetCause(c.in)
require.Equal(t, c.out, actual)
})
}
}
func TestHandle(t *testing.T) {
t.Run("stores the error when invoked", func(t *testing.T) {
ctx := context.Background()
eh := MockHandler{}
err := New(ctx, "new")
eh.StoreImpl = func(serr error) {
require.Equal(t, serr, err)
}
ctx = NewContext(ctx, eh)
Handle(ctx, err)
})
t.Run("wraps when there's no FleetError in the chain", func(t *testing.T) {
ctx := context.Background()
eh := MockHandler{}
err := errors.New("new")
eh.StoreImpl = func(serr error) {
var ferr *FleetError
require.ErrorAs(t, serr, &ferr)
}
ctx = NewContext(ctx, eh)
Handle(ctx, err)
})
}
func TestAdditionalMetadata(t *testing.T) {
t.Run("saves additional data about the host if present", func(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
hctx := host.NewContext(ctx, &fleet.Host{Platform: "test_platform", OsqueryVersion: "5.0"})
err := New(hctx, "with host context").(*FleetError)
require.JSONEq(t, string(err.data), `{"host":{"osquery_version":"5.0","platform":"test_platform"},"timestamp":"1969-06-19T21:44:05Z"}`)
})
t.Run("saves additional data about the viewer if present", func(t *testing.T) {
ctx, cleanup := setup()
defer cleanup()
vctx := viewer.NewContext(ctx, viewer.Viewer{Session: &fleet.Session{ID: 1}, User: &fleet.User{SSOEnabled: true}})
err := New(vctx, "with host context").(*FleetError)
require.JSONEq(t, string(err.data), `{"viewer":{"is_logged_in":true,"sso_enabled":true},"timestamp":"1969-06-19T21:44:05Z"}`)
})
}
func TestRetrieve(t *testing.T) {
t.Run("returns an error if unable to retrieve a handler from ctx", func(t *testing.T) {
_, err := Retrieve(context.Background())
require.Error(t, err)
})
t.Run("retrieves an error from the error handler", func(t *testing.T) {
eh := MockHandler{}
eh.RetrieveImpl = func(flush bool) ([]*StoredError, error) {
require.False(t, flush)
return make([]*StoredError, 2), nil
}
ctx := NewContext(context.Background(), eh)
rerrs, err := Retrieve(ctx)
require.NoError(t, err)
require.Len(t, rerrs, 2)
})
}