Merge pull request #7 from Neur0toxine/master

[feature] csrf middleware & error collector (mostly for migrations)
This commit is contained in:
Alex Lushpai 2019-10-31 15:09:37 +03:00 committed by GitHub
commit 41e873016f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 805 additions and 32 deletions

233
core/csrf.go Normal file
View File

@ -0,0 +1,233 @@
package core
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"io"
"io/ioutil"
"math/rand"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
)
// CSRFTokenGetter func type
type CSRFTokenGetter func(c *gin.Context) string
// DefaultCSRFTokenGetter default getter
var DefaultCSRFTokenGetter = func(c *gin.Context) string {
r := c.Request
if t := r.URL.Query().Get("csrf_token"); len(t) > 0 {
return t
} else if t := r.Header.Get("X-CSRF-Token"); len(t) > 0 {
return t
} else if t := r.Header.Get("X-XSRF-Token"); len(t) > 0 {
return t
} else if c.Request.Body != nil {
data, _ := ioutil.ReadAll(c.Request.Body)
c.Request.Body = ioutil.NopCloser(bytes.NewReader(data))
t := r.FormValue("csrf_token")
c.Request.Body = ioutil.NopCloser(bytes.NewReader(data))
if len(t) > 0 {
return t
}
}
return ""
}
// DefaultIgnoredMethods ignored methods for CSRF verifier middleware
var DefaultIgnoredMethods = []string{"GET", "HEAD", "OPTIONS"}
type CSRF struct {
salt string
secret string
sessionName string
abortFunc gin.HandlerFunc
csrfTokenGetter CSRFTokenGetter
store sessions.Store
locale *Localizer
}
// NewCSRF creates CSRF struct with specified configuration and session store.
// GenerateCSRFMiddleware and VerifyCSRFMiddleware returns CSRF middlewares.
// Salt must be different every time (pass empty salt to use random), secret must be provided, sessionName is optional - pass empty to use default,
// store will be used to store sessions, abortFunc will be called to return error if token is invalid, csrfTokenGetter will be used to obtain token.
// Usage (with random salt):
// core.NewCSRF("", "super secret", "csrf_session", store, func (c *gin.Context) {
// c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid CSRF token"})
// }, core.DefaultCSRFTokenGetter)
// Note for csrfTokenGetter: if you want to read token from request body (for example, from form field) - don't forget to restore Body data!
// Body in http.Request is io.ReadCloser instance. Reading CSRF token from form like that:
// if t := r.FormValue("csrf_token"); len(t) > 0 {
// return t
// }
// will close body - and all next middlewares won't be able to read body at all!
// Use DefaultCSRFTokenGetter as example to implement your own token getter.
func NewCSRF(salt, secret, sessionName string, store sessions.Store, abortFunc gin.HandlerFunc, csrfTokenGetter CSRFTokenGetter) *CSRF {
if store == nil {
panic("store must not be nil")
}
if secret == "" {
panic("at least secret must be provided")
}
csrf := &CSRF{
store: store,
secret: secret,
abortFunc: abortFunc,
csrfTokenGetter: csrfTokenGetter,
}
if salt == "" {
salt = csrf.generateSalt()
}
if sessionName == "" {
sessionName = "csrf_token_session"
}
csrf.salt = salt
csrf.sessionName = sessionName
return csrf
}
// strInSlice checks whether string exists in slice
func (x *CSRF) strInSlice(slice []string, v string) bool {
exists := false
for _, i := range slice {
if i == v {
exists = true
break
}
}
return exists
}
// generateCSRFToken generates new CSRF token
func (x *CSRF) generateCSRFToken() string {
h := sha1.New()
io.WriteString(h, x.salt+"#"+x.secret)
hash := base64.URLEncoding.EncodeToString(h.Sum(nil))
return hash
}
// generateSalt generates salt from random bytes. If it fails to generate cryptographically
// secure salt - it will generate pseudo-random, weaker salt.
// It will be used automatically if no salt provided.
// Default secure salt length: 8 bytes.
// Default pseudo-random salt length: 64 bytes.
func (x *CSRF) generateSalt() string {
salt := securecookie.GenerateRandomKey(8)
if salt == nil {
return x.pseudoRandomString(64)
}
return string(salt)
}
// pseudoRandomString generates pseudo-random string with specified length
func (x *CSRF) pseudoRandomString(length int) string {
rand.Seed(time.Now().UnixNano())
data := make([]byte, length)
for i := 0; i < length; i++ {
data[i] = byte(65 + rand.Intn(90-65))
}
return string(data)
}
// CSRFFromContext returns csrf token or random token. It shouldn't return empty string because it will make csrf protection useless.
// e.g. any request without token will work fine, which is inacceptable.
func (x *CSRF) CSRFFromContext(c *gin.Context) string {
if i, ok := c.Get("csrf_token"); ok {
if token, ok := i.(string); ok {
return token
} else {
return x.generateCSRFToken()
}
} else {
return x.generateCSRFToken()
}
}
// GenerateCSRFMiddleware returns gin.HandlerFunc which will generate CSRF token
// Usage:
// engine := gin.New()
// csrf := NewCSRF("salt", "secret", "not_found", "incorrect", localizer)
// engine.Use(csrf.GenerateCSRFMiddleware())
func (x *CSRF) GenerateCSRFMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
session, _ := x.store.Get(c.Request, x.sessionName)
if i, ok := session.Values["csrf_token"]; ok {
if i, ok := i.(string); !ok || i == "" {
if x.fillToken(session, c) != nil {
x.abortFunc(c)
c.Abort()
return
}
}
} else {
if x.fillToken(session, c) != nil {
x.abortFunc(c)
c.Abort()
return
}
}
}
}
// fillToken stores token in session and context
func (x *CSRF) fillToken(s *sessions.Session, c *gin.Context) error {
s.Values["csrf_token"] = x.generateCSRFToken()
c.Set("csrf_token", s.Values["csrf_token"])
return s.Save(c.Request, c.Writer)
}
// VerifyCSRFMiddleware verifies CSRF token
// Usage:
// engine := gin.New()
// engine.Use(csrf.VerifyCSRFMiddleware())
func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc {
return func(c *gin.Context) {
if x.strInSlice(ignoredMethods, c.Request.Method) {
return
}
var token string
session, _ := x.store.Get(c.Request, x.sessionName)
if i, ok := session.Values["csrf_token"]; ok {
if i, ok := i.(string); !ok || i == "" {
x.abortFunc(c)
c.Abort()
return
} else {
token = i
}
} else {
x.abortFunc(c)
c.Abort()
return
}
if x.csrfTokenGetter(c) != token {
x.abortFunc(c)
c.Abort()
return
}
}
}

305
core/csrf_test.go Normal file
View File

@ -0,0 +1,305 @@
package core
import (
"bytes"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type CSRFTest struct {
suite.Suite
csrf *CSRF
}
type requestOptions struct {
Method string
URL string
Headers map[string]string
Body io.Reader
}
func TestCSRF_DefaultCSRFTokenGetter_Empty(t *testing.T) {
c := &gin.Context{Request: &http.Request{
URL: &url.URL{
RawQuery: "",
},
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}}
assert.Empty(t, DefaultCSRFTokenGetter(c))
}
func TestCSRF_DefaultCSRFTokenGetter_URL(t *testing.T) {
c := &gin.Context{Request: &http.Request{
URL: &url.URL{
RawQuery: "csrf_token=token",
},
}}
assert.NotEmpty(t, DefaultCSRFTokenGetter(c))
assert.Equal(t, "token", DefaultCSRFTokenGetter(c))
}
func TestCSRF_DefaultCSRFTokenGetter_Header_CSRF(t *testing.T) {
header := http.Header{}
header.Add("X-CSRF-Token", "token")
c := &gin.Context{Request: &http.Request{
URL: &url.URL{
RawQuery: "",
},
Header: header,
}}
assert.NotEmpty(t, DefaultCSRFTokenGetter(c))
assert.Equal(t, "token", DefaultCSRFTokenGetter(c))
}
func TestCSRF_DefaultCSRFTokenGetter_Header_XSRC(t *testing.T) {
header := http.Header{}
header.Add("X-XSRF-Token", "token")
c := &gin.Context{Request: &http.Request{
URL: &url.URL{
RawQuery: "",
},
Header: header,
}}
assert.NotEmpty(t, DefaultCSRFTokenGetter(c))
assert.Equal(t, "token", DefaultCSRFTokenGetter(c))
}
func TestCSRF_DefaultCSRFTokenGetter_Form(t *testing.T) {
headers := http.Header{}
headers.Add("Content-Type", "application/x-www-form-urlencoded")
c := &gin.Context{Request: &http.Request{
URL: &url.URL{
RawQuery: "",
},
Header: headers,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}}
c.Request.PostForm = url.Values{"csrf_token": {"token"}}
assert.NotEmpty(t, DefaultCSRFTokenGetter(c))
assert.Equal(t, "token", DefaultCSRFTokenGetter(c))
_, err := ioutil.ReadAll(c.Request.Body)
assert.NoError(t, err)
}
func TestCSRF_NewCSRF_NilStore(t *testing.T) {
defer func() {
assert.NotNil(t, recover())
}()
NewCSRF("salt", "secret", "csrf", nil, func(context *gin.Context) {}, DefaultCSRFTokenGetter)
}
func TestCSRF_NewCSRF_EmptySecret(t *testing.T) {
defer func() {
assert.NotNil(t, recover())
}()
store := sessions.NewCookieStore([]byte("keys"))
NewCSRF("salt", "", "csrf", store, func(context *gin.Context) {}, DefaultCSRFTokenGetter)
}
func TestCSRF_NewCSRF_SaltAndSessionNotEmpty(t *testing.T) {
store := sessions.NewCookieStore([]byte("keys"))
csrf := NewCSRF("salt", "secret", "", store, func(context *gin.Context) {}, DefaultCSRFTokenGetter)
assert.NotEmpty(t, csrf.salt)
assert.NotEmpty(t, csrf.sessionName)
}
func TestCSRF_Suite(t *testing.T) {
suite.Run(t, new(CSRFTest))
}
func (x *CSRFTest) SetupSuite() {
store := sessions.NewCookieStore([]byte("keys"))
x.csrf = NewCSRF("salt", "secret", "", store, func(context *gin.Context) {
context.AbortWithStatus(900)
}, DefaultCSRFTokenGetter)
}
func (x *CSRFTest) NewServer() *gin.Engine {
gin.SetMode(gin.TestMode)
g := gin.New()
g.Use(x.csrf.GenerateCSRFMiddleware(), x.csrf.VerifyCSRFMiddleware(DefaultIgnoredMethods))
return g
}
func (x *CSRFTest) request(server *gin.Engine, options requestOptions) (*httptest.ResponseRecorder, *http.Request) {
if options.Method == "" {
options.Method = "GET"
}
w := httptest.NewRecorder()
req, err := http.NewRequest(options.Method, options.URL, options.Body)
if options.Headers != nil {
for key, value := range options.Headers {
req.Header.Set(key, value)
}
}
server.ServeHTTP(w, req)
if err != nil {
panic(err)
}
return w, req
}
func (x *CSRFTest) Test_strInSlice() {
slice := []string{"alpha", "beta", "gamma"}
assert.False(x.T(), x.csrf.strInSlice(slice, "lambda"))
assert.True(x.T(), x.csrf.strInSlice(slice, "alpha"))
}
func (x *CSRFTest) Test_generateCSRFToken() {
assert.NotEmpty(x.T(), x.csrf.generateCSRFToken())
}
func (x *CSRFTest) Test_generateSalt() {
salt := x.csrf.generateSalt()
assert.NotEmpty(x.T(), salt)
}
func (x *CSRFTest) Test_pseudoRandomString() {
assert.Len(x.T(), x.csrf.pseudoRandomString(12), 12)
assert.Len(x.T(), x.csrf.pseudoRandomString(64), 64)
}
func (x *CSRFTest) Test_CSRFFromContext_NotExist() {
c := &gin.Context{}
token := x.csrf.CSRFFromContext(c)
assert.NotEmpty(x.T(), token)
}
func (x *CSRFTest) Test_CSRFFromContext_NotString() {
c := &gin.Context{}
c.Set("csrf_token", struct{}{})
token := x.csrf.CSRFFromContext(c)
assert.NotEmpty(x.T(), token)
}
func (x *CSRFTest) Test_CSRFFromContext_Exist() {
c := &gin.Context{}
c.Set("csrf_token", "token")
token := x.csrf.CSRFFromContext(c)
assert.NotEmpty(x.T(), token)
assert.Equal(x.T(), "token", token)
}
func (x *CSRFTest) Test_GenerateCSRFMiddleware() {
assert.NotNil(x.T(), x.csrf.GenerateCSRFMiddleware())
}
func (x *CSRFTest) Test_GenerateCSRFMiddleware_Middleware() {
x.csrf.store = sessions.NewCookieStore([]byte("secret"))
g := x.NewServer()
g.GET("/get", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
g.POST("/post", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
get, getReq := x.request(g, requestOptions{
Method: "GET",
URL: "/get",
})
session, _ := x.csrf.store.Get(getReq, x.csrf.sessionName)
post, _ := x.request(g, requestOptions{
Method: "POST",
URL: "/post",
Headers: map[string]string{
"Cookie": get.Header().Get("Set-Cookie"),
"X-CSRF-Token": session.Values["csrf_token"].(string),
},
})
getWithToken, getReqWithToken := x.request(g, requestOptions{
Method: "GET",
URL: "/get",
Headers: map[string]string{
"Cookie": get.Header().Get("Set-Cookie"),
"X-CSRF-Token": session.Values["csrf_token"].(string),
},
})
secondSession, _ := x.csrf.store.Get(getReqWithToken, x.csrf.sessionName)
assert.Equal(x.T(), session.Values["csrf_token"].(string), secondSession.Values["csrf_token"].(string))
assert.Equal(x.T(), "OK", get.Body.String())
assert.Equal(x.T(), http.StatusOK, get.Result().StatusCode)
assert.Equal(x.T(), "OK", getWithToken.Body.String())
assert.Equal(x.T(), http.StatusOK, getWithToken.Result().StatusCode)
assert.Equal(x.T(), "OK", post.Body.String())
assert.Equal(x.T(), http.StatusOK, post.Result().StatusCode)
}
func (x *CSRFTest) Test_VerifyCSRFMiddleware_NoToken() {
x.csrf.store = sessions.NewCookieStore([]byte("secret"))
g := x.NewServer()
g.GET("/get", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
g.POST("/post", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
postWithoutSession, _ := x.request(g, requestOptions{
Method: "POST",
URL: "/post",
})
get, getReq := x.request(g, requestOptions{
Method: "GET",
URL: "/get",
})
session, _ := x.csrf.store.Get(getReq, x.csrf.sessionName)
post, _ := x.request(g, requestOptions{
Method: "POST",
URL: "/post",
Headers: map[string]string{
"Cookie": get.Header().Get("Set-Cookie"),
"X-CSRF-Token": session.Values["csrf_token"].(string),
},
})
postIncorrectToken, _ := x.request(g, requestOptions{
Method: "POST",
URL: "/post",
Headers: map[string]string{
"Cookie": get.Header().Get("Set-Cookie"),
"X-CSRF-Token": "incorrect token",
},
})
assert.NotEqual(x.T(), "OK", postWithoutSession.Body.String())
assert.Equal(x.T(), 900, postWithoutSession.Result().StatusCode)
assert.NotEqual(x.T(), "OK", postIncorrectToken.Body.String())
assert.Equal(x.T(), 900, postIncorrectToken.Result().StatusCode)
assert.Equal(x.T(), "OK", get.Body.String())
assert.Equal(x.T(), http.StatusOK, get.Result().StatusCode)
assert.Equal(x.T(), "OK", post.Body.String())
assert.Equal(x.T(), http.StatusOK, post.Result().StatusCode)
}

View File

@ -6,6 +6,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/gobuffalo/packr/v2"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"github.com/op/go-logging"
)
@ -18,6 +20,8 @@ type Engine struct {
ginEngine *gin.Engine
httpClient *http.Client
Logger *logging.Logger
csrf *CSRF
Sessions sessions.Store
Config ConfigInterface
LogFormatter logging.Formatter
prepared bool
@ -157,6 +161,75 @@ func (e *Engine) HTTPClient() *http.Client {
}
}
// WithCookieSessions generates new CookieStore with optional key length.
// Default key length is 32 bytes.
func (e *Engine) WithCookieSessions(keyLength ...int) *Engine {
length := 32
if len(keyLength) > 0 && keyLength[0] > 0 {
length = keyLength[0]
}
e.Sessions = sessions.NewCookieStore(securecookie.GenerateRandomKey(length))
return e
}
// WithCookieSessions generates new FilesystemStore with optional key length.
// Default key length is 32 bytes.
func (e *Engine) WithFilesystemSessions(path string, keyLength ...int) *Engine {
length := 32
if len(keyLength) > 0 && keyLength[0] > 0 {
length = keyLength[0]
}
e.Sessions = sessions.NewFilesystemStore(path, securecookie.GenerateRandomKey(length))
return e
}
// InitCSRF initializes CSRF middleware. engine.Sessions must be already initialized,
// use engine.WithCookieStore or engine.WithFilesystemStore for that.
// Syntax is similar to core.NewCSRF, but you shouldn't pass sessionName, store and salt.
func (e *Engine) InitCSRF(secret string, abortFunc gin.HandlerFunc, getter CSRFTokenGetter) *Engine {
if e.Sessions == nil {
panic("engine.Sessions must be initialized first")
}
e.csrf = NewCSRF("", secret, "", e.Sessions, abortFunc, getter)
return e
}
// VerifyCSRFMiddleware returns CSRF verifier middleware
// Usage:
// engine.Router().Use(engine.VerifyCSRFMiddleware(core.DefaultIgnoredMethods))
func (e *Engine) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc {
if e.csrf == nil {
panic("csrf is not initialized")
}
return e.csrf.VerifyCSRFMiddleware(ignoredMethods)
}
// GenerateCSRFMiddleware returns CSRF generator middleware
// Usage:
// engine.Router().Use(engine.GenerateCSRFMiddleware())
func (e *Engine) GenerateCSRFMiddleware() gin.HandlerFunc {
if e.csrf == nil {
panic("csrf is not initialized")
}
return e.csrf.GenerateCSRFMiddleware()
}
// GetCSRFToken returns CSRF token from provided context
func (e *Engine) GetCSRFToken(c *gin.Context) string {
if e.csrf == nil {
panic("csrf is not initialized")
}
return e.csrf.CSRFFromContext(c)
}
// ConfigureRouter will call provided callback with current gin.Engine, or panic if engine is not present
func (e *Engine) ConfigureRouter(callback func(*gin.Engine)) *Engine {
callback(e.Router())

View File

@ -1,9 +1,12 @@
package core
import (
"bytes"
"database/sql"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"os"
"testing"
@ -180,6 +183,112 @@ func (e *EngineTest) Test_HTTPClient() {
assert.NotNil(e.T(), e.engine.httpClient)
}
func (e *EngineTest) Test_WithCookieSessions() {
e.engine.Sessions = nil
e.engine.WithCookieSessions(4)
assert.NotNil(e.T(), e.engine.Sessions)
}
func (e *EngineTest) Test_WithFilesystemSessions() {
e.engine.Sessions = nil
e.engine.WithFilesystemSessions(os.TempDir(), 4)
assert.NotNil(e.T(), e.engine.Sessions)
}
func (e *EngineTest) Test_InitCSRF_Fail() {
defer func() {
assert.NotNil(e.T(), recover())
}()
e.engine.csrf = nil
e.engine.Sessions = nil
e.engine.InitCSRF("test", func(context *gin.Context) {}, DefaultCSRFTokenGetter)
assert.Nil(e.T(), e.engine.csrf)
}
func (e *EngineTest) Test_InitCSRF() {
defer func() {
assert.Nil(e.T(), recover())
}()
e.engine.csrf = nil
e.engine.WithCookieSessions(4)
e.engine.InitCSRF("test", func(context *gin.Context) {}, DefaultCSRFTokenGetter)
assert.NotNil(e.T(), e.engine.csrf)
}
func (e *EngineTest) Test_VerifyCSRFMiddleware_Fail() {
defer func() {
assert.NotNil(e.T(), recover())
}()
e.engine.csrf = nil
e.engine.VerifyCSRFMiddleware(DefaultIgnoredMethods)
}
func (e *EngineTest) Test_VerifyCSRFMiddleware() {
defer func() {
assert.Nil(e.T(), recover())
}()
e.engine.csrf = nil
e.engine.WithCookieSessions(4)
e.engine.InitCSRF("test", func(context *gin.Context) {}, DefaultCSRFTokenGetter)
e.engine.VerifyCSRFMiddleware(DefaultIgnoredMethods)
}
func (e *EngineTest) Test_GenerateCSRFMiddleware_Fail() {
defer func() {
assert.NotNil(e.T(), recover())
}()
e.engine.csrf = nil
e.engine.GenerateCSRFMiddleware()
}
func (e *EngineTest) Test_GenerateCSRFMiddleware() {
defer func() {
assert.Nil(e.T(), recover())
}()
e.engine.csrf = nil
e.engine.WithCookieSessions(4)
e.engine.InitCSRF("test", func(context *gin.Context) {}, DefaultCSRFTokenGetter)
e.engine.GenerateCSRFMiddleware()
}
func (e *EngineTest) Test_GetCSRFToken_Fail() {
defer func() {
assert.NotNil(e.T(), recover())
}()
e.engine.csrf = nil
e.engine.GetCSRFToken(nil)
}
func (e *EngineTest) Test_GetCSRFToken() {
defer func() {
assert.Nil(e.T(), recover())
}()
c := &gin.Context{Request: &http.Request{
URL: &url.URL{
RawQuery: "",
},
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
Header: http.Header{"X-CSRF-Token": []string{"token"}},
}}
c.Set("csrf_token", "token")
e.engine.csrf = nil
e.engine.WithCookieSessions(4)
e.engine.InitCSRF("test", func(context *gin.Context) {}, DefaultCSRFTokenGetter)
assert.NotEmpty(e.T(), e.engine.GetCSRFToken(c))
assert.Equal(e.T(), "token", e.engine.GetCSRFToken(c))
}
func (e *EngineTest) Test_Run_Fail() {
defer func() {
assert.NotNil(e.T(), recover())

38
core/error_collector.go Normal file
View File

@ -0,0 +1,38 @@
package core
import (
"github.com/pkg/errors"
)
// ErrorCollector can be used to group several error calls into one call.
// It is mostly useful in GORM migrations, where you should return only one errors, but several can occur.
// Error messages are composed into one message. For example:
// err := core.ErrorCollector(
// errors.New("first"),
// errors.New("second")
// )
//
// // Will output `first < second`
// fmt.Println(err.Error())
// Example with GORM migration, returns one migration error with all error messages:
// return core.ErrorCollector(
// db.CreateTable(models.Account{}, models.Connection{}).Error,
// db.Table("account").AddUniqueIndex("account_key", "channel").Error,
// )
func ErrorCollector(errorsList ...error) error {
var errorMsg string
for _, errItem := range errorsList {
if errItem == nil {
continue
}
errorMsg += "< " + errItem.Error() + " "
}
if errorMsg != "" {
return errors.New(errorMsg[2 : len(errorMsg)-1])
}
return nil
}

View File

@ -0,0 +1,40 @@
package core
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestErrorCollector_NoError(t *testing.T) {
err := ErrorCollector(nil, nil, nil)
assert.NoError(t, err)
assert.Nil(t, err)
}
func TestErrorCollector_SeveralErrors(t *testing.T) {
err := ErrorCollector(nil, errors.New("error text"), nil)
assert.Error(t, err)
assert.Equal(t, "error text", err.Error())
}
func TestErrorCollector_EmptyErrorMessage(t *testing.T) {
err := ErrorCollector(nil, errors.New(""), nil)
assert.Error(t, err)
assert.Equal(t, "", err.Error())
}
func TestErrorCollector_AllErrors(t *testing.T) {
err := ErrorCollector(
errors.New("first"),
errors.New("second"),
errors.New("third"),
)
assert.Error(t, err)
assert.Equal(t, "first < second < third", err.Error())
}

3
go.mod
View File

@ -15,11 +15,12 @@ require (
github.com/gobuffalo/packr/v2 v2.7.1
github.com/golang/protobuf v1.3.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
github.com/jessevdk/go-flags v1.4.0
github.com/jinzhu/gorm v1.9.11
github.com/json-iterator/go v1.1.7 // indirect
github.com/karrick/godirwalk v1.12.0 // indirect
github.com/lib/pq v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect

36
go.sum
View File

@ -15,13 +15,9 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aws/aws-sdk-go v1.23.9 h1:UYWPGrBMlrW5VCYeWMbog1T/kqZzkvvheUDQaaUAhqI=
github.com/aws/aws-sdk-go v1.23.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.25.14 h1:hEsU+cukBOQe1wRRuvEgG+y6AVCyS2eyHWuTefhGxTY=
github.com/aws/aws-sdk-go v1.25.14/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/certifi/gocertifi v0.0.0-20190506164543-d2eda7129713 h1:UNOqI3EKhvbqV8f1Vm3NIwkrhq388sGCeAH2Op7w0rc=
github.com/certifi/gocertifi v0.0.0-20190506164543-d2eda7129713/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/certifi/gocertifi v0.0.0-20190905060710-a5e0173ced67 h1:8k9FLYBLKT+9v2HQJ/a95ZemmTx+/ltJcAiRhVushG8=
github.com/certifi/gocertifi v0.0.0-20190905060710-a5e0173ced67/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@ -43,19 +39,13 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/getsentry/raven-go v0.0.0-20180903072508-084a9de9eb03 h1:G/9fPivTr5EiyqE9OlW65iMRUxFXMGRHgZFGo50uG8Q=
github.com/getsentry/raven-go v0.0.0-20180903072508-084a9de9eb03/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/gin-contrib/multitemplate v0.0.0-20180827023943-5799bbbb6dce h1:KqeVCdb+M2iwyF6GzdYxTazfE1cE+133RXuGaZ5Sc1E=
github.com/gin-contrib/multitemplate v0.0.0-20180827023943-5799bbbb6dce/go.mod h1:62qM8p4crGvNKE413gTzn4eMFin1VOJfMDWMRzHdvqM=
github.com/gin-contrib/multitemplate v0.0.0-20190914010127-bba2ccfe37ec h1:mfeHJfPwsXp/iovjrTwxtkTMRAIXZu6Uxg6O95En1+Y=
github.com/gin-contrib/multitemplate v0.0.0-20190914010127-bba2ccfe37ec/go.mod h1:2tmLQ8sVzr2XKwquGd7zNq3zB6fGyjJL+47JoxoF8yM=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -67,14 +57,10 @@ github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
github.com/gobuffalo/logger v1.0.0 h1:xw9Ko9EcC5iAFprrjJ6oZco9UpzS5MQ4jAwghsLHdy4=
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg=
github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
github.com/gobuffalo/packr/v2 v2.6.0 h1:EMUzJIb5rof6r087PtGmgdzdLKpRBESJ/8jyL9MexfY=
github.com/gobuffalo/packr/v2 v2.6.0/go.mod h1:sgEE1xNZ6G0FNN5xn9pevVu4nywaxHvgup67xisti08=
github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@ -99,6 +85,10 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
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/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -109,8 +99,6 @@ github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGAR
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw=
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/gorm v1.9.10 h1:HvrsqdhCW78xpJF67g1hMxS6eCToo9PZH4LDB8WKPac=
github.com/jinzhu/gorm v1.9.10/go.mod h1:Kh6hTsSGffh4ui079FHrR5Gg+5D0hgihqDcsDN2BBJY=
github.com/jinzhu/gorm v1.9.11 h1:gaHGvE+UnWGlbWG4Y3FUwY1EcZ5n6S9WtqBA/uySMLE=
github.com/jinzhu/gorm v1.9.11/go.mod h1:bu/pK8szGZ2puuErfU0RwyeNdsf3e6nCX/noXaVxkfw=
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@ -128,9 +116,6 @@ github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62F
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/karrick/godirwalk v1.10.12 h1:BqUm+LuJcXjGv1d2mj3gBiQyrQ57a0rYoAmhvJQ7RDU=
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/karrick/godirwalk v1.12.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
@ -147,8 +132,6 @@ github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -168,8 +151,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5 h1:/TjjTS4kg7vC+05gD0LE4+97f/+PRFICnK/7wJPk7kE=
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU=
github.com/nicksnyder/go-i18n/v2 v2.0.2 h1:KsHGcTByIM0mHZKQGy0nlJLOjPNjQ6MVib/3PvsBDNY=
github.com/nicksnyder/go-i18n/v2 v2.0.2/go.mod h1:JXS4+OKhbcwDoVTEj0sLFWL1vOwec2g/YBAxZ9owJqY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -193,12 +174,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.1.1 h1:yqsyYjBDdmDwExVlTdGucY9/IpEokXpkfTfA6z5AZ7M=
github.com/retailcrm/api-client-go v1.1.1/go.mod h1:QRoPE2SM6ST7i2g0yEdqm7Iw98y7cYuq3q14Ot+6N8c=
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/mg-transport-api-client-go v1.1.31 h1:21pE1JhT49rvbMLDYJa0iiqbb/roz+eSp27fPck4uUw=
github.com/retailcrm/mg-transport-api-client-go v1.1.31/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI=
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.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@ -268,6 +245,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -281,12 +259,9 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20171214130843-f21a4dfb5e38/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
@ -300,7 +275,6 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=