diff --git a/README-ru.md b/README-ru.md index 6ae2700..70f88ed 100644 --- a/README-ru.md +++ b/README-ru.md @@ -1044,6 +1044,45 @@ Example: ... ``` +###### basedOnRequest + +Использует разные стратегии ответа, в зависимости от пути запрашиваемого ресурса. Разрешает объявлять несколько стратегий для одного пути. Конкурентно безопасен. + +При получении запроса на ресурс, который не задан в параметрах, тест считается проваленным. + +Параметры: +- `uris` (обязательный) - список ресурсов, каждый ресурс можно сконфигурировать как отдельный мок-сервис, используя любые доступные проверки запросов и стратегии ответов (см. пример) + +Example: +```yaml + ... + mocks: + service1: + strategy: basedOnRequest + uris: + - strategy: constant + body: > + { + "ok": true + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value1" + - kind: pathMatches + path: /request + - strategy: constant + body: > + { + "ok": true + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value2" + - kind: pathMatches + path: /request + ... +``` + ##### Подсчет количества вызовов Вы можете указать, сколько раз должен быть вызван мок или отдельный ресурс мока (используя `uriVary`). Если фактическое количество вызовов будет отличаться от ожидаемого, тест будет считаться проваленным. diff --git a/README.md b/README.md index bba9a82..c670d0f 100644 --- a/README.md +++ b/README.md @@ -1046,6 +1046,45 @@ 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) + +Example: +```yaml + ... + mocks: + service1: + strategy: basedOnRequest + uris: + - strategy: constant + body: > + { + "ok": true + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value1" + - kind: pathMatches + path: /request + - strategy: constant + body: > + { + "ok": true + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value2" + - kind: pathMatches + path: /request + ... +``` + ##### 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..aa02425 --- /dev/null +++ b/examples/mock-based-on-request/cases/do.yaml @@ -0,0 +1,30 @@ +- name: Test concurrent with query mathing + mocks: + backend: + strategy: basedOnRequest + uris: + - strategy: constant + body: > + { + "value": 1 + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value1" + - kind: pathMatches + path: /request + - strategy: constant + body: > + { + "value": 22 + } + requestConstraints: + - kind: queryMatches + expectedQuery: "key=value2" + - kind: pathMatches + path: /request + 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..27226ac --- /dev/null +++ b/examples/mock-based-on-request/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "encoding/json" + "fmt" + "golang.org/x/sync/errgroup" + "io" + "io/ioutil" + "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 := ioutil.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..125af31 100644 --- a/mocks/definition.go +++ b/mocks/definition.go @@ -78,3 +78,35 @@ func (d *definition) EndRunningContext() []error { } return errs } + +func verifyRequestConstraints(requestConstraints []verifier, r *http.Request) []error { + var errors []error + if len(requestConstraints) > 0 { + requestDump, err := httputil.DumpRequest(r, true) + if err != nil { + requestDump = []byte(fmt.Sprintf("failed to dump request: %s", 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 +} +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..7d9aac1 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,29 @@ 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 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([]*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") + } + def, err := l.loadDefinition(path+"."+strconv.Itoa(i), v) + if err != nil { + return nil, err + } + uris = append(uris, def) + } + } + return newBasedOnRequestReply(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..deb7042 100644 --- a/mocks/reply_strategy.go +++ b/mocks/reply_strategy.go @@ -195,3 +195,46 @@ func (s *sequentialReply) HandleRequest(w http.ResponseWriter, r *http.Request) s.count++ return def.Execute(w, r) } + +type basedOnRequestReply struct { + sync.Mutex + replyStrategy + contextAwareStrategy + + variants []*definition +} + +func newBasedOnRequestReply(variants []*definition) replyStrategy { + return &basedOnRequestReply{ + variants: variants, + } +} + +func (s *basedOnRequestReply) HandleRequest(w http.ResponseWriter, r *http.Request) []error { + s.Lock() + defer s.Unlock() + + var errors []error + for _, def := range s.variants { + errs := verifyRequestConstraints(def.requestConstraints, r) + if errs == nil { + return def.ExecuteWithoutVerifying(w, r) + } + errors = append(errors, errs...) + } + return append(errors, 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 +}