From 70632e7d531f0ab54e1b4594b85b5e6f9518f4bb Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Tue, 6 Aug 2024 17:38:33 +0800 Subject: [PATCH] feat: init error --- .gitignore | 2 + go.mod | 27 + pkg/errors/code/define.go | 99 ++++ pkg/errors/code/messsage.go | 13 + pkg/errors/easy_func.go | 442 +++++++++++++++ pkg/errors/easy_func_test.go | 1031 ++++++++++++++++++++++++++++++++++ pkg/errors/errors.go | 195 +++++++ pkg/errors/errors_test.go | 297 ++++++++++ pkg/errors/readme.md | 125 +++++ 9 files changed, 2231 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 pkg/errors/code/define.go create mode 100644 pkg/errors/code/messsage.go create mode 100644 pkg/errors/easy_func.go create mode 100644 pkg/errors/easy_func_test.go create mode 100644 pkg/errors/errors.go create mode 100644 pkg/errors/errors_test.go create mode 100644 pkg/errors/readme.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2df91c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d4c3d32 --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module code.30cm.net/wanderland/library-go + +go 1.22.3 + +require ( + github.com/golang/mock v1.6.0 + github.com/stretchr/testify v1.9.0 + github.com/zeromicro/go-zero v1.7.0 + google.golang.org/grpc v1.65.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.uber.org/automaxprocs v1.5.3 // indirect + golang.org/x/sys v0.22.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/pkg/errors/code/define.go b/pkg/errors/code/define.go new file mode 100644 index 0000000..bc36826 --- /dev/null +++ b/pkg/errors/code/define.go @@ -0,0 +1,99 @@ +package code + +const ( + OK uint32 = 0 +) + +// Scope +const ( + Unset uint32 = iota + CloudEPPortalGW + CloudEPMember + CloudEPPermission +) + +// Category for general operations: 100 - 4900 +const ( + _ = iota + CatInput uint32 = iota * 100 + CatDB + CatResource + CatGRPC + CatAuth + CatSystem + CatPubSub +) + +// CatArk Category for specific app/service: 5000 - 9900 +const ( + CatArk uint32 = (iota + 50) * 100 +) + +// Detail - Input 1xx +const ( + _ = iota + CatInput + InvalidFormat + NotValidImplementation + InvalidRange +) + +// Detail - Database 2xx +const ( + _ = iota + CatDB + DBError // general error + DBDataConvert + DBDuplicate +) + +// Detail - Resource 3xx +const ( + _ = iota + CatResource + ResourceNotFound + InvalidResourceFormat + ResourceAlreadyExist + ResourceInsufficient + InsufficientPermission + InvalidMeasurementID + ResourceExpired + ResourceMigrated + InvalidResourceState + InsufficientQuota + ResourceHasMultiOwner +) + +/* Detail - GRPC */ +// The GRPC detail code uses Go GRPC's built-in codes. +// Refer to "google.golang.org/grpc/codes" for more detail. + +// Detail - Auth 5xx +const ( + _ = iota + CatAuth + Unauthorized + AuthExpired + InvalidPosixTime + SigAndPayloadNotMatched + Forbidden +) + +// Detail - System 6xx +const ( + _ = iota + CatSystem + SystemInternalError + SystemMaintainError + SystemTimeoutError +) + +// Detail - PubSub 7xx +const ( + _ = iota + CatPubSub + Publish + Consume + MsgSizeTooLarge +) + +// Detail - Ark 5xxx +const ( + _ = iota + CatArk + ArkInternal + ArkHttp400 +) diff --git a/pkg/errors/code/messsage.go b/pkg/errors/code/messsage.go new file mode 100644 index 0000000..18a4d4f --- /dev/null +++ b/pkg/errors/code/messsage.go @@ -0,0 +1,13 @@ +package code + +// CatToStr collects general error messages for each Category +// It is used to send back to API caller +var CatToStr = map[uint32]string{ + CatInput: "Invalid Input Data", + CatDB: "Database Error", + CatResource: "Resource Error", + CatGRPC: "Internal Service Communication Error", + CatAuth: "Authentication Error", + CatArk: "Internal Service Communication Error", + CatSystem: "System Error", +} diff --git a/pkg/errors/easy_func.go b/pkg/errors/easy_func.go new file mode 100644 index 0000000..e970175 --- /dev/null +++ b/pkg/errors/easy_func.go @@ -0,0 +1,442 @@ +package error + +import ( + "code.30cm.net/wanderland/library-go/pkg/errors/code" + "errors" + "fmt" + "strings" + + "github.com/zeromicro/go-zero/core/logx" + _ "github.com/zeromicro/go-zero/core/logx" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func newErr(scope, detail uint32, msg string) *Err { + cat := detail / 100 * 100 + return &Err{ + category: cat, + code: detail, + scope: scope, + msg: msg, + } +} + +func newBuiltinGRPCErr(scope, detail uint32, msg string) *Err { + return &Err{ + category: code.CatGRPC, + code: detail, + scope: scope, + msg: msg, + } +} + +// FromError tries to let error as Err +// it supports to unwrap error that has Err +// return nil if failed to transfer +func FromError(err error) *Err { + if err == nil { + return nil + } + + var e *Err + if errors.As(err, &e) { + return e + } + + return nil +} + +// FromCode parses code as following +// Decimal: 120314 +// 12 represents Scope +// 03 represents Category +// 14 represents Detail error code +func FromCode(code uint32) *Err { + scope := code / 10000 + detail := code % 10000 + return &Err{ + category: detail / 100 * 100, + code: detail, + scope: scope, + msg: "", + } +} + +// FromGRPCError transfer error to Err +// useful for gRPC client +func FromGRPCError(err error) *Err { + s, _ := status.FromError(err) + e := FromCode(uint32(s.Code())) + e.msg = s.Message() + + // For GRPC built-in code + if e.Scope() == code.Unset && e.Category() == 0 && e.Code() != code.OK { + e = newBuiltinGRPCErr(Scope, e.Code(), s.Message()) + } + + return e +} + +// Deprecated: check GRPCStatus() in Errs struct +// ToGRPCError returns the status.Status +// Useful to return error in gRPC server +func ToGRPCError(e *Err) error { + return status.New(codes.Code(e.FullCode()), e.Error()).Err() +} + +/*** System ***/ + +// SystemTimeoutError returns Err +func SystemTimeoutError(s ...string) *Err { + return newErr(Scope, code.SystemTimeoutError, fmt.Sprintf("system timeout: %s", strings.Join(s, " "))) +} + +// SystemTimeoutErrorL logs error message and returns Err +func SystemTimeoutErrorL(l logx.Logger, s ...string) *Err { + e := SystemTimeoutError(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// SystemInternalError returns Err struct +func SystemInternalError(s ...string) *Err { + return newErr(Scope, code.SystemInternalError, fmt.Sprintf("internal error: %s", strings.Join(s, " "))) +} + +// SystemInternalErrorL logs error message and returns Err +func SystemInternalErrorL(l logx.Logger, s ...string) *Err { + e := SystemInternalError(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// SystemMaintainErrorL logs error message and returns Err +func SystemMaintainErrorL(l logx.Logger, s ...string) *Err { + e := SystemMaintainError(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// SystemMaintainError returns Err struct +func SystemMaintainError(s ...string) *Err { + return newErr(Scope, code.SystemMaintainError, fmt.Sprintf("service under maintenance: %s", strings.Join(s, " "))) +} + +/*** CatInput ***/ + +// InvalidFormat returns Err struct +func InvalidFormat(s ...string) *Err { + return newErr(Scope, code.InvalidFormat, fmt.Sprintf("invalid format: %s", strings.Join(s, " "))) +} + +// InvalidFormatL logs error message and returns Err +func InvalidFormatL(l logx.Logger, s ...string) *Err { + e := InvalidFormat(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InvalidRange returns Err struct +func InvalidRange(s ...string) *Err { + return newErr(Scope, code.InvalidRange, fmt.Sprintf("invalid range: %s", strings.Join(s, " "))) +} + +// InvalidRangeL logs error message and returns Err +func InvalidRangeL(l logx.Logger, s ...string) *Err { + e := InvalidRange(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// NotValidImplementation returns Err struct +func NotValidImplementation(s ...string) *Err { + return newErr(Scope, code.NotValidImplementation, fmt.Sprintf("not valid implementation: %s", strings.Join(s, " "))) +} + +// NotValidImplementationL logs error message and returns Err +func NotValidImplementationL(l logx.Logger, s ...string) *Err { + e := NotValidImplementation(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +/*** CatDB ***/ + +// DBError returns Err +func DBError(s ...string) *Err { + return newErr(Scope, code.DBError, fmt.Sprintf("db error: %s", strings.Join(s, " "))) +} + +// DBErrorL logs error message and returns Err +func DBErrorL(l logx.Logger, s ...string) *Err { + e := DBError(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// DBDataConvert returns Err +func DBDataConvert(s ...string) *Err { + return newErr(Scope, code.DBDataConvert, fmt.Sprintf("data from db convert error: %s", strings.Join(s, " "))) +} + +// DBDataConvertL logs error message and returns Err +func DBDataConvertL(l logx.Logger, s ...string) *Err { + e := DBDataConvert(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// DBDuplicate returns Err +func DBDuplicate(s ...string) *Err { + return newErr(Scope, code.DBDuplicate, fmt.Sprintf("data Duplicate key error: %s", strings.Join(s, " "))) +} + +// DBDuplicateL logs error message and returns Err +func DBDuplicateL(l logx.Logger, s ...string) *Err { + e := DBDuplicate(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +/*** CatResource ***/ + +// ResourceNotFound returns Err and logging +func ResourceNotFound(s ...string) *Err { + return newErr(Scope, code.ResourceNotFound, fmt.Sprintf("resource not found: %s", strings.Join(s, " "))) +} + +// ResourceNotFoundL logs error message and returns Err +func ResourceNotFoundL(l logx.Logger, s ...string) *Err { + e := ResourceNotFound(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InvalidResourceFormat returns Err +func InvalidResourceFormat(s ...string) *Err { + return newErr(Scope, code.InvalidResourceFormat, fmt.Sprintf("invalid resource format: %s", strings.Join(s, " "))) +} + +// InvalidResourceFormatL logs error message and returns Err +func InvalidResourceFormatL(l logx.Logger, s ...string) *Err { + e := InvalidResourceFormat(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InvalidResourceState returns status not correct. +// for example: company should be destroy, agent should be no-sensor/fail-install ... +func InvalidResourceState(s ...string) *Err { + return newErr(Scope, code.InvalidResourceState, fmt.Sprintf("invalid resource state: %s", strings.Join(s, " "))) +} + +// InvalidResourceStateL logs error message and returns status not correct. +func InvalidResourceStateL(l logx.Logger, s ...string) *Err { + e := InvalidResourceState(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +func ResourceInsufficient(s ...string) *Err { + return newErr(Scope, code.ResourceInsufficient, + fmt.Sprintf("insufficient resource: %s", strings.Join(s, " "))) +} + +func ResourceInsufficientL(l logx.Logger, s ...string) *Err { + e := ResourceInsufficient(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InsufficientPermission returns Err +func InsufficientPermission(s ...string) *Err { + return newErr(Scope, code.InsufficientPermission, + fmt.Sprintf("insufficient permission: %s", strings.Join(s, " "))) +} + +// InsufficientPermissionL returns Err and log +func InsufficientPermissionL(l logx.Logger, s ...string) *Err { + e := InsufficientPermission(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// ResourceAlreadyExist returns Err +func ResourceAlreadyExist(s ...string) *Err { + return newErr(Scope, code.ResourceAlreadyExist, fmt.Sprintf("resource already exist: %s", strings.Join(s, " "))) +} + +// ResourceAlreadyExistL logs error message and returns Err +func ResourceAlreadyExistL(l logx.Logger, s ...string) *Err { + e := ResourceAlreadyExist(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InvalidMeasurementID returns Err +func InvalidMeasurementID(s ...string) *Err { + return newErr(Scope, code.InvalidMeasurementID, fmt.Sprintf("missing measurement id: %s", strings.Join(s, " "))) +} + +// InvalidMeasurementIDL logs error message and returns Err +func InvalidMeasurementIDL(l logx.Logger, s ...string) *Err { + e := InvalidMeasurementID(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// ResourceExpired returns Err +func ResourceExpired(s ...string) *Err { + return newErr(Scope, code.ResourceExpired, fmt.Sprintf("resource expired: %s", strings.Join(s, " "))) +} + +// ResourceExpiredL logs error message and returns Err +func ResourceExpiredL(l logx.Logger, s ...string) *Err { + e := ResourceExpired(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// ResourceMigrated returns Err +func ResourceMigrated(s ...string) *Err { + return newErr(Scope, code.ResourceMigrated, fmt.Sprintf("resource migrated: %s", strings.Join(s, " "))) +} + +// ResourceMigratedL logs error message and returns Err +func ResourceMigratedL(l logx.Logger, s ...string) *Err { + e := ResourceMigrated(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InsufficientQuota returns Err +func InsufficientQuota(s ...string) *Err { + return newErr(Scope, code.InsufficientQuota, fmt.Sprintf("insufficient quota: %s", strings.Join(s, " "))) +} + +// InsufficientQuotaL logs error message and returns Err +func InsufficientQuotaL(l logx.Logger, s ...string) *Err { + e := InsufficientQuota(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +/*** CatAuth ***/ + +// Unauthorized returns Err +func Unauthorized(s ...string) *Err { + return newErr(Scope, code.Unauthorized, fmt.Sprintf("unauthorized: %s", strings.Join(s, " "))) +} + +// UnauthorizedL logs error message and returns Err +func UnauthorizedL(l logx.Logger, s ...string) *Err { + e := Unauthorized(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// AuthExpired returns Err +func AuthExpired(s ...string) *Err { + return newErr(Scope, code.AuthExpired, fmt.Sprintf("expired: %s", strings.Join(s, " "))) +} + +// AuthExpiredL logs error message and returns Err +func AuthExpiredL(l logx.Logger, s ...string) *Err { + e := AuthExpired(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// InvalidPosixTime returns Err +func InvalidPosixTime(s ...string) *Err { + return newErr(Scope, code.InvalidPosixTime, fmt.Sprintf("invalid posix time: %s", strings.Join(s, " "))) +} + +// InvalidPosixTimeL logs error message and returns Err +func InvalidPosixTimeL(l logx.Logger, s ...string) *Err { + e := InvalidPosixTime(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// SigAndPayloadNotMatched returns Err +func SigAndPayloadNotMatched(s ...string) *Err { + return newErr(Scope, code.SigAndPayloadNotMatched, fmt.Sprintf("signature and the payload are not match: %s", strings.Join(s, " "))) +} + +// SigAndPayloadNotMatchedL logs error message and returns Err +func SigAndPayloadNotMatchedL(l logx.Logger, s ...string) *Err { + e := SigAndPayloadNotMatched(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// Forbidden returns Err +func Forbidden(s ...string) *Err { + return newErr(Scope, code.Forbidden, fmt.Sprintf("forbidden: %s", strings.Join(s, " "))) +} + +// ForbiddenL logs error message and returns Err +func ForbiddenL(l logx.Logger, s ...string) *Err { + e := Forbidden(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// IsAuthUnauthorizedError check the err is unauthorized error +func IsAuthUnauthorizedError(err *Err) bool { + switch err.Code() { + case code.Unauthorized, code.AuthExpired, code.InvalidPosixTime, + code.SigAndPayloadNotMatched, code.Forbidden, + code.InvalidFormat, code.ResourceNotFound: + return true + default: + return false + } +} + +/*** CatXBC ***/ + +// ArkInternal returns Err +func ArkInternal(s ...string) *Err { + return newErr(Scope, code.ArkInternal, fmt.Sprintf("ark internal error: %s", strings.Join(s, " "))) +} + +// ArkInternalL logs error message and returns Err +func ArkInternalL(l logx.Logger, s ...string) *Err { + e := ArkInternal(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +/*** CatPubSub ***/ + +// Publish returns Err +func Publish(s ...string) *Err { + return newErr(Scope, code.Publish, fmt.Sprintf("publish: %s", strings.Join(s, " "))) +} + +// PublishL logs error message and returns Err +func PublishL(l logx.Logger, s ...string) *Err { + e := Publish(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} + +// Consume returns Err +func Consume(s ...string) *Err { + return newErr(Scope, code.Consume, fmt.Sprintf("consume: %s", strings.Join(s, " "))) +} + +// MsgSizeTooLarge returns Err +func MsgSizeTooLarge(s ...string) *Err { + return newErr(Scope, code.MsgSizeTooLarge, fmt.Sprintf("kafka error: %s", strings.Join(s, " "))) +} + +// MsgSizeTooLargeL logs error message and returns Err +func MsgSizeTooLargeL(l logx.Logger, s ...string) *Err { + e := MsgSizeTooLarge(s...) + l.WithCallerSkip(1).Error(e.Error()) + return e +} diff --git a/pkg/errors/easy_func_test.go b/pkg/errors/easy_func_test.go new file mode 100644 index 0000000..8806b10 --- /dev/null +++ b/pkg/errors/easy_func_test.go @@ -0,0 +1,1031 @@ +package error + +import ( + "code.30cm.net/wanderland/library-go/pkg/errors/code" + "context" + "errors" + "fmt" + "reflect" + "strconv" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/core/logx" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestFromGRPCError_GivenStatusWithCodeAndMessage_ShouldReturnErr(t *testing.T) { + // setup + s := status.Error(codes.Code(102399), "FAKE ERROR") + + // act + e := FromGRPCError(s) + + // assert + assert.Equal(t, uint32(10), e.Scope()) + assert.Equal(t, uint32(2300), e.Category()) + assert.Equal(t, uint32(2399), e.Code()) + assert.Equal(t, "FAKE ERROR", e.Error()) +} + +func TestFromGRPCError_GivenNilError_ShouldReturnErr_Scope0_Cat0_Detail0(t *testing.T) { + // setup + var nilError error = nil + + // act + e := FromGRPCError(nilError) + + // assert + assert.Equal(t, uint32(0), e.Scope()) + assert.Equal(t, uint32(0), e.Category()) + assert.Equal(t, uint32(0), e.Code()) + assert.Equal(t, "", e.Error()) +} + +func TestFromGRPCError_GivenGRPCNativeError_ShouldReturnErr_Scope0_CatGRPC_DetailGRPCUnavailable(t *testing.T) { + // setup + msg := "GRPC Unavailable ERROR" + s := status.Error(codes.Code(codes.Unavailable), msg) + + // act + e := FromGRPCError(s) + + // assert + assert.Equal(t, code.Unset, e.Scope()) + assert.Equal(t, code.CatGRPC, e.Category()) + assert.Equal(t, uint32(codes.Unavailable), e.Code()) + assert.Equal(t, msg, e.Error()) +} + +func TestFromGRPCError_GivenGeneralError_ShouldReturnErr_Scope0_CatGRPC_DetailGRPCUnknown(t *testing.T) { + // setup + generalErr := errors.New("general error") + + // act + e := FromGRPCError(generalErr) + + // assert + assert.Equal(t, code.Unset, e.Scope()) + assert.Equal(t, code.CatGRPC, e.Category()) + assert.Equal(t, uint32(codes.Unknown), e.Code()) +} + +func TestToGRPCError_GivenErr_StatusShouldHave_Code112233(t *testing.T) { + // setup + e := Err{scope: 11, code: 2233, msg: "FAKE MSG"} + + // act + err := ToGRPCError(&e) + s, _ := status.FromError(err) + + // assert + assert.Equal(t, 112233, int(s.Code())) + assert.Equal(t, "FAKE MSG", s.Message()) +} + +func TestInvalidFormat_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidFormat("field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.InvalidFormat, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Equal(t, e.Error(), "invalid format: field A Error description") +} + +func TestInvalidFormatL_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + // act + e := InvalidFormatL(logx.WithContext(ctx), "field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.InvalidFormat, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidRange_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidRange("field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.InvalidRange, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Equal(t, e.Error(), "invalid range: field A Error description") +} + +func TestInvalidRangeL_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + // act + e := InvalidRangeL(logx.WithContext(ctx), "field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.InvalidRange, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestNotValidImplementation_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := NotValidImplementation("field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.NotValidImplementation, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Equal(t, e.Error(), "not valid implementation: field A Error description") +} + +func TestNotValidImplementationL_WithStrings_ShouldHasCatInputAndDetailCode(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + // act + e := NotValidImplementationL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatInput, e.Category()) + assert.Equal(t, code.NotValidImplementation, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestDBError_WithStrings_ShouldHasCatDBAndDetailCodeDBError(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := DBError("field A", "Error description") + + // assert + assert.Equal(t, code.CatDB, e.Category()) + assert.Equal(t, code.DBError, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestDBDataConvert_WithStrings_ShouldHasCatDBAndDetailCodeDBDataConvert(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := DBDataConvert("field A", "Error description") + + // assert + assert.Equal(t, code.CatDB, e.Category()) + assert.Equal(t, code.DBDataConvert, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceNotFound_WithStrings_ShouldHasCatResource_DetailCodeResourceNotFound(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := ResourceNotFound("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceNotFound, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidResourceFormat_WithStrings_ShouldHasCatResource_DetailCodeInvalidResourceFormat(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidResourceFormat("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InvalidResourceFormat, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidResourceState_OK(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidResourceState("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InvalidResourceState, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.EqualError(t, e, "invalid resource state: field A Error description") +} + +func TestInvalidResourceStateL_LogError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := InvalidResourceStateL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InvalidResourceState, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.EqualError(t, e, "invalid resource state: field A Error description") +} + +func TestAuthExpired_OK(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := AuthExpired("field A", "Error description") + + // assert + assert.Equal(t, code.CatAuth, e.Category()) + assert.Equal(t, code.AuthExpired, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestUnauthorized_WithStrings_ShouldHasCatAuth_DetailCodeUnauthorized(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := Unauthorized("field A", "Error description") + + // assert + assert.Equal(t, code.CatAuth, e.Category()) + assert.Equal(t, code.Unauthorized, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidPosixTime_WithStrings_ShouldHasCatAuth_DetailCodeInvalidPosixTime(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidPosixTime("field A", "Error description") + + // assert + assert.Equal(t, code.CatAuth, e.Category()) + assert.Equal(t, code.InvalidPosixTime, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestSigAndPayloadNotMatched_WithStrings_ShouldHasCatAuth_DetailCodeSigAndPayloadNotMatched(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := SigAndPayloadNotMatched("field A", "Error description") + + // assert + assert.Equal(t, code.CatAuth, e.Category()) + assert.Equal(t, code.SigAndPayloadNotMatched, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestForbidden_WithStrings_ShouldHasCatAuth_DetailCodeForbidden(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := Forbidden("field A", "Error description") + + // assert + assert.Equal(t, code.CatAuth, e.Category()) + assert.Equal(t, code.Forbidden, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestXBCInternal_WithStrings_ShouldHasCatResource_DetailCodeXBCInternal(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := ArkInternal("field A", "Error description") + + // assert + assert.Equal(t, code.CatArk, e.Category()) + assert.Equal(t, code.ArkInternal, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestGeneralInternalError_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := SystemInternalError("field A", "Error description") + + // assert + assert.Equal(t, code.CatSystem, e.Category()) + assert.Equal(t, code.SystemInternalError, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestGeneralInternalErrorL_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := SystemInternalErrorL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatSystem, e.Category()) + assert.Equal(t, code.SystemInternalError, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestSystemMaintainError_WithStrings_DetailSystemMaintainError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := SystemMaintainErrorL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatSystem, e.Category()) + assert.Equal(t, code.SystemMaintainError, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceAlreadyExist_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := ResourceAlreadyExist("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceAlreadyExist, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceAlreadyExistL_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := ResourceAlreadyExistL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceAlreadyExist, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceInsufficient_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := ResourceInsufficient("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceInsufficient, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceInsufficientL_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := ResourceInsufficientL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceInsufficient, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInsufficientPermission_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InsufficientPermission("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InsufficientPermission, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInsufficientPermissionL_WithStrings_DetailInternalError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := InsufficientPermissionL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InsufficientPermission, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidMeasurementID_WithErrorStrings_ShouldReturnCorrectCodeAndErrorString(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InvalidMeasurementID("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InvalidMeasurementID, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInvalidMeasurementIDL_WithErrorStrings_ShouldReturnCorrectCodeAndErrorStringAndCallLogger(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := InvalidMeasurementIDL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InvalidMeasurementID, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceExpired_OK(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := ResourceExpired("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceExpired, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceExpiredL_LogError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := ResourceExpiredL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceExpired, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceMigrated_OK(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := ResourceMigrated("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceMigrated, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestResourceMigratedL_LogError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := ResourceMigratedL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.ResourceMigrated, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInsufficientQuota_OK(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := InsufficientQuota("field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InsufficientQuota, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestInsufficientQuotaL_LogError(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := InsufficientQuotaL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatResource, e.Category()) + assert.Equal(t, code.InsufficientQuota, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestPublish_WithErrorStrings_ShouldReturnCorrectCodeAndErrorString(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := Publish("field A", "Error description") + + // assert + assert.Equal(t, code.CatPubSub, e.Category()) + assert.Equal(t, code.Publish, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestPublishL_WithErrorStrings_ShouldReturnCorrectCodeAndErrorStringAndCallLogger(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := PublishL(l, "field A", "Error description") + + // assert + assert.Equal(t, code.CatPubSub, e.Category()) + assert.Equal(t, code.Publish, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "field A") + assert.Contains(t, e.Error(), "Error description") +} + +func TestMsgSizeTooLarge_WithErrorStrings_ShouldReturnCorrectCodeAndErrorString(t *testing.T) { + // setup + Scope = 99 + defer func() { + Scope = code.Unset + }() + + // act + e := MsgSizeTooLarge("Error description") + + // assert + assert.Equal(t, code.CatPubSub, e.Category()) + assert.Equal(t, code.MsgSizeTooLarge, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "kafka error: Error description") +} + +func TestMsgSizeTooLargeL_WithErrorStrings_ShouldReturnCorrectCodeAndErrorStringAndCallLogger(t *testing.T) { + // setup + Scope = 99 + defer func() { Scope = code.Unset }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + l := logx.WithContext(context.Background()) + + // act + e := MsgSizeTooLargeL(l, "Error description") + + // assert + assert.Equal(t, code.CatPubSub, e.Category()) + assert.Equal(t, code.MsgSizeTooLarge, e.Code()) + assert.Equal(t, uint32(99), e.Scope()) + assert.Contains(t, e.Error(), "kafka error: Error description") +} + +// func TestStructErr_WithInternalErr_ShouldIsFuncReportCorrectly(t *testing.T) { +// // setup +// Scope = 99 +// defer func() { Scope = code.Unset }() +// // arrange 2 layers err +// layer1Err := fmt.Errorf("layer 1 error") +// layer2Err := fmt.Errorf("layer 2: %w", layer1Err) +// +// // act with error chain: InvalidFormat -> layer 2 err -> layer 1 err +// e := InvalidFormat("field A", "Error description") +// err := e.Wrap(layer2Err) +// if err != nil { +// t.Fatalf("Failed to wrap error: %v", err) +// } +// +// // assert +// assert.Equal(t, code.CatInput, e.Category()) +// assert.Equal(t, code.InvalidFormat, e.Code()) +// assert.Equal(t, uint32(99), e.Scope()) +// assert.Contains(t, e.Error(), "field A") +// assert.Contains(t, e.Error(), "Error description") +// +// // errors.Is should report correctly +// assert.True(t, errors.Is(e, layer1Err)) +// assert.True(t, errors.Is(e, layer2Err)) +// } + +// func TestStructErr_WithInternalErr_ShouldErrorOutputChainErrMessage(t *testing.T) { +// // setup +// Scope = 99 +// defer func() { Scope = code.Unset }() +// +// // arrange 2 layers err +// layer1Err := fmt.Errorf("layer 1 error") +// // act with error chain: InvalidFormat -> layer 1 err +// e := InvalidFormat("field A", "Error description") +// err := e.Wrap(layer1Err) +// if err != nil { +// t.Fatalf("Failed to wrap error: %v", err) +// } +// +// // assert +// assert.Equal(t, "invalid format: field A Error description: layer 1 error", e.Error()) +// } + +// arrange a specific err type just for UT +type testErr struct { + code int +} + +func (e *testErr) Error() string { + return strconv.Itoa(e.code) +} + +// func TestStructErr_WithInternalErr_ShouldAsFuncReportCorrectly(t *testing.T) { +// // setup +// Scope = 99 +// defer func() { Scope = code.Unset }() +// +// testE := &testErr{code: 123} +// layer2Err := fmt.Errorf("layer 2: %w", testE) +// +// // act with error chain: InvalidFormat -> layer 2 err -> testErr +// e := InvalidFormat("field A", "Error description") +// err := e.Wrap(layer2Err) +// if err != nil { +// t.Fatalf("Failed to wrap error: %v", err) +// } +// +// // assert +// assert.Equal(t, code.CatInput, e.Category()) +// assert.Equal(t, code.InvalidFormat, e.Code()) +// assert.Equal(t, uint32(99), e.Scope()) +// assert.Contains(t, e.Error(), "field A") +// assert.Contains(t, e.Error(), "Error description") +// +// // errors.As should report correctly +// var internalErr *testErr +// assert.True(t, errors.As(e, &internalErr)) +// assert.Equal(t, testE, internalErr) +// } + +/* +benchmark run for 1 second: +Benchmark_ErrorsIs_OneLayerError-4 148281332 8.68 ns/op 0 B/op 0 allocs/op +Benchmark_ErrorsIs_TwoLayerError-4 35048202 32.4 ns/op 0 B/op 0 allocs/op +Benchmark_ErrorsIs_FourLayerError-4 15309349 81.7 ns/op 0 B/op 0 allocs/op + +Benchmark_ErrorsAs_OneLayerError-4 16893205 70.4 ns/op 0 B/op 0 allocs/op +Benchmark_ErrorsAs_TwoLayerError-4 10568083 112 ns/op 0 B/op 0 allocs/op +Benchmark_ErrorsAs_FourLayerError-4 6307729 188 ns/op 0 B/op 0 allocs/op +*/ +func Benchmark_ErrorsIs_OneLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + var err error = layer1Err + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + errors.Is(err, layer1Err) + } +} + +func Benchmark_ErrorsIs_TwoLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + + // act with error chain: InvalidFormat(layer 2) -> testErr(layer 1) + layer2Err := InvalidFormat("field A", "Error description") + err := layer2Err.Wrap(layer1Err) + if err != nil { + b.Fatalf("Failed to wrap error: %v", err) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + errors.Is(layer2Err, layer1Err) + } +} + +func Benchmark_ErrorsIs_FourLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + layer2Err := fmt.Errorf("layer 2: %w", layer1Err) + layer3Err := fmt.Errorf("layer 3: %w", layer2Err) + // act with error chain: InvalidFormat(layer 4) -> Error(layer 3) -> Error(layer 2) -> testErr(layer 1) + layer4Err := InvalidFormat("field A", "Error description") + err := layer4Err.Wrap(layer3Err) + if err != nil { + b.Fatalf("Failed to wrap error: %v", err) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + errors.Is(layer4Err, layer1Err) + } +} + +func Benchmark_ErrorsAs_OneLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + var err error = layer1Err + + b.ReportAllocs() + b.ResetTimer() + var internalErr *testErr + for i := 0; i < b.N; i++ { + errors.As(err, &internalErr) + } +} + +func Benchmark_ErrorsAs_TwoLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + + // act with error chain: InvalidFormat(layer 2) -> testErr(layer 1) + layer2Err := InvalidFormat("field A", "Error description") + err := layer2Err.Wrap(layer1Err) + if err != nil { + b.Fatalf("Failed to wrap error: %v", err) + } + + b.ReportAllocs() + b.ResetTimer() + var internalErr *testErr + for i := 0; i < b.N; i++ { + errors.As(layer2Err, &internalErr) + } +} + +func Benchmark_ErrorsAs_FourLayerError(b *testing.B) { + layer1Err := &testErr{code: 123} + layer2Err := fmt.Errorf("layer 2: %w", layer1Err) + layer3Err := fmt.Errorf("layer 3: %w", layer2Err) + // act with error chain: InvalidFormat(layer 4) -> Error(layer 3) -> Error(layer 2) -> testErr(layer 1) + layer4Err := InvalidFormat("field A", "Error description") + err := layer4Err.Wrap(layer3Err) + if err != nil { + b.Fatalf("Failed to wrap error: %v", err) + } + + b.ReportAllocs() + b.ResetTimer() + var internalErr *testErr + for i := 0; i < b.N; i++ { + errors.As(layer4Err, &internalErr) + } +} + +func TestFromError(t *testing.T) { + tests := []struct { + name string + givenError error + want *Err + }{ + { + "given nil error should return nil", + nil, + nil, + }, + { + "given normal error should return nil", + errors.New("normal error"), + nil, + }, + { + "given Err should return Err", + ResourceNotFound("fake error"), + ResourceNotFound("fake error"), + }, + { + "given error wraps Err should return Err", + fmt.Errorf("outter error wraps %w", ResourceNotFound("fake error")), + ResourceNotFound("fake error"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FromError(tt.givenError); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FromError() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 0000000..ca9e8c3 --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,195 @@ +package error + +import ( + "code.30cm.net/wanderland/library-go/pkg/errors/code" + "errors" + "fmt" + "net/http" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Scope 全域變數應由服務或模組設置 +var Scope = code.Unset + +type Err struct { + category uint32 + code uint32 + scope uint32 + msg string + internalErr error +} + +// Error 是錯誤的介面 +// 私有屬性 "msg" 的 getter 函數 +func (e *Err) Error() string { + if e == nil { + return "" + } + + // 如果 internal err 存在,連接錯誤字串 + var internalErrStr string + if e.internalErr != nil { + internalErrStr = e.internalErr.Error() + } + + if e.msg != "" { + if internalErrStr != "" { + return fmt.Sprintf("%s: %s", e.msg, internalErrStr) + } + return e.msg + } + + generalErrStr := e.GeneralError() + if internalErrStr != "" { + return fmt.Sprintf("%s: %s", generalErrStr, internalErrStr) + } + return generalErrStr +} + +// Category 私有屬性 "category" 的 getter 函數 +func (e *Err) Category() uint32 { + if e == nil { + return 0 + } + return e.category +} + +// Scope 私有屬性 "scope" 的 getter 函數 +func (e *Err) Scope() uint32 { + if e == nil { + return code.Unset + } + + return e.scope +} + +// CodeStr 返回帶有零填充的錯誤代碼字串 +func (e *Err) CodeStr() string { + if e == nil { + return "00000" + } + + if e.Category() == code.CatGRPC { + return fmt.Sprintf("%d%04d", e.Scope(), e.Category()+e.Code()) + } + + return fmt.Sprintf("%d%04d", e.Scope(), e.Code()) +} + +// Code 私有屬性 "code" 的 getter 函數 +func (e *Err) Code() uint32 { + if e == nil { + return code.OK + } + + return e.code +} + +func (e *Err) FullCode() uint32 { + if e == nil { + return 0 + } + + if e.Category() == code.CatGRPC { + return e.Scope()*10000 + e.Category() + e.Code() + } + + return e.Scope()*10000 + e.Code() +} + +// HTTPStatus 返回對應的 HTTP 狀態碼 +func (e *Err) HTTPStatus() int { + if e == nil || e.Code() == code.OK { + return http.StatusOK + } + // 根據 code 判斷狀態碼 + switch e.Code() { + case code.ResourceInsufficient: + // 400 + return http.StatusBadRequest + case code.Unauthorized, code.InsufficientPermission: + // 401 + return http.StatusUnauthorized + case code.InsufficientQuota: + // 402 + return http.StatusPaymentRequired + case code.InvalidPosixTime, code.Forbidden: + // 403 + return http.StatusForbidden + case code.ResourceNotFound: + // 404 + return http.StatusNotFound + case code.ResourceAlreadyExist, code.InvalidResourceState: + // 409 + return http.StatusConflict + case code.NotValidImplementation: + // 501 + return http.StatusNotImplemented + default: + } + + // 根據 category 判斷狀態碼 + switch e.Category() { + case code.CatInput: + return http.StatusBadRequest + default: + // 如果沒有符合的條件,返回狀態碼 500 + return http.StatusInternalServerError + } +} + +// GeneralError 轉換 category 級別錯誤訊息 +// 這是給客戶或 API 調用者的一般錯誤訊息 +func (e *Err) GeneralError() string { + if e == nil { + return "" + } + + errStr, ok := code.CatToStr[e.Category()] + if !ok { + return "" + } + + return errStr +} + +// Is 在執行 errors.Is() 時調用。 +// 除非你非常確定你在做什麼,否則不要直接使用這個函數。 +// 請使用 errors.Is 代替。 +// 此函數比較兩個錯誤變量是否都是 *Err,並且具有相同的 code(不檢查包裹的內部錯誤) +func (e *Err) Is(f error) bool { + var err *Err + ok := errors.As(f, &err) + if !ok { + return false + } + return e.Code() == err.Code() +} + +// Unwrap 返回底層錯誤 +// 解除包裹錯誤的結果本身可能具有 Unwrap 方法; +// 我們稱通過反覆解除包裹產生的錯誤序列為錯誤鏈。 +func (e *Err) Unwrap() error { + if e == nil { + return nil + } + return e.internalErr +} + +// Wrap 將內部錯誤設置到 Err 結構 +func (e *Err) Wrap(internalErr error) *Err { + if e != nil { + e.internalErr = internalErr + } + return e +} + +func (e *Err) GRPCStatus() *status.Status { + if e == nil { + return status.New(codes.OK, "") + } + + return status.New(codes.Code(e.FullCode()), e.Error()) +} diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go new file mode 100644 index 0000000..8adef4d --- /dev/null +++ b/pkg/errors/errors_test.go @@ -0,0 +1,297 @@ +package error + +import ( + "code.30cm.net/wanderland/library-go/pkg/errors/code" + "errors" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestCode_GivenNilReceiver_CodeReturnOK_CodeStrReturns00000(t *testing.T) { + // setup + var e *Err = nil + + // act & assert + assert.Equal(t, code.OK, e.Code()) + assert.Equal(t, "00000", e.CodeStr()) + assert.Equal(t, "", e.Error()) +} + +func TestCode_GivenScope99DetailCode6687_ShouldReturn996687(t *testing.T) { + // setup + e := Err{scope: 99, code: 6687} + + // act & assert + assert.Equal(t, uint32(6687), e.Code()) + assert.Equal(t, "996687", e.CodeStr()) +} + +func TestCode_GivenScope0DetailCode87_ShouldReturn87(t *testing.T) { + // setup + e := Err{scope: 0, code: 87} + + // act & assert + assert.Equal(t, uint32(87), e.Code()) + assert.Equal(t, "00087", e.CodeStr()) +} + +func TestFromCode_Given870005_ShouldHasScope87_Cat0_Detail5(t *testing.T) { + // setup + e := FromCode(870005) + + // assert + assert.Equal(t, uint32(87), e.Scope()) + assert.Equal(t, uint32(0), e.Category()) + assert.Equal(t, uint32(5), e.Code()) + assert.Equal(t, "", e.Error()) +} + +func TestFromCode_Given0_ShouldHasScope0_Cat0_Detail0(t *testing.T) { + // setup + e := FromCode(0) + + // assert + assert.Equal(t, uint32(0), e.Scope()) + assert.Equal(t, uint32(0), e.Category()) + assert.Equal(t, uint32(0), e.Code()) + assert.Equal(t, "", e.Error()) +} + +func TestFromCode_Given9105_ShouldHasScope0_Cat9100_Detail9105(t *testing.T) { + // setup + e := FromCode(9105) + + // assert + assert.Equal(t, uint32(0), e.Scope()) + assert.Equal(t, uint32(9100), e.Category()) + assert.Equal(t, uint32(9105), e.Code()) + assert.Equal(t, "", e.Error()) +} + +func TestErr_ShouldImplementErrorFunction(t *testing.T) { + // setup a func return error + f := func() error { return InvalidFormat("fake field") } + + // act + err := f() + + // assert + assert.NotNil(t, err) + assert.Contains(t, fmt.Sprint(err), "fake field") // can be printed +} + +func TestGeneralError_GivenNilErr_ShouldReturnEmptyString(t *testing.T) { + // setup + var e *Err = nil + + // act & assert + assert.Equal(t, "", e.GeneralError()) +} + +func TestGeneralError_GivenNotExistCat_ShouldReturnEmptyString(t *testing.T) { + // setup + e := Err{category: 123456} + + // act & assert + assert.Equal(t, "", e.GeneralError()) +} + +func TestGeneralError_GivenCatDB_ShouldReturnDBError(t *testing.T) { + // setup + e := Err{category: code.CatDB} + catErrStr := code.CatToStr[code.CatDB] + + // act & assert + assert.Equal(t, catErrStr, e.GeneralError()) +} + +func TestError_GivenEmptyMsg_ShouldReturnCatGeneralErrorMessage(t *testing.T) { + // setup + e := Err{category: code.CatDB, msg: ""} + + // act + errMsg := e.Error() + + // assert + assert.Equal(t, code.CatToStr[code.CatDB], errMsg) +} + +func TestError_GivenMsg_ShouldReturnGiveMsg(t *testing.T) { + // setup + e := Err{msg: "FAKE"} + + // act + errMsg := e.Error() + + // assert + assert.Equal(t, "FAKE", errMsg) +} + +func TestIs_GivenNilErr_ShouldReturnFalse(t *testing.T) { + var nilErrs *Err + // act + result := errors.Is(nilErrs, DBError()) + result2 := errors.Is(DBError(), nilErrs) + + // assert + assert.False(t, result) + assert.False(t, result2) +} + +func TestIs_GivenNil_ShouldReturnFalse(t *testing.T) { + // act + result := errors.Is(nil, DBError()) + result2 := errors.Is(DBError(), nil) + + // assert + assert.False(t, result) + assert.False(t, result2) +} + +func TestIs_GivenNilReceiver_ShouldReturnCorrectResult(t *testing.T) { + var nilErr *Err = nil + + // test 1: nilErr != DBError + var dbErr error = DBError("fake db error") + assert.False(t, nilErr.Is(dbErr)) + + // test 2: nilErr != nil error + var nilError error + assert.False(t, nilErr.Is(nilError)) + + // test 3: nilErr == another nilErr + var nilErr2 *Err = nil + assert.True(t, nilErr.Is(nilErr2)) +} + +func TestIs_GivenDBError_ShouldReturnTrue(t *testing.T) { + // setup + dbErr := DBError("fake db error") + + // act + result := errors.Is(dbErr, DBError("not care")) + result2 := errors.Is(DBError(), dbErr) + + // assert + assert.True(t, result) + assert.True(t, result2) +} + +func TestIs_GivenDBErrorAssignToErrorType_ShouldReturnTrue(t *testing.T) { + // setup + var dbErr error = DBError("fake db error") + + // act + result := errors.Is(dbErr, DBError("not care")) + result2 := errors.Is(DBError(), dbErr) + + // assert + assert.True(t, result) + assert.True(t, result2) +} + +func TestWrap_GivenNilErr_ShouldNoPanic(t *testing.T) { + // act & assert + assert.NotPanics(t, func() { + var e *Err = nil + _ = e.Wrap(fmt.Errorf("test")) + }) +} + +func TestWrap_GivenErrorToWrap_ShouldReturnErrorWithWrappedError(t *testing.T) { + // act & assert + wrappedErr := fmt.Errorf("test") + wrappingErr := SystemInternalError("WrappingError").Wrap(wrappedErr) + unWrappedErr := wrappingErr.Unwrap() + + assert.Equal(t, wrappedErr, unWrappedErr) +} + +func TestUnwrap_GivenNilErr_ShouldReturnNil(t *testing.T) { + var e *Err = nil + internalErr := e.Unwrap() + assert.Nil(t, internalErr) +} + +func TestErrorsIs_GivenNilErr_ShouldReturnFalse(t *testing.T) { + var e *Err = nil + assert.False(t, errors.Is(e, fmt.Errorf("test"))) +} + +func TestErrorsAs_GivenNilErr_ShouldReturnFalse(t *testing.T) { + var internalErr *testErr + var e *Err = nil + assert.False(t, errors.As(e, &internalErr)) +} + +func TestGRPCStatus(t *testing.T) { + // setup table driven tests + tests := []struct { + name string + given *Err + expect *status.Status + expectConvert error + }{ + { + "nil errs.Err", + nil, + status.New(codes.OK, ""), + nil, + }, + { + "InvalidFormat Err", + InvalidFormat("fake"), + status.New(codes.Code(101), "invalid format: fake"), + status.New(codes.Code(101), "invalid format: fake").Err(), + }, + } + + // act & assert + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := test.given.GRPCStatus() + assert.Equal(t, test.expect.Code(), s.Code()) + assert.Equal(t, test.expect.Message(), s.Message()) + assert.Equal(t, test.expectConvert, status.Convert(test.given).Err()) + }) + } +} + +func TestErr_HTTPStatus(t *testing.T) { + tests := []struct { + name string + err *Err + want int + }{ + {name: "nil error", err: nil, want: http.StatusOK}, + {name: "invalid measurement id", err: &Err{category: code.CatResource, code: code.InvalidMeasurementID}, want: http.StatusInternalServerError}, + {name: "resource already exists", err: &Err{category: code.CatResource, code: code.ResourceAlreadyExist}, want: http.StatusConflict}, + {name: "invalid resource state", err: &Err{category: code.CatResource, code: code.InvalidResourceState}, want: http.StatusConflict}, + {name: "invalid posix time", err: &Err{category: code.CatAuth, code: code.InvalidPosixTime}, want: http.StatusForbidden}, + {name: "unauthorized", err: &Err{category: code.CatAuth, code: code.Unauthorized}, want: http.StatusUnauthorized}, + {name: "db error", err: &Err{category: code.CatDB, code: code.DBError}, want: http.StatusInternalServerError}, + {name: "insufficient permission", err: &Err{category: code.CatResource, code: code.InsufficientPermission}, want: http.StatusUnauthorized}, + {name: "resource insufficient", err: &Err{category: code.CatResource, code: code.ResourceInsufficient}, want: http.StatusBadRequest}, + {name: "invalid format", err: &Err{category: code.CatInput, code: code.InvalidFormat}, want: http.StatusBadRequest}, + {name: "resource not found", err: &Err{code: code.ResourceNotFound}, want: http.StatusNotFound}, + {name: "ok", err: &Err{code: code.OK}, want: http.StatusOK}, + {name: "not valid implementation", err: &Err{category: code.CatInput, code: code.NotValidImplementation}, want: http.StatusNotImplemented}, + {name: "forbidden", err: &Err{category: code.CatAuth, code: code.Forbidden}, want: http.StatusForbidden}, + {name: "insufficient quota", err: &Err{category: code.CatResource, code: code.InsufficientQuota}, want: http.StatusPaymentRequired}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + // act + got := tt.err.HTTPStatus() + + // assert + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/errors/readme.md b/pkg/errors/readme.md new file mode 100644 index 0000000..5af9e87 --- /dev/null +++ b/pkg/errors/readme.md @@ -0,0 +1,125 @@ +## Purpose of errs package +1. compatible with `error` interface +2. encapsulate error message with functions in `easy_function.go` +3. easy for gRPC client/server +4. support err's chain by [Working with Errors in Go 1.13](https://blog.golang.org/go1.13-errors) + + +## Example - Normal function +Using builtin functions `InvalidInput` to generate `Err struct` + +**please add your own functions if not exist** + +```go +package main + +import "adc.github.trendmicro.com/commercial-mgcp/library-go/pkg/errs" + +func handleParam(s string) error { + // check user_id format + if ok := userIDFormat(s); !ok { + return errs.InvalidFormat("param user_id") + } + return nil +} +``` + +## Example - gRPC Server +`GetAgent` is a method of gRPC server, it wraps `Err` struct to `status.Status` struct +```go +func (as *agentService) GetAgent(ctx context.Context, req *cloudep.GetAgentRequest) (*cloudep.GetAgentResponse, error) { + l := log.WithFields(logger.Fields{"tenant_id": req.TenantId, "agent_id": req.AgentId, "method": "GetAgent"}) + + tenantID, err := primitive.ObjectIDFromHex(req.TenantId) + if err != nil { + // err maybe errs.Err or general error + // it's safe to use Convert() here + return nil, status.Convert(err).Err() + } + ... +} +``` + + +## Example - gRPC Client +Calling `GetAgent` and retry when Category is "DB" +```go +client := cloudep.NewAgentServiceClient(conn) +req := cloudep.GetAgentRequest{ + TenantId: "not-a-valid-object-id", + AgentId: "5eb4fa99006d53c0cb6f9cfe", +} + +// Retry if DB error +for retry := 3; retry > 0 ; retry-- { + resp, err := client.GetAgent(context.Background(), &req) + if err != nil { + e := errs.FromGRPCError(err) + if e.Category() == code.CatGRPC { + if e.Code() == uint32(codes.Unavailable) { + log.warn("GRPC service unavailable. Retrying...") + continue + } + log.errorf("GRPC built-in error: %v", e) + } + if e.Category() == code.CatDB { + log.warn("retry...") + continue + } + } + + break +} +``` + +## Example - REST server +1. handling gRPC client error +2. transfer to HTTP code +3. transfer to Error body + +```go +func Handler(c *gin.Context) { + + // handle error from gRPC client + resp, err := client.GetAgent(context.Background(), &req) + if err != nil { + // to Err + e := errs.FromGRPCError(err) + + // get HTTP code & response struct + // 2nd parameter true means return general error message to user + c.JSON(e.HTTPStatus(), general.NewError(e, true)) + } + +} +``` + +## Example - Error Chain +1. set internal error by func `Wrap` +2. check Err has any error in err's chain matches the target by `errors.Is` +3. finds the first error in err's chain that matches target by `errors.As` + +```go +// define a specific err type +type testErr struct { + code int +} + +func (e *testErr) Error() string { + return strconv.Itoa(e.code) +} + +func main() { + layer1Err := &testErr{code: 123} + // error chain: InvalidFormat -> layer 1 err + layer2Err := InvalidFormat("field A", "") + layer2Err.Wrap(layer1Err) //set internal error + + // errors.Is should report true + hasLayer1Err := errors.Is(layer2Err, layer1Err) + + // errors.As should return internal error + var internalErr *testErr + ok := errors.As(layer2Err, &internalErr) +} +``` \ No newline at end of file