Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
146
native/desktop/maplefile/pkg/httperror/httperror.go
Normal file
146
native/desktop/maplefile/pkg/httperror/httperror.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
package httperror
|
||||
|
||||
// This package introduces a new `error` type that combines an HTTP status code and a message.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HTTPError represents an http error that occurred while handling a request
|
||||
type HTTPError struct {
|
||||
Code int `json:"-"` // HTTP Status code. We use `-` to skip json marshaling.
|
||||
Errors *map[string]string `json:"-"` // The original error. Same reason as above.
|
||||
}
|
||||
|
||||
// New creates a new HTTPError instance with a multi-field errors.
|
||||
func New(statusCode int, errorsMap *map[string]string) error {
|
||||
return HTTPError{
|
||||
Code: statusCode,
|
||||
Errors: errorsMap,
|
||||
}
|
||||
}
|
||||
|
||||
// NewForSingleField create a new HTTPError instance for a single field. This is a convinience constructor.
|
||||
func NewForSingleField(statusCode int, field string, message string) error {
|
||||
return HTTPError{
|
||||
Code: statusCode,
|
||||
Errors: &map[string]string{field: message},
|
||||
}
|
||||
}
|
||||
|
||||
// NewForBadRequest create a new HTTPError instance pertaining to 403 bad requests with the multi-errors. This is a convinience constructor.
|
||||
func NewForBadRequest(err *map[string]string) error {
|
||||
return HTTPError{
|
||||
Code: http.StatusBadRequest,
|
||||
Errors: err,
|
||||
}
|
||||
}
|
||||
|
||||
// NewForBadRequestWithSingleField create a new HTTPError instance pertaining to 403 bad requests for a single field. This is a convinience constructor.
|
||||
func NewForBadRequestWithSingleField(field string, message string) error {
|
||||
return HTTPError{
|
||||
Code: http.StatusBadRequest,
|
||||
Errors: &map[string]string{field: message},
|
||||
}
|
||||
}
|
||||
|
||||
func NewForInternalServerErrorWithSingleField(field string, message string) error {
|
||||
return HTTPError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Errors: &map[string]string{field: message},
|
||||
}
|
||||
}
|
||||
|
||||
// NewForNotFoundWithSingleField create a new HTTPError instance pertaining to 404 not found for a single field. This is a convinience constructor.
|
||||
func NewForNotFoundWithSingleField(field string, message string) error {
|
||||
return HTTPError{
|
||||
Code: http.StatusNotFound,
|
||||
Errors: &map[string]string{field: message},
|
||||
}
|
||||
}
|
||||
|
||||
// NewForServiceUnavailableWithSingleField create a new HTTPError instance pertaining service unavailable for a single field. This is a convinience constructor.
|
||||
func NewForServiceUnavailableWithSingleField(field string, message string) error {
|
||||
return HTTPError{
|
||||
Code: http.StatusServiceUnavailable,
|
||||
Errors: &map[string]string{field: message},
|
||||
}
|
||||
}
|
||||
|
||||
// NewForLockedWithSingleField create a new HTTPError instance pertaining to 424 locked for a single field. This is a convinience constructor.
|
||||
func NewForLockedWithSingleField(field string, message string) error {
|
||||
return HTTPError{
|
||||
Code: http.StatusLocked,
|
||||
Errors: &map[string]string{field: message},
|
||||
}
|
||||
}
|
||||
|
||||
// NewForForbiddenWithSingleField create a new HTTPError instance pertaining to 403 bad requests for a single field. This is a convinience constructor.
|
||||
func NewForForbiddenWithSingleField(field string, message string) error {
|
||||
return HTTPError{
|
||||
Code: http.StatusForbidden,
|
||||
Errors: &map[string]string{field: message},
|
||||
}
|
||||
}
|
||||
|
||||
// NewForUnauthorizedWithSingleField create a new HTTPError instance pertaining to 401 unauthorized for a single field. This is a convinience constructor.
|
||||
func NewForUnauthorizedWithSingleField(field string, message string) error {
|
||||
return HTTPError{
|
||||
Code: http.StatusUnauthorized,
|
||||
Errors: &map[string]string{field: message},
|
||||
}
|
||||
}
|
||||
|
||||
// NewForGoneWithSingleField create a new HTTPError instance pertaining to 410 gone for a single field. This is a convinience constructor.
|
||||
func NewForGoneWithSingleField(field string, message string) error {
|
||||
return HTTPError{
|
||||
Code: http.StatusGone,
|
||||
Errors: &map[string]string{field: message},
|
||||
}
|
||||
}
|
||||
|
||||
// Error function used to implement the `error` interface for returning errors.
|
||||
func (err HTTPError) Error() string {
|
||||
b, e := json.Marshal(err.Errors)
|
||||
if e != nil { // Defensive code
|
||||
return e.Error()
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// ResponseError function returns the HTTP error response based on the httpcode used.
|
||||
func ResponseError(rw http.ResponseWriter, err error) {
|
||||
// Copied from:
|
||||
// https://dev.to/tigorlazuardi/go-creating-custom-error-wrapper-and-do-proper-error-equality-check-11k7
|
||||
|
||||
rw.Header().Set("Content-Type", "Application/json")
|
||||
|
||||
//
|
||||
// CASE 1 OF 2: Handle API Errors.
|
||||
//
|
||||
|
||||
var ew HTTPError
|
||||
if errors.As(err, &ew) {
|
||||
rw.WriteHeader(ew.Code)
|
||||
_ = json.NewEncoder(rw).Encode(ew.Errors)
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
// CASE 2 OF 2: Handle non ErrorWrapper types.
|
||||
//
|
||||
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
_ = json.NewEncoder(rw).Encode(err.Error())
|
||||
}
|
||||
|
||||
// NewForInternalServerError create a new HTTPError instance pertaining to 500 internal server error with the multi-errors. This is a convinience constructor.
|
||||
func NewForInternalServerError(err string) error {
|
||||
return HTTPError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Errors: &map[string]string{"message": err},
|
||||
}
|
||||
}
|
||||
327
native/desktop/maplefile/pkg/httperror/httperror_test.go
Normal file
327
native/desktop/maplefile/pkg/httperror/httperror_test.go
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
package httperror
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code int
|
||||
errors map[string]string
|
||||
wantCode int
|
||||
}{
|
||||
{
|
||||
name: "basic error",
|
||||
code: http.StatusBadRequest,
|
||||
errors: map[string]string{"field": "error message"},
|
||||
wantCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "empty errors map",
|
||||
code: http.StatusNotFound,
|
||||
errors: map[string]string{},
|
||||
wantCode: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "multiple errors",
|
||||
code: http.StatusBadRequest,
|
||||
errors: map[string]string{"field1": "error1", "field2": "error2"},
|
||||
wantCode: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := New(tt.code, &tt.errors)
|
||||
|
||||
httpErr, ok := err.(HTTPError)
|
||||
if !ok {
|
||||
t.Fatal("expected HTTPError type")
|
||||
}
|
||||
if httpErr.Code != tt.wantCode {
|
||||
t.Errorf("Code = %v, want %v", httpErr.Code, tt.wantCode)
|
||||
}
|
||||
for k, v := range tt.errors {
|
||||
if (*httpErr.Errors)[k] != v {
|
||||
t.Errorf("Errors[%s] = %v, want %v", k, (*httpErr.Errors)[k], v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewForBadRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
errors map[string]string
|
||||
}{
|
||||
{
|
||||
name: "single error",
|
||||
errors: map[string]string{"field": "error"},
|
||||
},
|
||||
{
|
||||
name: "multiple errors",
|
||||
errors: map[string]string{"field1": "error1", "field2": "error2"},
|
||||
},
|
||||
{
|
||||
name: "empty errors",
|
||||
errors: map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := NewForBadRequest(&tt.errors)
|
||||
|
||||
httpErr, ok := err.(HTTPError)
|
||||
if !ok {
|
||||
t.Fatal("expected HTTPError type")
|
||||
}
|
||||
if httpErr.Code != http.StatusBadRequest {
|
||||
t.Errorf("Code = %v, want %v", httpErr.Code, http.StatusBadRequest)
|
||||
}
|
||||
for k, v := range tt.errors {
|
||||
if (*httpErr.Errors)[k] != v {
|
||||
t.Errorf("Errors[%s] = %v, want %v", k, (*httpErr.Errors)[k], v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewForSingleField(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code int
|
||||
field string
|
||||
message string
|
||||
}{
|
||||
{
|
||||
name: "basic error",
|
||||
code: http.StatusBadRequest,
|
||||
field: "test",
|
||||
message: "error",
|
||||
},
|
||||
{
|
||||
name: "empty field",
|
||||
code: http.StatusNotFound,
|
||||
field: "",
|
||||
message: "error",
|
||||
},
|
||||
{
|
||||
name: "empty message",
|
||||
code: http.StatusBadRequest,
|
||||
field: "field",
|
||||
message: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := NewForSingleField(tt.code, tt.field, tt.message)
|
||||
|
||||
httpErr, ok := err.(HTTPError)
|
||||
if !ok {
|
||||
t.Fatal("expected HTTPError type")
|
||||
}
|
||||
if httpErr.Code != tt.code {
|
||||
t.Errorf("Code = %v, want %v", httpErr.Code, tt.code)
|
||||
}
|
||||
if (*httpErr.Errors)[tt.field] != tt.message {
|
||||
t.Errorf("Errors[%s] = %v, want %v", tt.field, (*httpErr.Errors)[tt.field], tt.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
errors map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid json",
|
||||
errors: map[string]string{"field": "error"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
errors: map[string]string{},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := HTTPError{
|
||||
Code: http.StatusBadRequest,
|
||||
Errors: &tt.errors,
|
||||
}
|
||||
|
||||
errStr := err.Error()
|
||||
var jsonMap map[string]string
|
||||
if jsonErr := json.Unmarshal([]byte(errStr), &jsonMap); (jsonErr != nil) != tt.wantErr {
|
||||
t.Errorf("Error() json.Unmarshal error = %v, wantErr %v", jsonErr, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !tt.wantErr {
|
||||
for k, v := range tt.errors {
|
||||
if jsonMap[k] != v {
|
||||
t.Errorf("Error() jsonMap[%s] = %v, want %v", k, jsonMap[k], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
wantCode int
|
||||
wantContent string
|
||||
}{
|
||||
{
|
||||
name: "http error",
|
||||
err: NewForBadRequestWithSingleField("field", "invalid"),
|
||||
wantCode: http.StatusBadRequest,
|
||||
wantContent: `{"field":"invalid"}`,
|
||||
},
|
||||
{
|
||||
name: "standard error",
|
||||
err: fmt.Errorf("standard error"),
|
||||
wantCode: http.StatusInternalServerError,
|
||||
wantContent: `"standard error"`,
|
||||
},
|
||||
{
|
||||
name: "nil error",
|
||||
err: errors.New("<nil>"),
|
||||
wantCode: http.StatusInternalServerError,
|
||||
wantContent: `"\u003cnil\u003e"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
ResponseError(rr, tt.err)
|
||||
|
||||
// Check status code
|
||||
if rr.Code != tt.wantCode {
|
||||
t.Errorf("ResponseError() code = %v, want %v", rr.Code, tt.wantCode)
|
||||
}
|
||||
|
||||
// Check content type
|
||||
if ct := rr.Header().Get("Content-Type"); ct != "Application/json" {
|
||||
t.Errorf("ResponseError() Content-Type = %v, want Application/json", ct)
|
||||
}
|
||||
|
||||
// Trim newline from response for comparison
|
||||
got := rr.Body.String()
|
||||
got = got[:len(got)-1] // Remove trailing newline added by json.Encoder
|
||||
if got != tt.wantContent {
|
||||
t.Errorf("ResponseError() content = %v, want %v", got, tt.wantContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorWrapping(t *testing.T) {
|
||||
originalErr := errors.New("original error")
|
||||
wrappedErr := fmt.Errorf("wrapped: %w", originalErr)
|
||||
httpErr := NewForBadRequestWithSingleField("field", wrappedErr.Error())
|
||||
|
||||
// Test error unwrapping
|
||||
if !errors.Is(httpErr, httpErr) {
|
||||
t.Error("errors.Is failed for same error")
|
||||
}
|
||||
|
||||
var targetErr HTTPError
|
||||
if !errors.As(httpErr, &targetErr) {
|
||||
t.Error("errors.As failed to get HTTPError")
|
||||
}
|
||||
}
|
||||
|
||||
// Test all convenience constructors
|
||||
func TestConvenienceConstructors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
create func() error
|
||||
wantCode int
|
||||
}{
|
||||
{
|
||||
name: "NewForBadRequestWithSingleField",
|
||||
create: func() error {
|
||||
return NewForBadRequestWithSingleField("field", "message")
|
||||
},
|
||||
wantCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "NewForNotFoundWithSingleField",
|
||||
create: func() error {
|
||||
return NewForNotFoundWithSingleField("field", "message")
|
||||
},
|
||||
wantCode: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "NewForServiceUnavailableWithSingleField",
|
||||
create: func() error {
|
||||
return NewForServiceUnavailableWithSingleField("field", "message")
|
||||
},
|
||||
wantCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
{
|
||||
name: "NewForLockedWithSingleField",
|
||||
create: func() error {
|
||||
return NewForLockedWithSingleField("field", "message")
|
||||
},
|
||||
wantCode: http.StatusLocked,
|
||||
},
|
||||
{
|
||||
name: "NewForForbiddenWithSingleField",
|
||||
create: func() error {
|
||||
return NewForForbiddenWithSingleField("field", "message")
|
||||
},
|
||||
wantCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "NewForUnauthorizedWithSingleField",
|
||||
create: func() error {
|
||||
return NewForUnauthorizedWithSingleField("field", "message")
|
||||
},
|
||||
wantCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "NewForGoneWithSingleField",
|
||||
create: func() error {
|
||||
return NewForGoneWithSingleField("field", "message")
|
||||
},
|
||||
wantCode: http.StatusGone,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.create()
|
||||
httpErr, ok := err.(HTTPError)
|
||||
if !ok {
|
||||
t.Fatal("expected HTTPError type")
|
||||
}
|
||||
if httpErr.Code != tt.wantCode {
|
||||
t.Errorf("Code = %v, want %v", httpErr.Code, tt.wantCode)
|
||||
}
|
||||
if (*httpErr.Errors)["field"] != "message" {
|
||||
t.Errorf("Error message = %v, want 'message'", (*httpErr.Errors)["field"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue