Skip to content
Snippets Groups Projects
errors_test.go 10 KiB
Newer Older
  • Learn to ignore specific revisions
  • package errors_test
    
    import (
    	"encoding/json"
    	"fmt"
    	"net/http"
    	"net/http/httptest"
    	"testing"
    
    	"github.com/stretchr/testify/assert"
    
    	"code.vereign.com/gaiax/tsa/golib/errors"
    )
    
    func TestNew(t *testing.T) {
    	e := errors.New("something went wrong")
    	assert.Implements(t, (*error)(nil), e)
    
    	// create error with from a Kind
    	e = errors.New(errors.Forbidden)
    	assert.IsType(t, &errors.Error{}, e)
    	assert.True(t, errors.Is(errors.Forbidden, e))
    	assert.Contains(t, e.Error(), "permission denied")
    	if ec, ok := e.(*errors.Error); ok {
    		assert.NotEmpty(t, ec.ID)
    		assert.Empty(t, ec.Message)
    		assert.Equal(t, errors.Forbidden, ec.Kind)
    		assert.Nil(t, ec.Err)
    	}
    
    	// create error with a string message only
    	e = errors.New("something went wrong")
    	assert.IsType(t, &errors.Error{}, e)
    	assert.True(t, errors.Is(errors.Unknown, e))
    	assert.Contains(t, e.Error(), "something went wrong")
    	if ec, ok := e.(*errors.Error); ok {
    		assert.NotEmpty(t, ec.ID)
    		assert.Equal(t, "something went wrong", ec.Message)
    		assert.Equal(t, errors.Unknown, ec.Kind)
    		assert.Nil(t, ec.Err)
    	}
    
    	// create error with Kind and Message
    	e = errors.New(errors.Internal, "something went wrong")
    	assert.IsType(t, &errors.Error{}, e)
    	assert.True(t, errors.Is(errors.Internal, e))
    	assert.Contains(t, e.Error(), "something went wrong: internal error")
    	if ec, ok := e.(*errors.Error); ok {
    		assert.NotEmpty(t, ec.ID)
    		assert.Equal(t, "something went wrong", ec.Message)
    		assert.Equal(t, errors.Internal, ec.Kind)
    		assert.Nil(t, ec.Err)
    	}
    
    	// create error from a previous error
    	e = errors.New(fmt.Errorf("oops it did it again"))
    	assert.IsType(t, &errors.Error{}, e)
    	assert.True(t, errors.Is(errors.Unknown, e))
    	assert.Contains(t, e.Error(), "oops it did it again")
    	if ec, ok := e.(*errors.Error); ok {
    		assert.NotEmpty(t, ec.ID)
    		assert.Equal(t, errors.Unknown, ec.Kind)
    		assert.NotNil(t, ec.Err)
    		assert.Equal(t, ec.Err.Error(), "oops it did it again")
    	}
    
    	// create error from a previous structured error
    	e = errors.New(errors.New(errors.Unauthorized, "no way out"))
    	assert.IsType(t, &errors.Error{}, e)
    	assert.True(t, errors.Is(errors.Unauthorized, e))
    	assert.Contains(t, e.Error(), "no way out: not authenticated")
    	if ec, ok := e.(*errors.Error); ok {
    		assert.NotEmpty(t, ec.ID)
    		assert.Equal(t, errors.Unauthorized, ec.Kind)
    		assert.Equal(t, ec.Message, "no way out")
    		assert.NotNil(t, ec.Err)
    		assert.Contains(t, ec.Err.Error(), "no way out: not authenticated")
    	}
    
    	// create error from a previous structured error
    	e = errors.New(errors.BadRequest, "bad request", errors.New(errors.Unauthorized, "no way out"))
    	assert.IsType(t, &errors.Error{}, e)
    	assert.True(t, errors.Is(errors.BadRequest, e))
    	assert.Contains(t, e.Error(), "bad request: no way out: not authenticated")
    	if ec, ok := e.(*errors.Error); ok {
    		assert.NotEmpty(t, ec.ID)
    		assert.Equal(t, errors.BadRequest, ec.Kind)
    		assert.Equal(t, "bad request", ec.Message)
    		assert.NotNil(t, ec.Err)
    		assert.Contains(t, ec.Err.Error(), "no way out: not authenticated")
    	}
    
    	// create error from a previous structured error and a message
    	e = errors.New("bad request", errors.New(errors.Unauthorized, "no way out"))
    	assert.IsType(t, &errors.Error{}, e)
    	assert.True(t, errors.Is(errors.Unauthorized, e))
    	assert.Contains(t, e.Error(), "bad request: not authenticated: no way out: not authenticated")
    	if ec, ok := e.(*errors.Error); ok {
    		assert.NotEmpty(t, ec.ID)
    		assert.Equal(t, errors.Unauthorized, ec.Kind)
    		assert.Equal(t, "bad request", ec.Message)
    		assert.NotNil(t, ec.Err)
    		assert.Contains(t, ec.Err.Error(), "no way out: not authenticated")
    	}
    
    	// create error from a previous structured error and a message
    	e = errors.New(errors.BadRequest, errors.New(errors.Unauthorized, "no way out"))
    	assert.IsType(t, &errors.Error{}, e)
    	assert.True(t, errors.Is(errors.BadRequest, e))
    	assert.Contains(t, e.Error(), "bad request: no way out: not authenticated")
    	if ec, ok := e.(*errors.Error); ok {
    		assert.NotEmpty(t, ec.ID)
    		assert.Equal(t, errors.BadRequest, ec.Kind)
    		assert.Equal(t, "no way out", ec.Message)
    		assert.NotNil(t, ec.Err)
    		assert.Contains(t, ec.Err.Error(), "no way out: not authenticated")
    	}
    
    	// create three nested errors
    	e1 := fmt.Errorf("cannot insert record")
    	e2 := errors.New("account already exists", e1)
    	e3 := errors.New("failed to create account", e2)
    	assert.Contains(t, e3.Error(), "failed to create account: account already exists: cannot insert record")
    	if ec, ok := e3.(*errors.Error); ok {
    		assert.NotEmpty(t, ec.ID)
    		assert.Equal(t, errors.Unknown, ec.Kind)
    		assert.Equal(t, "failed to create account", ec.Message)
    		assert.NotNil(t, ec.Err)
    		assert.Contains(t, ec.Err.Error(), "account already exists")
    	}
    }
    
    func TestIs(t *testing.T) {
    	e := errors.New(errors.Timeout)
    	assert.IsType(t, &errors.Error{}, e)
    	assert.True(t, errors.Is(errors.Timeout, e))
    
    	// create error from a previous structured error and a message
    	e = errors.New(errors.Timeout, errors.New(errors.Unauthorized))
    	assert.IsType(t, &errors.Error{}, e)
    	assert.True(t, errors.Is(errors.Timeout, e))
    }
    
    func TestError_StatusCode(t *testing.T) {
    	tests := []struct {
    		name string
    		kind errors.Kind
    		code int
    	}{
    		{
    			name: "unknown error",
    			kind: errors.Unknown,
    			code: http.StatusInternalServerError,
    		},
    		{
    			name: "bad request",
    			kind: errors.BadRequest,
    			code: http.StatusBadRequest,
    		},
    		{
    			name: "unauthorized",
    			kind: errors.Unauthorized,
    			code: http.StatusUnauthorized,
    		},
    		{
    			name: "forbidden",
    			kind: errors.Forbidden,
    			code: http.StatusForbidden,
    		},
    		{
    			name: "exists",
    			kind: errors.Exist,
    			code: http.StatusConflict,
    		},
    		{
    			name: "not found",
    			kind: errors.NotFound,
    			code: http.StatusNotFound,
    		},
    		{
    			name: "timeout",
    			kind: errors.Timeout,
    			code: http.StatusRequestTimeout,
    		},
    		{
    			name: "internal",
    			kind: errors.Internal,
    			code: http.StatusInternalServerError,
    		},
    	}
    
    	for _, test := range tests {
    		t.Run(test.name, func(t *testing.T) {
    			e := errors.New(test.kind)
    			assert.IsType(t, &errors.Error{}, e)
    			if ec, ok := e.(*errors.Error); ok {
    				assert.Equal(t, test.code, ec.StatusCode())
    			}
    		})
    	}
    }
    
    func TestError_MarshalJSON(t *testing.T) {
    	e := errors.New(errors.NotFound, "item does not exist")
    	ec, ok := e.(*errors.Error)
    	assert.True(t, ok)
    	id := ec.ID
    
    	data, err := ec.MarshalJSON()
    	assert.NoError(t, err)
    	assert.NotEmpty(t, data)
    
    	ee := &errors.Error{}
    	err = ee.UnmarshalJSON(data)
    	assert.NoError(t, err)
    
    	assert.Equal(t, id, ee.ID)
    	assert.Equal(t, ec.Kind, ee.Kind)
    	assert.Equal(t, ec.Message, ee.Message)
    	assert.Equal(t, ec.Error(), ee.Error())
    	assert.Equal(t, errors.NotFound, ee.Kind)
    }
    
    func TestError_Temporary(t *testing.T) {
    	e := errors.New(errors.Forbidden)
    	ec, ok := e.(*errors.Error)
    	assert.True(t, ok)
    	assert.False(t, ec.Temporary())
    
    	e = errors.New(errors.Internal)
    	ec, ok = e.(*errors.Error)
    	assert.True(t, ok)
    	assert.True(t, ec.Temporary())
    }
    
    func TestGetKind(t *testing.T) {
    	tests := []struct {
    		name string
    		code int
    		kind errors.Kind
    	}{
    		{
    			name: "undefined HTTP status code",
    			code: 9999,
    			kind: errors.Unknown,
    		},
    		{
    			name: "bad request",
    			code: http.StatusBadRequest,
    			kind: errors.BadRequest,
    		},
    		{
    			name: "unauthorized",
    			code: http.StatusUnauthorized,
    			kind: errors.Unauthorized,
    		},
    		{
    			name: "forbidden",
    			code: http.StatusForbidden,
    			kind: errors.Forbidden,
    		},
    		{
    			name: "exists",
    			code: http.StatusConflict,
    			kind: errors.Exist,
    		},
    		{
    			name: "not found",
    			code: http.StatusNotFound,
    			kind: errors.NotFound,
    		},
    		{
    			name: "timeout",
    			code: http.StatusRequestTimeout,
    			kind: errors.Timeout,
    		},
    		{
    			name: "internal",
    			code: http.StatusInternalServerError,
    			kind: errors.Internal,
    		},
    	}
    
    	for _, test := range tests {
    		t.Run(test.name, func(t *testing.T) {
    			kind := errors.GetKind(test.code)
    			assert.Equal(t, test.kind, kind)
    		})
    	}
    }
    
    func TestJSON(t *testing.T) {
    	tests := []struct {
    		// input
    		name       string
    		err        error
    		statusCode int
    
    		// output
    		responseCode  int
    		responseError *errors.Error
    	}{
    		{
    			name:         "error is nil",
    			err:          nil,
    			responseCode: http.StatusInternalServerError,
    			responseError: &errors.Error{
    				Kind: errors.Unknown,
    			},
    		},
    		{
    			name:         "simple text error",
    			err:          fmt.Errorf("simple text error"),
    			responseCode: http.StatusInternalServerError,
    			responseError: &errors.Error{
    				Kind:    errors.Unknown,
    				Message: "",
    			},
    		},
    		{
    			name:         "structured error",
    			err:          errors.New("structured error"),
    			responseCode: http.StatusInternalServerError,
    			responseError: &errors.Error{
    				Kind:    errors.Unknown,
    				Message: "structured error",
    			},
    		},
    		{
    			name:         "structured error with kind",
    			err:          errors.New(errors.Forbidden, "structured error with kind"),
    			responseCode: http.StatusForbidden,
    			responseError: &errors.Error{
    				Kind:    errors.Forbidden,
    				Message: "structured error with kind",
    			},
    		},
    		{
    			name:         "structured error with kind and embedded error",
    			err:          errors.New(errors.NotFound, "structured error with kind and embedded error", fmt.Errorf("embedded error")),
    			responseCode: http.StatusNotFound,
    			responseError: &errors.Error{
    				Kind:    errors.NotFound,
    				Message: "structured error with kind and embedded error",
    				Err:     fmt.Errorf("embedded error"),
    			},
    		},
    		{
    			name:         "structured error with kind only",
    			err:          errors.New(errors.Timeout),
    			responseCode: http.StatusRequestTimeout,
    			responseError: &errors.Error{
    				Kind:    errors.Timeout,
    				Message: "",
    			},
    		},
    	}
    
    	for _, test := range tests {
    		t.Run(test.name, func(t *testing.T) {
    			rr := httptest.NewRecorder()
    			if test.statusCode > 0 {
    				errors.JSON(rr, test.err, test.statusCode)
    			} else {
    				errors.JSON(rr, test.err)
    			}
    
    			assert.Equal(t, test.responseCode, rr.Code)
    
    			var responseError *errors.Error
    			err := json.NewDecoder(rr.Body).Decode(&responseError)
    			assert.NoError(t, err)
    			assert.Equal(t, test.responseError.Kind, responseError.Kind)
    			assert.Equal(t, test.responseError.Message, responseError.Message)
    		})
    	}
    }