From 65f0ecfa6aff88c5659e517d52e79152b37c7921 Mon Sep 17 00:00:00 2001 From: tishmaria90 Date: Wed, 3 Nov 2021 17:27:56 +0300 Subject: [PATCH] Improve errors handling --- core/config.go | 4 --- core/error_scopes.go | 50 +++++++++++++++++++++++++++++++ core/error_scopes_test.go | 15 ++++++++++ core/utils.go | 63 +++++++++++++++------------------------ core/utils_test.go | 46 ++++++++++++++++++---------- go.mod | 4 +-- go.sum | 13 +++++--- 7 files changed, 129 insertions(+), 66 deletions(-) create mode 100644 core/error_scopes.go create mode 100644 core/error_scopes_test.go diff --git a/core/config.go b/core/config.go index 672fcd1..efdf986 100644 --- a/core/config.go +++ b/core/config.go @@ -11,10 +11,6 @@ import ( ) var ( - credentialsTransport = []string{ - "/api/integration-modules/{code}", - "/api/integration-modules/{code}/edit", - } markdownSymbols = []string{"*", "_", "`", "["} slashRegex = regexp.MustCompile(`/+$`) ) diff --git a/core/error_scopes.go b/core/error_scopes.go new file mode 100644 index 0000000..2c295e3 --- /dev/null +++ b/core/error_scopes.go @@ -0,0 +1,50 @@ +package core + +import ( + "errors" + "fmt" + "strings" +) + +// ErrInsufficientScopes is wrapped by the insufficientScopesErr. +// If errors.Is(err, ErrInsufficientScopes) returns true then error implements ScopesList. +var ErrInsufficientScopes = errors.New("insufficient scopes") + +// ScopesList is a contract for the scopes list. +type ScopesList interface { + Scopes() []string +} + +// insufficientScopesErr contains information about missing auth scopes. +type insufficientScopesErr struct { + scopes []string + wrapped error +} + +// Error message. +func (e insufficientScopesErr) Error() string { + return e.wrapped.Error() +} + +// Unwrap underlying error. +func (e insufficientScopesErr) Unwrap() error { + return e.wrapped +} + +// Scopes that are missing. +func (e insufficientScopesErr) Scopes() []string { + return e.scopes +} + +// String returns string representation of an error with scopes that are missing. +func (e insufficientScopesErr) String() string { + return fmt.Sprintf("Missing scopes: %s", strings.Join(e.Scopes(), ", ")) +} + +// NewInsufficientScopesErr is a insufficientScopesErr constructor. +func NewInsufficientScopesErr(scopes []string) error { + return insufficientScopesErr{ + scopes: scopes, + wrapped: ErrInsufficientScopes, + } +} diff --git a/core/error_scopes_test.go b/core/error_scopes_test.go new file mode 100644 index 0000000..05697a2 --- /dev/null +++ b/core/error_scopes_test.go @@ -0,0 +1,15 @@ +package core + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestError_NewScopesError(t *testing.T) { + scopes := []string{"scope1", "scope2"} + scopesError := NewInsufficientScopesErr(scopes) + + assert.True(t, errors.Is(scopesError, ErrInsufficientScopes)) +} diff --git a/core/utils.go b/core/utils.go index 50ec066..ac2cddd 100644 --- a/core/utils.go +++ b/core/utils.go @@ -5,7 +5,6 @@ import ( "crypto/sha1" "crypto/sha256" "encoding/json" - "errors" "fmt" "net/http" "regexp" @@ -17,10 +16,15 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3/s3manager" - v5 "github.com/retailcrm/api-client-go/v5" + retailcrm "github.com/retailcrm/api-client-go/v2" v1 "github.com/retailcrm/mg-transport-api-client-go/v1" ) +var DefaultScopes = []string{ + "integration_read", + "integration_write", +} + var defaultCurrencies = map[string]string{ "rub": "₽", "uah": "₴", @@ -108,60 +112,41 @@ func (u *Utils) GenerateToken() string { } // GetAPIClient will initialize RetailCRM api client from url and key. -func (u *Utils) GetAPIClient(url, key string) (*v5.Client, int, error) { - client := v5.New(url, key) +func (u *Utils) GetAPIClient(url, key string, scopes []string) (*retailcrm.Client, int, error) { + client := retailcrm.New(url, key). + WithLogger(retailcrm.DebugLoggerAdapter(u.Logger)) client.Debug = u.IsDebug - cr, status, e := client.APICredentials() - if e != nil && e.Error() != "" { - u.Logger.Error(url, status, e.Error(), cr) - return nil, http.StatusInternalServerError, errors.New(e.Error()) + cr, status, err := client.APICredentials() + if err != nil { + return nil, status, err } - if !cr.Success { - errMsg := "unknown error" - - if e != nil { - if e.ApiError() != "" { - errMsg = e.ApiError() - } else if e.ApiErrors() != nil { - errMsg = "" - - for key, errText := range e.ApiErrors() { - errMsg += fmt.Sprintf("[%s: %s] ", key, errText) - } - } - } - - u.Logger.Error(url, status, errMsg, cr) - return nil, http.StatusBadRequest, errors.New("invalid credentials") - } - - if res := u.checkCredentials(cr.Credentials); len(res) != 0 { + if res := u.checkScopes(cr.Scopes, scopes); len(res) != 0 { u.Logger.Error(url, status, res) - return nil, http.StatusBadRequest, errors.New("missing credentials") + return nil, http.StatusBadRequest, NewInsufficientScopesErr(res) } return client, 0, nil } -func (u *Utils) checkCredentials(credential []string) []string { - rc := make([]string, len(credentialsTransport)) - copy(rc, credentialsTransport) +func (u *Utils) checkScopes(scopes []string, scopesRequired []string) []string { + rs := make([]string, len(scopesRequired)) + copy(rs, scopesRequired) - for _, vc := range credential { - for kn, vn := range rc { - if vn == vc { - if len(rc) == 1 { - rc = rc[:0] + for _, vs := range scopes { + for kn, vn := range rs { + if vn == vs { + if len(rs) == 1 { + rs = rs[:0] break } - rc = append(rc[:kn], rc[kn+1:]...) + rs = append(rs[:kn], rs[kn+1:]...) } } } - return rc + return rs } // UploadUserAvatar will upload avatar for user. diff --git a/core/utils_test.go b/core/utils_test.go index 0e1ebb7..d77c122 100644 --- a/core/utils_test.go +++ b/core/utils_test.go @@ -2,6 +2,7 @@ package core import ( "encoding/json" + "errors" "net/http" "strings" "testing" @@ -9,7 +10,7 @@ import ( "github.com/h2non/gock" "github.com/op/go-logging" - v5 "github.com/retailcrm/api-client-go/v5" + retailcrm "github.com/retailcrm/api-client-go/v2" v1 "github.com/retailcrm/mg-transport-api-client-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -68,9 +69,10 @@ func (u *UtilsTest) Test_GenerateToken() { func (u *UtilsTest) Test_GetAPIClient_FailRuntime() { defer gock.Off() - gock.New(testCRMURL) + gock.New(testCRMURL). + Reply(http.StatusInternalServerError) - _, status, err := u.utils.GetAPIClient(testCRMURL, "key") + _, status, err := u.utils.GetAPIClient(testCRMURL, "key", []string{}) assert.Equal(u.T(), http.StatusInternalServerError, status) assert.NotNil(u.T(), err) } @@ -80,9 +82,9 @@ func (u *UtilsTest) Test_GetAPIClient_FailAPI() { gock.New(testCRMURL). Get("/credentials"). Reply(http.StatusBadRequest). - BodyString(`{"success": false, "errorMsg": "error message"}`) + BodyString(`{"success": false, "errorMsg": "invalid credentials"}`) - _, status, err := u.utils.GetAPIClient(testCRMURL, "key") + _, status, err := u.utils.GetAPIClient(testCRMURL, "key", []string{}) assert.Equal(u.T(), http.StatusBadRequest, status) if assert.NotNil(u.T(), err) { assert.Equal(u.T(), "invalid credentials", err.Error()) @@ -90,9 +92,9 @@ func (u *UtilsTest) Test_GetAPIClient_FailAPI() { } func (u *UtilsTest) Test_GetAPIClient_FailAPICredentials() { - resp := v5.CredentialResponse{ + resp := retailcrm.CredentialResponse{ Success: true, - Credentials: []string{}, + Scopes: []string{}, SiteAccess: "all", SitesAvailable: []string{}, } @@ -105,20 +107,17 @@ func (u *UtilsTest) Test_GetAPIClient_FailAPICredentials() { Reply(http.StatusOK). BodyString(string(data)) - _, status, err := u.utils.GetAPIClient(testCRMURL, "key") + _, status, err := u.utils.GetAPIClient(testCRMURL, "key", DefaultScopes) assert.Equal(u.T(), http.StatusBadRequest, status) if assert.NotNil(u.T(), err) { - assert.Equal(u.T(), "missing credentials", err.Error()) + assert.True(u.T(), errors.Is(err, ErrInsufficientScopes)) } } func (u *UtilsTest) Test_GetAPIClient_Success() { - resp := v5.CredentialResponse{ - Success: true, - Credentials: []string{ - "/api/integration-modules/{code}", - "/api/integration-modules/{code}/edit", - }, + resp := retailcrm.CredentialResponse{ + Success: true, + Scopes: DefaultScopes, SiteAccess: "all", SitesAvailable: []string{"site"}, } @@ -131,7 +130,7 @@ func (u *UtilsTest) Test_GetAPIClient_Success() { Reply(http.StatusOK). BodyString(string(data)) - _, status, err := u.utils.GetAPIClient(testCRMURL, "key") + _, status, err := u.utils.GetAPIClient(testCRMURL, "key", DefaultScopes) require.NoError(u.T(), err) assert.Equal(u.T(), 0, status) } @@ -162,6 +161,21 @@ func (u *UtilsTest) Test_RemoveTrailingSlash() { assert.Equal(u.T(), testCRMURL, u.utils.RemoveTrailingSlash(testCRMURL)) } +func (u *UtilsTest) TestUtils_CheckScopes() { + required := []string{"one", "two"} + + scopes := []string{"one", "two"} + + diff := u.utils.checkScopes(scopes, required) + assert.Equal(u.T(), 0, len(diff)) + + scopes = []string{"three", "four"} + + diff = u.utils.checkScopes(scopes, required) + assert.Equal(u.T(), 2, len(diff)) + assert.Equal(u.T(), []string{"one", "two"}, diff) +} + func TestUtils_GetMGItemData_FailRuntime_GetImage(t *testing.T) { defer gock.Off() gock.New(testMGURL) diff --git a/go.mod b/go.mod index f441cd0..e593180 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/go-playground/validator/v10 v10.8.0 github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-querystring v1.0.0 // indirect github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.2.0 github.com/h2non/gock v1.0.10 @@ -24,12 +23,11 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.9.0 // indirect github.com/mattn/go-isatty v0.0.13 // indirect - github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect github.com/nicksnyder/go-i18n/v2 v2.0.2 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/pkg/errors v0.9.1 - github.com/retailcrm/api-client-go v1.3.0 + github.com/retailcrm/api-client-go/v2 v2.0.1 github.com/retailcrm/mg-transport-api-client-go v1.1.32 github.com/stretchr/testify v1.7.0 github.com/ugorji/go v1.2.6 // indirect diff --git a/go.sum b/go.sum index f09666c..34eb572 100644 --- a/go.sum +++ b/go.sum @@ -136,10 +136,11 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -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/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -160,6 +161,8 @@ github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYb github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/h2non/gock v1.0.10 h1:EzHYzKKSLN4xk0w193uAy3tp8I3+L1jmaI2Mjg4lCgU= github.com/h2non/gock v1.0.10/go.mod h1:CZMcB0Lg5IWnr9bF79pPMg9WeV6WumxQiUJ1UvdO1iE= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -246,8 +249,8 @@ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/retailcrm/api-client-go v1.3.0 h1:8cEtLZ9gk+nTEVuzA/wzQhb8tRfaxfCQHLdPtO+/gek= -github.com/retailcrm/api-client-go v1.3.0/go.mod h1:QRoPE2SM6ST7i2g0yEdqm7Iw98y7cYuq3q14Ot+6N8c= +github.com/retailcrm/api-client-go/v2 v2.0.1 h1:wyM0F1VTSJPO8PVEXB0u7s6ZEs0xRCnUu7YdQ1E4UZ8= +github.com/retailcrm/api-client-go/v2 v2.0.1/go.mod h1:1yTZl9+gd3+/k0kAJe7sYvC+mL4fqMwIwtnSgSWZlkQ= github.com/retailcrm/mg-transport-api-client-go v1.1.32 h1:IBPltSoD5q2PPZJbNC/prK5F9rEVPXVx/ZzDpi7HKhs= github.com/retailcrm/mg-transport-api-client-go v1.1.32/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -530,6 +533,8 @@ gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8 gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI= gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw= +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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=