From 80c5306dfc7377fb8d09d8bac6a8c623a6e109d5 Mon Sep 17 00:00:00 2001 From: StephanHCB Date: Fri, 21 Jul 2023 10:56:16 +0200 Subject: [PATCH 1/3] fix(#21): clean up deps and actions --- .github/workflows/go.yml | 2 +- go.mod | 1 - go.sum | 13 ------------- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 60c189e..29934be 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: '1.18' + go-version: '1.20' - name: Build run: go build -v ./... diff --git a/go.mod b/go.mod index d06aae2..e509309 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,5 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.5.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 317f400..9c1aacb 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,16 @@ github.com/StephanHCB/go-autumn-logging v0.3.0 h1:G0zs8xoth8i8mOeoFgG3Dvk6dIY9dPPJ7wkm6mjaPyY= github.com/StephanHCB/go-autumn-logging v0.3.0/go.mod h1:dPABYdECU3XrFib03uXbQFVLftUP5c4YaKSineiw37U= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/tidwall/tinylru v1.1.0 h1:XY6IUfzVTU9rpwdhKUF6nQdChgCdGjkMfLzbWyiau6I= -github.com/tidwall/tinylru v1.1.0/go.mod h1:3+bX+TJ2baOLMWTnlyNWHh4QMnFyARg2TLTQ6OFbzw8= github.com/tidwall/tinylru v1.2.1 h1:VgBr72c2IEr+V+pCdkPZUwiQ0KJknnWIYbhxAVkYfQk= github.com/tidwall/tinylru v1.2.1/go.mod h1:9bQnEduwB6inr2Y7AkBP7JPgCkyrhTV/ZpX0oOOpBI4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From b82701306ad6a7574d98eb4c94d2e15568c1f6e8 Mon Sep 17 00:00:00 2001 From: StephanHCB Date: Fri, 21 Jul 2023 11:00:00 +0200 Subject: [PATCH 2/3] feat(#21): provide basic verifier client --- README.md | 7 ++ implementation/verifier/verifier.go | 145 ++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 implementation/verifier/verifier.go diff --git a/README.md b/README.md index 3f739cb..54b1908 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,13 @@ Useful for unit testing. mockClient := aurestmock.New(mockResponses, mockErrors) ``` +#### 1c. Or use verifier (super-basic consumer interaction testing) + +This mock client doesn't make actual requests, but instead you set up a list of +expected interactions. + +This allows doing very simple consumer tests, useful mostly for their documentation value. + #### 2. Response recording If your tests use Option 1a (playback), you should insert a response recorder in your production stack. diff --git a/implementation/verifier/verifier.go b/implementation/verifier/verifier.go new file mode 100644 index 0000000..ee803ae --- /dev/null +++ b/implementation/verifier/verifier.go @@ -0,0 +1,145 @@ +package aurestverifier + +import ( + "context" + "encoding/json" + "errors" + "fmt" + aurestclientapi "github.com/StephanHCB/go-autumn-restclient/api" + "io" + "net/url" +) + +type VerifierImpl struct { + expectations []Expectation + firstUnexpected *Request +} + +type Request struct { + Name string // key for the request + Method string + Url string + Body interface{} +} + +type ResponseOrError struct { + Response aurestclientapi.ParsedResponse + Error error +} + +type Expectation struct { + Request Request + Response ResponseOrError + Matched bool +} + +func (e Expectation) matches(method string, requestUrl string, requestBody interface{}) bool { + // this is a very simple "must match 100%" for the first version + urlMatches := e.Request.Url == requestUrl + methodMatches := e.Request.Method == method + bodyMatches := requestBodyAsString(e.Request.Body) == requestBodyAsString(requestBody) + + return urlMatches && methodMatches && bodyMatches +} + +func requestBodyAsString(requestBody interface{}) string { + if requestBody == nil { + return "" + } + if asCustom, ok := requestBody.(aurestclientapi.CustomRequestBody); ok { + if b, err := io.ReadAll(asCustom.BodyReader); err == nil { + return string(b) + } else { + return fmt.Sprintf("ERROR: %s", err.Error()) + } + } + if asString, ok := requestBody.(string); ok { + return asString + } + if asUrlValues, ok := requestBody.(url.Values); ok { + asString := asUrlValues.Encode() + return asString + } + + marshalled, err := json.Marshal(requestBody) + if err != nil { + return fmt.Sprintf("ERROR: %s", err.Error()) + } + return string(marshalled) +} + +func New() (aurestclientapi.Client, *VerifierImpl) { + instance := &VerifierImpl{ + expectations: make([]Expectation, 0), + } + return instance, instance +} + +func (c *VerifierImpl) Perform(ctx context.Context, method string, requestUrl string, requestBody interface{}, response *aurestclientapi.ParsedResponse) error { + expected, err := c.currentExpectation(method, requestUrl, requestBody) + if err != nil { + return err + } + + if expected.Response.Error != nil { + return expected.Response.Error + } + + mockResponse := expected.Response.Response + + response.Header = mockResponse.Header + response.Status = mockResponse.Status + response.Time = mockResponse.Time + if response.Body != nil && mockResponse.Body != nil { + // copy over through json round trip + marshalled, _ := json.Marshal(mockResponse.Body) + _ = json.Unmarshal(marshalled, response.Body) + } + + return nil +} + +func (c *VerifierImpl) currentExpectation(method string, requestUrl string, requestBody interface{}) (Expectation, error) { + for i, e := range c.expectations { + if !e.Matched { + if e.matches(method, requestUrl, requestBody) { + c.expectations[i].Matched = true + return e, nil + } else { + if c.firstUnexpected == nil { + c.firstUnexpected = &Request{ + Name: fmt.Sprintf("unmatched expectation %d - %s", i+1, e.Request.Name), + Method: method, + Url: requestUrl, + Body: requestBody, + } + } + return Expectation{}, fmt.Errorf("unmatched expectation %d - %s", i+1, e.Request.Name) + } + } + } + + if c.firstUnexpected == nil { + c.firstUnexpected = &Request{ + Name: fmt.Sprintf("no expectations remaining - unexpected request at end"), + Method: method, + Url: requestUrl, + Body: requestBody, + } + } + return Expectation{}, errors.New("no expectations remaining - unexpected request at end") +} + +func (c *VerifierImpl) AddExpectation(requestMatcher Request, response aurestclientapi.ParsedResponse, err error) { + c.expectations = append(c.expectations, Expectation{ + Request: requestMatcher, + Response: ResponseOrError{ + Response: response, + Error: err, + }, + }) +} + +func (c *VerifierImpl) FirstUnexpectedOrNil() *Request { + return c.firstUnexpected +} From d3941b9c0972adfdd92354b59d6f99e51b2ef5bd Mon Sep 17 00:00:00 2001 From: StephanHCB Date: Fri, 21 Jul 2023 11:36:56 +0200 Subject: [PATCH 3/3] feat(#21): allow supplying header (not checked atm) --- implementation/verifier/verifier.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/implementation/verifier/verifier.go b/implementation/verifier/verifier.go index ee803ae..3cf0795 100644 --- a/implementation/verifier/verifier.go +++ b/implementation/verifier/verifier.go @@ -7,7 +7,10 @@ import ( "fmt" aurestclientapi "github.com/StephanHCB/go-autumn-restclient/api" "io" + "net/http" "net/url" + "sort" + "strings" ) type VerifierImpl struct { @@ -18,6 +21,7 @@ type VerifierImpl struct { type Request struct { Name string // key for the request Method string + Header http.Header // currently not tested, just supplied as documentation for now Url string Body interface{} } @@ -68,6 +72,24 @@ func requestBodyAsString(requestBody interface{}) string { return string(marshalled) } +func headersSortedAsString(spec http.Header) string { + var result strings.Builder + + sortedKeys := make([]string, 0) + for k, _ := range spec { + sortedKeys = append(sortedKeys, k) + } + sort.Strings(sortedKeys) + + for _, k := range sortedKeys { + result.WriteString(fmt.Sprintf("%s: ", k)) + for _, v := range spec[k] { + result.WriteString(fmt.Sprintf("%s ", v)) + } + } + return result.String() +} + func New() (aurestclientapi.Client, *VerifierImpl) { instance := &VerifierImpl{ expectations: make([]Expectation, 0), @@ -110,6 +132,7 @@ func (c *VerifierImpl) currentExpectation(method string, requestUrl string, requ c.firstUnexpected = &Request{ Name: fmt.Sprintf("unmatched expectation %d - %s", i+1, e.Request.Name), Method: method, + Header: nil, // not currently available Url: requestUrl, Body: requestBody, } @@ -123,6 +146,7 @@ func (c *VerifierImpl) currentExpectation(method string, requestUrl string, requ c.firstUnexpected = &Request{ Name: fmt.Sprintf("no expectations remaining - unexpected request at end"), Method: method, + Header: nil, // not currently available Url: requestUrl, Body: requestBody, }