diff --git a/go.sum b/go.sum index 2ffd78e..cdd46b5 100644 --- a/go.sum +++ b/go.sum @@ -15,7 +15,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/v1/client_test.go b/v1/client_test.go index 442593d..de92d72 100644 --- a/v1/client_test.go +++ b/v1/client_test.go @@ -3,6 +3,7 @@ package v1 import ( "bytes" "encoding/base64" + "errors" "fmt" "net/http" "strings" @@ -45,61 +46,65 @@ func (t *MGClientTest) Test_TransportChannels() { t.gock(). Get(t.transportURL("channels")). Reply(http.StatusOK). - JSON([]ChannelListItem{{ - ID: 1, - ExternalID: "external_id", - Type: "whatsapp", - Name: &chName, - Settings: ChannelSettings{ - Status: Status{ - Delivered: ChannelFeatureNone, - Read: ChannelFeatureSend, - }, - Text: ChannelSettingsText{ - Creating: ChannelFeatureBoth, - Editing: ChannelFeatureBoth, - Quoting: ChannelFeatureBoth, - Deleting: ChannelFeatureReceive, - MaxCharsCount: 4096, - }, - Product: Product{ - Creating: ChannelFeatureReceive, - Editing: ChannelFeatureReceive, - }, - Order: Order{ - Creating: ChannelFeatureReceive, - Editing: ChannelFeatureReceive, - }, - File: ChannelSettingsFilesBase{ - Creating: ChannelFeatureBoth, - Editing: ChannelFeatureBoth, - Quoting: ChannelFeatureBoth, - Deleting: ChannelFeatureReceive, - Max: 1, - }, - Image: ChannelSettingsFilesBase{ - Creating: ChannelFeatureBoth, - Editing: ChannelFeatureBoth, - Quoting: ChannelFeatureBoth, - Deleting: ChannelFeatureReceive, - Max: 1, // nolint:gomnd - }, - Suggestions: ChannelSettingsSuggestions{ - Text: ChannelFeatureBoth, - Phone: ChannelFeatureBoth, - Email: ChannelFeatureBoth, - }, - CustomerExternalID: ChannelFeatureCustomerExternalIDPhone, - SendingPolicy: SendingPolicy{ - NewCustomer: ChannelFeatureSendingPolicyTemplate, + JSON( + []ChannelListItem{ + { + ID: 1, + ExternalID: "external_id", + Type: "whatsapp", + Name: &chName, + Settings: ChannelSettings{ + Status: Status{ + Delivered: ChannelFeatureNone, + Read: ChannelFeatureSend, + }, + Text: ChannelSettingsText{ + Creating: ChannelFeatureBoth, + Editing: ChannelFeatureBoth, + Quoting: ChannelFeatureBoth, + Deleting: ChannelFeatureReceive, + MaxCharsCount: 4096, + }, + Product: Product{ + Creating: ChannelFeatureReceive, + Editing: ChannelFeatureReceive, + }, + Order: Order{ + Creating: ChannelFeatureReceive, + Editing: ChannelFeatureReceive, + }, + File: ChannelSettingsFilesBase{ + Creating: ChannelFeatureBoth, + Editing: ChannelFeatureBoth, + Quoting: ChannelFeatureBoth, + Deleting: ChannelFeatureReceive, + Max: 1, + }, + Image: ChannelSettingsFilesBase{ + Creating: ChannelFeatureBoth, + Editing: ChannelFeatureBoth, + Quoting: ChannelFeatureBoth, + Deleting: ChannelFeatureReceive, + Max: 1, // nolint:gomnd + }, + Suggestions: ChannelSettingsSuggestions{ + Text: ChannelFeatureBoth, + Phone: ChannelFeatureBoth, + Email: ChannelFeatureBoth, + }, + CustomerExternalID: ChannelFeatureCustomerExternalIDPhone, + SendingPolicy: SendingPolicy{ + NewCustomer: ChannelFeatureSendingPolicyTemplate, + }, + }, + CreatedAt: createdAt, + UpdatedAt: &createdAt, + ActivatedAt: createdAt, + DeactivatedAt: nil, + IsActive: true, }, }, - CreatedAt: createdAt, - UpdatedAt: &createdAt, - ActivatedAt: createdAt, - DeactivatedAt: nil, - IsActive: true, - }}) + ) data, status, err := c.TransportChannels(Channels{Active: true}) t.Require().NoError(err) @@ -147,11 +152,13 @@ func (t *MGClientTest) Test_ActivateTransportChannel() { t.gock(). Post(t.transportURL("channels")). Reply(http.StatusCreated). - JSON(ActivateResponse{ - ChannelID: 1, - ExternalID: "external_id_1", - ActivatedAt: time.Now(), - }) + JSON( + ActivateResponse{ + ChannelID: 1, + ExternalID: "external_id_1", + ActivatedAt: time.Now(), + }, + ) data, status, err := c.ActivateTransportChannel(ch) t.Require().NoError(err) @@ -200,19 +207,23 @@ func (t *MGClientTest) Test_ActivateNewTransportChannel() { t.gock(). Post(t.transportURL("channels")). Reply(http.StatusCreated). - JSON(ActivateResponse{ - ChannelID: 1, - ExternalID: "external_id_1", - ActivatedAt: time.Now(), - }) + JSON( + ActivateResponse{ + ChannelID: 1, + ExternalID: "external_id_1", + ActivatedAt: time.Now(), + }, + ) t.gock(). Delete(t.transportURL("channels/1")). Reply(http.StatusOK). - JSON(DeleteResponse{ - ChannelID: 1, - DeactivatedAt: time.Now(), - }) + JSON( + DeleteResponse{ + ChannelID: 1, + DeactivatedAt: time.Now(), + }, + ) data, status, err := c.ActivateTransportChannel(ch) t.Require().NoError(err) @@ -266,11 +277,13 @@ func (t *MGClientTest) Test_UpdateTransportChannel() { t.gock(). Put(t.transportURL("channels/1")). Reply(http.StatusOK). - JSON(UpdateResponse{ - ChannelID: uint64(1), - ExternalID: "external_id_1", - UpdatedAt: time.Now(), - }) + JSON( + UpdateResponse{ + ChannelID: uint64(1), + ExternalID: "external_id_1", + UpdatedAt: time.Now(), + }, + ) data, status, err := c.UpdateTransportChannel(ch) t.Require().NoError(err) @@ -287,27 +300,31 @@ func (t *MGClientTest) Test_TransportTemplates() { t.gock(). Get(t.transportURL("templates")). Reply(http.StatusOK). - JSON([]Template{{ - Code: "tpl_code", - ChannelID: 1, - Name: "Test Template", - Enabled: true, - Type: TemplateTypeText, - Template: []TemplateItem{ + JSON( + []Template{ { - Type: TemplateItemTypeText, - Text: "Hello, ", - }, - { - Type: TemplateItemTypeVar, - VarType: TemplateVarFirstName, - }, - { - Type: TemplateItemTypeText, - Text: "! We're glad to see you back in our store.", + Code: "tpl_code", + ChannelID: 1, + Name: "Test Template", + Enabled: true, + Type: TemplateTypeText, + Template: []TemplateItem{ + { + Type: TemplateItemTypeText, + Text: "Hello, ", + }, + { + Type: TemplateItemTypeVar, + VarType: TemplateVarFirstName, + }, + { + Type: TemplateItemTypeText, + Text: "! We're glad to see you back in our store.", + }, + }, }, }, - }}) + ) data, status, err := c.TransportTemplates() t.Assert().NoError(err, fmt.Sprintf("%d %s", status, err)) @@ -390,10 +407,12 @@ func (t *MGClientTest) Test_UpdateTemplate() { defer gock.Off() t.gock(). - Filter(func(r *http.Request) bool { - return r.Method == http.MethodPut && - r.URL.Path == "/api/transport/v1/channels/1/templates/encodable#code" - }). + Filter( + func(r *http.Request) bool { + return r.Method == http.MethodPut && + r.URL.Path == "/api/transport/v1/channels/1/templates/encodable#code" + }, + ). Reply(http.StatusOK). JSON(map[string]interface{}{}) @@ -440,9 +459,11 @@ func (t *MGClientTest) Test_UpdateTemplateFail() { defer gock.Off() t.gock(). Reply(http.StatusBadRequest). - JSON(map[string][]string{ - "errors": {"Some weird error message..."}, - }) + JSON( + map[string][]string{ + "errors": {"Some weird error message..."}, + }, + ) status, err := c.UpdateTemplate(tpl) t.Assert().Error(err, fmt.Sprintf("%d %s", status, err)) @@ -453,10 +474,12 @@ func (t *MGClientTest) Test_DeactivateTemplate() { defer gock.Off() t.gock(). - Filter(func(r *http.Request) bool { - return r.Method == http.MethodDelete && - r.URL.Path == t.transportURL("channels/1/templates/test_template#code") - }). + Filter( + func(r *http.Request) bool { + return r.Method == http.MethodDelete && + r.URL.Path == t.transportURL("channels/1/templates/test_template#code") + }, + ). Reply(http.StatusOK). JSON(map[string]interface{}{}) @@ -487,10 +510,12 @@ func (t *MGClientTest) Test_TextMessages() { t.gock(). Post(t.transportURL("messages")). Reply(http.StatusOK). - JSON(MessagesResponse{ - MessageID: 1, - Time: time.Now(), - }) + JSON( + MessagesResponse{ + MessageID: 1, + Time: time.Now(), + }, + ) data, status, err := c.Messages(snd) t.Require().NoError(err) @@ -507,26 +532,32 @@ func (t *MGClientTest) Test_ImageMessages() { t.gock(). Post(t.transportURL("files/upload_by_url")). Reply(http.StatusOK). - JSON(UploadFileResponse{ - ID: "1", - Hash: "1", - Type: "image/png", - MimeType: "", - Size: 1024, - CreatedAt: time.Now(), - }) + JSON( + UploadFileResponse{ + ID: "1", + Hash: "1", + Type: "image/png", + MimeType: "", + Size: 1024, + CreatedAt: time.Now(), + }, + ) t.gock(). Post(t.transportURL("messages")). Reply(http.StatusOK). - JSON(MessagesResponse{ - MessageID: 1, - Time: time.Now(), - }) + JSON( + MessagesResponse{ + MessageID: 1, + Time: time.Now(), + }, + ) - uploadFileResponse, st, err := c.UploadFileByURL(UploadFileByUrlRequest{ - Url: "https://via.placeholder.com/1", - }) + uploadFileResponse, st, err := c.UploadFileByURL( + UploadFileByUrlRequest{ + Url: "https://via.placeholder.com/1", + }, + ) t.Require().NoError(err) t.Assert().Equal(http.StatusOK, st) t.Assert().Equal("1", uploadFileResponse.ID) @@ -569,10 +600,12 @@ func (t *MGClientTest) Test_UpdateMessages() { t.gock(). Put(t.transportURL("messages")). Reply(http.StatusOK). - JSON(MessagesResponse{ - MessageID: 1, - Time: time.Now(), - }) + JSON( + MessagesResponse{ + MessageID: 1, + Time: time.Now(), + }, + ) dataU, status, err := c.UpdateMessages(sndU) t.Require().NoError(err) @@ -599,28 +632,34 @@ func (t *MGClientTest) Test_MarkMessageReadAndDelete() { t.gock(). Delete(t.transportURL("messages")). - JSON(DeleteData{ - Message: Message{ - ExternalID: "deleted", + JSON( + DeleteData{ + Message: Message{ + ExternalID: "deleted", + }, + Channel: 1, }, - Channel: 1, - }). + ). Reply(http.StatusOK). - JSON(MessagesResponse{ - MessageID: 2, - Time: time.Now(), - }) + JSON( + MessagesResponse{ + MessageID: 2, + Time: time.Now(), + }, + ) _, status, err := c.MarkMessageRead(snd) t.Require().NoError(err) t.Assert().Equal(http.StatusOK, status) - previousChatMessage, status, err := c.DeleteMessage(DeleteData{ - Message{ - ExternalID: "deleted", + previousChatMessage, status, err := c.DeleteMessage( + DeleteData{ + Message{ + ExternalID: "deleted", + }, + 1, }, - 1, - }) + ) t.Require().NoError(err) t.Assert().Equal(http.StatusOK, status) t.Assert().Equal(2, previousChatMessage.MessageID) @@ -633,10 +672,12 @@ func (t *MGClientTest) Test_DeactivateTransportChannel() { t.gock(). Delete(t.transportURL("channels/1")). Reply(http.StatusOK). - JSON(DeleteResponse{ - ChannelID: 1, - DeactivatedAt: time.Now(), - }) + JSON( + DeleteResponse{ + ChannelID: 1, + DeactivatedAt: time.Now(), + }, + ) deleteData, status, err := c.DeactivateTransportChannel(1) t.Require().NoError(err) @@ -679,9 +720,10 @@ func (t *MGClientTest) Test_UploadFile() { t.Assert().Equal(resp, data) } -func (t *MGClientTest) Test_SuccessHandleAPIError() { +func (t *MGClientTest) Test_SuccessHandleError() { client := t.client() - handleError := client.Error([]byte(`{"errors": ["Channel not found"]}`)) + json := `{"errors": ["Channel not found"]}` + handleError := client.Error([]byte(json)) t.Assert().IsType(new(httpClientError), handleError) t.Assert().Equal(handleError.Error(), "Channel not found") @@ -691,9 +733,25 @@ func (t *MGClientTest) Test_SuccessHandleAPIError() { Delete(t.transportURL("channels/123")). Reply(http.StatusInternalServerError) + t.gock(). + Delete(t.transportURL("channels/455")). + Reply(http.StatusBadRequest). + JSON(json) + _, statusCode, err := client.DeactivateTransportChannel(123) t.Assert().Equal(http.StatusInternalServerError, statusCode) t.Assert().IsType(new(httpClientError), err) t.Assert().Equal("Internal server error", err.Error()) + var serverErr *httpClientError + if errors.As(err, &serverErr) { + t.Assert().NotNil(serverErr.ResponseBody) + } else { + t.Fail("Unexpected type of error") + } + + _, statusCode, err = client.DeactivateTransportChannel(455) + t.Assert().Equal(http.StatusBadRequest, statusCode) + t.Assert().IsType(new(httpClientError), err) + t.Assert().Equal("Channel not found", err.Error()) } diff --git a/v1/errors.go b/v1/errors.go index 326f455..233bfdb 100644 --- a/v1/errors.go +++ b/v1/errors.go @@ -2,14 +2,19 @@ package v1 import ( "encoding/json" + "errors" + "fmt" + "io" + "net/http" ) var defaultErrorMessage = "Internal http client error" var internalServerError = "Internal server error" type httpClientError struct { - ErrorMsg string - BaseError error + ErrorMsg string + BaseError error + ResponseBody io.ReadCloser } func (err *httpClientError) Unwrap() error { @@ -20,10 +25,10 @@ func (err *httpClientError) Error() string { message := defaultErrorMessage if err.BaseError != nil { - message = err.BaseError.Error() + message = fmt.Sprintf("%s - %s", defaultErrorMessage, err.BaseError.Error()) } - if len([]rune(err.ErrorMsg)) > 0 { + if len(err.ErrorMsg) > 0 { message = err.ErrorMsg } @@ -50,4 +55,22 @@ func NewAPIClientError(responseBody []byte) error { } return &httpClientError{ErrorMsg: message} -} \ No newline at end of file +} + +func NewServerError(response *http.Response) error { + var data []byte + body, err := buildRawResponse(response) + if err == nil { + data = body + } + + err = NewAPIClientError(data) + var serverError *httpClientError + + if errors.As(err, &serverError) { + serverError.ResponseBody = response.Body + return serverError + } + + return err +} diff --git a/v1/errors_test.go b/v1/errors_test.go index 130e780..74e2462 100644 --- a/v1/errors_test.go +++ b/v1/errors_test.go @@ -1,7 +1,11 @@ package v1 import ( + "bytes" "errors" + "fmt" + "io" + "net/http" "net/url" "testing" @@ -15,7 +19,7 @@ func TestNewCriticalHTTPError(t *testing.T) { assert.IsType(t, new(httpClientError), httpErr) assert.IsType(t, new(url.Error), errors.Unwrap(httpErr)) assert.IsType(t, new(url.Error), errors.Unwrap(httpErr)) - assert.Equal(t, httpErr.Error(), err.Error()) + assert.Equal(t, httpErr.Error(), fmt.Sprintf("%s - %s", defaultErrorMessage, err.Error())) } func TestNewApiClientError(t *testing.T) { @@ -30,4 +34,21 @@ func TestNewApiClientError(t *testing.T) { assert.IsType(t, new(httpClientError), httpErr) assert.Equal(t, httpErr.Error(), internalServerError) -} \ No newline at end of file +} + +func TestNewServerError(t *testing.T) { + body := []byte(`{"errors" : ["Something went wrong"]}`) + response := new(http.Response) + response.Body = io.NopCloser(bytes.NewReader(body)) + serverErr := NewServerError(response) + + assert.IsType(t, new(httpClientError), serverErr) + assert.Equal(t, serverErr.Error(), "Something went wrong") + + var err *httpClientError + if errors.As(serverErr, &err) { + assert.NotNil(t, err.ResponseBody) + } else { + t.Fatal("Unexpected type of error") + } +} diff --git a/v1/helpers.go b/v1/helpers.go new file mode 100644 index 0000000..3c11ef1 --- /dev/null +++ b/v1/helpers.go @@ -0,0 +1,17 @@ +package v1 + +import ( + "io" + "net/http" +) + +func buildRawResponse(resp *http.Response) ([]byte, error) { + defer resp.Body.Close() + + res, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + + return res, nil +} diff --git a/v1/request.go b/v1/request.go index 275327b..3fac2a9 100644 --- a/v1/request.go +++ b/v1/request.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "net/http" "strings" ) @@ -74,30 +73,19 @@ func makeRequest(reqType, url string, buf io.Reader, c *MgClient) ([]byte, int, return res, 0, NewCriticalHTTPError(err) } + if resp.StatusCode >= http.StatusInternalServerError { + err = NewServerError(resp) + return res, resp.StatusCode, err + } + res, err = buildRawResponse(resp) if err != nil { return res, 0, err } - if resp.StatusCode >= http.StatusInternalServerError { - err = NewAPIClientError(res) - return res, resp.StatusCode, err - } - if c.Debug { c.writeLog("MG TRANSPORT API Response: %s", res) } return res, resp.StatusCode, err } - -func buildRawResponse(resp *http.Response) ([]byte, error) { - defer resp.Body.Close() - - res, err := ioutil.ReadAll(resp.Body) - if err != nil { - return res, err - } - - return res, nil -} diff --git a/v1/types.go b/v1/types.go index d90d775..239741c 100644 --- a/v1/types.go +++ b/v1/types.go @@ -66,23 +66,6 @@ const ( OriginatorChannel ) -// ClientError is error of http-client or network. -type ClientError string - -func (err ClientError) Error() string { - return string(err) -} - -// APIError is error with 5xx status code. -type APIError struct { - code int - errorMsg string -} - -func (err APIError) Error() string { - return fmt.Sprintf("Error message: %s, code: %v", err.errorMsg, err.code) -} - type ErrorType string const (