mirror of https://github.com/hashicorp/boundary
New domain errors based on the ICU-019 RFC (#760)
parent
ea2e0d2be1
commit
fb3f11997e
@ -0,0 +1,35 @@
|
||||
package errors
|
||||
|
||||
// Code specifies a code for the error.
|
||||
type Code uint32
|
||||
|
||||
// String will return the Code's Info.Message
|
||||
func (c Code) String() string {
|
||||
return c.Info().Message
|
||||
}
|
||||
|
||||
// Info will look up the Code's Info. If the Info is not found, it will return
|
||||
// Info for an Unknown Code.
|
||||
func (c Code) Info() Info {
|
||||
if info, ok := errorCodeInfo[c]; ok {
|
||||
return info
|
||||
}
|
||||
return errorCodeInfo[Unknown]
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
// DB errors are resevered 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
|
||||
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
|
||||
|
||||
)
|
||||
@ -0,0 +1,79 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCode_Both_String_Info(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
c Code
|
||||
want Code
|
||||
}{
|
||||
{
|
||||
name: "undefined-code",
|
||||
c: Code(4294967295),
|
||||
want: Unknown,
|
||||
},
|
||||
{
|
||||
name: "default-value",
|
||||
want: Unknown,
|
||||
},
|
||||
{
|
||||
name: "Unknown",
|
||||
c: Unknown,
|
||||
want: Unknown,
|
||||
},
|
||||
{
|
||||
name: "InvalidParameter",
|
||||
c: InvalidParameter,
|
||||
want: InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "CheckConstraint",
|
||||
c: CheckConstraint,
|
||||
want: CheckConstraint,
|
||||
},
|
||||
{
|
||||
name: "NotNull",
|
||||
c: NotNull,
|
||||
want: NotNull,
|
||||
},
|
||||
{
|
||||
name: "NotUnique",
|
||||
c: NotUnique,
|
||||
want: NotUnique,
|
||||
},
|
||||
{
|
||||
name: "RecordNotFound",
|
||||
c: RecordNotFound,
|
||||
want: RecordNotFound,
|
||||
},
|
||||
{
|
||||
name: "MultipleRecords",
|
||||
c: MultipleRecords,
|
||||
want: MultipleRecords,
|
||||
},
|
||||
{
|
||||
name: "NotSpecificIntegrity",
|
||||
c: NotSpecificIntegrity,
|
||||
want: NotSpecificIntegrity,
|
||||
},
|
||||
{
|
||||
name: "MissingTable",
|
||||
c: MissingTable,
|
||||
want: MissingTable,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
assert.Equal(errorCodeInfo[tt.want], tt.c.Info())
|
||||
assert.Equal(errorCodeInfo[tt.want].Message, tt.c.String())
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,150 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// Op represents an operation (package.function).
|
||||
// For example iam.CreateRole
|
||||
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().
|
||||
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
|
||||
Code Code
|
||||
|
||||
// Msg for the error
|
||||
Msg string
|
||||
|
||||
// Op represents the operation raising/propagating an error and is optional
|
||||
Op Op
|
||||
|
||||
// Wrapped is the error which this Err wraps and will be nil if there's no
|
||||
// error to wrap.
|
||||
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
|
||||
// msg for the error Code is not sufficient.
|
||||
// WithWrap() - allows you to specify
|
||||
// an error to wrap
|
||||
func New(c Code, opt ...Option) error {
|
||||
opts := GetOpts(opt...)
|
||||
return &Err{
|
||||
Code: c,
|
||||
Op: opts.withOp,
|
||||
Wrapped: opts.withErrWrapped,
|
||||
Msg: opts.withErrMsg,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
func Convert(e error) *Err {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
if err, ok := e.(*Err); ok {
|
||||
return err
|
||||
}
|
||||
var pqError *pq.Error
|
||||
if As(e, &pqError) {
|
||||
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)
|
||||
case "23502": // not_null_violation
|
||||
msg := fmt.Sprintf("%s must not be empty", pqError.Column)
|
||||
return New(NotNull, 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)
|
||||
default:
|
||||
return New(NotSpecificIntegrity, WithMsg(pqError.Message)).(*Err)
|
||||
}
|
||||
}
|
||||
if pqError.Code == "42P01" {
|
||||
return New(MissingTable, WithMsg(pqError.Message)).(*Err)
|
||||
}
|
||||
}
|
||||
// unfortunately, we can't help.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Info about the Err
|
||||
func (e *Err) Info() Info {
|
||||
if e == nil {
|
||||
return errorCodeInfo[Unknown]
|
||||
}
|
||||
return e.Code.Info()
|
||||
}
|
||||
|
||||
// Error satisfies the error interface and returns a string representation of
|
||||
// the Err
|
||||
func (e *Err) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
var s strings.Builder
|
||||
if e.Op != "" {
|
||||
join(&s, ": ", string(e.Op))
|
||||
}
|
||||
if e.Msg != "" {
|
||||
join(&s, ": ", e.Msg)
|
||||
}
|
||||
|
||||
if info, ok := errorCodeInfo[e.Code]; ok {
|
||||
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))
|
||||
|
||||
if e.Wrapped != nil {
|
||||
join(&s, ": \n", e.Wrapped.Error())
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func join(str *strings.Builder, delim string, s string) {
|
||||
if str.Len() == 0 {
|
||||
_, _ = str.WriteString(s)
|
||||
return
|
||||
}
|
||||
_, _ = str.WriteString(delim + s)
|
||||
}
|
||||
|
||||
// Unwrap implements the errors.Unwrap interface and allows callers to use the
|
||||
// errors.Is() and errors.As() functions effectively for any wrapped errors.
|
||||
func (e *Err) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.Wrapped
|
||||
}
|
||||
|
||||
// Is the equivalent of the std errors.Is, but allows Devs to only import
|
||||
// this package for the capability.
|
||||
func Is(err, target error) bool {
|
||||
return errors.Is(err, target)
|
||||
}
|
||||
|
||||
// As is the equivalent of the std errors.As, and allows devs to only import
|
||||
// this package for the capability.
|
||||
func As(err error, target interface{}) bool {
|
||||
return errors.As(err, target)
|
||||
}
|
||||
@ -0,0 +1,275 @@
|
||||
package errors_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/lib/pq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_NewError(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.WithOp("alice.Bob"),
|
||||
errors.WithWrap(errors.ErrRecordNotFound),
|
||||
errors.WithMsg("test msg"),
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
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...)
|
||||
require.Error(err)
|
||||
assert.Equal(tt.want, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_Info(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
err *errors.Err
|
||||
want errors.Code
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
err: nil,
|
||||
want: errors.Unknown,
|
||||
},
|
||||
{
|
||||
name: "Unknown",
|
||||
err: errors.New(errors.Unknown).(*errors.Err),
|
||||
want: errors.Unknown,
|
||||
},
|
||||
{
|
||||
name: "InvalidParameter",
|
||||
err: errors.New(errors.InvalidParameter).(*errors.Err),
|
||||
want: errors.InvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
assert.Equal(tt.want.Info(), tt.err.Info())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_Error(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "msg",
|
||||
err: errors.New(errors.Unknown, errors.WithMsg("test msg")),
|
||||
want: "test msg: unknown: error #0",
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
err: errors.New(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")),
|
||||
want: "alice.bob: test msg: integrity violation: error #1000",
|
||||
},
|
||||
{
|
||||
name: "unknown",
|
||||
err: errors.New(errors.Unknown),
|
||||
want: "unknown, unknown: error #0",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
got := tt.err.Error()
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
t.Run("nil *Err", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
var err *errors.Err
|
||||
got := err.Error()
|
||||
assert.Equal("", got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestError_Unwrap(t *testing.T) {
|
||||
t.Parallel()
|
||||
testErr := errors.New(errors.Unknown, errors.WithMsg("test error"))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want error
|
||||
wantIsErr error
|
||||
}{
|
||||
{
|
||||
name: "ErrInvalidParameter",
|
||||
err: errors.New(errors.InvalidParameter, errors.WithWrap(errors.ErrInvalidParameter)),
|
||||
want: errors.ErrInvalidParameter,
|
||||
wantIsErr: errors.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "testErr",
|
||||
err: testErr,
|
||||
want: nil,
|
||||
wantIsErr: testErr,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
err := tt.err.(interface {
|
||||
Unwrap() error
|
||||
}).Unwrap()
|
||||
assert.Equal(tt.want, err)
|
||||
assert.True(stderrors.Is(tt.err, tt.wantIsErr))
|
||||
})
|
||||
}
|
||||
t.Run("nil *Err", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
var err *errors.Err
|
||||
got := err.Unwrap()
|
||||
assert.Equal(nil, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConvertError(t *testing.T) {
|
||||
t.Parallel()
|
||||
const (
|
||||
createTable = `
|
||||
create table if not exists test_table (
|
||||
id bigint generated always as identity primary key,
|
||||
name text unique,
|
||||
description text not null,
|
||||
five text check(length(trim(five)) > 5)
|
||||
);
|
||||
`
|
||||
truncateTable = `truncate test_table;`
|
||||
insert = `insert into test_table(name, description, five) values (?, ?, ?)`
|
||||
missingTable = `select * from not_a_defined_table`
|
||||
)
|
||||
ctx := context.Background()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
|
||||
_, err := rw.Exec(ctx, createTable, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
e error
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
e: nil,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "not-convertible",
|
||||
e: stderrors.New("test error"),
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "NotSpecificIntegrity",
|
||||
e: &pq.Error{
|
||||
Code: pq.ErrorCode("23001"),
|
||||
},
|
||||
wantErr: errors.New(errors.NotSpecificIntegrity),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
err := errors.Convert(tt.e)
|
||||
if tt.wantErr == nil {
|
||||
assert.Nil(err)
|
||||
return
|
||||
}
|
||||
require.NotNil(err)
|
||||
assert.Equal(tt.wantErr, err)
|
||||
})
|
||||
}
|
||||
t.Run("ErrCodeUnique", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
_, err := rw.Exec(ctx, truncateTable, nil)
|
||||
require.NoError(err)
|
||||
_, err = rw.Exec(ctx, insert, []interface{}{"alice", "coworker", nil})
|
||||
require.NoError(err)
|
||||
_, err = rw.Exec(ctx, insert, []interface{}{"alice", "dup coworker", nil})
|
||||
require.Error(err)
|
||||
|
||||
e := errors.Convert(err)
|
||||
require.NotNil(e)
|
||||
assert.True(stderrors.Is(e, errors.ErrNotUnique))
|
||||
assert.Equal("Key (name)=(alice) already exists.: integrity violation: error #1002: \nunique constraint violation: integrity violation: error #1002", e.Error())
|
||||
})
|
||||
t.Run("ErrCodeNotNull", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
_, err := rw.Exec(ctx, truncateTable, nil)
|
||||
require.NoError(err)
|
||||
_, err = rw.Exec(ctx, insert, []interface{}{"alice", nil, nil})
|
||||
require.Error(err)
|
||||
|
||||
e := errors.Convert(err)
|
||||
require.NotNil(e)
|
||||
assert.True(stderrors.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())
|
||||
})
|
||||
t.Run("ErrCodeCheckConstraint", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
_, err := rw.Exec(ctx, truncateTable, nil)
|
||||
require.NoError(err)
|
||||
_, err = rw.Exec(ctx, insert, []interface{}{"alice", "coworker", "one"})
|
||||
require.Error(err)
|
||||
|
||||
e := errors.Convert(err)
|
||||
require.NotNil(e)
|
||||
assert.True(stderrors.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())
|
||||
})
|
||||
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))
|
||||
assert.Equal("relation \"not_a_defined_table\" does not exist: integrity violation: error #1004", e.Error())
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package errors
|
||||
|
||||
type Info struct {
|
||||
Kind Kind
|
||||
Message string
|
||||
}
|
||||
|
||||
// errorCodeInfo provides a map of unique Codes (IDs) to their
|
||||
// corresponding Kind and a default Message.
|
||||
var errorCodeInfo = map[Code]Info{
|
||||
Unknown: {
|
||||
Message: "unknown",
|
||||
Kind: Other,
|
||||
},
|
||||
InvalidParameter: {
|
||||
Message: "invalid parameter",
|
||||
Kind: Parameter,
|
||||
},
|
||||
CheckConstraint: {
|
||||
Message: "constraint check failed",
|
||||
Kind: Integrity,
|
||||
},
|
||||
NotNull: {
|
||||
Message: "must not be empty (null) violation",
|
||||
Kind: Integrity,
|
||||
},
|
||||
NotUnique: {
|
||||
Message: "must be unique violation",
|
||||
Kind: Integrity,
|
||||
},
|
||||
NotSpecificIntegrity: {
|
||||
Message: "Integrity violation without specific details",
|
||||
Kind: Integrity,
|
||||
},
|
||||
MissingTable: {
|
||||
Message: "missing table",
|
||||
Kind: Integrity,
|
||||
},
|
||||
RecordNotFound: {
|
||||
Message: "record not found",
|
||||
Kind: Search,
|
||||
},
|
||||
MultipleRecords: {
|
||||
Message: "multiple records",
|
||||
Kind: Search,
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// IsUniqueError returns a boolean indicating whether the error is known to
|
||||
// report a unique constraint violation.
|
||||
func IsUniqueError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var domainErr *Err
|
||||
if errors.As(err, &domainErr) {
|
||||
if domainErr.Code == NotUnique {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var pqError *pq.Error
|
||||
if errors.As(err, &pqError) {
|
||||
if pqError.Code == "23505" { // unique_violation
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCheckConstraintError returns a boolean indicating whether the error is
|
||||
// known to report a check constraint violation.
|
||||
func IsCheckConstraintError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var domainErr *Err
|
||||
if errors.As(err, &domainErr) {
|
||||
if domainErr.Code == CheckConstraint {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var pqError *pq.Error
|
||||
if errors.As(err, &pqError) {
|
||||
if pqError.Code == "23514" { // check_violation
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsNotNullError returns a boolean indicating whether the error is known
|
||||
// to report a not-null constraint violation.
|
||||
func IsNotNullError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var domainErr *Err
|
||||
if errors.As(err, &domainErr) {
|
||||
if domainErr.Code == NotNull {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var pqError *pq.Error
|
||||
if errors.As(err, &pqError) {
|
||||
if pqError.Code == "23502" { // not_null_violation
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsMissingTableError returns a boolean indicating whether the error is known
|
||||
// to report a undefined/missing table violation.
|
||||
func IsMissingTableError(err error) bool {
|
||||
var pqError *pq.Error
|
||||
if errors.As(err, &pqError) {
|
||||
if pqError.Code == "42P01" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -0,0 +1,242 @@
|
||||
package errors_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/lib/pq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestError_IsUnique(t *testing.T) {
|
||||
t.Parallel()
|
||||
var tests = []struct {
|
||||
name string
|
||||
in error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil-error",
|
||||
in: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "postgres-not-unique",
|
||||
in: &pq.Error{
|
||||
Code: pq.ErrorCode("23503"),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "postgres-is-unique2",
|
||||
in: &pq.Error{
|
||||
Code: pq.ErrorCode("23505"),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "ErrCodeUnique",
|
||||
in: errors.ErrNotUnique,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "wrapped-pq-is-unique",
|
||||
in: errors.New(errors.NotUnique,
|
||||
errors.WithWrap(&pq.Error{
|
||||
Code: pq.ErrorCode("23505"),
|
||||
}),
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "ErrRecordNotFound",
|
||||
in: errors.ErrRecordNotFound,
|
||||
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.IsUniqueError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_IsCheckConstraint(t *testing.T) {
|
||||
t.Parallel()
|
||||
var tests = []struct {
|
||||
name string
|
||||
in error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil-error",
|
||||
in: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "postgres-not-check-constraint",
|
||||
in: &pq.Error{
|
||||
Code: pq.ErrorCode("23505"),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "postgres-is-check-constraint",
|
||||
in: &pq.Error{
|
||||
Code: pq.ErrorCode("23514"),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "ErrCodeCheckConstraint",
|
||||
in: errors.New(errors.CheckConstraint),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "wrapped-pq-is-check-constraint",
|
||||
in: errors.New(errors.CheckConstraint,
|
||||
errors.WithWrap(&pq.Error{
|
||||
Code: pq.ErrorCode("23514"),
|
||||
}),
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "ErrRecordNotFound",
|
||||
in: errors.ErrRecordNotFound,
|
||||
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.IsCheckConstraintError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_IsNotNullError(t *testing.T) {
|
||||
t.Parallel()
|
||||
var tests = []struct {
|
||||
name string
|
||||
in error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil-error",
|
||||
in: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "postgres-is-unique-not-not-null",
|
||||
in: &pq.Error{
|
||||
Code: pq.ErrorCode("23505"),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "postgres-is-check-constraint-not-not-null",
|
||||
in: &pq.Error{
|
||||
Code: pq.ErrorCode("23514"),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "postgres-is-not-null",
|
||||
in: &pq.Error{
|
||||
Code: pq.ErrorCode("23502"),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "ErrCodeNotNull",
|
||||
in: errors.New(errors.NotNull),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "wrapped-pq-is-not-null",
|
||||
in: errors.New(errors.NotNull,
|
||||
errors.WithWrap(&pq.Error{
|
||||
Code: pq.ErrorCode("23502"),
|
||||
}),
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "ErrRecordNotFound",
|
||||
in: errors.ErrRecordNotFound,
|
||||
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.IsNotNullError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_IsMissingTableError(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
in error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil-error",
|
||||
in: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "postgres-is-unique-not-not-null",
|
||||
in: &pq.Error{
|
||||
Code: pq.ErrorCode("23505"),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "postgres-is-check-constraint-not-not-null",
|
||||
in: &pq.Error{
|
||||
Code: pq.ErrorCode("23514"),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "postgres-is-missing-table",
|
||||
in: &pq.Error{
|
||||
Code: pq.ErrorCode("42P01"),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
err := tt.in
|
||||
got := errors.IsMissingTableError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
t.Run("query-missing-table", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
_, err := rw.Query(context.Background(), "select * from non_existent_table", nil)
|
||||
require.Error(err)
|
||||
assert.True(errors.IsMissingTableError(err))
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package errors
|
||||
|
||||
// Kind specifies the kind of error (unknown, parameter, integrity, etc).
|
||||
type Kind uint32
|
||||
|
||||
const (
|
||||
Other Kind = iota
|
||||
Parameter
|
||||
Integrity
|
||||
Search
|
||||
)
|
||||
|
||||
func (e Kind) String() string {
|
||||
return map[Kind]string{
|
||||
Other: "unknown",
|
||||
Parameter: "parameter violation",
|
||||
Integrity: "integrity violation",
|
||||
Search: "search issue",
|
||||
}[e]
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestKind_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
e Kind
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Other",
|
||||
e: Other,
|
||||
want: "unknown",
|
||||
},
|
||||
{
|
||||
name: "Parameter",
|
||||
e: Parameter,
|
||||
want: "parameter violation",
|
||||
},
|
||||
{
|
||||
name: "Integrity",
|
||||
e: Integrity,
|
||||
want: "integrity violation",
|
||||
},
|
||||
{
|
||||
name: "Search",
|
||||
e: Search,
|
||||
want: "search issue",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
got := tt.e.String()
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
package errors
|
||||
|
||||
// Template is useful constructing Match Err templates. Templates allow you to
|
||||
// match Errs without specifying a Code. In other words, just Match using the
|
||||
// Errs: Kind, Op, etc.
|
||||
type Template struct {
|
||||
Err // Err embedded to support matching Errs
|
||||
Kind Kind // Kind allows explicit matching on a Template without a Code.
|
||||
}
|
||||
|
||||
// T creates a new Template for matching Errs. Invalid parameters are ignored.
|
||||
// If more than is one parameter for a given type, only the last one is used.
|
||||
func T(args ...interface{}) *Template {
|
||||
t := &Template{}
|
||||
for _, a := range args {
|
||||
switch arg := a.(type) {
|
||||
case Code:
|
||||
t.Code = arg
|
||||
case string:
|
||||
t.Msg = arg
|
||||
case Op:
|
||||
t.Op = arg
|
||||
case *Err: // order is important, this match must before "case error:"
|
||||
c := *arg
|
||||
t.Wrapped = &c
|
||||
case error:
|
||||
t.Wrapped = arg
|
||||
case Kind:
|
||||
t.Kind = arg
|
||||
default:
|
||||
// ignore it
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Info about the Template, which is useful when matching a Template's Kind with
|
||||
// an Err's Kind.
|
||||
func (t *Template) Info() Info {
|
||||
if t == nil {
|
||||
return errorCodeInfo[Unknown]
|
||||
}
|
||||
switch {
|
||||
case t.Code != Unknown:
|
||||
return t.Code.Info()
|
||||
case t.Kind != Other:
|
||||
return Info{
|
||||
Message: "Unknown",
|
||||
Kind: t.Kind,
|
||||
}
|
||||
default:
|
||||
return errorCodeInfo[Unknown]
|
||||
}
|
||||
}
|
||||
|
||||
// Error satisfies the error interface but we intentionally don't return
|
||||
// anything of value, in an effort to stop users from substituting Templates in
|
||||
// place of Errs, when creating domain errors.
|
||||
func (t *Template) Error() string {
|
||||
return "Template error"
|
||||
}
|
||||
|
||||
// Match the template against the error. The error must be a *Err, or match
|
||||
// will return false. Matches all non-empty fields of the template against the
|
||||
// error.
|
||||
func Match(t *Template, err error) bool {
|
||||
if t == nil || err == nil {
|
||||
return false
|
||||
}
|
||||
e, ok := err.(*Err)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if t.Code != Unknown && t.Code != e.Code {
|
||||
return false
|
||||
}
|
||||
if t.Msg != "" && t.Msg != e.Msg {
|
||||
return false
|
||||
}
|
||||
if t.Op != "" && t.Op != e.Op {
|
||||
return false
|
||||
}
|
||||
if t.Kind != Other && t.Info().Kind != e.Info().Kind {
|
||||
return false
|
||||
}
|
||||
if t.Wrapped != nil {
|
||||
if wrappedT, ok := t.Wrapped.(*Template); ok {
|
||||
return Match(wrappedT, e.Wrapped)
|
||||
}
|
||||
if e.Wrapped != nil && t.Wrapped.Error() != e.Wrapped.Error() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -0,0 +1,296 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
stderrors "errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestT(t *testing.T) {
|
||||
t.Parallel()
|
||||
stdErr := stderrors.New("test error")
|
||||
tests := []struct {
|
||||
name string
|
||||
args []interface{}
|
||||
want *Template
|
||||
}{
|
||||
{
|
||||
name: "all fields",
|
||||
args: []interface{}{
|
||||
"test error msg",
|
||||
Op("alice.Bob"),
|
||||
InvalidParameter,
|
||||
stdErr,
|
||||
Integrity,
|
||||
},
|
||||
want: &Template{
|
||||
Err: Err{
|
||||
Code: InvalidParameter,
|
||||
Msg: "test error msg",
|
||||
Op: "alice.Bob",
|
||||
Wrapped: stdErr,
|
||||
},
|
||||
Kind: Integrity,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Kind only",
|
||||
args: []interface{}{
|
||||
Integrity,
|
||||
},
|
||||
want: &Template{
|
||||
Kind: Integrity,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple Kinds",
|
||||
args: []interface{}{
|
||||
Search,
|
||||
Integrity,
|
||||
},
|
||||
want: &Template{
|
||||
Kind: Integrity,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignore",
|
||||
args: []interface{}{
|
||||
32,
|
||||
},
|
||||
want: &Template{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
got := T(tt.args...)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplate_Info(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
template *Template
|
||||
want Info
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
template: nil,
|
||||
want: errorCodeInfo[Unknown],
|
||||
},
|
||||
{
|
||||
name: "Code",
|
||||
template: T(InvalidParameter),
|
||||
want: errorCodeInfo[InvalidParameter],
|
||||
},
|
||||
{
|
||||
name: "Code and Kind",
|
||||
template: T(InvalidParameter, Integrity),
|
||||
want: errorCodeInfo[InvalidParameter],
|
||||
},
|
||||
{
|
||||
name: "Kind without Code",
|
||||
template: T(Integrity),
|
||||
want: Info{Kind: Integrity, Message: "Unknown"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
assert.Equal(tt.want, tt.template.Info())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplate_Error(t *testing.T) {
|
||||
t.Parallel()
|
||||
stdErr := stderrors.New("test error")
|
||||
tests := []struct {
|
||||
name string
|
||||
template *Template
|
||||
}{
|
||||
{
|
||||
name: "Kind only",
|
||||
template: T(Integrity),
|
||||
},
|
||||
{
|
||||
name: "all params",
|
||||
template: T(
|
||||
"test error msg",
|
||||
Op("alice.Bob"),
|
||||
InvalidParameter,
|
||||
stdErr,
|
||||
Integrity,
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
got := tt.template.Error()
|
||||
assert.Equal("Template error", got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
stdErr := stderrors.New("test error")
|
||||
tests := []struct {
|
||||
name string
|
||||
template *Template
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil template",
|
||||
template: nil,
|
||||
err: New(NotUnique, WithMsg("this thing was must be unique")),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "nil err",
|
||||
template: T(Integrity),
|
||||
err: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "match on Kind only",
|
||||
template: T(Integrity),
|
||||
err: New(
|
||||
NotUnique,
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match on Kind only",
|
||||
template: T(Integrity),
|
||||
err: New(
|
||||
RecordNotFound,
|
||||
WithMsg("this thing is missing"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "match on Code only",
|
||||
template: T(NotUnique),
|
||||
err: New(
|
||||
NotUnique,
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match on Code only",
|
||||
template: T(NotUnique),
|
||||
err: New(
|
||||
RecordNotFound,
|
||||
WithMsg("this thing is missing"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "match on Op only",
|
||||
template: T(Op("alice.Bob")),
|
||||
err: New(
|
||||
NotUnique,
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match on Op only",
|
||||
template: T(Op("alice.Alice")),
|
||||
err: New(
|
||||
RecordNotFound,
|
||||
WithMsg("this thing is missing"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "match on everything",
|
||||
template: T(
|
||||
"this thing must be unique",
|
||||
Integrity,
|
||||
InvalidParameter,
|
||||
ErrInvalidFieldMask,
|
||||
Op("alice.Bob"),
|
||||
),
|
||||
err: New(
|
||||
InvalidParameter,
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "match on Wrapped only",
|
||||
template: T(ErrInvalidFieldMask),
|
||||
err: New(
|
||||
NotUnique,
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match on Wrapped only",
|
||||
template: T(ErrNotUnique),
|
||||
err: New(
|
||||
RecordNotFound,
|
||||
WithMsg("this thing is missing"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(ErrInvalidFieldMask),
|
||||
),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "match on Wrapped only stderror",
|
||||
template: T(stdErr),
|
||||
err: New(
|
||||
NotUnique,
|
||||
WithMsg("this thing must be unique"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(stdErr),
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match on Wrapped only stderror",
|
||||
template: T(stderrors.New("no match")),
|
||||
err: New(
|
||||
RecordNotFound,
|
||||
WithMsg("this thing is missing"),
|
||||
WithOp("alice.Bob"),
|
||||
WithWrap(stdErr),
|
||||
),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
got := Match(tt.template, tt.err)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package errors
|
||||
|
||||
// GetOpts - iterate the inbound Options and return a struct.
|
||||
func GetOpts(opt ...Option) Options {
|
||||
opts := getDefaultOptions()
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// Option - how Options are passed as arguments.
|
||||
type Option func(*Options)
|
||||
|
||||
// Options - how Options are represented.
|
||||
type Options struct {
|
||||
withErrWrapped error
|
||||
withErrMsg string
|
||||
withOp Op
|
||||
}
|
||||
|
||||
func getDefaultOptions() Options {
|
||||
return Options{}
|
||||
}
|
||||
|
||||
// WithErrCode provides an option to provide an error to wrap when creating a
|
||||
// new error.
|
||||
func WithWrap(e error) Option {
|
||||
return func(o *Options) {
|
||||
o.withErrWrapped = e
|
||||
}
|
||||
}
|
||||
|
||||
// WithMsg provides an option to provide a message when creating a new
|
||||
// error.
|
||||
func WithMsg(msg string) Option {
|
||||
return func(o *Options) {
|
||||
o.withErrMsg = msg
|
||||
}
|
||||
}
|
||||
|
||||
// WithOp provides an option to provide the operation that's raising/propagating
|
||||
// the error.
|
||||
func WithOp(op Op) Option {
|
||||
return func(o *Options) {
|
||||
o.withOp = op
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test_getOpts provides unit tests for GetOpts and all the options
|
||||
func Test_getOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("WithMsg", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// test default
|
||||
opts := GetOpts()
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withErrMsg = ""
|
||||
assert.Equal(opts, testOpts)
|
||||
|
||||
// try setting it
|
||||
opts = GetOpts(WithMsg("test msg"))
|
||||
testOpts.withErrMsg = "test msg"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithWrap", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// test default
|
||||
opts := GetOpts()
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withErrWrapped = nil
|
||||
assert.Equal(opts, testOpts)
|
||||
|
||||
// try setting it
|
||||
opts = GetOpts(WithWrap(ErrInvalidParameter))
|
||||
testOpts.withErrWrapped = ErrInvalidParameter
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithOp", 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(WithOp("alice.bob"))
|
||||
testOpts.withOp = "alice.bob"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package errors
|
||||
|
||||
// Errors returned from this package may be tested against these errors
|
||||
// with errors.Is. Creating new Sentinel type errors like these should be
|
||||
// deprecated in favor of the new Err type that includes unique Codes and a
|
||||
// Matching function.
|
||||
var (
|
||||
// ErrInvalidPublicId indicates an invalid PublicId.
|
||||
ErrInvalidPublicId = New(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"))
|
||||
|
||||
// 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"))
|
||||
|
||||
// ErrEmptyFieldMask is returned by update methods if the field mask is
|
||||
// empty.
|
||||
ErrEmptyFieldMask = New(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"))
|
||||
|
||||
// ErrNotNull is returned by methods when a write to the repository resulted
|
||||
// in a check constraint violation
|
||||
ErrCheckConstraint = New(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"))
|
||||
|
||||
// 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"))
|
||||
|
||||
// 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"))
|
||||
)
|
||||
Loading…
Reference in new issue