Skip to content
Snippets Groups Projects
errors_test.go 10 KiB
Newer Older
package errors_test

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"

	"code.vereign.com/gaiax/tsa/golib.git/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)
		})
	}
}