fleet/errors/errors_test.go
Zachary Wasserman 604e3e4fb0 Improve error handling throughout backend (#50)
* New function `errors.ReturnError` for writing errors into the HTTP response
* New type `KolideError` that includes additional error context
* Validation and application errors are reported in a consistent JSON format
* Add 404 handler
* Refactored error handling throughout codebase to use new error patterns
2016-08-09 19:04:28 -07:00

211 lines
5.1 KiB
Go

package errors
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"sort"
"testing"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"
"gopkg.in/go-playground/validator.v8"
)
func TestNew(t *testing.T) {
kolideErr := New("Public message", "Private message")
expect := &KolideError{
Err: nil,
StatusCode: http.StatusInternalServerError,
PublicMessage: "Public message",
PrivateMessage: "Private message",
}
assert.Equal(t, expect, kolideErr)
}
func TestNewWithStatus(t *testing.T) {
kolideErr := NewWithStatus(http.StatusUnauthorized, "Public message", "Private message")
expect := &KolideError{
Err: nil,
StatusCode: http.StatusUnauthorized,
PublicMessage: "Public message",
PrivateMessage: "Private message",
}
assert.Equal(t, expect, kolideErr)
}
func TestNewFromError(t *testing.T) {
err := errors.New("Foo error")
kolideErr := NewFromError(err, StatusUnprocessableEntity, "Public error")
assert.Equal(t, "Public error", kolideErr.Error())
expect := &KolideError{
Err: err,
StatusCode: StatusUnprocessableEntity,
PublicMessage: "Public error",
PrivateMessage: "Foo error",
}
assert.Equal(t, expect, kolideErr)
}
func TestDatabaseError(t *testing.T) {
err := errors.New("Foo error")
kolideErr := DatabaseError(err)
expect := &KolideError{
Err: err,
StatusCode: http.StatusInternalServerError,
PublicMessage: "Database error",
PrivateMessage: "Foo error",
}
assert.Equal(t, expect, kolideErr)
}
func TestReturnErrorUnspecified(t *testing.T) {
r := gin.New()
r.POST("/foo", func(c *gin.Context) {
ReturnError(c, errors.New("foo"))
})
req, _ := http.NewRequest("POST", "/foo", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusInternalServerError {
t.Errorf("Should respond with 500, got %d", resp.Code)
}
expect := `{"message": "Unspecified error"}`
assert.JSONEq(t, expect, resp.Body.String())
}
func TestReturnErrorKolideError(t *testing.T) {
r := gin.New()
r.POST("/foo", func(c *gin.Context) {
ReturnError(c, &KolideError{
StatusCode: http.StatusUnauthorized,
PublicMessage: "Some error",
})
})
req, _ := http.NewRequest("POST", "/foo", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusUnauthorized {
t.Errorf("Should respond with 403, got %d", resp.Code)
}
expect := `{"message": "Some error"}`
assert.JSONEq(t, expect, resp.Body.String())
}
// These types and functions for performing an unordered comparison on a
// []map[string]string] as parsed from the error JSON
type errorField map[string]string
type errorFields []errorField
func (e errorFields) Len() int {
return len(e)
}
func (e errorFields) Less(i, j int) bool {
return e[i]["field"] <= e[j]["field"] &&
e[i]["code"] <= e[j]["code"] &&
e[i]["message"] <= e[j]["message"]
}
func (e errorFields) Swap(i, j int) {
e[i], e[j] = e[j], e[i]
}
func TestReturnErrorValidationError(t *testing.T) {
r := gin.New()
type Foo struct {
Email string `json:"email_foo" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
validate := validator.New(&validator.Config{TagName: "validate", FieldNameTag: "json"})
r.POST("/foo", func(c *gin.Context) {
ReturnError(c, validate.Struct(&Foo{Email: "foo", Password: ""}))
})
req, _ := http.NewRequest("POST", "/foo", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != StatusUnprocessableEntity {
t.Errorf("Should respond with 422, got %d", resp.Code)
}
t.Log(resp.Body.String())
var bodyJson map[string]interface{}
if err := json.Unmarshal(resp.Body.Bytes(), &bodyJson); err != nil {
t.Errorf("Error unmarshaling JSON: %s", err.Error())
}
assert.Equal(t, "Validation error", bodyJson["message"])
fields, ok := bodyJson["errors"].([]interface{})
if !ok {
t.Errorf("Unexpected type for errors")
}
// The error fields must be copied from []interface{} to
// []map[string][string] before we can sort
compFields := make(errorFields, 0, 0)
for _, field := range fields {
field := field.(map[string]interface{})
compFields = append(
compFields,
errorField{
"code": field["code"].(string),
"field": field["field"].(string),
"message": field["message"].(string),
})
}
expect := errorFields{
{"code": "invalid", "field": "email_foo", "message": "email"},
{"code": "invalid", "field": "password", "message": "required"},
}
// Sort to standardize ordering before comparison
sort.Sort(compFields)
sort.Sort(expect)
assert.Equal(t, expect, compFields)
}
func TestReturnErrorGormError(t *testing.T) {
r := gin.New()
r.POST("/foo", func(c *gin.Context) {
err := gorm.Errors{}
err.Add(gorm.ErrInvalidSQL)
ReturnError(c, err)
})
req, _ := http.NewRequest("POST", "/foo", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusInternalServerError {
t.Errorf("Should respond with 500, got %d", resp.Code)
}
assert.JSONEq(t, `{"message": "Database error"}`, resp.Body.String())
}