1
0
mirror of synced 2024-11-25 06:26:03 +03:00

Custom error type instead of builtin

This commit is contained in:
Pavel 2022-10-28 13:08:45 +03:00 committed by GitHub
commit d40fe94ec5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 401 additions and 188 deletions

View File

@ -36,7 +36,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.11', '1.12', '1.13', '1.14', '1.15', '1.16', '1.17']
go-version: ['1.13', '1.14', '1.15', '1.16', '1.17']
steps:
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2

2
go.mod
View File

@ -1,6 +1,6 @@
module github.com/retailcrm/mg-transport-api-client-go
go 1.11
go 1.13
require (
github.com/google/go-querystring v1.0.0

1
go.sum
View File

@ -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=

View File

@ -71,7 +71,7 @@ func (c *MgClient) TransportTemplates() ([]Template, int, error) {
}
if status > http.StatusCreated || status < http.StatusOK {
return resp, status, c.Error(data)
return resp, status, NewAPIClientError(data)
}
return resp, status, err
@ -116,7 +116,7 @@ func (c *MgClient) ActivateTemplate(channelID uint64, request ActivateTemplateRe
}
if status > http.StatusCreated || status < http.StatusOK {
return status, c.Error(data)
return status, NewAPIClientError(data)
}
return status, err
@ -165,7 +165,7 @@ func (c *MgClient) UpdateTemplate(request Template) (int, error) {
}
if status != http.StatusOK {
return status, c.Error(data)
return status, NewAPIClientError(data)
}
return status, err
@ -190,7 +190,7 @@ func (c *MgClient) DeactivateTemplate(channelID uint64, templateCode string) (in
}
if status > http.StatusCreated || status < http.StatusOK {
return status, c.Error(data)
return status, NewAPIClientError(data)
}
return status, err
@ -224,7 +224,7 @@ func (c *MgClient) TransportChannels(request Channels) ([]ChannelListItem, int,
}
if status > http.StatusCreated || status < http.StatusOK {
return resp, status, c.Error(data)
return resp, status, NewAPIClientError(data)
}
return resp, status, err
@ -283,7 +283,7 @@ func (c *MgClient) ActivateTransportChannel(request Channel) (ActivateResponse,
}
if status > http.StatusCreated || status < http.StatusOK {
return resp, status, c.Error(data)
return resp, status, NewAPIClientError(data)
}
return resp, status, err
@ -342,7 +342,7 @@ func (c *MgClient) UpdateTransportChannel(request Channel) (UpdateResponse, int,
}
if status != http.StatusOK {
return resp, status, c.Error(data)
return resp, status, NewAPIClientError(data)
}
return resp, status, err
@ -378,7 +378,7 @@ func (c *MgClient) DeactivateTransportChannel(id uint64) (DeleteResponse, int, e
}
if status != http.StatusOK {
return resp, status, c.Error(data)
return resp, status, NewAPIClientError(data)
}
return resp, status, err
@ -427,7 +427,7 @@ func (c *MgClient) Messages(request SendData) (MessagesResponse, int, error) {
}
if status != http.StatusOK {
return resp, status, c.Error(data)
return resp, status, NewAPIClientError(data)
}
return resp, status, err
@ -471,7 +471,7 @@ func (c *MgClient) UpdateMessages(request EditMessageRequest) (MessagesResponse,
}
if status != http.StatusOK {
return resp, status, c.Error(data)
return resp, status, NewAPIClientError(data)
}
return resp, status, err
@ -510,7 +510,7 @@ func (c *MgClient) MarkMessageRead(request MarkMessageReadRequest) (MarkMessageR
}
if status != http.StatusOK {
return resp, status, c.Error(data)
return resp, status, NewAPIClientError(data)
}
return resp, status, err
@ -541,7 +541,7 @@ func (c *MgClient) AckMessage(request AckMessageRequest) (int, error) {
}
if status != http.StatusOK {
return status, c.Error(data)
return status, NewAPIClientError(data)
}
return status, err
@ -579,7 +579,7 @@ func (c *MgClient) DeleteMessage(request DeleteData) (*MessagesResponse, int, er
return nil, status, err
}
if status != http.StatusOK {
return nil, status, c.Error(data)
return nil, status, NewAPIClientError(data)
}
var previousChatMessage *MessagesResponse
@ -618,7 +618,7 @@ func (c *MgClient) GetFile(request string) (FullFileResponse, int, error) {
}
if status != http.StatusOK {
return resp, status, c.Error(data)
return resp, status, NewAPIClientError(data)
}
return resp, status, err
@ -638,7 +638,7 @@ func (c *MgClient) UploadFile(request io.Reader) (UploadFileResponse, int, error
}
if status != http.StatusOK {
return resp, status, c.Error(data)
return resp, status, NewAPIClientError(data)
}
return resp, status, err
@ -659,24 +659,12 @@ func (c *MgClient) UploadFileByURL(request UploadFileByUrlRequest) (UploadFileRe
}
if status != http.StatusOK {
return resp, status, c.Error(data)
return resp, status, NewAPIClientError(data)
}
return resp, status, err
}
func (c *MgClient) Error(info []byte) error {
var data map[string]interface{}
if err := json.Unmarshal(info, &data); err != nil {
return err
}
values := data["errors"].([]interface{})
return errors.New(values[0].(string))
}
// MakeTimestamp returns current unix timestamp.
func MakeTimestamp() int64 {
return time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))

View File

@ -3,6 +3,7 @@ package v1
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"net/http"
"strings"
@ -45,7 +46,9 @@ func (t *MGClientTest) Test_TransportChannels() {
t.gock().
Get(t.transportURL("channels")).
Reply(http.StatusOK).
JSON([]ChannelListItem{{
JSON(
[]ChannelListItem{
{
ID: 1,
ExternalID: "external_id",
Type: "whatsapp",
@ -99,7 +102,9 @@ func (t *MGClientTest) Test_TransportChannels() {
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{
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{
JSON(
ActivateResponse{
ChannelID: 1,
ExternalID: "external_id_1",
ActivatedAt: time.Now(),
})
},
)
t.gock().
Delete(t.transportURL("channels/1")).
Reply(http.StatusOK).
JSON(DeleteResponse{
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{
JSON(
UpdateResponse{
ChannelID: uint64(1),
ExternalID: "external_id_1",
UpdatedAt: time.Now(),
})
},
)
data, status, err := c.UpdateTransportChannel(ch)
t.Require().NoError(err)
@ -287,7 +300,9 @@ func (t *MGClientTest) Test_TransportTemplates() {
t.gock().
Get(t.transportURL("templates")).
Reply(http.StatusOK).
JSON([]Template{{
JSON(
[]Template{
{
Code: "tpl_code",
ChannelID: 1,
Name: "Test Template",
@ -307,7 +322,9 @@ func (t *MGClientTest) Test_TransportTemplates() {
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 {
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{
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 {
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{
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{
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{
JSON(
MessagesResponse{
MessageID: 1,
Time: time.Now(),
})
},
)
uploadFileResponse, st, err := c.UploadFileByURL(UploadFileByUrlRequest{
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{
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{
JSON(
DeleteData{
Message: Message{
ExternalID: "deleted",
},
Channel: 1,
}).
},
).
Reply(http.StatusOK).
JSON(MessagesResponse{
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{
previousChatMessage, status, err := c.DeleteMessage(
DeleteData{
Message{
ExternalID: "deleted",
},
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{
JSON(
DeleteResponse{
ChannelID: 1,
DeactivatedAt: time.Now(),
})
},
)
deleteData, status, err := c.DeactivateTransportChannel(1)
t.Require().NoError(err)
@ -678,3 +719,35 @@ func (t *MGClientTest) Test_UploadFile() {
resp.CreatedAt = data.CreatedAt
t.Assert().Equal(resp, data)
}
func (t *MGClientTest) Test_SuccessHandleError() {
client := t.client()
json := `{"errors": ["Channel not found"]}`
defer gock.Off()
t.gock().
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(internalServerError, err.Error())
var serverErr *HTTPClientError
if errors.As(err, &serverErr) {
t.Assert().Nil(serverErr.Response)
} 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())
}

77
v1/errors.go Normal file
View File

@ -0,0 +1,77 @@
package v1
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
var defaultErrorMessage = "http client error"
var internalServerError = "internal server error"
var marshalError = "cannot unmarshal response body"
type MGErrors struct {
Errors []string
}
type HTTPClientError struct {
ErrorMsg string
BaseError error
Response io.Reader
}
func (err *HTTPClientError) Unwrap() error {
return err.BaseError
}
func (err *HTTPClientError) Error() string {
message := defaultErrorMessage
if err.BaseError != nil {
message = fmt.Sprintf("%s: %s", defaultErrorMessage, err.BaseError.Error())
} else if len(err.ErrorMsg) > 0 {
message = err.ErrorMsg
}
return message
}
func NewCriticalHTTPError(err error) error {
return &HTTPClientError{BaseError: err}
}
func NewAPIClientError(responseBody []byte) error {
var data MGErrors
var message string
if len(responseBody) == 0 {
message = internalServerError
} else {
err := json.Unmarshal(responseBody, &data)
if err != nil {
message = marshalError
} else if len(data.Errors) > 0 {
message = data.Errors[0]
}
}
return &HTTPClientError{ErrorMsg: message}
}
func NewServerError(response *http.Response) error {
var serverError *HTTPClientError
body, _ := buildLimitedRawResponse(response)
err := NewAPIClientError(body)
if errors.As(err, &serverError) && len(body) > 0 {
serverError.Response = bytes.NewBuffer(body)
return serverError
}
return err
}

66
v1/errors_test.go Normal file
View File

@ -0,0 +1,66 @@
package v1
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewCriticalHTTPError(t *testing.T) {
err := &url.Error{Op: "Get", URL: "http//example.com", Err: errors.New("EOF")}
httpErr := NewCriticalHTTPError(err)
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(), fmt.Sprintf("%s: %s", defaultErrorMessage, err.Error()))
}
func TestNewApiClientError(t *testing.T) {
body := []byte(`{"errors" : ["Channel not found"]}`)
httpErr := NewAPIClientError(body)
assert.IsType(t, new(HTTPClientError), httpErr)
assert.Equal(t, httpErr.Error(), "Channel not found")
body = []byte{}
httpErr = NewAPIClientError(body)
assert.IsType(t, new(HTTPClientError), httpErr)
assert.Equal(t, httpErr.Error(), internalServerError)
}
func TestNewServerError(t *testing.T) {
body := []byte(`{"errors" : ["Something went wrong"]}`)
response := new(http.Response)
response.Body = ioutil.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.Response)
} else {
t.Fatal("Unexpected type of error")
}
body = []byte(`{"invalid_json"`)
response = new(http.Response)
response.Body = ioutil.NopCloser(bytes.NewReader(body))
serverErr = NewServerError(response)
assert.IsType(t, new(HTTPClientError), serverErr)
assert.Equal(t, serverErr.Error(), marshalError)
if errors.As(serverErr, &err) {
assert.NotNil(t, err.Response)
}
}

22
v1/helpers.go Normal file
View File

@ -0,0 +1,22 @@
package v1
import (
"io"
"io/ioutil"
"net/http"
)
const MB = 1 << 20
func buildLimitedRawResponse(resp *http.Response) ([]byte, error) {
defer resp.Body.Close()
limitReader := io.LimitReader(resp.Body, MB)
body, err := ioutil.ReadAll(limitReader)
if err != nil {
return body, err
}
return body, nil
}

View File

@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
)
@ -71,15 +70,15 @@ func makeRequest(reqType, url string, buf io.Reader, c *MgClient) ([]byte, int,
resp, err := c.httpClient.Do(req)
if err != nil {
return res, 0, err
return res, 0, NewCriticalHTTPError(err)
}
if resp.StatusCode >= http.StatusInternalServerError {
err = fmt.Errorf("http request error. status code: %d", resp.StatusCode)
err = NewServerError(resp)
return res, resp.StatusCode, err
}
res, err = buildRawResponse(resp)
res, err = buildLimitedRawResponse(resp)
if err != nil {
return res, 0, err
}
@ -90,14 +89,3 @@ func makeRequest(reqType, url string, buf io.Reader, c *MgClient) ([]byte, int,
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
}