Skip to content

Commit

Permalink
feat(matching): deprecate matching JSON formatted objects
Browse files Browse the repository at this point in the history
- Removes ability for user to supply JSON document
in matching DSL.
- Warning displayed to console if used

BREAKING CHANGE

JSON formatted objects such as `{"foo": "bar"}` are no
longer supported in the matching DSL. Marshalling these
to the Mock Service correctly is problematic, especially
when combining with matchers and other nested JSON
formatted objects.

Type information is lost re-assembling into a workable
map[string]interface{} which meant that, for example, a "100"
is converted to 100 losing key information.

If we detect a JSON formatted object in the DSL, we now log
a warning indicating this potential problem.

See #73 for
background.
  • Loading branch information
mefellows committed Mar 28, 2018
1 parent 92e3b83 commit 0c84cdc
Show file tree
Hide file tree
Showing 11 changed files with 77 additions and 165 deletions.
63 changes: 32 additions & 31 deletions dsl/interaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,63 +21,64 @@ type Interaction struct {
}

// Given specifies a provider state. Optional.
func (p *Interaction) Given(state string) *Interaction {
p.State = state
return p
func (i *Interaction) Given(state string) *Interaction {
i.State = state

return i
}

// UponReceiving specifies the name of the test case. This becomes the name of
// the consumer/provider pair in the Pact file. Mandatory.
func (p *Interaction) UponReceiving(description string) *Interaction {
p.Description = description
return p
func (i *Interaction) UponReceiving(description string) *Interaction {
i.Description = description

return i
}

// WithRequest specifies the details of the HTTP request that will be used to
// confirm that the Provider provides an API listening on the given interface.
// Mandatory.
func (p *Interaction) WithRequest(request Request) *Interaction {
p.Request = request

// Need to fix any weird JSON marshalling issues with the body Here
// If body is a string, not an object, we need to put it back into an object
// so that it's not double encoded
p.Request.Body = toObject(request.Body)
func (i *Interaction) WithRequest(request Request) *Interaction {
i.Request = request

// 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(request.Body) {
log.Println("[WARN] request body appears to be a JSON formatted object, " +
"no structural matching will occur. Support for structured strings has been" +
"deprecated as of 0.13.0")
}

return p
return i
}

// WillRespondWith specifies the details of the HTTP response that will be used to
// confirm that the Provider must satisfy. Mandatory.
func (p *Interaction) WillRespondWith(response Response) *Interaction {
p.Response = response
func (i *Interaction) WillRespondWith(response Response) *Interaction {
i.Response = response

// Need to fix any weird JSON marshalling issues with the body Here
// If body is a string, not an object, we need to put it back into an object
// so that it's not double encoded
p.Response.Body = toObject(response.Body)

return p
return i
}

// Takes a string body and converts it to an interface{} representation.
func toObject(stringOrObject interface{}) interface{} {

// Checks to see if someone has tried to submit a JSON string
// for an object, which is no longer supported
func isJSONFormattedObject(stringOrObject interface{}) bool {
switch content := stringOrObject.(type) {
case []byte:
case string:
var obj interface{}
err := json.Unmarshal([]byte(content), &obj)

if err != nil {
log.Printf("[DEBUG] interaction: error unmarshaling string '%v' into an object. Probably not an object: %v\n", stringOrObject, err.Error())
return content
return false
}

return obj
default:
// leave alone
// Check if a map type
if _, ok := obj.(map[string]interface{}); ok {
return true
}
}

return stringOrObject
return false
}
60 changes: 20 additions & 40 deletions dsl/interaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ func TestInteraction_WithRequest(t *testing.T) {
Given("Some state").
UponReceiving("Some name for the test").
WithRequest(Request{
Body: `{
"foo": "bar",
"baz": "bat"
}`,
Body: map[string]string{
"foo": "bar",
"baz": "bat",
},
})

obj := map[string]string{
Expand All @@ -60,12 +60,8 @@ func TestInteraction_WithRequest(t *testing.T) {
body, _ := json.Marshal(obj)
json.Unmarshal(body, &expect)

if _, ok := i.Request.Body.(map[string]interface{}); !ok {
t.Fatalf("Expected response to be of type 'map[string]string'")
}

if !reflect.DeepEqual(i.Request.Body, expect) {
t.Fatalf("Expected response object body '%v' to match '%v'", i.Request.Body, expect)
if _, ok := i.Request.Body.(map[string]string); !ok {
t.Fatal("Expected response to be of type 'map[string]string', but got", reflect.TypeOf(i.Request.Body))
}
}

Expand Down Expand Up @@ -95,10 +91,10 @@ func TestInteraction_WillRespondWith(t *testing.T) {
UponReceiving("Some name for the test").
WithRequest(Request{}).
WillRespondWith(Response{
Body: `{
Body: map[string]string{
"foo": "bar",
"baz": "bat"
}`,
"baz": "bat",
},
})

obj := map[string]string{
Expand All @@ -110,37 +106,21 @@ func TestInteraction_WillRespondWith(t *testing.T) {
body, _ := json.Marshal(obj)
json.Unmarshal(body, &expect)

if _, ok := i.Response.Body.(map[string]interface{}); !ok {
t.Fatalf("Expected response to be of type 'map[string]string'")
}

if !reflect.DeepEqual(i.Response.Body, expect) {
t.Fatalf("Expected response object body '%v' to match '%v'", i.Response.Body, expect)
if _, ok := i.Response.Body.(map[string]string); !ok {
t.Fatal("Expected response to be of type 'map[string]string', but got", reflect.TypeOf(i.Response.Body))
}
}

func TestInteraction_toObject(t *testing.T) {
// unstructured string should not be changed
res := toObject("somestring")
content, ok := res.(string)

if !ok {
t.Fatalf("must be a string")
}

if content != "somestring" {
t.Fatalf("Expected 'somestring' but got '%s'", content)
}

// errors should return a string repro of original interface{}
res = toObject("")
content, ok = res.(string)

if !ok {
t.Fatalf("must be a string")
func TestInteraction_isStringLikeObject(t *testing.T) {
testCases := map[string]bool{
"somestring": false,
"": false,
`{"foo":"bar"}`: true,
}

if content != "" {
t.Fatalf("Expected '' but got '%s'", content)
for testCase, want := range testCases {
if isJsonFormattedObject(testCase) != want {
t.Fatal("want", want, "got", !want, "for test case", testCase)
}
}
}
40 changes: 4 additions & 36 deletions dsl/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type term struct {
func EachLike(content interface{}, minRequired int) Matcher {
return Matcher{
"json_class": "Pact::ArrayLike",
"contents": toObject(content),
"contents": content,
"min": minRequired,
}
}
Expand All @@ -57,7 +57,7 @@ func EachLike(content interface{}, minRequired int) Matcher {
func Like(content interface{}) Matcher {
return Matcher{
"json_class": "Pact::SomethingLike",
"contents": toObject(content),
"contents": content,
}
}

Expand All @@ -67,11 +67,11 @@ func Term(generate string, matcher string) Matcher {
return Matcher{
"json_class": "Pact::Term",
"data": map[string]interface{}{
"generate": toObject(generate),
"generate": generate,
"matcher": map[string]interface{}{
"json_class": "Regexp",
"o": 0,
"s": toObject(matcher),
"s": matcher,
},
},
}
Expand Down Expand Up @@ -162,42 +162,10 @@ type Matcher map[string]interface{}

func (m Matcher) isMatcher() {}

// MarshalJSON is a custom encoder for Header type
func (m Matcher) MarshalJSON() ([]byte, error) {
obj := map[string]interface{}{}

for header, value := range m {
obj[header] = toObject(value)
}

return json.Marshal(obj)
}

// UnmarshalJSON is a custom decoder for Header type
func (m *Matcher) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &m)
}

// MapMatcher allows a map[string]string-like object
// to also contain complex matchers
type MapMatcher map[string]StringMatcher

// MarshalJSON is a custom encoder for Header type
func (h MapMatcher) MarshalJSON() ([]byte, error) {
obj := map[string]interface{}{}

for header, value := range h {
obj[header] = toObject(value)
}

return json.Marshal(obj)
}

// UnmarshalJSON is a custom decoder for Header type
func (h *MapMatcher) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &h)
}

// Takes an object and converts it to a JSON representation
func objectToString(obj interface{}) string {
switch content := obj.(type) {
Expand Down
49 changes: 5 additions & 44 deletions dsl/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,6 @@ func TestMatcher_LikeAsObject(t *testing.T) {
}
}

func TestMatcher_LikeAsObjectString(t *testing.T) {
expected := formatJSON(`
{
"contents": {"baz":"bat"},
"json_class": "Pact::SomethingLike"
}`)

match := formatJSON(Like(`{"baz":"bat"}`))
if expected != match {
t.Fatalf("Expected Term to match. '%s' != '%s'", expected, match)
}
}

func TestMatcher_LikeNumber(t *testing.T) {
expected := formatJSON(`
{
Expand All @@ -87,7 +74,7 @@ func TestMatcher_LikeNumber(t *testing.T) {
func TestMatcher_LikeNumberAsString(t *testing.T) {
expected := formatJSON(`
{
"contents": 42,
"contents": "42",
"json_class": "Pact::SomethingLike"
}`)

Expand All @@ -113,7 +100,7 @@ func TestMatcher_EachLikeNumber(t *testing.T) {
func TestMatcher_EachLikeNumberAsString(t *testing.T) {
expected := formatJSON(`
{
"contents": 42,
"contents": "42",
"json_class": "Pact::ArrayLike",
"min": 1
}`)
Expand Down Expand Up @@ -154,7 +141,7 @@ func TestMatcher_EachLikeObject(t *testing.T) {
}
}

func TestMatcher_EachLikeObjectAsString(t *testing.T) {
func TestMatcher_EachLikeObjectAsStringFail(t *testing.T) {
expected := formatJSON(`
{
"contents": {"somekey":"someval"},
Expand All @@ -163,8 +150,8 @@ func TestMatcher_EachLikeObjectAsString(t *testing.T) {
}`)

match := formatJSON(EachLike(`{"somekey":"someval"}`, 3))
if expected != match {
t.Fatalf("Expected Term to match. '%s' != '%s'", expected, match)
if expected == match {
t.Fatalf("Expected Term to NOT match. '%s' != '%s'", expected, match)
}
}

Expand All @@ -182,20 +169,6 @@ func TestMatcher_EachLikeArray(t *testing.T) {
}
}

func TestMatcher_EachLikeArrayString(t *testing.T) {
expected := formatJSON(`
{
"contents": [1,2,3],
"json_class": "Pact::ArrayLike",
"min": 1
}`)

match := formatJSON(EachLike("[1,2,3]", 1))
if expected != match {
t.Fatalf("Expected Term to match. '%s' != '%s'", expected, match)
}
}

func TestMatcher_NestLikeInEachLike(t *testing.T) {
expected := formatJSON(`
{
Expand Down Expand Up @@ -510,18 +483,6 @@ func ExampleLike_object() {
// "json_class": "Pact::SomethingLike"
//}
}
func ExampleLike_objectString() {
match := Like(`{"baz":"bat"}`)
fmt.Println(formatJSON(match))
// Output:
//{
// "contents": {
// "baz": "bat"
// },
// "json_class": "Pact::SomethingLike"
//}
}

func ExampleLike_number() {
match := Like(42)
fmt.Println(formatJSON(match))
Expand Down
2 changes: 1 addition & 1 deletion dsl/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (p *Message) WithMetadata(metadata MapMatcher) *Message {
// confirm that the Provider provides an API listening on the given interface.
// Mandatory.
func (p *Message) WithContent(content interface{}) *Message {
p.Content = toObject(content)
p.Content = content

return p
}
5 changes: 1 addition & 4 deletions dsl/mock_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,12 @@ import (
// Simple mock server for testing. This is getting confusing...
func setupMockServer(success bool, t *testing.T) *httptest.Server {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
request, err := ioutil.ReadAll(r.Body)
_, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
log.Fatal(err)
}

t.Logf("%v\n", r)
t.Logf("Request Body: %s\n", request)

if success {
fmt.Fprintln(w, "Hello, client")
} else {
Expand Down
Loading

0 comments on commit 0c84cdc

Please sign in to comment.