Skip to content

Commit

Permalink
feat: panic a test if they attempt to use a matcher with a higher spec
Browse files Browse the repository at this point in the history
  • Loading branch information
mefellows committed May 30, 2021
1 parent de379ad commit bf9b83b
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 107 deletions.
2 changes: 2 additions & 0 deletions examples/v3/consumer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func TestConsumerV2(t *testing.T) {
"name": v3.Like("billy"),
"datetime": v3.Like("2020-01-01'T'08:00:45"),
"lastName": v3.Like("billy"),
// "equality": v3.Equality("a thing"), // Add this in and watch me panic
}).
WillRespondWith(200).
Headers(v3.MapMatcher{"Content-Type": v3.Regex("application/json", "application\\/json")}).
Expand All @@ -58,6 +59,7 @@ func TestConsumerV2(t *testing.T) {
"name": s("Billy"),
"lastName": s("Sampson"),
"itemsMin": v3.ArrayMinLike("thereshouldbe3ofthese", 3),
// "equality": v3.Equality("a thing"), // Add this in and watch me panic
})

// Execute pact test
Expand Down
11 changes: 5 additions & 6 deletions v3/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func (p *httpMockProvider) ExecuteTest(integrationTest func(MockServerConfig) er
return fmt.Errorf("pact validation failed: %+v", mismatches)
}

return p.WritePact()
return p.writePact()
}

// TODO: pretty print this to make it really easy to understand the problems
Expand Down Expand Up @@ -247,11 +247,10 @@ func (p *httpMockProvider) displayMismatches(mismatches []native.MismatchedReque
}
}

// WritePact should be called when all tests have been performed for a
// given Consumer <-> Provider pair. It will write out the Pact to the
// configured file. This is safe to call multiple times as the service is smart
// enough to merge pacts and avoid duplicates.
func (p *httpMockProvider) WritePact() error {
// writePact may be called after each interaction with a mock server is completed
// the shared core is threadsafe and will merge, as long as the requests come from a single process
// (that is, there isn't separate) instances of the FFI running simultaneously
func (p *httpMockProvider) writePact() error {
log.Println("[DEBUG] write pact file")
if p.config.Port != 0 {
return p.mockserver.WritePactFile(p.config.Port, p.config.PactDir)
Expand Down
12 changes: 7 additions & 5 deletions v3/http_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ func (p *HTTPMockProviderV2) AddInteraction() *InteractionV2 {

i := &InteractionV2{
Interaction: Interaction{
interaction: interaction,
specificationVersion: V2,
interaction: interaction,
},
}

Expand All @@ -48,8 +49,9 @@ func (p *HTTPMockProviderV2) AddInteraction() *InteractionV2 {
}

// SetMatchingConfig allows specific contract file serialisation adjustments
func (p *HTTPMockProviderV2) SetMatchingConfig(config PactSerialisationOptionsV2) *HTTPMockProviderV2 {
p.config.matchingConfig = config
// TODO: review if this is even used now we've moved to FFI
// func (p *HTTPMockProviderV2) SetMatchingConfig(config PactSerialisationOptionsV2) *HTTPMockProviderV2 {
// p.config.matchingConfig = config

return p
}
// return p
// }
3 changes: 2 additions & 1 deletion v3/http_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ func (p *HTTPMockProviderV3) AddInteraction() *InteractionV3 {

i := &InteractionV3{
Interaction: Interaction{
interaction: interaction,
specificationVersion: V3,
interaction: interaction,
},
}

Expand Down
104 changes: 85 additions & 19 deletions v3/interaction.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
package v3

import (
"encoding/json"
"fmt"
"log"
"strings"

"github.com/pact-foundation/pact-go/v3/internal/native/mockserver"
)

// Interaction is the main implementation of the Pact interface.
type Interaction struct {
// Reference to the native rust handle
interaction *mockserver.Interaction
interaction *mockserver.Interaction
specificationVersion SpecificationVersion
}

type InteractionRequest struct {
// Reference to the native rust handle
interaction *mockserver.Interaction
interactionHandle *mockserver.Interaction
interaction *Interaction
}

type InteractionResponse struct {
// Reference to the native rust handle
interaction *mockserver.Interaction
interactionHandle *mockserver.Interaction
interaction *Interaction
}

// UponReceiving specifies the name of the test case. This becomes the name of
Expand All @@ -37,7 +43,8 @@ func (i *Interaction) WithRequest(method Method, path Matcher) *InteractionReque
i.interaction.WithRequest(string(method), path)

return &InteractionRequest{
interaction: i.interaction,
interactionHandle: i.interaction,
interaction: i,
}
}

Expand All @@ -48,24 +55,29 @@ func (i *InteractionRequest) Query(query QueryMatcher) *InteractionRequest {
q[k] = append(q[k], v)
}
}
i.interaction.WithQuery(q)
i.interactionHandle.WithQuery(q)

return i
}

func (i *InteractionRequest) HeadersArray(headers HeadersMatcher) *InteractionRequest {
i.interaction.WithRequestHeaders(headersMatcherToNativeHeaders(headers))
i.interactionHandle.WithRequestHeaders(headersMatcherToNativeHeaders(headers))

return i
}

func (i *InteractionRequest) Headers(headers MapMatcher) *InteractionRequest {
i.interaction.WithRequestHeaders(mapMatcherToNativeHeaders(headers))
i.interactionHandle.WithRequestHeaders(mapMatcherToNativeHeaders(headers))

return i
}

func (i *InteractionRequest) JSON(body interface{}) *InteractionRequest {
// TODO: Don't like panic, how to build a better builder here - nil return + log?
if err := validateMatchers(i.interaction.specificationVersion, body); err != nil {
panic(err)
}

if s, ok := body.(string); ok {
// Check if someone tried to add an object as a string representation
// as per original allowed implementation, e.g.
Expand All @@ -77,13 +89,13 @@ func (i *InteractionRequest) JSON(body interface{}) *InteractionRequest {
}
}

i.interaction.WithJSONRequestBody(body)
i.interactionHandle.WithJSONRequestBody(body)

return i
}

func (i *InteractionRequest) Binary(body []byte) *InteractionRequest {
i.interaction.WithBinaryRequestBody(body)
i.interactionHandle.WithBinaryRequestBody(body)

return i
}
Expand All @@ -98,13 +110,13 @@ func (i *InteractionRequest) Body(contentType string, body []byte) *InteractionR
"deprecated as of 0.13.0. Please use the JSON() method instead")
}

i.interaction.WithRequestBody(contentType, body)
i.interactionHandle.WithRequestBody(contentType, body)

return i
}

func (i *InteractionRequest) BodyMatch(body interface{}) *InteractionRequest {
i.interaction.WithJSONRequestBody(MatchV2(body))
i.interactionHandle.WithJSONRequestBody(MatchV2(body))

return i
}
Expand All @@ -114,21 +126,22 @@ func (i *InteractionRequest) BodyMatch(body interface{}) *InteractionRequest {
// Defaults to application/json.
// Use WillResponseWithContent to define custom type
func (i *InteractionRequest) WillRespondWith(status int) *InteractionResponse {
i.interaction.WithStatus(status)
i.interactionHandle.WithStatus(status)

return &InteractionResponse{
interaction: i.interaction,
interactionHandle: i.interactionHandle,
interaction: i.interaction,
}
}

func (i *InteractionResponse) HeadersArray(headers HeadersMatcher) *InteractionResponse {
i.interaction.WithResponseHeaders(headersMatcherToNativeHeaders(headers))
i.interactionHandle.WithResponseHeaders(headersMatcherToNativeHeaders(headers))

return i
}

func (i *InteractionResponse) Headers(headers MapMatcher) *InteractionResponse {
i.interaction.WithRequestHeaders(mapMatcherToNativeHeaders(headers))
i.interactionHandle.WithRequestHeaders(mapMatcherToNativeHeaders(headers))

return i
}
Expand Down Expand Up @@ -157,29 +170,82 @@ func mapMatcherToNativeHeaders(headers MapMatcher) map[string][]interface{} {
}

func (i *InteractionResponse) JSON(body interface{}) *InteractionResponse {
i.interaction.WithJSONResponseBody(body)
// TODO: Don't like panic, how to build a better builder here - nil return + log?
if err := validateMatchers(i.interaction.specificationVersion, body); err != nil {
panic(err)
}

if s, ok := body.(string); ok {
// Check if someone tried to add an object as a string representation
// as per original allowed implementation, e.g.
// { "foo": "bar", "baz": like("bat") }
if isJSONFormattedObject(string(s)) {
log.Println("[WARN] request body appears to be a JSON formatted object, " +
"no matching will occur. Support for structured strings has been" +
"deprecated as of 0.13.0. Please use the JSON() method instead")
}
}
i.interactionHandle.WithJSONResponseBody(body)

return i
}

func (i *InteractionResponse) Binary(body []byte) *InteractionResponse {
i.interaction.WithBinaryResponseBody(body)
i.interactionHandle.WithBinaryResponseBody(body)

return i
}

func (i *InteractionResponse) Body(contentType string, body []byte) *InteractionResponse {
i.interaction.WithResponseBody(contentType, body)
i.interactionHandle.WithResponseBody(contentType, body)

return i
}

func (i *InteractionResponse) BodyMatch(body interface{}) *InteractionResponse {
i.interaction.WithJSONRequestBody(MatchV2(body))
i.interactionHandle.WithJSONRequestBody(MatchV2(body))

return i
}

func validateMatchers(version SpecificationVersion, obj interface{}) error {
str, err := json.Marshal(obj)
if err != nil {
return err
}

var maybeMatchers map[string]interface{}
err = json.Unmarshal(str, &maybeMatchers)
if err != nil {
return err
}

invalidMatchers := hasMatcherGreaterThanSpec(version, maybeMatchers)

if len(invalidMatchers) > 0 {
return fmt.Errorf("the current pact file with specification version %s has attempted to use matchers from a higher spec version: %s", version, strings.Join(invalidMatchers, ", "))
}

return nil
}

func hasMatcherGreaterThanSpec(version SpecificationVersion, obj map[string]interface{}) []string {
results := make([]string, 0)

for k, v := range obj {
if k == "pact:specification" && v.(string) > string(version) {
results = append(results, obj["pact:matcher:type"].(string))
}

m, ok := v.(map[string]interface{})
if ok {
results = append(results, hasMatcherGreaterThanSpec(version, m)...)
}
}

return results
}

// TODO: allow these old interfaces?
//
// func (i *InteractionResponse) WillRespondWithContent(contentType string, response Response) *InteractionResponse {
Expand Down
5 changes: 3 additions & 2 deletions v3/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ func (m eachLike) MarshalJSON() ([]byte, error) {
}

type like struct {
Type string `json:"pact:matcher:type"`
Value interface{} `json:"value"`
Specification SpecificationVersion `json:"specification"`
Type string `json:"pact:matcher:type"`
Value interface{} `json:"value"`
}

func (m like) GetValue() interface{} {
Expand Down

0 comments on commit bf9b83b

Please sign in to comment.