diff --git a/.travis.yml b/.travis.yml index d017f16..d2c303a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,14 @@ language: go +env: + - GO111MODULE=on go: - - '1.8' - - '1.9' - - '1.10' - '1.11' + - '1.12' + - '1.13' + - '1.14' before_install: - - go get -v github.com/google/go-querystring/query - - go get -v github.com/h2non/gock - - go get -v github.com/stretchr/testify/assert -script: go test -v ./... + - go mod tidy +script: + - go test ./... -v -cpu 2 -timeout 2m -race -cover -coverprofile=coverage.txt -covermode=atomic +after_success: + - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ad616cb --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/retailcrm/mg-transport-api-client-go + +go 1.11 + +require ( + github.com/google/go-querystring v1.0.0 + github.com/stretchr/testify v1.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b13bde2 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/v1/client.go b/v1/client.go index 2993c55..eb1c830 100644 --- a/v1/client.go +++ b/v1/client.go @@ -27,13 +27,159 @@ func NewWithClient(url string, token string, client *http.Client) *MgClient { } } +// TransportTemplates returns templates list +// +// Example: +// +// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// +// data, status, err := client.TransportTemplates() +// +// if err != nil { +// fmt.Printf("%v", err) +// } +// +// fmt.Printf("Status: %v, Templates found: %v", status, len(data)) +func (c *MgClient) TransportTemplates() ([]Template, int, error) { + var resp []Template + + data, status, err := c.GetRequest("/templates", []byte{}) + if err != nil { + return resp, status, err + } + + if e := json.Unmarshal(data, &resp); e != nil { + return resp, status, e + } + + if status > http.StatusCreated || status < http.StatusOK { + return resp, status, c.Error(data) + } + + return resp, status, err +} + +// ActivateTransportChannel implements template activation +// +// Example: +// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// +// request := v1.ActivateTemplateRequest{ +// Code: "code", +// Name: "name", +// Type: v1.TemplateTypeText, +// Template: []v1.TemplateItem{ +// { +// Type: v1.TemplateItemTypeText, +// Text: "Hello, ", +// }, +// { +// Type: v1.TemplateItemTypeVar, +// VarType: v1.TemplateVarName, +// }, +// { +// Type: v1.TemplateItemTypeText, +// Text: "!", +// }, +// }, +// } +// +// _, err := client.ActivateTemplate(uint64(1), request) +// +// if err != nil { +// fmt.Printf("%v", err) +// } +func (c *MgClient) ActivateTemplate(channelID uint64, request ActivateTemplateRequest) (int, error) { + outgoing, _ := json.Marshal(&request) + + data, status, err := c.PostRequest(fmt.Sprintf("/channels/%d/templates", channelID), bytes.NewBuffer(outgoing)) + if err != nil { + return status, err + } + + if status > http.StatusCreated || status < http.StatusOK { + return status, c.Error(data) + } + + return status, err +} + +// UpdateTemplate implements template updating +// Example: +// var client = New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// +// request := v1.Template{ +// Code: "templateCode", +// ChannelID: 1, +// Name: "templateName", +// Template: []v1.TemplateItem{ +// { +// Type: v1.TemplateItemTypeText, +// Text: "Welcome, ", +// }, +// { +// Type: v1.TemplateItemTypeVar, +// VarType: v1.TemplateVarName, +// }, +// { +// Type: v1.TemplateItemTypeText, +// Text: "!", +// }, +// }, +// } +// +// _, err := client.UpdateTemplate(request) +// +// if err != nil { +// fmt.Printf("%#v", err) +// } +func (c *MgClient) UpdateTemplate(request Template) (int, error) { + outgoing, _ := json.Marshal(&request) + + data, status, err := c.PutRequest(fmt.Sprintf("/channels/%d/templates/%s", request.ChannelID, request.Code), outgoing) + if err != nil { + return status, err + } + + if status != http.StatusOK { + return status, c.Error(data) + } + + return status, err +} + +// DeactivateTemplate implements template deactivation +// +// Example: +// +// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// +// _, err := client.DeactivateTemplate(3053450384, "templateCode") +// +// if err != nil { +// fmt.Printf("%v", err) +// } +func (c *MgClient) DeactivateTemplate(channelID uint64, templateCode string) (int, error) { + data, status, err := c.DeleteRequest( + fmt.Sprintf("/channels/%d/templates/%s", channelID, templateCode), []byte{}) + if err != nil { + return status, err + } + + if status > http.StatusCreated || status < http.StatusOK { + return status, c.Error(data) + } + + return status, err +} + // TransportChannels returns channels list // // Example: // // var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // -// data, status, err := client.TransportChannels{Channels{Active: true}} +// data, status, err := client.TransportChannels(Channels{Active: true}) // // if err != nil { // fmt.Printf("%v", err) diff --git a/v1/client_test.go b/v1/client_test.go index 0cfc4b5..d7443eb 100644 --- a/v1/client_test.go +++ b/v1/client_test.go @@ -1,18 +1,25 @@ package v1 import ( + "bytes" + "encoding/base64" + "fmt" "net/http" "os" "strconv" "testing" "time" + + "github.com/stretchr/testify/assert" ) var ( - mgURL = os.Getenv("MG_URL") - mgToken = os.Getenv("MG_TOKEN") - channelID, _ = strconv.ParseUint(os.Getenv("MG_CHANNEL"), 10, 64) - ext = strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + mgURL = os.Getenv("MG_URL") + mgToken = os.Getenv("MG_TOKEN") + channelID, _ = strconv.ParseUint(os.Getenv("MG_CHANNEL"), 10, 64) + ext = strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + tplCode = fmt.Sprintf("testTemplate_%d", time.Now().UnixNano()) + tplChannel uint64 = 0 ) func client() *MgClient { @@ -22,6 +29,62 @@ func client() *MgClient { return c } +func templateChannel(t *testing.T) uint64 { + if tplChannel == 0 { + c := client() + resp, _, err := c.ActivateTransportChannel(Channel{ + Type: "telegram", + Name: "@test_channel_templates", + Settings: ChannelSettings{ + SpamAllowed: false, + Status: Status{ + Delivered: ChannelFeatureBoth, + Read: ChannelFeatureBoth, + }, + Text: ChannelSettingsText{ + Creating: ChannelFeatureBoth, + Editing: ChannelFeatureBoth, + Quoting: ChannelFeatureBoth, + Deleting: ChannelFeatureBoth, + MaxCharsCount: 5000, + }, + Product: Product{ + Creating: ChannelFeatureBoth, + Editing: ChannelFeatureBoth, + Deleting: ChannelFeatureBoth, + }, + Order: Order{ + Creating: ChannelFeatureBoth, + Editing: ChannelFeatureBoth, + Deleting: ChannelFeatureBoth, + }, + File: ChannelSettingsFilesBase{ + Creating: ChannelFeatureBoth, + Editing: ChannelFeatureBoth, + Quoting: ChannelFeatureBoth, + Deleting: ChannelFeatureBoth, + Max: 1000000, + CommentMaxCharsCount: 128, + }, + Image: ChannelSettingsFilesBase{ + Creating: ChannelFeatureBoth, + Editing: ChannelFeatureBoth, + Quoting: ChannelFeatureBoth, + Deleting: ChannelFeatureBoth, + }, + }, + }) + + if err != nil { + t.FailNow() + } + + tplChannel = resp.ChannelID + } + + return tplChannel +} + func TestMgClient_TransportChannels(t *testing.T) { c := client() @@ -177,6 +240,101 @@ func TestMgClient_UpdateTransportChannel(t *testing.T) { t.Logf("Update selected channel: %v", data.ChannelID) } +func TestMgClient_TransportTemplates(t *testing.T) { + c := client() + + data, status, err := c.TransportTemplates() + assert.NoError(t, err, fmt.Sprintf("%d %s", status, err)) + + t.Logf("Templates found: %#v", len(data)) + + for _, item := range data { + for _, tpl := range item.Template { + if tpl.Type == TemplateItemTypeText { + assert.Empty(t, tpl.VarType) + } else { + assert.Empty(t, tpl.Text) + assert.NotEmpty(t, tpl.VarType) + + if _, ok := templateVarAssoc[tpl.VarType]; !ok { + t.Errorf("unknown TemplateVar type %s", tpl.VarType) + } + } + } + } +} + +func TestMgClient_ActivateTemplate(t *testing.T) { + c := client() + req := ActivateTemplateRequest{ + Code: tplCode, + Name: tplCode, + Type: TemplateTypeText, + Template: []TemplateItem{ + { + Type: TemplateItemTypeText, + Text: "Hello ", + }, + { + Type: TemplateItemTypeVar, + VarType: TemplateVarFirstName, + }, + { + Type: TemplateItemTypeText, + Text: "!", + }, + }, + } + + status, err := c.ActivateTemplate(templateChannel(t), req) + assert.NoError(t, err, fmt.Sprintf("%d %s", status, err)) + + t.Logf("Activated template with code `%s`", req.Code) +} + +func TestMgClient_UpdateTemplate(t *testing.T) { + c := client() + tpl := Template{ + Code: tplCode, + ChannelID: templateChannel(t), + Name: "updated name", + Enabled: true, + Type: TemplateTypeText, + Template: []TemplateItem{ + { + Type: TemplateItemTypeText, + Text: "Welcome ", + }, + { + Type: TemplateItemTypeVar, + VarType: TemplateVarFirstName, + }, + { + Type: TemplateItemTypeText, + Text: "!", + }, + }, + } + + status, err := c.UpdateTemplate(tpl) + assert.NoError(t, err, fmt.Sprintf("%d %s", status, err)) + + templates, status, err := c.TransportTemplates() + assert.NoError(t, err, fmt.Sprintf("%d %s", status, err)) + + for _, template := range templates { + if template.Code == tpl.Code { + assert.Equal(t, tpl.Name, template.Name) + } + } +} + +func TestMgClient_DeactivateTemplate(t *testing.T) { + c := client() + status, err := c.DeactivateTemplate(templateChannel(t), tplCode) + assert.NoError(t, err, fmt.Sprintf("%d %s", status, err)) +} + func TestMgClient_TextMessages(t *testing.T) { c := client() t.Logf("%v", ext) @@ -215,7 +373,7 @@ func TestMgClient_ImageMessages(t *testing.T) { t.Logf("%v", ext) uploadFileResponse, st, err := c.UploadFileByURL(UploadFileByUrlRequest{ - Url: "https://via.placeholder.com/300", + Url: "https://via.placeholder.com/1", }) if st != http.StatusOK { @@ -345,14 +503,14 @@ func TestMgClient_UploadFile(t *testing.T) { c := client() t.Logf("%v", ext) - resp, err := http.Get("https://via.placeholder.com/300") + // 1x1 png picture + img := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=" + binary, err := base64.StdEncoding.DecodeString(img) if err != nil { - t.Errorf("%v", err) + t.Errorf("cannot convert base64 to binary: %s", err) } - defer resp.Body.Close() - - data, status, err := c.UploadFile(resp.Body) + data, status, err := c.UploadFile(bytes.NewReader(binary)) if status != http.StatusOK { t.Errorf("%v", err) diff --git a/v1/template.go b/v1/template.go new file mode 100644 index 0000000..d852047 --- /dev/null +++ b/v1/template.go @@ -0,0 +1,109 @@ +package v1 + +import ( + "encoding/json" + "errors" + "fmt" +) + +// TemplateTypeText is a text template type. There is no other types for now. +const TemplateTypeText = "text" + +const ( + // TemplateItemTypeText is a type for text chunk in template + TemplateItemTypeText uint8 = iota + // TemplateItemTypeVar is a type for variable in template + TemplateItemTypeVar +) + +const ( + // TemplateVarCustom is a custom variable type + TemplateVarCustom = "custom" + // TemplateVarName is a name variable type + TemplateVarName = "name" + // TemplateVarFirstName is a first name variable type + TemplateVarFirstName = "first_name" + // TemplateVarLastName is a last name variable type + TemplateVarLastName = "last_name" +) + +// templateVarAssoc for checking variable validity, only for internal use +var templateVarAssoc = map[string]interface{}{ + TemplateVarCustom: nil, + TemplateVarName: nil, + TemplateVarFirstName: nil, + TemplateVarLastName: nil, +} + +// Template struct +type Template struct { + Code string `json:"code"` + ChannelID uint64 `json:"channel_id,omitempty"` + Name string `json:"name"` + Enabled bool `json:"enabled,omitempty"` + Type string `json:"type"` + Template []TemplateItem `json:"template"` +} + +// TemplateItem is a part of template +type TemplateItem struct { + Type uint8 + Text string + VarType string +} + +// MarshalJSON controls how TemplateItem will be marshaled into JSON +func (t TemplateItem) MarshalJSON() ([]byte, error) { + switch t.Type { + case TemplateItemTypeText: + return json.Marshal(t.Text) + case TemplateItemTypeVar: + // {} case, fast output without marshaling + if t.VarType == "" || t.VarType == TemplateVarCustom { + return []byte("{}"), nil + } + + return json.Marshal(map[string]interface{}{ + "var": t.VarType, + }) + } + + return nil, errors.New("unknown TemplateItem type") +} + +// UnmarshalJSON will correctly unmarshal TemplateItem +func (t *TemplateItem) UnmarshalJSON(b []byte) error { + var obj interface{} + err := json.Unmarshal(b, &obj) + if err != nil { + return err + } + + switch v := obj.(type) { + case string: + t.Type = TemplateItemTypeText + t.Text = v + case map[string]interface{}: + // {} case + if len(v) == 0 { + t.Type = TemplateItemTypeVar + t.VarType = TemplateVarCustom + return nil + } + + if varTypeCurr, ok := v["var"].(string); ok { + if _, ok := templateVarAssoc[varTypeCurr]; !ok { + return fmt.Errorf("invalid placeholder var '%s'", varTypeCurr) + } + + t.Type = TemplateItemTypeVar + t.VarType = varTypeCurr + } else { + return errors.New("invalid TemplateItem") + } + default: + return errors.New("invalid TemplateItem") + } + + return nil +} diff --git a/v1/template_test.go b/v1/template_test.go new file mode 100644 index 0000000..c9d9f26 --- /dev/null +++ b/v1/template_test.go @@ -0,0 +1,65 @@ +package v1 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTemplateItem_MarshalJSON(t *testing.T) { + text := TemplateItem{ + Type: TemplateItemTypeText, + Text: "text item", + } + + variable := TemplateItem{ + Type: TemplateItemTypeVar, + VarType: TemplateVarFirstName, + } + + emptyVariable := TemplateItem{ + Type: TemplateItemTypeVar, + VarType: "", + } + + data, err := json.Marshal(text) + assert.NoError(t, err) + assert.Equal(t, "\""+text.Text+"\"", string(data)) + + data, err = json.Marshal(variable) + assert.NoError(t, err) + assert.Equal(t, `{"var":"first_name"}`, string(data)) + + data, err = json.Marshal(emptyVariable) + assert.NoError(t, err) + assert.Equal(t, "{}", string(data)) +} + +func TestTemplateItem_UnmarshalJSON(t *testing.T) { + var ( + textResult TemplateItem + variableResult TemplateItem + emptyVariableResult TemplateItem + ) + + text := []byte("\"text block\"") + variable := []byte(`{"var":"first_name"}`) + emptyVariable := []byte("{}") + + require.NoError(t, json.Unmarshal(text, &textResult)) + require.NoError(t, json.Unmarshal(variable, &variableResult)) + require.NoError(t, json.Unmarshal(emptyVariable, &emptyVariableResult)) + + assert.Equal(t, TemplateItemTypeText, textResult.Type) + assert.Equal(t, string(text)[1:11], textResult.Text) + + assert.Equal(t, TemplateItemTypeVar, variableResult.Type) + assert.Equal(t, TemplateVarFirstName, variableResult.VarType) + assert.Empty(t, variableResult.Text) + + assert.Equal(t, TemplateItemTypeVar, emptyVariableResult.Type) + assert.Equal(t, TemplateVarCustom, emptyVariableResult.VarType) + assert.Empty(t, emptyVariableResult.Text) +} diff --git a/v1/types.go b/v1/types.go index 104d953..dc2ae8e 100644 --- a/v1/types.go +++ b/v1/types.go @@ -416,6 +416,13 @@ type TransportRequestMeta struct { Timestamp int64 `json:"timestamp"` } +type ActivateTemplateRequest struct { + Code string `binding:"required,min=1,max=512" json:"code"` + Name string `binding:"required,min=1,max=512" json:"name"` + Type string `binding:"required" json:"type"` + Template []TemplateItem `json:"template"` +} + var ErrInvalidOriginator = errors.New("invalid originator") // Originator of message diff --git a/v1/types_test.go b/v1/types_test.go index bb205fe..c69cfbf 100644 --- a/v1/types_test.go +++ b/v1/types_test.go @@ -3,8 +3,9 @@ package v1 import ( "encoding/json" "fmt" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestSendData_MarshalJSON(t *testing.T) { @@ -96,9 +97,5 @@ func TestOriginator(t *testing.T) { assert.Equal(t, err, ErrInvalidOriginator) }) - t.Run("MarshalJSON_Invalid", func(t *testing.T) { - data, err := json.Marshal(Originator(3)) - assert.Nil(t, data) - assert.EqualError(t, err, "json: error calling MarshalJSON for type v1.Originator: invalid originator") - }) + t.Run("MarshalJSON_Invalid", OriginatorMarshalJSONInvalid) } diff --git a/v1/types_test_1.13.go b/v1/types_test_1.13.go new file mode 100644 index 0000000..014f825 --- /dev/null +++ b/v1/types_test_1.13.go @@ -0,0 +1,16 @@ +// +build !go1.14 + +package v1 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func OriginatorMarshalJSONInvalid(t *testing.T) { + data, err := json.Marshal(Originator(3)) + assert.Nil(t, data) + assert.EqualError(t, err, "json: error calling MarshalJSON for type v1.Originator: invalid originator") +} diff --git a/v1/types_test_1.14.go b/v1/types_test_1.14.go new file mode 100644 index 0000000..f2c31b6 --- /dev/null +++ b/v1/types_test_1.14.go @@ -0,0 +1,16 @@ +// +build go1.14 + +package v1 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func OriginatorMarshalJSONInvalid(t *testing.T) { + data, err := json.Marshal(Originator(3)) + assert.Nil(t, data) + assert.EqualError(t, err, "json: error calling MarshalText for type v1.Originator: invalid originator") +}