Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 21 interaction checker #22

Merged
merged 3 commits into from Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Expand Up @@ -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 ./...
Expand Down
7 changes: 7 additions & 0 deletions README.md
Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion go.mod
Expand Up @@ -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
)
13 changes: 0 additions & 13 deletions 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=
169 changes: 169 additions & 0 deletions implementation/verifier/verifier.go
@@ -0,0 +1,169 @@
package aurestverifier

import (
"context"
"encoding/json"
"errors"
"fmt"
aurestclientapi "github.com/StephanHCB/go-autumn-restclient/api"
"io"
"net/http"
"net/url"
"sort"
"strings"
)

type VerifierImpl struct {
expectations []Expectation
firstUnexpected *Request
}

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{}
}

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 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),
}
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,
Header: nil, // not currently available
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,
Header: nil, // not currently available
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
}