diff --git a/scw/errors.go b/scw/errors.go index 3e0a61b7e..0f0dfdd82 100644 --- a/scw/errors.go +++ b/scw/errors.go @@ -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 { @@ -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 @@ -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 { @@ -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, } diff --git a/scw/errors_test.go b/scw/errors_test.go index 77bc68582..d7afc5d30 100644 --- a/scw/errors_test.go +++ b/scw/errors_test.go @@ -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) {