Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 39 additions & 9 deletions scw/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ type ResponseError struct {
RawBody json.RawMessage `json:"-"`
}

func (e *ResponseError) UnmarshalJSON(b []byte) error {
type tmpResponseError ResponseError
tmp := tmpResponseError(*e)

err := json.Unmarshal(b, &tmp)
if err != nil {
return err
}

tmp.Message = strings.ToLower(tmp.Message)

*e = ResponseError(tmp)
return nil
}

// IsScwSdkError implement SdkError interface
func (e *ResponseError) IsScwSdkError() {}
func (e *ResponseError) Error() string {
Expand All @@ -63,7 +78,7 @@ func (e *ResponseError) GetRawBody() json.RawMessage {
return e.RawBody
}

// hasResponseError throws an error when the HTTP status is not OK
// hasResponseError returns an SdkError when the HTTP status is not OK.
func hasResponseError(res *http.Response) error {
if res.StatusCode >= 200 && res.StatusCode <= 299 {
return nil
Expand Down Expand Up @@ -131,19 +146,27 @@ func unmarshalStandardError(errorType string, body []byte) error {
}

func unmarshalNonStandardError(errorType string, body []byte) error {
var stdErr SdkError
switch errorType {

// Only in instance API.
case "invalid_request_error":
invalidRequestError := &InvalidRequestError{RawBody: body}
err := json.Unmarshal(body, invalidRequestError)
if err != nil {
return errors.Wrap(err, "could not parse error %s response body", errorType)
}
stdErr = invalidRequestError.ToInvalidArgumentsError()
}

return stdErr
invalidArgumentsError := invalidRequestError.ToInvalidArgumentsError()
if invalidRequestError != nil {
return invalidArgumentsError
}

// At this point, the invalid_request_error is not an InvalidArgumentsError and
// the default marshalling will be used.
return nil

default:
return nil
}
}

type InvalidArgumentsErrorDetail struct {
Expand Down Expand Up @@ -185,16 +208,23 @@ func (e *InvalidArgumentsError) GetRawBody() json.RawMessage {
return e.RawBody
}

// InvalidRequestError are only returned by the compute API
// InvalidRequestError is only returned by the instance API.
// Warning: this is not a standard error.
type InvalidRequestError struct {
Message string `json:"message"`

Fields map[string][]string `json:"fields"`

RawBody json.RawMessage `json:"-"`
}

// ToInvalidArgumentsError converts it to the standard error InvalidArgumentsError
func (e *InvalidRequestError) ToInvalidArgumentsError() *InvalidArgumentsError {
// ToSdkError returns a standard error InvalidArgumentsError or nil Fields is nil.
func (e *InvalidRequestError) ToInvalidArgumentsError() SdkError {
// If error has no fields, it is not an InvalidArgumentsError.
if e.Fields == nil {
return nil
}

invalidArguments := &InvalidArgumentsError{
RawBody: e.RawBody,
}
Expand Down
87 changes: 65 additions & 22 deletions scw/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,31 +30,74 @@ func TestHasResponseErrorWithoutBody(t *testing.T) {
}

func TestNonStandardError(t *testing.T) {
// Create expected error response
testErrorReponse := &InvalidArgumentsError{
Details: []InvalidArgumentsErrorDetail{
{
ArgumentName: "volumes.5.id",
Reason: "constraint",
HelpMessage: "92 is not a valid UUID.",
},
{
ArgumentName: "volumes.5.name",
Reason: "constraint",
HelpMessage: "required key not provided",
},
},
RawBody: []byte(`{"fields":{"volumes.5.id":["92 is not a valid UUID."],"volumes.5.name":["required key not provided"]},"message":"Validation Error","type":"invalid_request_error"}`),
type testCase struct {
resStatus string
resStatusCode int
resBody string
expectedError SdkError
}

// Create response body with marshalled error response
body := `{"fields":{"volumes.5.id":["92 is not a valid UUID."],"volumes.5.name":["required key not provided"]},"message":"Validation Error","type":"invalid_request_error"}`
res := &http.Response{Status: "400 Bad Request", StatusCode: 400, Body: ioutil.NopCloser(strings.NewReader(body))}
run := func(c *testCase) func(t *testing.T) {
return func(t *testing.T) {
res := &http.Response{
Status: c.resStatus,
StatusCode: c.resStatusCode,
Body: ioutil.NopCloser(strings.NewReader(c.resBody)),
}

// Test that hasResponseError converts the response to the expected SdkError.
newErr := hasResponseError(res)
testhelpers.Assert(t, newErr != nil, "Should have error")
testhelpers.Equals(t, c.expectedError, newErr)
}
}

// Test hasResponseError convert the response into a InvalidArgumentsError error
newErr := hasResponseError(res)
testhelpers.Assert(t, newErr != nil, "Should have error")
testhelpers.Equals(t, testErrorReponse, newErr)
t.Run("invalid_request_error type with fields", run(&testCase{
resStatus: "400 Bad Request",
resStatusCode: http.StatusBadRequest,
resBody: `{"fields":{"volumes.5.id":["92 is not a valid UUID."],"volumes.5.name":["required key not provided"]},"message":"Validation Error","type":"invalid_request_error"}`,
expectedError: &InvalidArgumentsError{
Details: []InvalidArgumentsErrorDetail{
{
ArgumentName: "volumes.5.id",
Reason: "constraint",
HelpMessage: "92 is not a valid UUID.",
},
{
ArgumentName: "volumes.5.name",
Reason: "constraint",
HelpMessage: "required key not provided",
},
},
RawBody: []byte(`{"fields":{"volumes.5.id":["92 is not a valid UUID."],"volumes.5.name":["required key not provided"]},"message":"Validation Error","type":"invalid_request_error"}`),
},
}))

t.Run("invalid_request_error type with message", run(&testCase{
resStatus: "400 Bad Request",
resStatusCode: http.StatusBadRequest,
resBody: `{"message": "server should be running", "type": "invalid_request_error"}`,
expectedError: &ResponseError{
Status: "400 Bad Request",
StatusCode: http.StatusBadRequest,
Message: "server should be running",
Type: "invalid_request_error",
RawBody: []byte(`{"message": "server should be running", "type": "invalid_request_error"}`),
},
}))

t.Run("conflict type", run(&testCase{
resStatus: "409 Conflict",
resStatusCode: http.StatusConflict,
resBody: `{"message": "Group is in use. You cannot delete it.", "type": "conflict"}`,
expectedError: &ResponseError{
Status: "409 Conflict",
StatusCode: http.StatusConflict,
Message: "group is in use. you cannot delete it.",
Type: "conflict",
RawBody: []byte(`{"message": "Group is in use. You cannot delete it.", "type": "conflict"}`),
},
}))
}

func TestHasResponseErrorWithValidError(t *testing.T) {
Expand Down