591 lines
13 KiB
Go
591 lines
13 KiB
Go
package cassandra
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestError_Error(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err *Error
|
|
want string
|
|
contains []string // 如果 want 為空,則檢查是否包含這些字串
|
|
}{
|
|
{
|
|
name: "error with code and message only",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "record not found",
|
|
},
|
|
want: "cassandra[NOT_FOUND]: record not found",
|
|
},
|
|
{
|
|
name: "error with code, message and table",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "record not found",
|
|
Table: "users",
|
|
},
|
|
want: "cassandra[NOT_FOUND] (table: users): record not found",
|
|
},
|
|
{
|
|
name: "error with code, message and underlying error",
|
|
err: &Error{
|
|
Code: ErrCodeInvalidInput,
|
|
Message: "invalid input parameter",
|
|
Err: errors.New("validation failed"),
|
|
},
|
|
contains: []string{
|
|
"cassandra[INVALID_INPUT]",
|
|
"invalid input parameter",
|
|
"validation failed",
|
|
},
|
|
},
|
|
{
|
|
name: "error with all fields",
|
|
err: &Error{
|
|
Code: ErrCodeConflict,
|
|
Message: "acquire lock failed",
|
|
Table: "locks",
|
|
Err: errors.New("lock already exists"),
|
|
},
|
|
contains: []string{
|
|
"cassandra[CONFLICT]",
|
|
"(table: locks)",
|
|
"acquire lock failed",
|
|
"lock already exists",
|
|
},
|
|
},
|
|
{
|
|
name: "error with empty message",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
},
|
|
want: "cassandra[NOT_FOUND]: ",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.err.Error()
|
|
if tt.want != "" {
|
|
assert.Equal(t, tt.want, result)
|
|
} else {
|
|
for _, substr := range tt.contains {
|
|
assert.Contains(t, result, substr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestError_Unwrap(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err *Error
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "error with underlying error",
|
|
err: &Error{
|
|
Code: ErrCodeInvalidInput,
|
|
Message: "invalid input",
|
|
Err: errors.New("underlying error"),
|
|
},
|
|
wantErr: errors.New("underlying error"),
|
|
},
|
|
{
|
|
name: "error without underlying error",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "not found",
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "error with nil underlying error",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "not found",
|
|
Err: nil,
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.err.Unwrap()
|
|
if tt.wantErr == nil {
|
|
assert.Nil(t, result)
|
|
} else {
|
|
assert.Equal(t, tt.wantErr.Error(), result.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestError_WithTable(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err *Error
|
|
table string
|
|
wantCode ErrorCode
|
|
wantMsg string
|
|
wantTbl string
|
|
}{
|
|
{
|
|
name: "add table to error without table",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "record not found",
|
|
},
|
|
table: "users",
|
|
wantCode: ErrCodeNotFound,
|
|
wantMsg: "record not found",
|
|
wantTbl: "users",
|
|
},
|
|
{
|
|
name: "replace existing table",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "record not found",
|
|
Table: "old_table",
|
|
},
|
|
table: "new_table",
|
|
wantCode: ErrCodeNotFound,
|
|
wantMsg: "record not found",
|
|
wantTbl: "new_table",
|
|
},
|
|
{
|
|
name: "add table to error with underlying error",
|
|
err: &Error{
|
|
Code: ErrCodeInvalidInput,
|
|
Message: "invalid input",
|
|
Err: errors.New("validation failed"),
|
|
},
|
|
table: "products",
|
|
wantCode: ErrCodeInvalidInput,
|
|
wantMsg: "invalid input",
|
|
wantTbl: "products",
|
|
},
|
|
{
|
|
name: "add empty table",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "not found",
|
|
},
|
|
table: "",
|
|
wantCode: ErrCodeNotFound,
|
|
wantMsg: "not found",
|
|
wantTbl: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.err.WithTable(tt.table)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, tt.wantCode, result.Code)
|
|
assert.Equal(t, tt.wantMsg, result.Message)
|
|
assert.Equal(t, tt.wantTbl, result.Table)
|
|
// 確保是新的實例,不是修改原來的
|
|
assert.NotSame(t, tt.err, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestError_WithError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err *Error
|
|
underlying error
|
|
wantCode ErrorCode
|
|
wantMsg string
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "add underlying error to error without error",
|
|
err: &Error{
|
|
Code: ErrCodeInvalidInput,
|
|
Message: "invalid input",
|
|
},
|
|
underlying: errors.New("validation failed"),
|
|
wantCode: ErrCodeInvalidInput,
|
|
wantMsg: "invalid input",
|
|
wantErr: errors.New("validation failed"),
|
|
},
|
|
{
|
|
name: "replace existing underlying error",
|
|
err: &Error{
|
|
Code: ErrCodeInvalidInput,
|
|
Message: "invalid input",
|
|
Err: errors.New("old error"),
|
|
},
|
|
underlying: errors.New("new error"),
|
|
wantCode: ErrCodeInvalidInput,
|
|
wantMsg: "invalid input",
|
|
wantErr: errors.New("new error"),
|
|
},
|
|
{
|
|
name: "add nil underlying error",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "not found",
|
|
},
|
|
underlying: nil,
|
|
wantCode: ErrCodeNotFound,
|
|
wantMsg: "not found",
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "add error to error with table",
|
|
err: &Error{
|
|
Code: ErrCodeConflict,
|
|
Message: "conflict",
|
|
Table: "locks",
|
|
},
|
|
underlying: errors.New("lock exists"),
|
|
wantCode: ErrCodeConflict,
|
|
wantMsg: "conflict",
|
|
wantErr: errors.New("lock exists"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.err.WithError(tt.underlying)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, tt.wantCode, result.Code)
|
|
assert.Equal(t, tt.wantMsg, result.Message)
|
|
// 確保是新的實例
|
|
assert.NotSame(t, tt.err, result)
|
|
// 檢查 underlying error
|
|
if tt.wantErr == nil {
|
|
assert.Nil(t, result.Err)
|
|
} else {
|
|
require.NotNil(t, result.Err)
|
|
assert.Equal(t, tt.wantErr.Error(), result.Err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
code ErrorCode
|
|
message string
|
|
want *Error
|
|
}{
|
|
{
|
|
name: "create NOT_FOUND error",
|
|
code: ErrCodeNotFound,
|
|
message: "record not found",
|
|
want: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "record not found",
|
|
},
|
|
},
|
|
{
|
|
name: "create CONFLICT error",
|
|
code: ErrCodeConflict,
|
|
message: "lock acquisition failed",
|
|
want: &Error{
|
|
Code: ErrCodeConflict,
|
|
Message: "lock acquisition failed",
|
|
},
|
|
},
|
|
{
|
|
name: "create INVALID_INPUT error",
|
|
code: ErrCodeInvalidInput,
|
|
message: "invalid parameter",
|
|
want: &Error{
|
|
Code: ErrCodeInvalidInput,
|
|
Message: "invalid parameter",
|
|
},
|
|
},
|
|
{
|
|
name: "create error with empty message",
|
|
code: ErrCodeNotFound,
|
|
message: "",
|
|
want: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := NewError(tt.code, tt.message)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, tt.want.Code, result.Code)
|
|
assert.Equal(t, tt.want.Message, result.Message)
|
|
assert.Empty(t, result.Table)
|
|
assert.Nil(t, result.Err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsNotFound(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{
|
|
name: "Error with NOT_FOUND code",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "record not found",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "Error with CONFLICT code",
|
|
err: &Error{
|
|
Code: ErrCodeConflict,
|
|
Message: "conflict",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "Error with INVALID_INPUT code",
|
|
err: &Error{
|
|
Code: ErrCodeInvalidInput,
|
|
Message: "invalid input",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "wrapped Error with NOT_FOUND code",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "record not found",
|
|
Err: errors.New("underlying error"),
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "standard error",
|
|
err: errors.New("standard error"),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "nil error",
|
|
err: nil,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "predefined ErrNotFound",
|
|
err: ErrNotFound,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "predefined ErrNotFound with table",
|
|
err: ErrNotFound.WithTable("users"),
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := IsNotFound(tt.err)
|
|
assert.Equal(t, tt.want, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsConflict(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{
|
|
name: "Error with CONFLICT code",
|
|
err: &Error{
|
|
Code: ErrCodeConflict,
|
|
Message: "conflict",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "Error with NOT_FOUND code",
|
|
err: &Error{
|
|
Code: ErrCodeNotFound,
|
|
Message: "record not found",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "Error with INVALID_INPUT code",
|
|
err: &Error{
|
|
Code: ErrCodeInvalidInput,
|
|
Message: "invalid input",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "wrapped Error with CONFLICT code",
|
|
err: &Error{
|
|
Code: ErrCodeConflict,
|
|
Message: "conflict",
|
|
Err: errors.New("underlying error"),
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "standard error",
|
|
err: errors.New("standard error"),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "nil error",
|
|
err: nil,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "NewError with CONFLICT code",
|
|
err: NewError(ErrCodeConflict, "lock failed"),
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := IsConflict(tt.err)
|
|
assert.Equal(t, tt.want, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPredefinedErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err *Error
|
|
wantCode ErrorCode
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "ErrNotFound",
|
|
err: ErrNotFound,
|
|
wantCode: ErrCodeNotFound,
|
|
wantMsg: "record not found",
|
|
},
|
|
{
|
|
name: "ErrInvalidInput",
|
|
err: ErrInvalidInput,
|
|
wantCode: ErrCodeInvalidInput,
|
|
wantMsg: "invalid input parameter",
|
|
},
|
|
{
|
|
name: "ErrNoPartitionKey",
|
|
err: ErrNoPartitionKey,
|
|
wantCode: ErrCodeMissingPartition,
|
|
wantMsg: "no partition key defined in struct",
|
|
},
|
|
{
|
|
name: "ErrMissingTableName",
|
|
err: ErrMissingTableName,
|
|
wantCode: ErrCodeMissingTableName,
|
|
wantMsg: "struct must implement TableName() method",
|
|
},
|
|
{
|
|
name: "ErrNoFieldsToUpdate",
|
|
err: ErrNoFieldsToUpdate,
|
|
wantCode: ErrCodeNoFieldsToUpdate,
|
|
wantMsg: "no fields to update",
|
|
},
|
|
{
|
|
name: "ErrMissingWhereCondition",
|
|
err: ErrMissingWhereCondition,
|
|
wantCode: ErrCodeMissingWhereCondition,
|
|
wantMsg: "operation requires at least one WHERE condition for safety",
|
|
},
|
|
{
|
|
name: "ErrMissingPartitionKey",
|
|
err: ErrMissingPartitionKey,
|
|
wantCode: ErrCodeMissingPartition,
|
|
wantMsg: "operation requires all partition keys in WHERE clause",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.NotNil(t, tt.err)
|
|
assert.Equal(t, tt.wantCode, tt.err.Code)
|
|
assert.Equal(t, tt.wantMsg, tt.err.Message)
|
|
assert.Empty(t, tt.err.Table)
|
|
assert.Nil(t, tt.err.Err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestError_Chaining(t *testing.T) {
|
|
t.Run("chain WithTable and WithError", func(t *testing.T) {
|
|
err := NewError(ErrCodeNotFound, "record not found").
|
|
WithTable("users").
|
|
WithError(errors.New("database error"))
|
|
|
|
assert.Equal(t, ErrCodeNotFound, err.Code)
|
|
assert.Equal(t, "record not found", err.Message)
|
|
assert.Equal(t, "users", err.Table)
|
|
assert.NotNil(t, err.Err)
|
|
assert.Equal(t, "database error", err.Err.Error())
|
|
assert.True(t, IsNotFound(err))
|
|
})
|
|
|
|
t.Run("chain multiple WithTable calls", func(t *testing.T) {
|
|
err1 := ErrNotFound.WithTable("table1")
|
|
err2 := err1.WithTable("table2")
|
|
|
|
assert.Equal(t, "table1", err1.Table)
|
|
assert.Equal(t, "table2", err2.Table)
|
|
assert.NotSame(t, err1, err2)
|
|
})
|
|
|
|
t.Run("chain multiple WithError calls", func(t *testing.T) {
|
|
err1 := ErrInvalidInput.WithError(errors.New("error1"))
|
|
err2 := err1.WithError(errors.New("error2"))
|
|
|
|
assert.Equal(t, "error1", err1.Err.Error())
|
|
assert.Equal(t, "error2", err2.Err.Error())
|
|
assert.NotSame(t, err1, err2)
|
|
})
|
|
}
|
|
|
|
func TestError_ErrorsAs(t *testing.T) {
|
|
t.Run("errors.As works with Error", func(t *testing.T) {
|
|
err := ErrNotFound.WithTable("users")
|
|
var target *Error
|
|
ok := errors.As(err, &target)
|
|
assert.True(t, ok)
|
|
assert.NotNil(t, target)
|
|
assert.Equal(t, ErrCodeNotFound, target.Code)
|
|
assert.Equal(t, "users", target.Table)
|
|
})
|
|
|
|
t.Run("errors.As works with wrapped Error", func(t *testing.T) {
|
|
underlying := errors.New("underlying error")
|
|
err := ErrInvalidInput.WithError(underlying)
|
|
var target *Error
|
|
ok := errors.As(err, &target)
|
|
assert.True(t, ok)
|
|
assert.NotNil(t, target)
|
|
assert.Equal(t, ErrCodeInvalidInput, target.Code)
|
|
assert.Equal(t, underlying, target.Err)
|
|
})
|
|
|
|
t.Run("errors.Is works with Error", func(t *testing.T) {
|
|
err := ErrNotFound
|
|
assert.True(t, errors.Is(err, ErrNotFound))
|
|
assert.False(t, errors.Is(err, ErrInvalidInput))
|
|
})
|
|
}
|