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