From 677fa11b68322398fc92c402f42ce7781b2d4067 Mon Sep 17 00:00:00 2001 From: eliseeviam Date: Fri, 11 Feb 2022 22:17:50 +0300 Subject: [PATCH 1/4] basedOnRequest strategy implementation --- README-ru.md | 38 +++++++++++ README.md | 38 +++++++++++ examples/mock-based-on-request/cases/do.yaml | 28 ++++++++ examples/mock-based-on-request/func_test.go | 28 ++++++++ examples/mock-based-on-request/main.go | 70 ++++++++++++++++++++ go.mod | 1 + go.sum | 2 + mocks/definition.go | 33 +++++++++ mocks/loader.go | 35 ++++++++++ mocks/reply_strategy.go | 48 ++++++++++++++ 10 files changed, 321 insertions(+) create mode 100755 examples/mock-based-on-request/cases/do.yaml create mode 100755 examples/mock-based-on-request/func_test.go create mode 100755 examples/mock-based-on-request/main.go diff --git a/README-ru.md b/README-ru.md index 6ae2700..d1149bd 100644 --- a/README-ru.md +++ b/README-ru.md @@ -1044,6 +1044,44 @@ Example: ... ``` +###### basedOnRequest + +Использует разные стратегии ответа, в зависимости от пути запрашиваемого ресурса. Разрешает объявлять несколько стратегий для одного пути. Конкурентно безопасен. + +При получении запроса на ресурс, который не задан в параметрах, тест считается проваленным. + +Параметры: +- `uris` (обязательный) - список ресурсов, каждый ресурс можно сконфигурировать как отдельный мок-сервис, используя любые доступные проверки запросов и стратегии ответов (см. пример) +- `basePath` - общий базовый путь для всех ресурсов, по умолчанию пустой + +Example: +```yaml + ... + mocks: + service1: + strategy: basedOnRequest + uris: + - uri: /request + strategy: constant + body: > + { + "ok": true + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value1" + - uri: /request + strategy: constant + body: > + { + "ok": true + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value2" + ... +``` + ##### Подсчет количества вызовов Вы можете указать, сколько раз должен быть вызван мок или отдельный ресурс мока (используя `uriVary`). Если фактическое количество вызовов будет отличаться от ожидаемого, тест будет считаться проваленным. diff --git a/README.md b/README.md index bba9a82..b0c343d 100644 --- a/README.md +++ b/README.md @@ -1046,6 +1046,44 @@ Example: ... ``` +###### basedOnRequest + +Allows multiple requests with same request path. Concurrent safe. + +When receiving a request for a resource that is not defined in the parameters, the test will be considered failed. + +Parameters: +- `uris` (mandatory) - a list of resources, each resource can be configured as a separate mock-service using any available request constraints and response strategies (see example) +- `basePath` - common base route for all resources, empty by default + +Example: +```yaml + ... + mocks: + service1: + strategy: basedOnRequest + uris: + - uri: /request + strategy: constant + body: > + { + "ok": true + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value1" + - uri: /request + strategy: constant + body: > + { + "ok": true + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value2" + ... +``` + ##### Calls count You can define, how many times each mock or mock resource must be called (using `uriVary`). If the actual number of calls is different from expected, the test will be considered failed. diff --git a/examples/mock-based-on-request/cases/do.yaml b/examples/mock-based-on-request/cases/do.yaml new file mode 100755 index 0000000..6513e08 --- /dev/null +++ b/examples/mock-based-on-request/cases/do.yaml @@ -0,0 +1,28 @@ +- name: Test concurrent with query mathing + mocks: + backend: + strategy: basedOnRequest + uris: + - uri: /request + strategy: constant + body: > + { + "value": 1 + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value1" + - uri: /request + strategy: constant + body: > + { + "value": 22 + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value2" + method: GET + path: /do + response: + 200: | + {"total":23} \ No newline at end of file diff --git a/examples/mock-based-on-request/func_test.go b/examples/mock-based-on-request/func_test.go new file mode 100755 index 0000000..393be00 --- /dev/null +++ b/examples/mock-based-on-request/func_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "net/http/httptest" + "os" + "testing" + + "github.com/lamoda/gonkey/mocks" + "github.com/lamoda/gonkey/runner" +) + +func TestProxy(t *testing.T) { + m := mocks.NewNop("backend") + if err := m.Start(); err != nil { + t.Fatal(err) + } + defer m.Shutdown() + + os.Setenv("BACKEND_ADDR", m.Service("backend").ServerAddr()) + initServer() + srv := httptest.NewServer(nil) + + runner.RunWithTesting(t, &runner.RunWithTestingParams{ + Server: srv, + TestsDir: "cases", + Mocks: m, + }) +} diff --git a/examples/mock-based-on-request/main.go b/examples/mock-based-on-request/main.go new file mode 100755 index 0000000..658534f --- /dev/null +++ b/examples/mock-based-on-request/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "encoding/json" + "fmt" + "golang.org/x/sync/errgroup" + "io" + "log" + "net/http" + urlpkg "net/url" + "os" + "sync/atomic" +) + +func main() { + initServer() + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +func initServer() { + http.HandleFunc("/do", Do) +} + +func Do(w http.ResponseWriter, r *http.Request) { + params1 := urlpkg.Values{"key": []string{"value1"}}.Encode() + params2 := urlpkg.Values{"key": []string{"value2"}}.Encode() + doRequest := func(params string) (int64, error) { + url := fmt.Sprintf("http://%s/request?%s", os.Getenv("BACKEND_ADDR"), params) + res, err := http.Get(url) + if err != nil { + return 0, err + } + if res.StatusCode != http.StatusOK { + return 0, fmt.Errorf("backend response status code %d", res.StatusCode) + } + body, err := io.ReadAll(res.Body) + _ = res.Body.Close() + if err != nil { + return 0, fmt.Errorf("cannot read response body %w", err) + } + var resp struct { + Value int64 `json:"value"` + } + err = json.Unmarshal(body, &resp) + if err != nil { + return 0, fmt.Errorf("cannot unmarshal response body %w", err) + } + return resp.Value, nil + } + + var total int64 + errg := errgroup.Group{} + errg.Go(func() error { + v, err := doRequest(params1) + atomic.AddInt64(&total, v) + return err + }) + errg.Go(func() error { + v, err := doRequest(params2) + atomic.AddInt64(&total, v) + return err + }) + err := errg.Wait() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = io.WriteString(w, err.Error()) + return + } + _, _ = fmt.Fprintf(w, `{"total":%v}`, total) +} diff --git a/go.mod b/go.mod index 2f4d85c..b90a8e9 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/stretchr/testify v1.7.0 github.com/tidwall/gjson v1.13.0 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/go.sum b/go.sum index 0eac861..611bc77 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/mocks/definition.go b/mocks/definition.go index 5fd168d..6950056 100644 --- a/mocks/definition.go +++ b/mocks/definition.go @@ -78,3 +78,36 @@ func (d *definition) EndRunningContext() []error { } return errs } + +func verifyRequestConstraints(requestConstraints []verifier, r *http.Request) bool { + var errors []error + if len(requestConstraints) > 0 { + requestDump, err := httputil.DumpRequest(r, true) + if err != nil { + fmt.Printf("Gonkey internal error: %s\n", err) + } + + for _, c := range requestConstraints { + errs := c.Verify(r) + for _, e := range errs { + errors = append(errors, &RequestConstraintError{ + error: e, + Constraint: c, + RequestDump: requestDump, + }) + } + } + } + return errors == nil +} +func (d *definition) ExecuteWithoutVerifying(w http.ResponseWriter, r *http.Request) []error { + d.Lock() + d.calls++ + d.Unlock() + if d.replyStrategy != nil { + return d.replyStrategy.HandleRequest(w, r) + } + return []error{ + fmt.Errorf("reply strategy undefined"), + } +} diff --git a/mocks/loader.go b/mocks/loader.go index 9458ac7..0b9fe73 100644 --- a/mocks/loader.go +++ b/mocks/loader.go @@ -109,6 +109,9 @@ func (l *Loader) loadStrategy(path, strategyName string, definition map[interfac case "sequence": *ak = append(*ak, "sequence") return l.loadSequenceStrategy(path, definition) + case "basedOnRequest": + *ak = append(*ak, "basePath", "uris") + return l.loadBasedOnRequestStrategy(path, definition) default: return nil, fmt.Errorf("unknown strategy: %s", strategyName) } @@ -215,6 +218,38 @@ func (l *Loader) loadSequenceStrategy(path string, def map[interface{}]interface return newSequentialReply(strategies), nil } +func (l *Loader) loadBasedOnRequestStrategy(path string, def map[interface{}]interface{}) (replyStrategy, error) { + var basePath string + if b, ok := def["basePath"]; ok { + basePath = b.(string) + } + var uris []*definitionWithURI + if u, ok := def["uris"]; ok { + urisList, ok := u.([]interface{}) + if !ok { + return nil, errors.New("`basedOnRequest` requires list under `uris` key") + } + uris = make([]*definitionWithURI, 0, len(urisList)) + for _, v := range urisList { + v, ok := v.(map[interface{}]interface{}) + if !ok { + return nil, errors.New("`uris` list item must be a map") + } + uri, ok := v["uri"] + if !ok { + return nil, errors.New("`uris` list item requires `uri` key") + } + delete(v, "uri") + def, err := l.loadDefinition(path+"."+uri.(string), v) + if err != nil { + return nil, err + } + uris = append(uris, &definitionWithURI{uri.(string), def}) + } + } + return newBasedOnRequestReply(basePath, uris), nil +} + func (l *Loader) loadHeaders(def map[interface{}]interface{}) (map[string]string, error) { var headers map[string]string if h, ok := def["headers"]; ok { diff --git a/mocks/reply_strategy.go b/mocks/reply_strategy.go index 5ba4c53..ce414ee 100644 --- a/mocks/reply_strategy.go +++ b/mocks/reply_strategy.go @@ -195,3 +195,51 @@ func (s *sequentialReply) HandleRequest(w http.ResponseWriter, r *http.Request) s.count++ return def.Execute(w, r) } + +type definitionWithURI struct { + uri string + *definition +} + +type basedOnRequestReply struct { + sync.Mutex + replyStrategy + contextAwareStrategy + + basePath string + variants []*definitionWithURI +} + +func newBasedOnRequestReply(basePath string, variants []*definitionWithURI) replyStrategy { + return &basedOnRequestReply{ + basePath: strings.TrimRight(basePath, "/") + "/", + variants: variants, + } +} + +func (s *basedOnRequestReply) HandleRequest(w http.ResponseWriter, r *http.Request) []error { + s.Lock() + defer s.Unlock() + for _, def := range s.variants { + uri := strings.TrimLeft(def.uri, "/") + if s.basePath+uri == r.URL.Path && + verifyRequestConstraints(def.requestConstraints, r) { + return def.ExecuteWithoutVerifying(w, r) + } + } + return unhandledRequestError(r) +} + +func (s *basedOnRequestReply) ResetRunningContext() { + for _, def := range s.variants { + def.ResetRunningContext() + } +} + +func (s *basedOnRequestReply) EndRunningContext() []error { + var errs []error + for _, def := range s.variants { + errs = append(errs, def.EndRunningContext()...) + } + return errs +} From a2b9a9f7ff162049f2c953aa823c0cfad0e3e44f Mon Sep 17 00:00:00 2001 From: eliseeviam Date: Sat, 12 Feb 2022 19:49:10 +0300 Subject: [PATCH 2/4] replace io.ReadAll with ioutil.ReadAll dependency --- examples/mock-based-on-request/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/mock-based-on-request/main.go b/examples/mock-based-on-request/main.go index 658534f..27226ac 100755 --- a/examples/mock-based-on-request/main.go +++ b/examples/mock-based-on-request/main.go @@ -5,6 +5,7 @@ import ( "fmt" "golang.org/x/sync/errgroup" "io" + "io/ioutil" "log" "net/http" urlpkg "net/url" @@ -33,7 +34,7 @@ func Do(w http.ResponseWriter, r *http.Request) { if res.StatusCode != http.StatusOK { return 0, fmt.Errorf("backend response status code %d", res.StatusCode) } - body, err := io.ReadAll(res.Body) + body, err := ioutil.ReadAll(res.Body) _ = res.Body.Close() if err != nil { return 0, fmt.Errorf("cannot read response body %w", err) From b2688e96f6bc686b4e002124f878e6d62daed5a3 Mon Sep 17 00:00:00 2001 From: eliseeviam Date: Tue, 15 Feb 2022 12:11:07 +0300 Subject: [PATCH 3/4] move uri constraint under pathMatches --- README-ru.md | 11 +++++----- README.md | 11 +++++----- examples/mock-based-on-request/cases/do.yaml | 10 ++++++---- mocks/loader.go | 21 ++++++-------------- mocks/reply_strategy.go | 15 +++----------- 5 files changed, 27 insertions(+), 41 deletions(-) diff --git a/README-ru.md b/README-ru.md index d1149bd..70f88ed 100644 --- a/README-ru.md +++ b/README-ru.md @@ -1052,7 +1052,6 @@ Example: Параметры: - `uris` (обязательный) - список ресурсов, каждый ресурс можно сконфигурировать как отдельный мок-сервис, используя любые доступные проверки запросов и стратегии ответов (см. пример) -- `basePath` - общий базовый путь для всех ресурсов, по умолчанию пустой Example: ```yaml @@ -1061,8 +1060,7 @@ Example: service1: strategy: basedOnRequest uris: - - uri: /request - strategy: constant + - strategy: constant body: > { "ok": true @@ -1070,8 +1068,9 @@ Example: requestConstraints: - kind: queryMatches expectedQuery: "key=value1" - - uri: /request - strategy: constant + - kind: pathMatches + path: /request + - strategy: constant body: > { "ok": true @@ -1079,6 +1078,8 @@ Example: requestConstraints: - kind: queryMatches expectedQuery: "key=value2" + - kind: pathMatches + path: /request ... ``` diff --git a/README.md b/README.md index b0c343d..c670d0f 100644 --- a/README.md +++ b/README.md @@ -1054,7 +1054,6 @@ When receiving a request for a resource that is not defined in the parameters, t Parameters: - `uris` (mandatory) - a list of resources, each resource can be configured as a separate mock-service using any available request constraints and response strategies (see example) -- `basePath` - common base route for all resources, empty by default Example: ```yaml @@ -1063,8 +1062,7 @@ Example: service1: strategy: basedOnRequest uris: - - uri: /request - strategy: constant + - strategy: constant body: > { "ok": true @@ -1072,8 +1070,9 @@ Example: requestConstraints: - kind: queryMatches expectedQuery: "key=value1" - - uri: /request - strategy: constant + - kind: pathMatches + path: /request + - strategy: constant body: > { "ok": true @@ -1081,6 +1080,8 @@ Example: requestConstraints: - kind: queryMatches expectedQuery: "key=value2" + - kind: pathMatches + path: /request ... ``` diff --git a/examples/mock-based-on-request/cases/do.yaml b/examples/mock-based-on-request/cases/do.yaml index 6513e08..aa02425 100755 --- a/examples/mock-based-on-request/cases/do.yaml +++ b/examples/mock-based-on-request/cases/do.yaml @@ -3,8 +3,7 @@ backend: strategy: basedOnRequest uris: - - uri: /request - strategy: constant + - strategy: constant body: > { "value": 1 @@ -12,8 +11,9 @@ requestConstraints: - kind: queryMatches expectedQuery: "key=value1" - - uri: /request - strategy: constant + - kind: pathMatches + path: /request + - strategy: constant body: > { "value": 22 @@ -21,6 +21,8 @@ requestConstraints: - kind: queryMatches expectedQuery: "key=value2" + - kind: pathMatches + path: /request method: GET path: /do response: diff --git a/mocks/loader.go b/mocks/loader.go index 0b9fe73..7d9aac1 100644 --- a/mocks/loader.go +++ b/mocks/loader.go @@ -219,35 +219,26 @@ func (l *Loader) loadSequenceStrategy(path string, def map[interface{}]interface } func (l *Loader) loadBasedOnRequestStrategy(path string, def map[interface{}]interface{}) (replyStrategy, error) { - var basePath string - if b, ok := def["basePath"]; ok { - basePath = b.(string) - } - var uris []*definitionWithURI + var uris []*definition if u, ok := def["uris"]; ok { urisList, ok := u.([]interface{}) if !ok { return nil, errors.New("`basedOnRequest` requires list under `uris` key") } - uris = make([]*definitionWithURI, 0, len(urisList)) - for _, v := range urisList { + uris = make([]*definition, 0, len(urisList)) + for i, v := range urisList { v, ok := v.(map[interface{}]interface{}) if !ok { return nil, errors.New("`uris` list item must be a map") } - uri, ok := v["uri"] - if !ok { - return nil, errors.New("`uris` list item requires `uri` key") - } - delete(v, "uri") - def, err := l.loadDefinition(path+"."+uri.(string), v) + def, err := l.loadDefinition(path+"."+strconv.Itoa(i), v) if err != nil { return nil, err } - uris = append(uris, &definitionWithURI{uri.(string), def}) + uris = append(uris, def) } } - return newBasedOnRequestReply(basePath, uris), nil + return newBasedOnRequestReply(uris), nil } func (l *Loader) loadHeaders(def map[interface{}]interface{}) (map[string]string, error) { diff --git a/mocks/reply_strategy.go b/mocks/reply_strategy.go index ce414ee..fe0c85b 100644 --- a/mocks/reply_strategy.go +++ b/mocks/reply_strategy.go @@ -196,23 +196,16 @@ func (s *sequentialReply) HandleRequest(w http.ResponseWriter, r *http.Request) return def.Execute(w, r) } -type definitionWithURI struct { - uri string - *definition -} - type basedOnRequestReply struct { sync.Mutex replyStrategy contextAwareStrategy - basePath string - variants []*definitionWithURI + variants []*definition } -func newBasedOnRequestReply(basePath string, variants []*definitionWithURI) replyStrategy { +func newBasedOnRequestReply(variants []*definition) replyStrategy { return &basedOnRequestReply{ - basePath: strings.TrimRight(basePath, "/") + "/", variants: variants, } } @@ -221,9 +214,7 @@ func (s *basedOnRequestReply) HandleRequest(w http.ResponseWriter, r *http.Reque s.Lock() defer s.Unlock() for _, def := range s.variants { - uri := strings.TrimLeft(def.uri, "/") - if s.basePath+uri == r.URL.Path && - verifyRequestConstraints(def.requestConstraints, r) { + if verifyRequestConstraints(def.requestConstraints, r) { return def.ExecuteWithoutVerifying(w, r) } } From dca563b4567fb722d7e28bd132f50f0adb7bac4f Mon Sep 17 00:00:00 2001 From: eliseeviam Date: Tue, 22 Feb 2022 10:57:41 +0300 Subject: [PATCH 4/4] show all verification diffs if request was unhandled --- mocks/definition.go | 7 +++---- mocks/reply_strategy.go | 8 ++++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mocks/definition.go b/mocks/definition.go index 6950056..125af31 100644 --- a/mocks/definition.go +++ b/mocks/definition.go @@ -79,14 +79,13 @@ func (d *definition) EndRunningContext() []error { return errs } -func verifyRequestConstraints(requestConstraints []verifier, r *http.Request) bool { +func verifyRequestConstraints(requestConstraints []verifier, r *http.Request) []error { var errors []error if len(requestConstraints) > 0 { requestDump, err := httputil.DumpRequest(r, true) if err != nil { - fmt.Printf("Gonkey internal error: %s\n", err) + requestDump = []byte(fmt.Sprintf("failed to dump request: %s", err)) } - for _, c := range requestConstraints { errs := c.Verify(r) for _, e := range errs { @@ -98,7 +97,7 @@ func verifyRequestConstraints(requestConstraints []verifier, r *http.Request) bo } } } - return errors == nil + return errors } func (d *definition) ExecuteWithoutVerifying(w http.ResponseWriter, r *http.Request) []error { d.Lock() diff --git a/mocks/reply_strategy.go b/mocks/reply_strategy.go index fe0c85b..deb7042 100644 --- a/mocks/reply_strategy.go +++ b/mocks/reply_strategy.go @@ -213,12 +213,16 @@ func newBasedOnRequestReply(variants []*definition) replyStrategy { func (s *basedOnRequestReply) HandleRequest(w http.ResponseWriter, r *http.Request) []error { s.Lock() defer s.Unlock() + + var errors []error for _, def := range s.variants { - if verifyRequestConstraints(def.requestConstraints, r) { + errs := verifyRequestConstraints(def.requestConstraints, r) + if errs == nil { return def.ExecuteWithoutVerifying(w, r) } + errors = append(errors, errs...) } - return unhandledRequestError(r) + return append(errors, unhandledRequestError(r)...) } func (s *basedOnRequestReply) ResetRunningContext() {