Merge pull request #10 from Neur0toxine/master

updated gin to v1.5.0, several other changes
This commit is contained in:
Alex Lushpai 2019-12-13 16:17:22 +03:00 committed by GitHub
commit f42723ab28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 681 additions and 157 deletions

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019 RetailDriver LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,7 +1,9 @@
## MG Transport Library
[![Build Status](https://travis-ci.org/retailcrm/mg-transport-core.svg?branch=master)](https://travis-ci.org/retailcrm/mg-transport-core)
[![codecov](https://codecov.io/gh/retailcrm/mg-transport-core/branch/master/graph/badge.svg)](https://codecov.io/gh/retailcrm/mg-transport-core)
[![GoDoc](https://godoc.org/github.com/retailcrm/mg-transport-core/core?status.svg)](https://godoc.org/github.com/retailcrm/mg-transport-core/core)
[![GoDoc](https://godoc.org/github.com/retailcrm/mg-transport-core/core?status.svg)](https://godoc.org/github.com/retailcrm/mg-transport-core/core)
[![Go Report Card](https://goreportcard.com/badge/github.com/retailcrm/mg-transport-core)](https://goreportcard.com/report/github.com/retailcrm/mg-transport-core)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/retailcrm/mg-transport-core/blob/master/LICENSE.md)
This library provides different functions like error-reporting, logging, localization, etc. in order to make it easier to create transports.
Usage:
```go

View File

@ -7,6 +7,7 @@ import (
"github.com/retailcrm/mg-transport-core/core"
)
// Options for tool command
type Options struct{}
var (

View File

@ -16,8 +16,9 @@ var (
"/api/integration-modules/{code}/edit",
}
markdownSymbols = []string{"*", "_", "`", "["}
regCommandName = regexp.MustCompile(`^https://?[\da-z.-]+\.(retailcrm\.(ru|pro|es)|ecomlogic\.com|simlachat\.(com|ru))/?$`)
slashRegex = regexp.MustCompile(`/+$`)
regCommandName = regexp.MustCompile(
`^https://?[\da-z.-]+\.(retailcrm\.(ru|pro|es)|ecomlogic\.com|simlachat\.(com|ru))/?$`)
slashRegex = regexp.MustCompile(`/+$`)
)
// ConfigInterface settings data structure
@ -85,7 +86,7 @@ type DatabaseConfig struct {
// HTTPClientConfig struct
type HTTPClientConfig struct {
Timeout time.Duration `yaml:"timeout"`
SSLVerification bool `yaml:"ssl_verification"`
SSLVerification *bool `yaml:"ssl_verification"`
MockAddress string `yaml:"mock_address"`
MockedDomains []string `yaml:"mocked_domains"`
}
@ -198,3 +199,12 @@ func (t Info) GetCode() string {
func (t Info) GetLogoPath() string {
return t.LogoPath
}
// IsSSLVerificationEnabled returns SSL verification flag (default is true)
func (h *HTTPClientConfig) IsSSLVerificationEnabled() bool {
if h.SSLVerification == nil {
return true
}
return *h.SSLVerification
}

View File

@ -41,6 +41,10 @@ log_level: 5
debug: true
update_interval: 24
http_client:
ssl_verification: false
timeout: 30
config_aws:
access_key_id: key
secret_access_key: secret

View File

@ -2,6 +2,7 @@ package core
import (
"bytes"
// nolint:gosec
"crypto/sha1"
"encoding/base64"
"io"
@ -14,8 +15,31 @@ import (
"github.com/gorilla/sessions"
)
// CSRFErrorReason is a error reason type
type CSRFErrorReason uint8
// CSRFTokenGetter func type
type CSRFTokenGetter func(c *gin.Context) string
type CSRFTokenGetter func(*gin.Context) string
// CSRFAbortFunc is a callback which
type CSRFAbortFunc func(*gin.Context, CSRFErrorReason)
const (
// CSRFErrorNoTokenInSession will be returned if token is not present in session
CSRFErrorNoTokenInSession CSRFErrorReason = iota
// CSRFErrorCannotStoreTokenInSession will be returned if middleware cannot store token in session
CSRFErrorCannotStoreTokenInSession
// CSRFErrorIncorrectTokenType will be returned if data type of token in session is not string
CSRFErrorIncorrectTokenType
// CSRFErrorEmptyToken will be returned if token in session is empty
CSRFErrorEmptyToken
// CSRFErrorTokenMismatch will be returned in case of invalid token
CSRFErrorTokenMismatch
)
// DefaultCSRFTokenGetter default getter
var DefaultCSRFTokenGetter = func(c *gin.Context) string {
@ -44,14 +68,14 @@ var DefaultCSRFTokenGetter = func(c *gin.Context) string {
// DefaultIgnoredMethods ignored methods for CSRF verifier middleware
var DefaultIgnoredMethods = []string{"GET", "HEAD", "OPTIONS"}
// CSRF struct. Provides CSRF token verification.
type CSRF struct {
salt string
secret string
sessionName string
abortFunc gin.HandlerFunc
abortFunc CSRFAbortFunc
csrfTokenGetter CSRFTokenGetter
store sessions.Store
locale *Localizer
}
// NewCSRF creates CSRF struct with specified configuration and session store.
@ -59,7 +83,7 @@ type CSRF struct {
// 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) {
// core.NewCSRF("", "super secret", "csrf_session", store, func (c *gin.Context, reason core.CSRFErrorReason) {
// 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!
@ -69,7 +93,8 @@ type CSRF struct {
// }
// 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 {
// CSRFErrorReason will be passed to abortFunc and can be used for better error messages.
func NewCSRF(salt, secret, sessionName string, store sessions.Store, abortFunc CSRFAbortFunc, csrfTokenGetter CSRFTokenGetter) *CSRF {
if store == nil {
panic("store must not be nil")
}
@ -115,8 +140,12 @@ func (x *CSRF) strInSlice(slice []string, v string) bool {
// generateCSRFToken generates new CSRF token
func (x *CSRF) generateCSRFToken() string {
// nolint:gosec
h := sha1.New()
io.WriteString(h, x.salt+"#"+x.secret)
// Fallback to less secure method - token must be always filled even if we cannot properly generate it
if _, err := io.WriteString(h, x.salt+"#"+x.secret); err != nil {
return base64.URLEncoding.EncodeToString([]byte(time.Now().String()))
}
hash := base64.URLEncoding.EncodeToString(h.Sum(nil))
return hash
@ -155,12 +184,10 @@ 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()
}
return x.generateCSRFToken()
}
// GenerateCSRFMiddleware returns gin.HandlerFunc which will generate CSRF token
@ -175,14 +202,14 @@ func (x *CSRF) GenerateCSRFMiddleware() gin.HandlerFunc {
if i, ok := session.Values["csrf_token"]; ok {
if i, ok := i.(string); !ok || i == "" {
if x.fillToken(session, c) != nil {
x.abortFunc(c)
x.abortFunc(c, CSRFErrorCannotStoreTokenInSession)
c.Abort()
return
}
}
} else {
if x.fillToken(session, c) != nil {
x.abortFunc(c)
x.abortFunc(c, CSRFErrorCannotStoreTokenInSession)
c.Abort()
return
}
@ -211,23 +238,47 @@ func (x *CSRF) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc {
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)
var v string
if v, ok = i.(string); !ok || v == "" {
if !ok {
x.abortFunc(c, CSRFErrorIncorrectTokenType)
} else if v == "" {
x.abortFunc(c, CSRFErrorEmptyToken)
}
c.Abort()
return
} else {
token = i
}
token = v
} else {
x.abortFunc(c)
x.abortFunc(c, CSRFErrorNoTokenInSession)
c.Abort()
return
}
if x.csrfTokenGetter(c) != token {
x.abortFunc(c)
x.abortFunc(c, CSRFErrorTokenMismatch)
c.Abort()
return
}
}
}
// GetCSRFErrorMessage returns generic error message for CSRFErrorReason in English (useful for logs)
func GetCSRFErrorMessage(r CSRFErrorReason) string {
switch r {
case CSRFErrorNoTokenInSession:
return "token is not present in session"
case CSRFErrorCannotStoreTokenInSession:
return "cannot store token in session"
case CSRFErrorIncorrectTokenType:
return "incorrect token type"
case CSRFErrorEmptyToken:
return "empty token present in session"
case CSRFErrorTokenMismatch:
return "token mismatch"
default:
return "unknown error"
}
}

View File

@ -101,7 +101,7 @@ func TestCSRF_NewCSRF_NilStore(t *testing.T) {
assert.NotNil(t, recover())
}()
NewCSRF("salt", "secret", "csrf", nil, func(context *gin.Context) {}, DefaultCSRFTokenGetter)
NewCSRF("salt", "secret", "csrf", nil, func(c *gin.Context, r CSRFErrorReason) {}, DefaultCSRFTokenGetter)
}
func TestCSRF_NewCSRF_EmptySecret(t *testing.T) {
@ -110,23 +110,38 @@ func TestCSRF_NewCSRF_EmptySecret(t *testing.T) {
}()
store := sessions.NewCookieStore([]byte("keys"))
NewCSRF("salt", "", "csrf", store, func(context *gin.Context) {}, DefaultCSRFTokenGetter)
NewCSRF("salt", "", "csrf", store, func(c *gin.Context, r CSRFErrorReason) {}, DefaultCSRFTokenGetter)
}
func TestCSRF_NewCSRF_SaltAndSessionNotEmpty(t *testing.T) {
store := sessions.NewCookieStore([]byte("keys"))
csrf := NewCSRF("salt", "secret", "", store, func(context *gin.Context) {}, DefaultCSRFTokenGetter)
csrf := NewCSRF("salt", "secret", "", store, func(c *gin.Context, r CSRFErrorReason) {}, DefaultCSRFTokenGetter)
assert.NotEmpty(t, csrf.salt)
assert.NotEmpty(t, csrf.sessionName)
}
func TestCSRF_GetCSRFErrorMessage(t *testing.T) {
items := map[CSRFErrorReason]string{
CSRFErrorNoTokenInSession: "token is not present in session",
CSRFErrorCannotStoreTokenInSession: "cannot store token in session",
CSRFErrorIncorrectTokenType: "incorrect token type",
CSRFErrorEmptyToken: "empty token present in session",
CSRFErrorTokenMismatch: "token mismatch",
99: "unknown error",
}
for reason, message := range items {
assert.Equal(t, message, GetCSRFErrorMessage(reason))
}
}
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) {
x.csrf = NewCSRF("salt", "secret", "", store, func(context *gin.Context, r CSRFErrorReason) {
context.AbortWithStatus(900)
}, DefaultCSRFTokenGetter)
}

97
core/doc.go Normal file
View File

@ -0,0 +1,97 @@
// Copyright (c) 2019 RetailDriver LLC
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.
/*
Package core provides different functions like error-reporting, logging, localization, etc. in order to make it easier to create transports.
Usage:
package main
import (
"os"
"fmt"
"html/template"
"github.com/gin-gonic/gin"
"github.com/gobuffalo/packr/v2"
"github.com/retailcrm/mg-transport-core/core"
)
func main() {
app := core.New()
app.Config = core.NewConfig("config.yml")
app.DefaultError = "unknown_error"
app.TranslationsPath = "./translations"
app.ConfigureRouter(func(engine *gin.Engine) {
engine.Static("/static", "./static")
engine.HTMLRender = app.CreateRenderer(
func(renderer *core.Renderer) {
// insert templates here. Example:
r.Push("home", "templates/layout.html", "templates/home.html")
},
template.FuncMap{},
)
})
if err := app.Prepare().Run(); err != nil {
fmt.Printf("Fatal error: %s", err.Error())
os.Exit(1)
}
}
Resource embedding
packr can be used to provide resource embedding, see:
https://github.com/gobuffalo/packr/tree/master/v2
In order to use packr you must follow instruction, and provide boxes with templates, translations and assets to library.
You can find instruction here:
https://github.com/gobuffalo/packr/tree/master/v2#library-installation
Example of usage:
package main
import (
"os"
"fmt"
"html/template"
"github.com/gin-gonic/gin"
"github.com/gobuffalo/packr/v2"
"github.com/retailcrm/mg-transport-core/core"
)
func main() {
static := packr.New("assets", "./static")
templates := packr.New("templates", "./templates")
translations := packr.New("translations", "./translate")
app := core.New()
app.Config = core.NewConfig("config.yml")
app.DefaultError = "unknown_error"
app.TranslationsBox = translations
app.ConfigureRouter(func(engine *gin.Engine) {
engine.StaticFS("/static", static)
engine.HTMLRender = app.CreateRendererFS(
templates,
func(renderer *core.Renderer) {
// insert templates here. Example:
r.Push("home", "layout.html", "home.html")
},
template.FuncMap{},
)
})
if err := app.Prepare().Run(); err != nil {
fmt.Printf("Fatal error: %s", err.Error())
os.Exit(1)
}
}
Migration generator
This library contains helper tool for transports. You can install it via go:
$ go get -u github.com/retailcrm/mg-transport-core/cmd/transport-core-tool
Currently, it only can generate new migrations for your transport.
*/
package core

View File

@ -92,7 +92,7 @@ func (e *Engine) Prepare() *Engine {
return e
}
// templateFuncMap combines func map for templates
// TemplateFuncMap combines func map for templates
func (e *Engine) TemplateFuncMap(functions template.FuncMap) template.FuncMap {
funcMap := e.LocalizationFuncMap()
@ -100,6 +100,10 @@ func (e *Engine) TemplateFuncMap(functions template.FuncMap) template.FuncMap {
funcMap[name] = fn
}
funcMap["version"] = func() string {
return e.Config.GetVersion()
}
return funcMap
}
@ -133,7 +137,12 @@ func (e *Engine) Router() *gin.Engine {
// BuildHTTPClient builds HTTP client with provided configuration
func (e *Engine) BuildHTTPClient(replaceDefault ...bool) *Engine {
if e.Config.GetHTTPClientConfig() != nil {
if client, err := NewHTTPClientBuilder().FromEngine(e).Build(replaceDefault...); err != nil {
client, err := NewHTTPClientBuilder().
WithLogger(e.Logger).
SetLogging(e.Config.IsDebug()).
FromEngine(e).Build(replaceDefault...)
if err != nil {
panic(err)
} else {
e.httpClient = client
@ -156,9 +165,9 @@ func (e *Engine) SetHTTPClient(client *http.Client) *Engine {
func (e *Engine) HTTPClient() *http.Client {
if e.httpClient == nil {
return http.DefaultClient
} else {
return e.httpClient
}
return e.httpClient
}
// WithCookieSessions generates new CookieStore with optional key length.
@ -174,7 +183,7 @@ func (e *Engine) WithCookieSessions(keyLength ...int) *Engine {
return e
}
// WithCookieSessions generates new FilesystemStore with optional key length.
// WithFilesystemSessions generates new FilesystemStore with optional key length.
// Default key length is 32 bytes.
func (e *Engine) WithFilesystemSessions(path string, keyLength ...int) *Engine {
length := 32
@ -190,7 +199,7 @@ func (e *Engine) WithFilesystemSessions(path string, keyLength ...int) *Engine {
// 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 {
func (e *Engine) InitCSRF(secret string, abortFunc CSRFAbortFunc, getter CSRFTokenGetter) *Engine {
if e.Sessions == nil {
panic("engine.Sessions must be initialized first")
}

View File

@ -154,7 +154,7 @@ func (e *EngineTest) Test_BuildHTTPClient() {
e.engine.Config = &Config{
HTTPClientConfig: &HTTPClientConfig{
Timeout: 30,
SSLVerification: true,
SSLVerification: boolPtr(true),
},
}
e.engine.BuildHTTPClient()
@ -204,7 +204,7 @@ func (e *EngineTest) Test_InitCSRF_Fail() {
e.engine.csrf = nil
e.engine.Sessions = nil
e.engine.InitCSRF("test", func(context *gin.Context) {}, DefaultCSRFTokenGetter)
e.engine.InitCSRF("test", func(context *gin.Context, r CSRFErrorReason) {}, DefaultCSRFTokenGetter)
assert.Nil(e.T(), e.engine.csrf)
}
@ -215,7 +215,7 @@ func (e *EngineTest) Test_InitCSRF() {
e.engine.csrf = nil
e.engine.WithCookieSessions(4)
e.engine.InitCSRF("test", func(context *gin.Context) {}, DefaultCSRFTokenGetter)
e.engine.InitCSRF("test", func(context *gin.Context, r CSRFErrorReason) {}, DefaultCSRFTokenGetter)
assert.NotNil(e.T(), e.engine.csrf)
}
@ -235,7 +235,7 @@ func (e *EngineTest) Test_VerifyCSRFMiddleware() {
e.engine.csrf = nil
e.engine.WithCookieSessions(4)
e.engine.InitCSRF("test", func(context *gin.Context) {}, DefaultCSRFTokenGetter)
e.engine.InitCSRF("test", func(context *gin.Context, r CSRFErrorReason) {}, DefaultCSRFTokenGetter)
e.engine.VerifyCSRFMiddleware(DefaultIgnoredMethods)
}
@ -255,7 +255,7 @@ func (e *EngineTest) Test_GenerateCSRFMiddleware() {
e.engine.csrf = nil
e.engine.WithCookieSessions(4)
e.engine.InitCSRF("test", func(context *gin.Context) {}, DefaultCSRFTokenGetter)
e.engine.InitCSRF("test", func(context *gin.Context, r CSRFErrorReason) {}, DefaultCSRFTokenGetter)
e.engine.GenerateCSRFMiddleware()
}
@ -284,7 +284,7 @@ func (e *EngineTest) Test_GetCSRFToken() {
e.engine.csrf = nil
e.engine.WithCookieSessions(4)
e.engine.InitCSRF("test", func(context *gin.Context) {}, DefaultCSRFTokenGetter)
e.engine.InitCSRF("test", func(context *gin.Context, r CSRFErrorReason) {}, DefaultCSRFTokenGetter)
assert.NotEmpty(e.T(), e.engine.GetCSRFToken(c))
assert.Equal(e.T(), "token", e.engine.GetCSRFToken(c))
}
@ -300,3 +300,8 @@ func (e *EngineTest) Test_Run_Fail() {
func TestEngine_Suite(t *testing.T) {
suite.Run(t, new(EngineTest))
}
func boolPtr(val bool) *bool {
b := val
return &b
}

View File

@ -28,4 +28,4 @@ func BadRequest(error string) (int, interface{}) {
// context.JSON(BadRequest("invalid data"))
func InternalServerError(error string) (int, interface{}) {
return GetErrorResponse(http.StatusInternalServerError, error)
}
}

View File

@ -8,13 +8,15 @@ import (
"net/http"
"time"
"github.com/op/go-logging"
"github.com/pkg/errors"
)
var (
DefaultClient = http.DefaultClient
DefaultTransport = http.DefaultTransport
)
// DefaultClient stores original http.DefaultClient
var DefaultClient = http.DefaultClient
// DefaultTransport stores original http.DefaultTransport
var DefaultTransport = http.DefaultTransport
// HTTPClientBuilder builds http client with mocks (if necessary) and timeout.
// Example:
@ -44,7 +46,7 @@ type HTTPClientBuilder struct {
httpClient *http.Client
httpTransport *http.Transport
dialer *net.Dialer
engine *Engine
logger *logging.Logger
built bool
logging bool
timeout time.Duration
@ -67,6 +69,15 @@ func NewHTTPClientBuilder() *HTTPClientBuilder {
}
}
// WithLogger sets provided logger into HTTPClientBuilder
func (b *HTTPClientBuilder) WithLogger(logger *logging.Logger) *HTTPClientBuilder {
if logger != nil {
b.logger = logger
}
return b
}
// SetTimeout sets timeout for http client
func (b *HTTPClientBuilder) SetTimeout(timeout time.Duration) *HTTPClientBuilder {
timeout = timeout * time.Second
@ -104,9 +115,9 @@ func (b *HTTPClientBuilder) SetSSLVerification(enabled bool) *HTTPClientBuilder
return b
}
// EnableLogging enables logging in mocks
func (b *HTTPClientBuilder) EnableLogging() *HTTPClientBuilder {
b.logging = true
// SetLogging enables or disables logging in mocks
func (b *HTTPClientBuilder) SetLogging(flag bool) *HTTPClientBuilder {
b.logging = flag
return b
}
@ -125,15 +136,13 @@ func (b *HTTPClientBuilder) FromConfig(config *HTTPClientConfig) *HTTPClientBuil
b.SetTimeout(config.Timeout)
}
b.SetSSLVerification(config.SSLVerification)
b.SetSSLVerification(config.IsSSLVerificationEnabled())
return b
}
// FromEngine fulfills mock configuration from ConfigInterface inside Engine
func (b *HTTPClientBuilder) FromEngine(engine *Engine) *HTTPClientBuilder {
b.engine = engine
b.logging = engine.Config.IsDebug()
return b.FromConfig(engine.Config.GetHTTPClientConfig())
}
@ -156,10 +165,11 @@ func (b *HTTPClientBuilder) parseAddress() error {
if host, port, err := net.SplitHostPort(b.mockAddress); err == nil {
b.mockHost = host
b.mockPort = port
return nil
} else {
return errors.Errorf("cannot split host and port: %s", err.Error())
}
return nil
}
// buildMocks builds mocks for http client
@ -177,21 +187,26 @@ func (b *HTTPClientBuilder) buildMocks() error {
}
b.httpTransport.DialContext = func(ctx context.Context, network, addr string) (conn net.Conn, e error) {
if host, port, err := net.SplitHostPort(addr); err != nil {
var (
host string
port string
err error
)
if host, port, err = net.SplitHostPort(addr); err != nil {
return b.dialer.DialContext(ctx, network, addr)
} else {
for _, mock := range b.mockedDomains {
if mock == host {
oldAddr := addr
}
if b.mockPort == "0" {
addr = net.JoinHostPort(b.mockHost, port)
} else {
addr = net.JoinHostPort(b.mockHost, b.mockPort)
}
for _, mock := range b.mockedDomains {
if mock == host {
oldAddr := addr
b.logf("Mocking \"%s\" with \"%s\"\n", oldAddr, addr)
if b.mockPort == "0" {
addr = net.JoinHostPort(b.mockHost, port)
} else {
addr = net.JoinHostPort(b.mockHost, b.mockPort)
}
b.logf("Mocking \"%s\" with \"%s\"\n", oldAddr, addr)
}
}
@ -205,8 +220,8 @@ func (b *HTTPClientBuilder) buildMocks() error {
// logf prints logs via Engine or via fmt.Printf
func (b *HTTPClientBuilder) logf(format string, args ...interface{}) {
if b.logging {
if b.engine != nil && b.engine.Logger != nil {
b.engine.Logger.Infof(format, args...)
if b.logger != nil {
b.logger.Infof(format, args...)
} else {
fmt.Printf(format, args...)
}

View File

@ -1,11 +1,21 @@
package core
import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"strings"
"testing"
"time"
"github.com/op/go-logging"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -25,6 +35,14 @@ func (t *HTTPClientBuilderTest) Test_SetTimeout() {
assert.Equal(t.T(), 90*time.Second, t.builder.httpClient.Timeout)
}
func (t *HTTPClientBuilderTest) Test_SetLogging() {
t.builder.SetLogging(true)
assert.True(t.T(), t.builder.logging)
t.builder.SetLogging(false)
assert.False(t.T(), t.builder.logging)
}
func (t *HTTPClientBuilderTest) Test_SetMockAddress() {
addr := "mock.local:3004"
t.builder.SetMockAddress(addr)
@ -56,16 +74,23 @@ func (t *HTTPClientBuilderTest) Test_SetSSLVerification() {
assert.True(t.T(), t.builder.httpTransport.TLSClientConfig.InsecureSkipVerify)
}
func (t *HTTPClientBuilderTest) Test_FromConfigNil() {
defer func() {
assert.Nil(t.T(), recover())
}()
t.builder.FromConfig(nil)
}
func (t *HTTPClientBuilderTest) Test_FromConfig() {
config := &HTTPClientConfig{
SSLVerification: true,
SSLVerification: boolPtr(true),
MockAddress: "anothermock.local:3004",
MockedDomains: []string{"example.gov"},
Timeout: 60,
}
t.builder.FromConfig(config)
assert.Equal(t.T(), !config.SSLVerification, t.builder.httpTransport.TLSClientConfig.InsecureSkipVerify)
assert.Equal(t.T(), !config.IsSSLVerificationEnabled(), t.builder.httpTransport.TLSClientConfig.InsecureSkipVerify)
assert.Equal(t.T(), config.MockAddress, t.builder.mockAddress)
assert.Equal(t.T(), config.MockedDomains[0], t.builder.mockedDomains[0])
assert.Equal(t.T(), config.Timeout*time.Second, t.builder.timeout)
@ -76,7 +101,7 @@ func (t *HTTPClientBuilderTest) Test_FromEngine() {
engine := &Engine{
Config: Config{
HTTPClientConfig: &HTTPClientConfig{
SSLVerification: true,
SSLVerification: boolPtr(true),
MockAddress: "anothermock.local:3004",
MockedDomains: []string{"example.gov"},
},
@ -85,7 +110,7 @@ func (t *HTTPClientBuilderTest) Test_FromEngine() {
}
t.builder.FromEngine(engine)
assert.NotNil(t.T(), engine, t.builder.engine)
assert.Equal(t.T(), engine.Config.GetHTTPClientConfig().MockAddress, t.builder.mockAddress)
}
func (t *HTTPClientBuilderTest) Test_buildDialer() {
@ -102,6 +127,18 @@ func (t *HTTPClientBuilderTest) Test_buildMocks() {
assert.NoError(t.T(), t.builder.buildMocks())
}
func (t *HTTPClientBuilderTest) Test_WithLogger() {
logger := NewLogger("telegram", logging.ERROR, DefaultLogFormatter())
builder := NewHTTPClientBuilder()
require.Nil(t.T(), builder.logger)
builder.WithLogger(nil)
assert.Nil(t.T(), builder.logger)
builder.WithLogger(logger)
assert.NotNil(t.T(), builder.logger)
}
func (t *HTTPClientBuilderTest) Test_logf() {
defer func() {
assert.Nil(t.T(), recover())
@ -130,6 +167,151 @@ func (t *HTTPClientBuilderTest) Test_RestoreDefault() {
assert.Equal(t.T(), http.DefaultTransport, DefaultTransport)
}
// Test_ClientMocksWorking is supposed to test mocking functionality of generated client.
// Using real HTTP requests and server doesn't look good obviously, but mocking something to check how
// mocks are working doesn't look that good too.
// In this case we are trying to test this in just-like-real environment, without mocks or etc.
// Fake "real" environment is easiest way to do this.
// You know how to make this test better? Let us know.
func (t *HTTPClientBuilderTest) Test_ClientMocksWorking() {
mockProto := "https://"
mockServerAddr := getOutboundIP().String() + ":27717"
mockDomainAddr := "example.com"
certFileData := `-----BEGIN CERTIFICATE-----
MIIC+TCCAeGgAwIBAgIJAPb0Qm9aV+93MA0GCSqGSIb3DQEBBQUAMBMxETAPBgNV
BAMMCGFwaV9tb2NrMB4XDTE5MTAxNzE0NDMzMloXDTI5MTAxNDE0NDMzMlowEzER
MA8GA1UEAwwIYXBpX21vY2swggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQCtxUg6aUBcZLjEYEgzzE+B8wAsyK6dT9fbbDfO/5n9PhHjxQLnjTYrs9xAAl7R
Caj8nUg0RzfX38jD1TGwkFrC1u1pjOf74OVMXsw2xa7gmZlnJeeL+QozXQX1rDPk
wO5QqomKAIwM3ab+i6k1tLBfDIOHGLhTEFQZ9cmKVuNdTlkeqBh+bKduRIr7DYhQ
Dsci/PacGJlt0W+r2YuRmm1KGearbS4HabPOkC0c6KbVD+bUyF+F7DxtJ8vg7O4W
SWAXlwoEHonIyJG8H7TQxL2g5w/4UQhp6awAzFNLhqtf/6pw6gPfI9joS+Z/Pxvz
Bry41s4LV7KP8v0GqRK3KH2rAgMBAAGjUDBOMB0GA1UdDgQWBBTEiHl+R8N0kLwH
1RTsKYe8joAwxzAfBgNVHSMEGDAWgBTEiHl+R8N0kLwH1RTsKYe8joAwxzAMBgNV
HRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAo/XBUFlrl5yuQ+5eFQjMmbn4T
9qVg4NVRy5qnTKzcOR21X3fB394TvfBiGstW0YQCOCtNakv94UAbZozYktQaYOtP
x5porosgI2RgOTTwmiYOcYQTS2650jYydHhK16Gu2b3UKernO16mAWXNDWfvS2bk
1ufbRWpuUXs0SIR6e/mgSwrBMBvq6fan4EVdEkx4Catjna15DgmBGRL215t5K4aq
nAI2GL2ACEdOCyRvgq16AycJJYU7nYQ+t9aveefx0uhbYYIVeYub9NxmCfD3MojI
saG/63vo0ng851n90DVoMRWx9n1CjEvss/vvz+jXIl9njaCtizN3WUf1NwUB
-----END CERTIFICATE-----`
keyFileData := `-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEArcVIOmlAXGS4xGBIM8xPgfMALMiunU/X22w3zv+Z/T4R48UC
5402K7PcQAJe0Qmo/J1INEc319/Iw9UxsJBawtbtaYzn++DlTF7MNsWu4JmZZyXn
i/kKM10F9awz5MDuUKqJigCMDN2m/oupNbSwXwyDhxi4UxBUGfXJilbjXU5ZHqgY
fmynbkSK+w2IUA7HIvz2nBiZbdFvq9mLkZptShnmq20uB2mzzpAtHOim1Q/m1Mhf
hew8bSfL4OzuFklgF5cKBB6JyMiRvB+00MS9oOcP+FEIaemsAMxTS4arX/+qcOoD
3yPY6Evmfz8b8wa8uNbOC1eyj/L9BqkStyh9qwIDAQABAoIBAG/jNUyW9KAJIocf
T546kX8kzhoH5ZbZEC3ykkSwg6Bx1OcJtYMOg7DEEK8OV6rVQ3/Ubedra+ON7iFa
JrJ/YFFQPnHWDpE6D6qK54bk5mMrw4CNAXg5FH5aCTpUdN139HrwM7st+v9VwO7p
UjyIaX/p+M6F9jlVrDFC91Ah/ifW+DzWS27IsQ/396HTvTUNgD/Lj9pKtC6eW3AQ
aTpLk/cVrkxxT1qc4rx8NRPe9/670CQDQYMvaNJtC54kRUzH87mn06IEy5fuLeI4
WwYgEIkh1YjQsJhg3Z788JGKR5iKuzjXpw7aD6QESISoWPW1Dh/5PJO1zkKxZril
xUepcgECgYEA5MLP4oCiuloShhaMc+UMYgH8YuglXegGbF5/iG9d7kgaiz4KiXuD
ObwaHegLDFNSxtVSiH6AsXErzd7HHojzTm6B3O5qtBl+QgpW9hiTyJnCxkdzxK7y
cVan8Jp3g/ojcYBY+QgZlR81QHQIwSUrAbMC0fjYlrKb5zJfExgFv4ECgYEAwnY7
MUoLsHs2eXVOekvb42ReCq7HKVz8TETlSEPN4B5XzOKCnXxCUUvF5iWjWj0XAMSU
yF6LJmmfFmOaVHpXDHhF9MmNKxYiIISS+ZM6B2DXP+hS/DXXuy5hGmYO6p4JC08d
qulIIR7JSJmHyI+5Ref40WTObJvJQDXi2p5a0ysCgYBjdONG2aBmHrUBARqtZH7e
uXhOVBmy2ya3xNnzql+PMl//+8g+/6kM1+AO8oyjHjLV6XcJit5OxyJBTkMJ3obR
qa/iKvHPPWosMiyesA7IXzlUVUpaz6juZ7t6Gt4tTfpM5X1JQCFHORtA23HW717k
TTzDp0obMqofeUHmnkIZgQKBgFBaGktblUjvIKs/VZYjElD7gABaB+GHkpjRPwyF
N+SLpSv7zIzWc3C0Jqnak40OARtIH1JL/qN4sUvHDFYr1xxH9mAXiEVtd9yH61NF
Co1R7p9xmBivBt1JZMZLtY4sjwAlSNT+X9ePqQxepESzXpMMLzwWs1UdaiMmIP7E
wDLRAoGAM/Cz7B+J7KcKI8VqXAsX5nMklIKvACScKU6oIPwNYXyxUfm7jsNaBqXG
weywTxDl/OD5ybNkZIRKsIXciFYG1VCGO2HNGN9qJcV+nJ63kyrIBauwUkuEhiN5
uf/TQPpjrGW5nxOf94qn6FzV2WSype9BcM5MD7z7rk202Fs7Zqc=
-----END RSA PRIVATE KEY-----`
certFile, err := ioutil.TempFile("/tmp", "cert_")
require.NoError(t.T(), err, "cannot create temp cert file")
keyFile, err := ioutil.TempFile("/tmp", "key_")
require.NoError(t.T(), err, "cannot create temp key file")
_, err = certFile.WriteString(certFileData)
require.NoError(t.T(), err, "cannot write temp cert file")
_, err = keyFile.WriteString(keyFileData)
require.NoError(t.T(), err, "cannot write temp key file")
require.NoError(t.T(),
ErrorCollector(certFile.Sync(), certFile.Close()), "cannot sync and close temp cert file")
require.NoError(t.T(),
ErrorCollector(keyFile.Sync(), keyFile.Close()), "cannot sync and close temp key file")
mux := &http.ServeMux{}
srv := &http.Server{Addr: mockServerAddr, Handler: mux}
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
_, _ = io.WriteString(w, "ok")
})
testSkipChan := make(chan error, 1)
go func(skip chan error) {
if err := srv.ListenAndServeTLS(certFile.Name(), keyFile.Name()); err != nil && err != http.ErrServerClosed {
skip <- fmt.Errorf("skipping test because server won't start: %s", err.Error())
}
}(testSkipChan)
select {
case errStartup := <-testSkipChan:
t.T().Skip(errStartup)
return
case <-time.After(time.Second):
t.T().Log("test server started")
}
defer func() {
if err := srv.Shutdown(context.TODO()); err != nil {
t.T().Log("warning > cannot shutdown server gracefully: ", err)
} else {
t.T().Log("test server stopped")
}
if err := os.Remove(certFile.Name()); err != nil {
t.T().Log("warning > cannot remove temp cert file properly: ", err)
}
if err := os.Remove(keyFile.Name()); err != nil {
t.T().Log("warning > cannot remove temp key file properly: ", err)
}
}()
client, err := NewHTTPClientBuilder().
SetLogging(true).
SetMockAddress(mockServerAddr).
SetMockedDomains([]string{mockDomainAddr}).
SetTimeout(time.Second).
SetSSLVerification(false).
Build()
require.NoError(t.T(), err, "cannot build client")
resp, err := client.Get(mockProto + mockDomainAddr)
if err != nil && strings.Contains(err.Error(), "connection refused") {
t.T().Skip("connection refused - skipping test: ", err)
}
require.NoError(t.T(), err, "error while making request")
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
require.NoError(t.T(), err, "error while reading body")
assert.Equal(t.T(), http.StatusCreated, resp.StatusCode, "invalid status code")
assert.Equal(t.T(), "ok", string(data), "invalid body contents")
}
// taken from https://stackoverflow.com/questions/23558425/how-do-i-get-the-local-ip-address-in-go
func getOutboundIP() net.IP {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP
}
func Test_HTTPClientBuilder(t *testing.T) {
suite.Run(t, new(HTTPClientBuilderTest))
}

View File

@ -145,9 +145,9 @@ func (l *Localizer) loadFromFS() error {
if err != nil {
return err
} else {
return nil
}
return nil
}
// SetLocale will change language for current localizer

View File

@ -8,7 +8,7 @@ import (
// NewLogger will create new logger with specified formatter.
// Usage:
// logger := NewLogger(config, DefaultLogFormatter())
// logger := NewLogger("telegram", logging.ERROR, DefaultLogFormatter())
func NewLogger(transportCode string, logLevel logging.Level, logFormat logging.Formatter) *logging.Logger {
logger := logging.MustGetLogger(transportCode)
logBackend := logging.NewLogBackend(os.Stdout, "", 0)

View File

@ -54,7 +54,7 @@ func (m *Migrate) Add(migration *gormigrate.Migration) {
m.migrations[migration.ID] = migration
}
// SetORM to migrate
// SetDB to migrate
func (m *Migrate) SetDB(db *gorm.DB) *Migrate {
m.db = db
return m
@ -83,15 +83,15 @@ func (m *Migrate) Rollback() error {
return errors.New("abnormal termination: first migration is nil")
}
if err := m.GORMigrate.RollbackTo(m.first.ID); err == nil {
if err := m.GORMigrate.RollbackMigration(m.first); err == nil {
return nil
} else {
return err
}
} else {
if err := m.GORMigrate.RollbackTo(m.first.ID); err != nil {
return err
}
if err := m.GORMigrate.RollbackMigration(m.first); err != nil {
return err
}
return nil
}
// MigrateTo specified version
@ -183,12 +183,12 @@ func (m *Migrate) Current() string {
return "0"
}
if err := m.db.Last(&migrationInfo).Error; err == nil {
return migrationInfo.ID
} else {
if err := m.db.Last(&migrationInfo).Error; err != nil {
fmt.Printf("warning => cannot fetch migration version: %s\n", err.Error())
return "0"
}
return migrationInfo.ID
}
// NextFrom returns next version from passed version
@ -197,9 +197,9 @@ func (m *Migrate) NextFrom(version string) (string, error) {
if ver == version {
if key < (len(m.versions) - 1) {
return m.versions[key+1], nil
} else {
return "", errors.New("this is last migration")
}
return "", errors.New("this is last migration")
}
}
@ -212,9 +212,9 @@ func (m *Migrate) PreviousFrom(version string) (string, error) {
if ver == version {
if key > 0 {
return m.versions[key-1], nil
} else {
return "0", nil
}
return "0", nil
}
}

View File

@ -54,7 +54,7 @@ func (m *MigrateTest) RefreshMigrate() {
}
}
func (m *MigrateTest) Migration_TestModelFirst() *gormigrate.Migration {
func (m *MigrateTest) MigrationTestModelFirst() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "1",
Migrate: func(db *gorm.DB) error {
@ -66,7 +66,7 @@ func (m *MigrateTest) Migration_TestModelFirst() *gormigrate.Migration {
}
}
func (m *MigrateTest) Migration_TestModelSecond() *gormigrate.Migration {
func (m *MigrateTest) MigrationTestModelSecond() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "2",
Migrate: func(db *gorm.DB) error {
@ -81,7 +81,7 @@ func (m *MigrateTest) Migration_TestModelSecond() *gormigrate.Migration {
func (m *MigrateTest) Test_Add() {
m.RefreshMigrate()
m.Migrate.Add(nil)
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.MigrationTestModelFirst())
assert.Equal(m.T(), 1, len(m.Migrate.migrations))
i, ok := m.Migrate.migrations["1"]
@ -112,7 +112,7 @@ func (m *MigrateTest) Test_prepareMigrations_AlreadyPrepared() {
func (m *MigrateTest) Test_prepareMigrations_OK() {
m.RefreshMigrate()
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.MigrationTestModelFirst())
err := m.Migrate.prepareMigrations()
require.NoError(m.T(), err)
@ -124,7 +124,7 @@ func (m *MigrateTest) Test_prepareMigrations_OK() {
func (m *MigrateTest) Test_Migrate_Fail_NilDB() {
m.RefreshMigrate()
m.Migrate.SetDB(nil)
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.MigrationTestModelFirst())
err := m.Migrate.Migrate()
@ -134,7 +134,7 @@ func (m *MigrateTest) Test_Migrate_Fail_NilDB() {
func (m *MigrateTest) Test_Migrate_Success_NoMigrations() {
m.RefreshMigrate()
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.MigrationTestModelFirst())
m.mock.ExpectBegin()
m.mock.
@ -157,7 +157,7 @@ func (m *MigrateTest) Test_Migrate_Success_NoMigrations() {
func (m *MigrateTest) Test_Migrate_Success() {
m.RefreshMigrate()
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.MigrationTestModelFirst())
m.mock.ExpectBegin()
m.mock.
@ -188,7 +188,7 @@ func (m *MigrateTest) Test_Migrate_Success() {
func (m *MigrateTest) Test_Rollback_Fail_NilDB() {
m.RefreshMigrate()
m.Migrate.SetDB(nil)
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.MigrationTestModelFirst())
err := m.Migrate.Rollback()
@ -198,7 +198,7 @@ func (m *MigrateTest) Test_Rollback_Fail_NilDB() {
func (m *MigrateTest) Test_Rollback_Fail_NoMigrations() {
m.RefreshMigrate()
m.Migrate.first = m.Migration_TestModelFirst()
m.Migrate.first = m.MigrationTestModelFirst()
err := m.Migrate.Rollback()
@ -208,7 +208,7 @@ func (m *MigrateTest) Test_Rollback_Fail_NoMigrations() {
func (m *MigrateTest) Test_Rollback_Fail_NoFirstMigration() {
m.RefreshMigrate()
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.MigrationTestModelFirst())
m.Migrate.first = nil
err := m.Migrate.Rollback()
@ -229,7 +229,7 @@ func (m *MigrateTest) Test_MigrateTo_Fail_NilDB() {
func (m *MigrateTest) Test_MigrateTo_DoNothing() {
m.RefreshMigrate()
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.MigrationTestModelFirst())
m.mock.
ExpectExec(regexp.QuoteMeta(`CREATE TABLE "migrations" ("id" varchar(255) , PRIMARY KEY ("id"))`)).
@ -247,7 +247,7 @@ func (m *MigrateTest) Test_MigrateTo_DoNothing() {
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
m.mock.ExpectCommit()
err := m.Migrate.MigrateTo(m.Migration_TestModelFirst().ID)
err := m.Migrate.MigrateTo(m.MigrationTestModelFirst().ID)
assert.NoError(m.T(), err)
assert.NoError(m.T(), m.mock.ExpectationsWereMet())
@ -255,7 +255,7 @@ func (m *MigrateTest) Test_MigrateTo_DoNothing() {
func (m *MigrateTest) Test_MigrateTo() {
m.RefreshMigrate()
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.MigrationTestModelFirst())
m.mock.
ExpectExec(regexp.QuoteMeta(`CREATE TABLE "migrations" ("id" varchar(255) , PRIMARY KEY ("id"))`)).
@ -280,7 +280,7 @@ func (m *MigrateTest) Test_MigrateTo() {
WillReturnResult(sqlmock.NewResult(1, 1))
m.mock.ExpectCommit()
err := m.Migrate.MigrateTo(m.Migration_TestModelFirst().ID)
err := m.Migrate.MigrateTo(m.MigrationTestModelFirst().ID)
assert.NoError(m.T(), err)
assert.NoError(m.T(), m.mock.ExpectationsWereMet())
@ -288,13 +288,13 @@ func (m *MigrateTest) Test_MigrateTo() {
func (m *MigrateTest) Test_RollbackTo() {
m.RefreshMigrate()
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.Migration_TestModelSecond())
m.Migrate.Add(m.MigrationTestModelFirst())
m.Migrate.Add(m.MigrationTestModelSecond())
m.mock.ExpectBegin()
m.mock.ExpectCommit()
err := m.Migrate.RollbackTo(m.Migration_TestModelSecond().ID)
err := m.Migrate.RollbackTo(m.MigrationTestModelSecond().ID)
assert.NoError(m.T(), err)
assert.NoError(m.T(), m.mock.ExpectationsWereMet())
@ -302,8 +302,8 @@ func (m *MigrateTest) Test_RollbackTo() {
func (m *MigrateTest) Test_MigrateNextTo() {
m.RefreshMigrate()
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.Migration_TestModelSecond())
m.Migrate.Add(m.MigrationTestModelFirst())
m.Migrate.Add(m.MigrationTestModelSecond())
m.mock.
ExpectExec(regexp.QuoteMeta(`CREATE TABLE "migrations" ("id" varchar(255) , PRIMARY KEY ("id"))`)).
@ -332,7 +332,7 @@ func (m *MigrateTest) Test_MigrateNextTo() {
WillReturnResult(sqlmock.NewResult(1, 1))
m.mock.ExpectCommit()
err := m.Migrate.MigrateNextTo(m.Migration_TestModelFirst().ID)
err := m.Migrate.MigrateNextTo(m.MigrationTestModelFirst().ID)
assert.NoError(m.T(), err)
assert.NoError(m.T(), m.mock.ExpectationsWereMet())
@ -340,14 +340,14 @@ func (m *MigrateTest) Test_MigrateNextTo() {
func (m *MigrateTest) Test_MigratePreviousTo() {
m.RefreshMigrate()
m.Migrate.Add(m.Migration_TestModelFirst())
m.Migrate.Add(m.Migration_TestModelSecond())
m.Migrate.Add(m.MigrationTestModelFirst())
m.Migrate.Add(m.MigrationTestModelSecond())
m.mock.
ExpectExec(regexp.QuoteMeta(`CREATE TABLE "migrations" ("id" varchar(255) , PRIMARY KEY ("id"))`)).
WillReturnResult(sqlmock.NewResult(1, 1))
err := m.Migrate.MigratePreviousTo(m.Migration_TestModelSecond().ID)
err := m.Migrate.MigratePreviousTo(m.MigrationTestModelSecond().ID)
assert.Error(m.T(), err)
assert.NoError(m.T(), m.mock.ExpectationsWereMet())

View File

@ -35,6 +35,7 @@ type NewMigrationCommand struct {
Directory string `short:"d" long:"directory" default:"./migrations" description:"Directory where migration will be created"`
}
// FileExists returns true if provided file exist and it's not directory
func (x *NewMigrationCommand) FileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
@ -43,6 +44,7 @@ func (x *NewMigrationCommand) FileExists(filename string) bool {
return !info.IsDir()
}
// Execute migration generator command
func (x *NewMigrationCommand) Execute(args []string) error {
version := strconv.FormatInt(time.Now().Unix(), 10)
directory := path.Clean(x.Directory)

View File

@ -42,7 +42,7 @@ func (s *MigrationGeneratorSuite) Test_Execute() {
}
for _, f := range files {
if strings.Index(f.Name(), "_app.go") != -1 {
if strings.Contains(f.Name(), "_app.go") {
found = true
assert.NoError(s.T(), os.Remove(path.Join(s.command.Directory, f.Name())))
}

View File

@ -25,7 +25,7 @@ func NewStaticRenderer(funcMap template.FuncMap) Renderer {
return newRendererWithMultitemplate(funcMap, multitemplate.New())
}
// NewStaticRenderer is a Renderer constructor with multitemplate.DynamicRender
// NewDynamicRenderer is a Renderer constructor with multitemplate.DynamicRender
func NewDynamicRenderer(funcMap template.FuncMap) Renderer {
return newRendererWithMultitemplate(funcMap, multitemplate.NewDynamic())
}
@ -47,9 +47,9 @@ func (r *Renderer) Push(name string, files ...string) *template.Template {
if r.TemplatesBox == nil {
return r.storeTemplate(name, r.AddFromFilesFuncs(name, r.FuncMap, files...))
} else {
return r.storeTemplate(name, r.addFromBox(name, r.FuncMap, files...))
}
return r.storeTemplate(name, r.addFromBox(name, r.FuncMap, files...))
}
// addFromBox adds embedded template
@ -67,7 +67,7 @@ func (r *Renderer) addFromBox(name string, funcMap template.FuncMap, files ...st
// storeTemplate stores built template if multitemplate.DynamicRender is used.
// Dynamic render doesn't store templates - it stores builders, that's why we can't just extract them.
// It possibly can cause data inconsistency in developer enviroments where return value from Renderer.Push is used.
// It possibly can cause data inconsistency in developer environments where return value from Renderer.Push is used.
func (r *Renderer) storeTemplate(name string, tpl *template.Template) *template.Template {
if _, ok := r.Renderer.(multitemplate.DynamicRender); ok {
r.alreadyAdded[name] = tpl

View File

@ -23,7 +23,7 @@ type TranslationsExtractor struct {
TranslationsPath string
}
// TranslationsExtractor constructor. Use "translate.{}.yml" as template if your translations are named like "translate.en.yml"
// NewTranslationsExtractor constructor. Use "translate.{}.yml" as template if your translations are named like "translate.en.yml"
func NewTranslationsExtractor(fileNameTemplate string) *TranslationsExtractor {
return &TranslationsExtractor{fileNameTemplate: fileNameTemplate}
}
@ -32,45 +32,59 @@ func NewTranslationsExtractor(fileNameTemplate string) *TranslationsExtractor {
func (t *TranslationsExtractor) unmarshalToMap(in []byte) (map[string]interface{}, error) {
var dataMap map[string]interface{}
if err := yaml.Unmarshal(in, &dataMap); err == nil {
return dataMap, nil
} else {
if err := yaml.Unmarshal(in, &dataMap); err != nil {
return dataMap, err
}
return dataMap, nil
}
// loadYAMLBox loads YAML from box
func (t *TranslationsExtractor) loadYAMLBox(fileName string) (map[string]interface{}, error) {
var dataMap map[string]interface{}
var (
dataMap map[string]interface{}
data []byte
err error
)
if data, err := t.TranslationsBox.Find(fileName); err == nil {
return t.unmarshalToMap(data)
} else {
if data, err = t.TranslationsBox.Find(fileName); err != nil {
return dataMap, err
}
return t.unmarshalToMap(data)
}
// loadYAMLFile loads YAML from file
func (t *TranslationsExtractor) loadYAMLFile(fileName string) (map[string]interface{}, error) {
var dataMap map[string]interface{}
var (
dataMap map[string]interface{}
info os.FileInfo
err error
)
if info, err := os.Stat(fileName); err == nil {
if info, err = os.Stat(fileName); err == nil {
if !info.IsDir() {
if path, err := filepath.Abs(fileName); err == nil {
if source, err := ioutil.ReadFile(path); err == nil {
return t.unmarshalToMap(source)
} else {
return dataMap, err
}
} else {
var (
path string
source []byte
err error
)
if path, err = filepath.Abs(fileName); err != nil {
return dataMap, err
}
} else {
return dataMap, errors.New("directory provided instead of file")
if source, err = ioutil.ReadFile(path); err != nil {
return dataMap, err
}
return t.unmarshalToMap(source)
}
} else {
return dataMap, err
return dataMap, errors.New("directory provided instead of file")
}
return dataMap, err
}
// loadYAML loads YAML from filesystem or from packr box - depends on what was configured. Can return error.
@ -106,9 +120,13 @@ func (t *TranslationsExtractor) LoadLocale(locale string) (map[string]interface{
// LoadLocaleKeys returns only sorted keys from translation file
func (t *TranslationsExtractor) LoadLocaleKeys(locale string) ([]string, error) {
if data, err := t.LoadLocale(locale); err == nil {
return t.GetMapKeys(data), nil
} else {
var (
data map[string]interface{}
err error
)
if data, err = t.LoadLocale(locale); err != nil {
return []string{}, err
}
return t.GetMapKeys(data), nil
}

View File

@ -38,6 +38,8 @@ func (t *TranslationsExtractorTest) SetupSuite() {
"another": "second",
}
data, _ := yaml.Marshal(translation)
// It's not regular temporary file. Little hack in order to test translations extractor.
// nolint:gosec
errWrite := ioutil.WriteFile("/tmp/translate.en.yml", data, os.ModePerm)
require.NoError(t.T(), errWrite)

View File

@ -1,6 +1,7 @@
package core
import (
// nolint:gosec
"crypto/sha1"
"crypto/sha256"
"encoding/json"
@ -24,9 +25,9 @@ import (
// Utils service object
type Utils struct {
IsDebug bool
TokenCounter uint32
ConfigAWS ConfigAWS
Logger *logging.Logger
TokenCounter uint32
slashRegex *regexp.Regexp
}
@ -127,6 +128,7 @@ func (u *Utils) UploadUserAvatar(url string) (picURLs3 string, err error) {
s := session.Must(session.NewSession(s3Config))
uploader := s3manager.NewUploader(s)
// nolint:gosec
resp, err := http.Get(url)
if err != nil {
return
@ -181,6 +183,7 @@ func GetMGItemData(client *v1.MgClient, url string, caption string) (v1.Item, in
func GetEntitySHA1(v interface{}) (hash string, err error) {
res, _ := json.Marshal(v)
// nolint:gosec
h := sha1.New()
_, err = h.Write(res)
hash = fmt.Sprintf("%x", h.Sum(nil))

View File

@ -1,12 +1,11 @@
package core
import (
"reflect"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
"gopkg.in/go-playground/validator.v9"
)
// init here will register `validatecrmurl` function for gin validator
func init() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
if err := v.RegisterValidation("validatecrmurl", validateCrmURL); err != nil {
@ -15,9 +14,7 @@ func init() {
}
}
func validateCrmURL(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
return regCommandName.Match([]byte(field.Interface().(string)))
// validateCrmURL will validate CRM URL
func validateCrmURL(fl validator.FieldLevel) bool {
return regCommandName.Match([]byte(fl.Field().String()))
}

63
core/validator_test.go Normal file
View File

@ -0,0 +1,63 @@
package core
import (
"testing"
"github.com/gin-gonic/gin/binding"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"gopkg.in/go-playground/validator.v9"
)
type ValidatorSuite struct {
suite.Suite
engine *validator.Validate
}
func Test_Validator(t *testing.T) {
suite.Run(t, new(ValidatorSuite))
}
func (s *ValidatorSuite) SetupSuite() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
s.engine = v
} else {
s.T().Fatal("cannot obtain validation engine")
}
}
func (s *ValidatorSuite) getError(err error) string {
if err == nil {
return ""
}
return err.Error()
}
func (s *ValidatorSuite) Test_ValidationInvalidType() {
assert.IsType(s.T(), &validator.InvalidValidationError{}, s.engine.Struct(nil))
}
func (s *ValidatorSuite) Test_ValidationFails() {
conn := Connection{
Key: "key",
URL: "url",
}
err := s.engine.Struct(conn)
require.IsType(s.T(), validator.ValidationErrors{}, err)
validatorErrors := err.(validator.ValidationErrors)
assert.Equal(
s.T(),
"Key: 'Connection.URL' Error:Field validation for 'URL' failed on the 'validatecrmurl' tag",
validatorErrors.Error())
}
func (s *ValidatorSuite) Test_ValidationSuccess() {
conn := Connection{
Key: "key",
URL: "https://test.retailcrm.pro",
}
err := s.engine.Struct(conn)
assert.NoError(s.T(), err, s.getError(err))
}

17
go.mod
View File

@ -9,20 +9,20 @@ require (
github.com/denisenkom/go-mssqldb v0.0.0-20190830225923-3302f0226fbd // indirect
github.com/getsentry/raven-go v0.2.0
github.com/gin-contrib/multitemplate v0.0.0-20190914010127-bba2ccfe37ec
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.4.0
github.com/gin-gonic/gin v1.5.0
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/gobuffalo/packd v0.3.0
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/json-iterator/go v1.1.8 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/lib/pq v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mattn/go-isatty v0.0.11 // indirect
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect
github.com/nicksnyder/go-i18n/v2 v2.0.2
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
@ -31,12 +31,11 @@ require (
github.com/retailcrm/mg-transport-api-client-go v1.1.32
github.com/rogpeppe/go-internal v1.5.0 // indirect
github.com/stretchr/testify v1.4.0
github.com/ugorji/go v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
golang.org/x/text v0.3.2
gopkg.in/go-playground/validator.v8 v8.18.2
gopkg.in/go-playground/validator.v9 v9.30.2
gopkg.in/gormigrate.v1 v1.6.0
gopkg.in/yaml.v2 v2.2.4
gopkg.in/yaml.v2 v2.2.7
)

30
go.sum
View File

@ -48,8 +48,18 @@ 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.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -114,6 +124,8 @@ github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@ -126,14 +138,21 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
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/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-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -259,9 +278,13 @@ 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/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/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
@ -296,8 +319,11 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/go-playground/validator.v9 v9.30.2 h1:icxYLlYflpazIV3ufMoNB9h9SYMQ37DZ8CTwkU4pnOs=
gopkg.in/go-playground/validator.v9 v9.30.2/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI=
gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
@ -306,6 +332,8 @@ 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.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=