// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. package gin import ( "errors" "fmt" "net/http/httptest" "testing" "github.com/gin-gonic/gin/codec/json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestError(t *testing.T) { baseError := errors.New("test error") err := &Error{ Err: baseError, Type: ErrorTypePrivate, } assert.Equal(t, err.Error(), baseError.Error()) assert.Equal(t, H{"error": baseError.Error()}, err.JSON()) assert.Equal(t, err.SetType(ErrorTypePublic), err) assert.Equal(t, ErrorTypePublic, err.Type) assert.Equal(t, err.SetMeta("some data"), err) assert.Equal(t, "some data", err.Meta) assert.Equal(t, H{ "error": baseError.Error(), "meta": "some data", }, err.JSON()) jsonBytes, _ := json.API.Marshal(err) assert.JSONEq(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes)) err.SetMeta(H{ //nolint: errcheck "status": "200", "data": "some data", }) assert.Equal(t, H{ "error": baseError.Error(), "status": "200", "data": "some data", }, err.JSON()) err.SetMeta(H{ //nolint: errcheck "error": "custom error", "status": "200", "data": "some data", }) assert.Equal(t, H{ "error": "custom error", "status": "200", "data": "some data", }, err.JSON()) type customError struct { status string data string } err.SetMeta(customError{status: "200", data: "other data"}) //nolint: errcheck assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON()) } func TestErrorSlice(t *testing.T) { errs := errorMsgs{ {Err: errors.New("first"), Type: ErrorTypePrivate}, {Err: errors.New("second"), Type: ErrorTypePrivate, Meta: "some data"}, {Err: errors.New("third"), Type: ErrorTypePublic, Meta: H{"status": "400"}}, } assert.Equal(t, errs, errs.ByType(ErrorTypeAny)) assert.Equal(t, "third", errs.Last().Error()) assert.Equal(t, []string{"first", "second", "third"}, errs.Errors()) assert.Equal(t, []string{"third"}, errs.ByType(ErrorTypePublic).Errors()) assert.Equal(t, []string{"first", "second"}, errs.ByType(ErrorTypePrivate).Errors()) assert.Equal(t, []string{"first", "second", "third"}, errs.ByType(ErrorTypePublic|ErrorTypePrivate).Errors()) assert.Empty(t, errs.ByType(ErrorTypeBind)) assert.Empty(t, errs.ByType(ErrorTypeBind).String()) assert.Equal(t, `Error #01: first Error #02: second Meta: some data Error #03: third Meta: map[status:400] `, errs.String()) assert.Equal(t, []any{ H{"error": "first"}, H{"error": "second", "meta": "some data"}, H{"error": "third", "status": "400"}, }, errs.JSON()) jsonBytes, _ := json.API.Marshal(errs) assert.JSONEq(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes)) errs = errorMsgs{ {Err: errors.New("first"), Type: ErrorTypePrivate}, } assert.Equal(t, H{"error": "first"}, errs.JSON()) jsonBytes, _ = json.API.Marshal(errs) assert.JSONEq(t, "{\"error\":\"first\"}", string(jsonBytes)) errs = errorMsgs{} assert.Nil(t, errs.Last()) assert.Nil(t, errs.JSON()) assert.Empty(t, errs.String()) } type TestErr string func (e TestErr) Error() string { return string(e) } // TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()". // "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13. func TestErrorUnwrap(t *testing.T) { innerErr := TestErr("some error") // 2 layers of wrapping : use 'fmt.Errorf("%w")' to wrap a gin.Error{}, which itself wraps innerErr err := fmt.Errorf("wrapped: %w", &Error{ Err: innerErr, Type: ErrorTypeAny, }) // check that 'errors.Is()' and 'errors.As()' behave as expected : require.ErrorIs(t, err, innerErr) var testErr TestErr require.ErrorAs(t, err, &testErr) // Test non-pointer usage of gin.Error errNonPointer := Error{ Err: innerErr, Type: ErrorTypeAny, } wrappedErr := fmt.Errorf("wrapped: %w", errNonPointer) // Check that 'errors.Is()' and 'errors.As()' behave as expected for non-pointer usage require.ErrorIs(t, wrappedErr, innerErr) var testErrNonPointer TestErr require.ErrorAs(t, wrappedErr, &testErrNonPointer) } // TestErrorJoinUnwrap tests that gin.Error() automatically unwraps errors.Join() created joined errors. func TestErrorJoinUnwrap(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) // Test with errors.Join() err1 := errors.New("service error") err2 := errors.New("store error") joinedErr := errors.Join(err1, err2) _ = c.Error(joinedErr) // Should be unwrapped into 2 separate errors assert.Len(t, c.Errors, 2) assert.Equal(t, "service error", c.Errors[0].Error()) assert.Equal(t, "store error", c.Errors[1].Error()) // Test mixed usage c2, _ := CreateTestContext(httptest.NewRecorder()) _ = c2.Error(errors.New("gin error")) _ = c2.Error(errors.Join(err1, err2)) _ = c2.Error(errors.New("other error")) assert.Len(t, c2.Errors, 4) expected := `Error #01: gin error Error #02: service error Error #03: store error Error #04: other error ` assert.Equal(t, expected, c2.Errors.String()) // Test empty join (edge case) c3, _ := CreateTestContext(httptest.NewRecorder()) emptyJoin := errors.Join() // Creates nil error if emptyJoin != nil { _ = c3.Error(emptyJoin) // errors.Join() with no arguments returns nil, so this shouldn't panic } // Test nested joins c4, _ := CreateTestContext(httptest.NewRecorder()) err3 := errors.New("nested1") err4 := errors.New("nested2") nestedJoin := errors.Join(errors.Join(err3, err4), errors.New("outer")) _ = c4.Error(nestedJoin) assert.Len(t, c4.Errors, 3) assert.Equal(t, "nested1", c4.Errors[0].Error()) assert.Equal(t, "nested2", c4.Errors[1].Error()) assert.Equal(t, "outer", c4.Errors[2].Error()) }