diff --git a/error/handler.go b/error/handler.go new file mode 100644 index 0000000..1ce5e62 --- /dev/null +++ b/error/handler.go @@ -0,0 +1,61 @@ +package error + +import ( + "fmt" + + "github.com/gin-gonic/gin" + + "net/http" + "strings" + "unicode" + + "gopkg.in/go-playground/validator.v8" +) + +type errorWrapper struct { + Error string `json:"error"` + ErrorCode int `json:"errorCode"` + ErrorDescription string `json:"errorDescription"` +} + +// Handler creates a gin middleware for handling errors. +func Handler() gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + + if len(c.Errors) > 0 { + for _, e := range c.Errors { + switch e.Type { + case gin.ErrorTypeBind: + errs := e.Err.(validator.ValidationErrors) + var stringErrors []string + for _, err := range errs { + stringErrors = append(stringErrors, validationErrorToText(err)) + } + writeError(c, strings.Join(stringErrors, "; ")) + default: + writeError(c, e.Err.Error()) + } + } + } + } +} + +func validationErrorToText(e *validator.FieldError) string { + runes := []rune(e.Field) + runes[0] = unicode.ToLower(runes[0]) + fieldName := string(runes) + switch e.Tag { + case "required": + return fmt.Sprintf("Field '%s' is required", fieldName) + } + return fmt.Sprintf("Field '%s' is not valid", fieldName) +} + +func writeError(ctx *gin.Context, errString string) { + status := http.StatusBadRequest + if ctx.Writer.Status() != http.StatusOK { + status = ctx.Writer.Status() + } + ctx.JSON(status, &errorWrapper{Error: http.StatusText(status), ErrorCode: status, ErrorDescription: errString}) +} diff --git a/error/handler_test.go b/error/handler_test.go new file mode 100644 index 0000000..d6f9143 --- /dev/null +++ b/error/handler_test.go @@ -0,0 +1,63 @@ +package error + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func TestDefaultErrorInternal(t *testing.T) { + gin.SetMode(gin.TestMode) + rec := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(rec) + ctx.AbortWithError(500, errors.New("something went wrong")) + + Handler()(ctx) + + assertJSONResponse(t, rec, 500, `{"errorCode":500, "errorDescription":"something went wrong", "error":"Internal Server Error"}`) +} + +func TestDefaultErrorBadRequest(t *testing.T) { + gin.SetMode(gin.TestMode) + rec := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(rec) + ctx.AbortWithError(400, errors.New("you need todo something")) + + Handler()(ctx) + + assertJSONResponse(t, rec, 400, `{"errorCode":400, "errorDescription":"you need todo something", "error":"Bad Request"}`) +} + +type testValidate struct { + Username string `json:"username" binding:"required"` + Mail string `json:"mail" binding:"email"` +} + +func TestValidationError(t *testing.T) { + gin.SetMode(gin.TestMode) + rec := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(rec) + ctx.Request = httptest.NewRequest("GET", "/uri", nil) + + assert.NotNil(t, ctx.Bind(&testValidate{})) + Handler()(ctx) + + err := new(errorWrapper) + json.NewDecoder(rec.Body).Decode(err) + assert.Equal(t, 400, rec.Code) + assert.Equal(t, "Bad Request", err.Error) + assert.Equal(t, 400, err.ErrorCode) + assert.Contains(t, err.ErrorDescription, "Field 'username' is required") + assert.Contains(t, err.ErrorDescription, "Field 'mail' is not valid") +} + +func assertJSONResponse(t *testing.T, rec *httptest.ResponseRecorder, code int, json string) { + bytes, _ := ioutil.ReadAll(rec.Body) + assert.Equal(t, code, rec.Code) + assert.JSONEq(t, json, string(bytes)) +}