backend/pkg/library/cassandra/errors_test.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))
})
}