Refactor host_repo -> domain errors (#786)
* Refactor domain errors: New -> E, create helper New and Wrap * Update host repo to use domain errors
This commit is contained in:
1 parent
aa4157639c
commit
ec0020de90
33 files changed
+996
-374
No files matched your search
@@ -21,13 +21,17 @@ const (
|
||||
Unknown Code = 0 // Unknown will be equal to a zero value for Codes
|
||||
|
||||
// General function errors are reserved Codes 100-999
|
||||
InvalidParameter Code = 100 // InvalidParameter represents and invalid parameter for an operation.
|
||||
InvalidParameter Code = 100 // InvalidParameter represents an invalid parameter for an operation.
|
||||
InvalidAddress Code = 101 // InvalidAddress represents an invalid host address for an operation
|
||||
InvalidPublicId Code = 102 // InvalidPublicId represents an invalid public Id for an operation
|
||||
InvalidFieldMask Code = 103 // InvalidFieldMask represents an invalid field mast for an operation
|
||||
EmptyFieldMask Code = 104 // EmptyFieldMask represents an empty field mask for an operation
|
||||
|
||||
// DB errors are resevered Codes from 1000-1999
|
||||
// DB errors are reserved Codes from 1000-1999
|
||||
CheckConstraint Code = 1000 // CheckConstraint represents a check constraint error
|
||||
NotNull Code = 1001 // NotNull represents a value must not be null error
|
||||
NotUnique Code = 1002 // NotUnique represents a value must be unique error
|
||||
NotSpecificIntegrity Code = 1003 // NotSpecificIntegrity represents an integrity error that has no specificy domain error code
|
||||
NotSpecificIntegrity Code = 1003 // NotSpecificIntegrity represents an integrity error that has no specific domain error code
|
||||
MissingTable Code = 1004 // Missing table represents an undefined table error
|
||||
RecordNotFound Code = 1100 // RecordNotFound represents that a record/row was not found matching the criteria
|
||||
MultipleRecords Code = 1101 // MultipleRecords represents that multiple records/rows were found matching the criteria
|
||||
|
||||
@@ -32,6 +32,26 @@ func TestCode_Both_String_Info(t *testing.T) {
|
||||
c: InvalidParameter,
|
||||
want: InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "InvalidAddress",
|
||||
c: InvalidAddress,
|
||||
want: InvalidAddress,
|
||||
},
|
||||
{
|
||||
name: "InvalidPublicId",
|
||||
c: InvalidPublicId,
|
||||
want: InvalidPublicId,
|
||||
},
|
||||
{
|
||||
name: "InvalidFieldMask",
|
||||
c: InvalidFieldMask,
|
||||
want: InvalidFieldMask,
|
||||
},
|
||||
{
|
||||
name: "EmptyFieldMask",
|
||||
c: EmptyFieldMask,
|
||||
want: EmptyFieldMask,
|
||||
},
|
||||
{
|
||||
name: "CheckConstraint",
|
||||
c: CheckConstraint,
|
||||
|
||||
+89
-19
@@ -13,9 +13,8 @@ import (
|
||||
type Op string
|
||||
|
||||
// Err provides the ability to specify a Msg, Op, Code and Wrapped error.
|
||||
// Errs must have a Code and all other fields are optional. We've chosen Err
|
||||
// over Error for the identifier to support the easy embedding of Errs. Errs
|
||||
// can be embedded without a conflict between the embedded Err and Err.Error().
|
||||
// We've chosen Err over Error for the identifier to support the easy embedding of Errs.
|
||||
// Errs can be embedded without a conflict between the embedded Err and Err.Error().
|
||||
type Err struct {
|
||||
// Code is the error's code, which can be used to get the error's
|
||||
// errorCodeInfo, which contains the error's Kind and Message
|
||||
@@ -32,22 +31,85 @@ type Err struct {
|
||||
Wrapped error
|
||||
}
|
||||
|
||||
// New creates a new Err and supports the options of:
|
||||
// WithOp - allows you to specify an optional Op (operation)
|
||||
// WithMsg() - allows you to specify an optional error msg, if the default
|
||||
// E creates a new Err with provided code and supports the options of:
|
||||
//
|
||||
// * WithOp() - allows you to specify an optional Op (operation).
|
||||
//
|
||||
// * WithMsg() - allows you to specify an optional error msg, if the default
|
||||
// msg for the error Code is not sufficient.
|
||||
// WithWrap() - allows you to specify
|
||||
// an error to wrap
|
||||
func New(c Code, opt ...Option) error {
|
||||
//
|
||||
// * WithWrap() - allows you to specify an error to wrap.
|
||||
// If the wrapped error is a boundary domain error, the wrapped error code
|
||||
// will be used as the returned error's code.
|
||||
//
|
||||
// * WithCode() - allows you to specify an optional Code, this code will be prioritized
|
||||
// over a code used from WithWrap().
|
||||
func E(opt ...Option) error {
|
||||
opts := GetOpts(opt...)
|
||||
var code Code
|
||||
|
||||
// check if options includes a wrapped error to take code from
|
||||
var err *Err
|
||||
if As(opts.withErrWrapped, &err) {
|
||||
code = err.Code
|
||||
}
|
||||
|
||||
// if options include withCode prioritize using that code
|
||||
// even if one was set via wrapped error above
|
||||
if opts.withCode != Unknown {
|
||||
code = opts.withCode
|
||||
}
|
||||
|
||||
return &Err{
|
||||
Code: c,
|
||||
Code: code,
|
||||
Op: opts.withOp,
|
||||
Wrapped: opts.withErrWrapped,
|
||||
Msg: opts.withErrMsg,
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Err with provided code, op and msg
|
||||
// It supports the options of:
|
||||
//
|
||||
// * WithWrap() - allows you to specify an error to wrap
|
||||
func New(c Code, op Op, msg string, opt ...Option) error {
|
||||
if c != Unknown {
|
||||
opt = append(opt, WithCode(c))
|
||||
}
|
||||
if op != "" {
|
||||
opt = append(opt, WithOp(op))
|
||||
}
|
||||
if msg != "" {
|
||||
opt = append(opt, WithMsg(msg))
|
||||
}
|
||||
|
||||
return E(opt...)
|
||||
}
|
||||
|
||||
// Wrap creates a new Err from the provided err and op,
|
||||
// preserving the code from the originating error.
|
||||
// It supports the options of:
|
||||
//
|
||||
// * WithMsg() - allows you to specify an optional error msg, if the default
|
||||
// msg for the error Code is not sufficient.
|
||||
func Wrap(e error, op Op, opt ...Option) error {
|
||||
if op != "" {
|
||||
opt = append(opt, WithOp(op))
|
||||
}
|
||||
if e != nil {
|
||||
// TODO: once db package has been refactored to only return domain errors,
|
||||
// this convert can be removed
|
||||
err := Convert(e)
|
||||
if err != nil {
|
||||
// wrapped converted error
|
||||
e = err
|
||||
}
|
||||
opt = append(opt, WithWrap(e))
|
||||
}
|
||||
|
||||
return E(opt...)
|
||||
}
|
||||
|
||||
// Convert will convert the error to a Boundary *Err (returning it as an error)
|
||||
// and attempt to add a helpful error msg as well. If that's not possible, it
|
||||
// will return nil
|
||||
@@ -55,7 +117,8 @@ func Convert(e error) *Err {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
if err, ok := e.(*Err); ok {
|
||||
var err *Err
|
||||
if As(e, &err) {
|
||||
return err
|
||||
}
|
||||
var pqError *pq.Error
|
||||
@@ -63,19 +126,19 @@ func Convert(e error) *Err {
|
||||
if pqError.Code.Class() == "23" { // class of integrity constraint violations
|
||||
switch pqError.Code {
|
||||
case "23505": // unique_violation
|
||||
return New(NotUnique, WithMsg(pqError.Detail), WithWrap(ErrNotUnique)).(*Err)
|
||||
return E(WithMsg(pqError.Detail), WithWrap(ErrNotUnique)).(*Err)
|
||||
case "23502": // not_null_violation
|
||||
msg := fmt.Sprintf("%s must not be empty", pqError.Column)
|
||||
return New(NotNull, WithMsg(msg), WithWrap(ErrNotNull)).(*Err)
|
||||
return E(WithMsg(msg), WithWrap(ErrNotNull)).(*Err)
|
||||
case "23514": // check_violation
|
||||
msg := fmt.Sprintf("%s constraint failed", pqError.Constraint)
|
||||
return New(CheckConstraint, WithMsg(msg), WithWrap(ErrCheckConstraint)).(*Err)
|
||||
return E(WithMsg(msg), WithWrap(ErrCheckConstraint)).(*Err)
|
||||
default:
|
||||
return New(NotSpecificIntegrity, WithMsg(pqError.Message)).(*Err)
|
||||
return E(WithCode(NotSpecificIntegrity), WithMsg(pqError.Message)).(*Err)
|
||||
}
|
||||
}
|
||||
if pqError.Code == "42P01" {
|
||||
return New(MissingTable, WithMsg(pqError.Message)).(*Err)
|
||||
return E(WithCode(MissingTable), WithMsg(pqError.Message)).(*Err)
|
||||
}
|
||||
}
|
||||
// unfortunately, we can't help.
|
||||
@@ -104,18 +167,25 @@ func (e *Err) Error() string {
|
||||
join(&s, ": ", e.Msg)
|
||||
}
|
||||
|
||||
if info, ok := errorCodeInfo[e.Code]; ok {
|
||||
var skipInfo bool
|
||||
var wrapped *Err
|
||||
if As(e.Wrapped, &wrapped) {
|
||||
// if wrapped error code is the same as this error, don't print redundant info
|
||||
skipInfo = wrapped.Code == e.Code
|
||||
}
|
||||
|
||||
if info, ok := errorCodeInfo[e.Code]; ok && !skipInfo {
|
||||
if e.Msg == "" {
|
||||
join(&s, ": ", info.Message) // provide a default.
|
||||
join(&s, ", ", info.Kind.String())
|
||||
} else {
|
||||
join(&s, ": ", info.Kind.String())
|
||||
}
|
||||
join(&s, ": ", fmt.Sprintf("error #%d", e.Code))
|
||||
}
|
||||
join(&s, ": ", fmt.Sprintf("error #%d", e.Code))
|
||||
|
||||
if e.Wrapped != nil {
|
||||
join(&s, ": \n", e.Wrapped.Error())
|
||||
join(&s, ": ", e.Wrapped.Error())
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
+231
-16
@@ -3,6 +3,7 @@ package errors_test
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
@@ -12,18 +13,17 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_NewError(t *testing.T) {
|
||||
func Test_ErrorE(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
code errors.Code
|
||||
opt []errors.Option
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "all-options",
|
||||
code: errors.InvalidParameter,
|
||||
opt: []errors.Option{
|
||||
errors.WithCode(errors.InvalidParameter),
|
||||
errors.WithOp("alice.Bob"),
|
||||
errors.WithWrap(errors.ErrRecordNotFound),
|
||||
errors.WithMsg("test msg"),
|
||||
@@ -42,11 +42,204 @@ func Test_NewError(t *testing.T) {
|
||||
Code: errors.Unknown,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "withCode",
|
||||
opt: []errors.Option{
|
||||
errors.WithCode(errors.RecordNotFound),
|
||||
},
|
||||
want: &errors.Err{
|
||||
Code: errors.RecordNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uses-wrapped-code",
|
||||
opt: []errors.Option{
|
||||
errors.WithWrap(errors.ErrRecordNotFound),
|
||||
},
|
||||
want: &errors.Err{
|
||||
Code: errors.RecordNotFound,
|
||||
Wrapped: errors.ErrRecordNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "conflicting-withCode-withWrap",
|
||||
opt: []errors.Option{
|
||||
errors.WithCode(errors.InvalidFieldMask),
|
||||
errors.WithWrap(errors.ErrRecordNotFound),
|
||||
},
|
||||
want: &errors.Err{
|
||||
Code: errors.InvalidFieldMask,
|
||||
Wrapped: errors.ErrRecordNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
err := errors.New(tt.code, tt.opt...)
|
||||
err := errors.E(tt.opt...)
|
||||
require.Error(err)
|
||||
assert.Equal(tt.want, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NewError(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
code errors.Code
|
||||
op errors.Op
|
||||
msg string
|
||||
opt []errors.Option
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "all-options",
|
||||
code: errors.InvalidParameter,
|
||||
op: "alice.Bob",
|
||||
msg: "test msg",
|
||||
opt: []errors.Option{
|
||||
errors.WithWrap(errors.ErrRecordNotFound),
|
||||
},
|
||||
want: &errors.Err{
|
||||
Op: "alice.Bob",
|
||||
Wrapped: errors.ErrRecordNotFound,
|
||||
Msg: "test msg",
|
||||
Code: errors.InvalidParameter,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-options",
|
||||
opt: nil,
|
||||
want: &errors.Err{
|
||||
Code: errors.Unknown,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "conflicting-op",
|
||||
op: "alice.Bob",
|
||||
opt: []errors.Option{
|
||||
errors.WithOp("bab.Op"),
|
||||
},
|
||||
want: &errors.Err{
|
||||
Op: "alice.Bob",
|
||||
Code: errors.Unknown,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "conflicting-msg",
|
||||
msg: "test msg",
|
||||
opt: []errors.Option{
|
||||
errors.WithMsg("dont use this message"),
|
||||
},
|
||||
want: &errors.Err{
|
||||
Msg: "test msg",
|
||||
Code: errors.Unknown,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
err := errors.New(tt.code, tt.op, tt.msg, tt.opt...)
|
||||
require.Error(err)
|
||||
assert.Equal(tt.want, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_WrapError(t *testing.T) {
|
||||
t.Parallel()
|
||||
testErr := errors.E(errors.WithCode(errors.InvalidParameter), errors.WithOp("alice.Bob"), errors.WithMsg("test msg"))
|
||||
tests := []struct {
|
||||
name string
|
||||
opt []errors.Option
|
||||
err error
|
||||
op errors.Op
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "boundary-error",
|
||||
err: testErr,
|
||||
op: "alice.Bob",
|
||||
opt: []errors.Option{
|
||||
errors.WithMsg("test msg"),
|
||||
},
|
||||
want: &errors.Err{
|
||||
Wrapped: testErr,
|
||||
Op: "alice.Bob",
|
||||
Msg: "test msg",
|
||||
Code: errors.InvalidParameter,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "boundary-error-no-op",
|
||||
err: testErr,
|
||||
opt: []errors.Option{
|
||||
errors.WithMsg("test msg"),
|
||||
},
|
||||
want: &errors.Err{
|
||||
Wrapped: testErr,
|
||||
Msg: "test msg",
|
||||
Code: errors.InvalidParameter,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "boundary-error-no-options",
|
||||
err: testErr,
|
||||
want: &errors.Err{
|
||||
Wrapped: testErr,
|
||||
Code: errors.InvalidParameter,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "std-error",
|
||||
err: fmt.Errorf("std error"),
|
||||
want: &errors.Err{
|
||||
Wrapped: fmt.Errorf("std error"),
|
||||
Code: errors.Unknown,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "conflicting-with-wrap",
|
||||
err: testErr,
|
||||
opt: []errors.Option{
|
||||
errors.WithWrap(fmt.Errorf("dont wrap this error")),
|
||||
},
|
||||
want: &errors.Err{
|
||||
Wrapped: testErr,
|
||||
Code: errors.InvalidParameter,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "conflicting-with-op",
|
||||
err: testErr,
|
||||
op: "alice.Bob",
|
||||
opt: []errors.Option{
|
||||
errors.WithOp("bad.Op"),
|
||||
},
|
||||
want: &errors.Err{
|
||||
Wrapped: testErr,
|
||||
Op: "alice.Bob",
|
||||
Code: errors.InvalidParameter,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NotSpecificIntegrity",
|
||||
err: &pq.Error{
|
||||
Code: pq.ErrorCode("23001"),
|
||||
Message: "test msg",
|
||||
},
|
||||
want: &errors.Err{
|
||||
Wrapped: errors.E(errors.WithCode(errors.NotSpecificIntegrity), errors.WithMsg("test msg")),
|
||||
Code: errors.NotSpecificIntegrity,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
err := errors.Wrap(tt.err, tt.op, tt.opt...)
|
||||
require.Error(err)
|
||||
assert.Equal(tt.want, err)
|
||||
})
|
||||
@@ -67,12 +260,12 @@ func TestError_Info(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Unknown",
|
||||
err: errors.New(errors.Unknown).(*errors.Err),
|
||||
err: errors.E().(*errors.Err),
|
||||
want: errors.Unknown,
|
||||
},
|
||||
{
|
||||
name: "InvalidParameter",
|
||||
err: errors.New(errors.InvalidParameter).(*errors.Err),
|
||||
err: errors.E(errors.WithCode(errors.InvalidParameter)).(*errors.Err),
|
||||
want: errors.InvalidParameter,
|
||||
},
|
||||
}
|
||||
@@ -93,24 +286,39 @@ func TestError_Error(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "msg",
|
||||
err: errors.New(errors.Unknown, errors.WithMsg("test msg")),
|
||||
err: errors.E(errors.WithMsg("test msg")),
|
||||
want: "test msg: unknown: error #0",
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
err: errors.New(errors.CheckConstraint),
|
||||
err: errors.E(errors.WithCode(errors.CheckConstraint)),
|
||||
want: "constraint check failed, integrity violation: error #1000",
|
||||
},
|
||||
{
|
||||
name: "op-msg-and-code",
|
||||
err: errors.New(errors.CheckConstraint, errors.WithOp("alice.bob"), errors.WithMsg("test msg")),
|
||||
err: errors.E(errors.WithCode(errors.CheckConstraint), errors.WithOp("alice.bob"), errors.WithMsg("test msg")),
|
||||
want: "alice.bob: test msg: integrity violation: error #1000",
|
||||
},
|
||||
{
|
||||
name: "unknown",
|
||||
err: errors.New(errors.Unknown),
|
||||
err: errors.E(),
|
||||
want: "unknown, unknown: error #0",
|
||||
},
|
||||
{
|
||||
name: "wrapped-no-code",
|
||||
err: errors.E(errors.WithWrap(errors.E(errors.WithCode(errors.InvalidParameter), errors.WithMsg("wrapped msg"))), errors.WithMsg("test msg")),
|
||||
want: "test msg: wrapped msg: parameter violation: error #100",
|
||||
},
|
||||
{
|
||||
name: "wrapped-different-error-codes",
|
||||
err: errors.E(errors.WithCode(errors.CheckConstraint), errors.WithWrap(errors.E(errors.WithCode(errors.InvalidParameter), errors.WithMsg("wrapped msg"))), errors.WithMsg("test msg")),
|
||||
want: "test msg: integrity violation: error #1000: wrapped msg: parameter violation: error #100",
|
||||
},
|
||||
{
|
||||
name: "wrapped-same-error-codes",
|
||||
err: errors.E(errors.WithCode(errors.CheckConstraint), errors.WithWrap(errors.E(errors.WithCode(errors.CheckConstraint), errors.WithMsg("wrapped msg"))), errors.WithMsg("test msg")),
|
||||
want: "test msg: wrapped msg: integrity violation: error #1000",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -129,7 +337,7 @@ func TestError_Error(t *testing.T) {
|
||||
|
||||
func TestError_Unwrap(t *testing.T) {
|
||||
t.Parallel()
|
||||
testErr := errors.New(errors.Unknown, errors.WithMsg("test error"))
|
||||
testErr := errors.E(errors.WithMsg("test error"))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -139,7 +347,7 @@ func TestError_Unwrap(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "ErrInvalidParameter",
|
||||
err: errors.New(errors.InvalidParameter, errors.WithWrap(errors.ErrInvalidParameter)),
|
||||
err: errors.E(errors.WithWrap(errors.ErrInvalidParameter)),
|
||||
want: errors.ErrInvalidParameter,
|
||||
wantIsErr: errors.ErrInvalidParameter,
|
||||
},
|
||||
@@ -170,6 +378,7 @@ func TestError_Unwrap(t *testing.T) {
|
||||
|
||||
func TestConvertError(t *testing.T) {
|
||||
t.Parallel()
|
||||
testErr := errors.E(errors.WithCode(errors.InvalidParameter), errors.WithOp("alice.Bob"), errors.WithMsg("test msg"))
|
||||
const (
|
||||
createTable = `
|
||||
create table if not exists test_table (
|
||||
@@ -210,7 +419,12 @@ func TestConvertError(t *testing.T) {
|
||||
e: &pq.Error{
|
||||
Code: pq.ErrorCode("23001"),
|
||||
},
|
||||
wantErr: errors.New(errors.NotSpecificIntegrity),
|
||||
wantErr: errors.E(errors.WithCode(errors.NotSpecificIntegrity)),
|
||||
},
|
||||
{
|
||||
name: "convert-domain-error",
|
||||
e: testErr,
|
||||
wantErr: testErr,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -237,7 +451,7 @@ func TestConvertError(t *testing.T) {
|
||||
e := errors.Convert(err)
|
||||
require.NotNil(e)
|
||||
assert.True(errors.Is(e, errors.ErrNotUnique))
|
||||
assert.Equal("Key (name)=(alice) already exists.: integrity violation: error #1002: \nunique constraint violation: integrity violation: error #1002", e.Error())
|
||||
assert.Equal("Key (name)=(alice) already exists.: unique constraint violation: integrity violation: error #1002", e.Error())
|
||||
})
|
||||
t.Run("ErrCodeNotNull", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
@@ -249,7 +463,7 @@ func TestConvertError(t *testing.T) {
|
||||
e := errors.Convert(err)
|
||||
require.NotNil(e)
|
||||
assert.True(errors.Is(e, errors.ErrNotNull))
|
||||
assert.Equal("description must not be empty: integrity violation: error #1001: \nnot null constraint violated: integrity violation: error #1001", e.Error())
|
||||
assert.Equal("description must not be empty: not null constraint violated: integrity violation: error #1001", e.Error())
|
||||
})
|
||||
t.Run("ErrCodeCheckConstraint", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
@@ -261,12 +475,13 @@ func TestConvertError(t *testing.T) {
|
||||
e := errors.Convert(err)
|
||||
require.NotNil(e)
|
||||
assert.True(errors.Is(e, errors.ErrCheckConstraint))
|
||||
assert.Equal("test_table_five_check constraint failed: integrity violation: error #1000: \ncheck constraint violated: integrity violation: error #1000", e.Error())
|
||||
assert.Equal("test_table_five_check constraint failed: check constraint violated: integrity violation: error #1000", e.Error())
|
||||
})
|
||||
t.Run("MissingTable", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
_, err := rw.Exec(ctx, missingTable, nil)
|
||||
require.Error(err)
|
||||
|
||||
e := errors.Convert(err)
|
||||
require.NotNil(e)
|
||||
assert.True(errors.Match(errors.T(errors.MissingTable), e))
|
||||
|
||||
+21
-1
@@ -1,7 +1,11 @@
|
||||
package errors
|
||||
|
||||
// Info contains details of the specific error code
|
||||
type Info struct {
|
||||
Kind Kind
|
||||
// Kind specifies the kind of error (unknown, parameter, integrity, etc).
|
||||
Kind Kind
|
||||
|
||||
// Message provides a default message for the error code
|
||||
Message string
|
||||
}
|
||||
|
||||
@@ -16,6 +20,22 @@ var errorCodeInfo = map[Code]Info{
|
||||
Message: "invalid parameter",
|
||||
Kind: Parameter,
|
||||
},
|
||||
InvalidAddress: {
|
||||
Message: "invalid address",
|
||||
Kind: Parameter,
|
||||
},
|
||||
InvalidPublicId: {
|
||||
Message: "invalid public id",
|
||||
Kind: Parameter,
|
||||
},
|
||||
InvalidFieldMask: {
|
||||
Message: "invalid field mask",
|
||||
Kind: Parameter,
|
||||
},
|
||||
EmptyFieldMask: {
|
||||
Message: "empty field mask",
|
||||
Kind: Parameter,
|
||||
},
|
||||
CheckConstraint: {
|
||||
Message: "constraint check failed",
|
||||
Kind: Integrity,
|
||||
|
||||
@@ -89,3 +89,20 @@ func IsMissingTableError(err error) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsNotFoundError returns a boolean indicating whether the error is known to
|
||||
// report a not found violation.
|
||||
func IsNotFoundError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var domainErr *Err
|
||||
if errors.As(err, &domainErr) {
|
||||
if domainErr.Code == RecordNotFound {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package errors_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
@@ -44,7 +45,7 @@ func TestError_IsUnique(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "wrapped-pq-is-unique",
|
||||
in: errors.New(errors.NotUnique,
|
||||
in: errors.E(errors.WithCode(errors.NotUnique),
|
||||
errors.WithWrap(&pq.Error{
|
||||
Code: pq.ErrorCode("23505"),
|
||||
}),
|
||||
@@ -56,6 +57,11 @@ func TestError_IsUnique(t *testing.T) {
|
||||
in: errors.ErrRecordNotFound,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "conflicting-wrapped-code",
|
||||
in: errors.E(errors.WithCode(errors.NotNull), errors.WithWrap(errors.E(errors.WithCode(errors.NotUnique)))),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
@@ -96,12 +102,12 @@ func TestError_IsCheckConstraint(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "ErrCodeCheckConstraint",
|
||||
in: errors.New(errors.CheckConstraint),
|
||||
in: errors.E(errors.WithCode(errors.CheckConstraint)),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "wrapped-pq-is-check-constraint",
|
||||
in: errors.New(errors.CheckConstraint,
|
||||
in: errors.E(errors.WithCode(errors.CheckConstraint),
|
||||
errors.WithWrap(&pq.Error{
|
||||
Code: pq.ErrorCode("23514"),
|
||||
}),
|
||||
@@ -113,6 +119,11 @@ func TestError_IsCheckConstraint(t *testing.T) {
|
||||
in: errors.ErrRecordNotFound,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "conflicting-wrapped-code",
|
||||
in: errors.E(errors.WithCode(errors.NotNull), errors.WithWrap(errors.E(errors.WithCode(errors.CheckConstraint)))),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
@@ -160,12 +171,12 @@ func TestError_IsNotNullError(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "ErrCodeNotNull",
|
||||
in: errors.New(errors.NotNull),
|
||||
in: errors.E(errors.WithCode(errors.NotNull)),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "wrapped-pq-is-not-null",
|
||||
in: errors.New(errors.NotNull,
|
||||
in: errors.E(errors.WithCode(errors.NotNull),
|
||||
errors.WithWrap(&pq.Error{
|
||||
Code: pq.ErrorCode("23502"),
|
||||
}),
|
||||
@@ -177,6 +188,11 @@ func TestError_IsNotNullError(t *testing.T) {
|
||||
in: errors.ErrRecordNotFound,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "conflicting-wrapped-code",
|
||||
in: errors.E(errors.WithCode(errors.CheckConstraint), errors.WithWrap(errors.E(errors.WithCode(errors.NotNull)))),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
@@ -240,3 +256,46 @@ func TestError_IsMissingTableError(t *testing.T) {
|
||||
assert.True(errors.IsMissingTableError(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestError_IsNotFoundError(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
in error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil-error",
|
||||
in: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not-found-error",
|
||||
in: errors.E(errors.WithCode(errors.RecordNotFound)),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "sentinel-not-found",
|
||||
in: errors.ErrRecordNotFound,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "std-err",
|
||||
in: fmt.Errorf("std error"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "conflicting-wrapped-code",
|
||||
in: errors.E(errors.WithCode(errors.NotNull), errors.WithWrap(errors.E(errors.WithCode(errors.RecordNotFound)))),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
err := tt.in
|
||||
got := errors.IsNotFoundError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
name: "nil template",
|
||||
template: nil,
|
||||
err: New(NotUnique, WithMsg("this thing was must be unique")),
|
||||
err: E(WithCode(NotUnique), WithMsg("this thing was must be unique")),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
@@ -161,8 +161,8 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
name: "match on Kind only",
|
||||
template: T(Integrity),
|
||||
err: New(
|
||||
NotUnique,
|
||||
err: E(
|
||||
WithCode(NotUnique),
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
@@ -172,8 +172,8 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
name: "no match on Kind only",
|
||||
template: T(Integrity),
|
||||
err: New(
|
||||
RecordNotFound,
|
||||
err: E(
|
||||
WithCode(RecordNotFound),
|
||||
WithMsg("this thing is missing"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
@@ -183,8 +183,8 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
name: "match on Code only",
|
||||
template: T(NotUnique),
|
||||
err: New(
|
||||
NotUnique,
|
||||
err: E(
|
||||
WithCode(NotUnique),
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
@@ -194,8 +194,8 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
name: "no match on Code only",
|
||||
template: T(NotUnique),
|
||||
err: New(
|
||||
RecordNotFound,
|
||||
err: E(
|
||||
WithCode(RecordNotFound),
|
||||
WithMsg("this thing is missing"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
@@ -205,8 +205,8 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
name: "match on Op only",
|
||||
template: T(Op("alice.Bob")),
|
||||
err: New(
|
||||
NotUnique,
|
||||
err: E(
|
||||
WithCode(NotUnique),
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
@@ -216,8 +216,8 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
name: "no match on Op only",
|
||||
template: T(Op("alice.Alice")),
|
||||
err: New(
|
||||
RecordNotFound,
|
||||
err: E(
|
||||
WithCode(RecordNotFound),
|
||||
WithMsg("this thing is missing"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
@@ -233,8 +233,8 @@ func TestMatch(t *testing.T) {
|
||||
ErrInvalidFieldMask,
|
||||
Op("alice.Bob"),
|
||||
),
|
||||
err: New(
|
||||
InvalidParameter,
|
||||
err: E(
|
||||
WithCode(InvalidParameter),
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
@@ -244,8 +244,8 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
name: "match on Wrapped only",
|
||||
template: T(ErrInvalidFieldMask),
|
||||
err: New(
|
||||
NotUnique,
|
||||
err: E(
|
||||
WithCode(NotUnique),
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
@@ -255,8 +255,8 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
name: "no match on Wrapped only",
|
||||
template: T(ErrNotUnique),
|
||||
err: New(
|
||||
RecordNotFound,
|
||||
err: E(
|
||||
WithCode(RecordNotFound),
|
||||
WithMsg("this thing is missing"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
@@ -266,8 +266,8 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
name: "match on Wrapped only stderror",
|
||||
template: T(stdErr),
|
||||
err: New(
|
||||
NotUnique,
|
||||
err: E(
|
||||
WithCode(NotUnique),
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(stdErr),
|
||||
@@ -277,8 +277,8 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
name: "no match on Wrapped only stderror",
|
||||
template: T(stderrors.New("no match")),
|
||||
err: New(
|
||||
RecordNotFound,
|
||||
err: E(
|
||||
WithCode(RecordNotFound),
|
||||
WithMsg("this thing is missing"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(stdErr),
|
||||
|
||||
@@ -14,6 +14,7 @@ type Option func(*Options)
|
||||
|
||||
// Options - how Options are represented.
|
||||
type Options struct {
|
||||
withCode Code
|
||||
withErrWrapped error
|
||||
withErrMsg string
|
||||
withOp Op
|
||||
@@ -46,3 +47,11 @@ func WithOp(op Op) Option {
|
||||
o.withOp = op
|
||||
}
|
||||
}
|
||||
|
||||
// WithCode provides an option to provide a code when creating a new
|
||||
// error.
|
||||
func WithCode(code Code) Option {
|
||||
return func(o *Options) {
|
||||
o.withCode = code
|
||||
}
|
||||
}
|
||||
@@ -48,4 +48,17 @@ func Test_getOpts(t *testing.T) {
|
||||
testOpts.withOp = "alice.bob"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithCode", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// test default
|
||||
opts := GetOpts()
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withOp = ""
|
||||
assert.Equal(opts, testOpts)
|
||||
|
||||
// try setting it
|
||||
opts = GetOpts(WithCode(NotUnique))
|
||||
testOpts.withCode = NotUnique
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
}
|
||||
@@ -6,39 +6,39 @@ package errors
|
||||
// Matching function.
|
||||
var (
|
||||
// ErrInvalidPublicId indicates an invalid PublicId.
|
||||
ErrInvalidPublicId = New(InvalidParameter, WithMsg("invalid publicId"))
|
||||
ErrInvalidPublicId = E(WithCode(InvalidParameter), WithMsg("invalid publicId"))
|
||||
|
||||
// ErrInvalidParameter is returned by create and update methods if
|
||||
// an attribute on a struct contains illegal or invalid values.
|
||||
ErrInvalidParameter = New(InvalidParameter, WithMsg("invalid parameter"))
|
||||
ErrInvalidParameter = E(WithCode(InvalidParameter), WithMsg("invalid parameter"))
|
||||
|
||||
// ErrInvalidFieldMask is returned by update methods if the field mask
|
||||
// contains unknown fields or fields that cannot be updated.
|
||||
ErrInvalidFieldMask = New(InvalidParameter, WithMsg("invalid field mask"))
|
||||
ErrInvalidFieldMask = E(WithCode(InvalidParameter), WithMsg("invalid field mask"))
|
||||
|
||||
// ErrEmptyFieldMask is returned by update methods if the field mask is
|
||||
// empty.
|
||||
ErrEmptyFieldMask = New(InvalidParameter, WithMsg("empty field mask"))
|
||||
ErrEmptyFieldMask = E(WithCode(InvalidParameter), WithMsg("empty field mask"))
|
||||
|
||||
// ErrNotUnique is returned by create and update methods when a write
|
||||
// to the repository resulted in a unique constraint violation.
|
||||
ErrNotUnique = New(NotUnique, WithMsg("unique constraint violation"))
|
||||
ErrNotUnique = E(WithCode(NotUnique), WithMsg("unique constraint violation"))
|
||||
|
||||
// ErrNotNull is returned by methods when a write to the repository resulted
|
||||
// in a check constraint violation
|
||||
ErrCheckConstraint = New(CheckConstraint, WithMsg("check constraint violated"))
|
||||
ErrCheckConstraint = E(WithCode(CheckConstraint), WithMsg("check constraint violated"))
|
||||
|
||||
// ErrNotNull is returned by methods when a write to the repository resulted
|
||||
// in a not null constraint violation
|
||||
ErrNotNull = New(NotNull, WithMsg("not null constraint violated"))
|
||||
ErrNotNull = E(WithCode(NotNull), WithMsg("not null constraint violated"))
|
||||
|
||||
// ErrRecordNotFound returns a "record not found" error and it only occurs
|
||||
// when attempting to read from the database into struct.
|
||||
// When reading into a slice it won't return this error.
|
||||
ErrRecordNotFound = New(RecordNotFound, WithMsg("record not found"))
|
||||
ErrRecordNotFound = E(WithCode(RecordNotFound), WithMsg("record not found"))
|
||||
|
||||
// ErrMultipleRecords is returned by update and delete methods when a
|
||||
// write to the repository would result in more than one record being
|
||||
// changed resulting in the transaction being rolled back.
|
||||
ErrMultipleRecords = New(MultipleRecords, WithMsg("multiple records"))
|
||||
ErrMultipleRecords = E(WithCode(MultipleRecords), WithMsg("multiple records"))
|
||||
)
|
||||
Reference in new issue
Block a user