//go:build unit package errors import ( stderrors "errors" "fmt" "io" "net/http" "testing" "github.com/stretchr/testify/require" ) func TestApplicationError_Basics(t *testing.T) { tests := []struct { name string err *ApplicationError want Status wantIs bool target error wrapped error }{ { name: "new", err: New(400, "BAD_REQUEST", "invalid input"), want: Status{ Code: 400, Reason: "BAD_REQUEST", Message: "invalid input", }, }, { name: "is_matches_code_and_reason", err: New(401, "UNAUTHORIZED", "nope"), want: Status{Code: 401, Reason: "UNAUTHORIZED", Message: "nope"}, target: New(401, "UNAUTHORIZED", "ignored message"), wantIs: true, }, { name: "is_does_not_match_reason", err: New(401, "UNAUTHORIZED", "nope"), want: Status{Code: 401, Reason: "UNAUTHORIZED", Message: "nope"}, target: New(401, "DIFFERENT", "ignored message"), wantIs: false, }, { name: "from_error_unwraps_wrapped_application_error", err: New(404, "NOT_FOUND", "missing"), wrapped: fmt.Errorf("wrap: %w", New(404, "NOT_FOUND", "missing")), want: Status{ Code: 404, Reason: "NOT_FOUND", Message: "missing", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.err != nil { require.Equal(t, tt.want, tt.err.Status) } if tt.target != nil { require.Equal(t, tt.wantIs, stderrors.Is(tt.err, tt.target)) } if tt.wrapped != nil { got := FromError(tt.wrapped) require.Equal(t, tt.want, got.Status) } }) } } func TestApplicationError_WithMetadataDeepCopy(t *testing.T) { tests := []struct { name string md map[string]string }{ {name: "non_nil", md: map[string]string{"a": "1"}}, {name: "nil", md: nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { appErr := BadRequest("BAD_REQUEST", "invalid input").WithMetadata(tt.md) if tt.md == nil { require.Nil(t, appErr.Metadata) return } tt.md["a"] = "changed" require.Equal(t, "1", appErr.Metadata["a"]) }) } } func TestFromError_Generic(t *testing.T) { tests := []struct { name string err error wantCode int32 wantReason string wantMsg string }{ { name: "plain_error", err: stderrors.New("boom"), wantCode: UnknownCode, wantReason: UnknownReason, wantMsg: UnknownMessage, }, { name: "wrapped_plain_error", err: fmt.Errorf("wrap: %w", io.EOF), wantCode: UnknownCode, wantReason: UnknownReason, wantMsg: UnknownMessage, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := FromError(tt.err) require.Equal(t, tt.wantCode, got.Code) require.Equal(t, tt.wantReason, got.Reason) require.Equal(t, tt.wantMsg, got.Message) require.Equal(t, tt.err, got.Unwrap()) }) } } func TestToHTTP(t *testing.T) { tests := []struct { name string err error wantStatusCode int wantBody Status }{ { name: "nil_error", err: nil, wantStatusCode: http.StatusOK, wantBody: Status{Code: int32(http.StatusOK)}, }, { name: "application_error", err: Forbidden("FORBIDDEN", "no access"), wantStatusCode: http.StatusForbidden, wantBody: Status{ Code: int32(http.StatusForbidden), Reason: "FORBIDDEN", Message: "no access", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { code, body := ToHTTP(tt.err) require.Equal(t, tt.wantStatusCode, code) require.Equal(t, tt.wantBody, body) }) } } func TestToHTTP_MetadataDeepCopy(t *testing.T) { md := map[string]string{"k": "v"} appErr := BadRequest("BAD_REQUEST", "invalid").WithMetadata(md) code, body := ToHTTP(appErr) require.Equal(t, http.StatusBadRequest, code) require.Equal(t, "v", body.Metadata["k"]) md["k"] = "changed" require.Equal(t, "v", body.Metadata["k"]) appErr.Metadata["k"] = "changed-again" require.Equal(t, "v", body.Metadata["k"]) } func TestHTTPStatusErrorFunctions(t *testing.T) { tests := []struct { name string createFn func(string, string) *ApplicationError isFn func(error) bool code int }{ {"BadRequest", BadRequest, IsBadRequest, http.StatusBadRequest}, {"TooManyRequests", TooManyRequests, IsTooManyRequests, http.StatusTooManyRequests}, {"Unauthorized", Unauthorized, IsUnauthorized, http.StatusUnauthorized}, {"Forbidden", Forbidden, IsForbidden, http.StatusForbidden}, {"NotFound", NotFound, IsNotFound, http.StatusNotFound}, {"Conflict", Conflict, IsConflict, http.StatusConflict}, {"InternalServer", InternalServer, IsInternalServer, http.StatusInternalServerError}, {"ServiceUnavailable", ServiceUnavailable, IsServiceUnavailable, http.StatusServiceUnavailable}, {"GatewayTimeout", GatewayTimeout, IsGatewayTimeout, http.StatusGatewayTimeout}, {"ClientClosed", ClientClosed, IsClientClosed, 499}, } for _, tt := range tests { t.Run(tt.name+"_create", func(t *testing.T) { err := tt.createFn("REASON", "message") require.Equal(t, int32(tt.code), err.Code) require.Equal(t, "REASON", err.Reason) require.Equal(t, "message", err.Message) }) t.Run(tt.name+"_is_true", func(t *testing.T) { err := New(tt.code, "ANY", "test") require.True(t, tt.isFn(err)) }) t.Run(tt.name+"_is_false", func(t *testing.T) { err := New(tt.code+1, "ANY", "test") require.False(t, tt.isFn(err)) }) t.Run(tt.name+"_is_nil", func(t *testing.T) { require.False(t, tt.isFn(nil)) }) } } func TestToHTTP_NonApplicationError(t *testing.T) { code, body := ToHTTP(stderrors.New("plain error")) require.Equal(t, http.StatusInternalServerError, code) require.Equal(t, int32(UnknownCode), body.Code) }