spring-cloud / spring-cloud-contract

Support for Consumer Driven Contracts in Spring
https://cloud.spring.io/spring-cloud-contract
Apache License 2.0
720 stars 440 forks source link

Incorrect mappings generated by contracts with json strings as attribute values within a json object #667

Closed kubejm closed 6 years ago

kubejm commented 6 years ago

We have a service that returns a response with structure similar to the following:

{
    "jsonString": "{\"attribute\": \"value\"}"
}

Example contract to support the response above:

org.springframework.cloud.contract.spec.Contract.make {
    priority 1
    request {
        method 'GET'
        urlPath(value(consumer(regex(/\/data\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)),
                producer('/data/444d57b2-e309-4576-83cb-5530ee03106a')))
    }

    response {
        status 200
        headers {
            header("Content-Type", "application/json;charset=UTF-8")
        }

        body([
                jsonString: '{\\"attribute\\": \\"value\\"}'
        ])
    }
}

The contract above does not produce the expected mapping. It produces the following, which introduces extra backslashes and when running stubrunner leads our clients to receiving improper responses.

{
  "uuid" : "02a7cca2-0a2e-459e-8f5c-b4030250018c",
  "request" : {
    "urlPathPattern" : "/data/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "{\"jsonString\":\"{\\\\\\\"attribute\\\\\\\": \\\\\\\"value\\\\\\\"}\"}",
    "headers" : {
      "Content-Type" : "application/json;charset=UTF-8"
    }
  },
  "priority" : 1
}

It appears the issue is within this method: https://github.com/spring-cloud/spring-cloud-contract/blob/1.2.x/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy#L162-L169

From debugging and attempting to change the implementation, it seems the Groovy packages to building json are responsible for introducing the backslashes. I tried using the JsonGenerator, found within Groovy 2.5, but also ran into the same issue. I attempted to use a custom converter without any luck. Another challenge I found when investigating this issue was around testing, because the test suite utilizes an AssertionUtils that also uses Groovy's json parsers/builders for evaluating the correctness of the maps and responses from wiremock.

It does not seem this issue is necessarily at the fault of Groovy, because it seems it is just trying to interpret the json it thinks it sees, even though I want it to be evaluated as and remain a string.

Versions tested in: 1.2.4.RELEASE, 1.2.0.RELEASE, and within the 1.2.x branch

As a temporary workaround, I introduced the following gradle task to manually update the problematic mapping file. This explicitly replaces the extraneous backslashes with the amount I would expect.

task fixSadContractIssue()  {
    dependsOn('generateWireMockClientStubs')
    doLast {
        File brokenMapping = new File(project.buildDir, '/stubs/mappings/myMapping.json')
        brokenMapping.text = brokenMapping.text.replace('\\\\\\\\\\\\\\', '\\\\\\')
    }
}
verifierStubsJar.dependsOn(fixSadContractIssue)

Also, thanks a lot to @marcingrzejszczak and his help with troubleshooting this problem with me!

macdao commented 6 years ago

I met another or maybe same issue: generated wiremock config (or mappings) is "body" : "{\"name\":\"hi\\\\r\\\\nname\"}" and return {"name":"hi\\r\\nname"} in HTTP response, while I expected {"name":"hi\r\nname"} (while should contains a newline)

What I have is a contract:

request:
  method: GET
  url: /my-url
response:
  status: 200
  bodyFromFile: hiname.json
  headers:
    Content-Type: application/json;charset=UTF-8

a hiname.json:

{
  "name":"hi\r\nname"
}

and a generated mapping:

{
  "id" : "c3cce57c-875d-4a45-85ce-ac094d07ec69",
  "request" : {
    "url" : "/my-url",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "{\"name\":\"hi\\\\r\\\\nname\"}",
    "headers" : {
      "Content-Type" : "application/json;charset=UTF-8"
    },
    "transformers" : [ "response-template" ]
  },
  "uuid" : "c3cce57c-875d-4a45-85ce-ac094d07ec69"
}
marcingrzejszczak commented 6 years ago

Check if the workaround works for you

macdao commented 6 years ago

hi @marcingrzejszczak the workaround works for me, thank you~

add more info

I'm using spring-cloud-contract-gradle-plugin 2.0.0.RELEASE (which is latest for now)

I modified lines to make it work:

task fixSadContractIssue()  {
    dependsOn('generateClientStubs')
    doLast {
        File brokenMapping = new File(project.buildDir, '/stubs/META-INF/xxx/mappings/my-contract.json')
        brokenMapping.text = brokenMapping.text.replace('\\\\', '\\')
    }
}
verifierStubsJar.dependsOn(fixSadContractIssue)
marcingrzejszczak commented 6 years ago

I guess I need your help. I've migrated to ObjectMapper to do the deserialization and I've written such a test but I'm not sure if the number of quotes is correct ;) (https://github.com/spring-cloud/spring-cloud-contract/commit/45ede5e1253001746af797cf09392cb406beecf6#diff-921272dc81327196df68e37608cdd717R2084)

@Issue('#667')
    def "should not double escape backslashes"() {
        given:
            Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make {
                priority 1
                request {
                    method 'GET'
                    urlPath(value(consumer(regex(/\/data\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)),
                            producer('/data/444d57b2-e309-4576-83cb-5530ee03106a')))
                }
                response {
                    status 200
                    headers {
                        header("Content-Type", "application/json;charset=UTF-8")
                    }
                    body([
                            jsonString: '{\\"attribute\\": \\"value\\"}'
                    ])
                }
            }
        when:
            String wireMockStub = new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null, groovyDsl), groovyDsl).toWireMockClientStub()
        then:
            AssertionUtil.assertThatJsonsAreEqual('''
                {
                    "request" : {
                        "urlPathPattern" : "/data/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
                        "method" : "GET"
                    },
                    "response" : {
                        "status" : 200,
                        "body" : "{\\"jsonString\\":\\"{\\\\\\"attribute\\\\\\": \\\\\\"value\\\\\\"}\\"}",
                        "headers" : {
                            "Content-Type" : "application/json;charset=UTF-8"
                    },
                    "transformers" : [ "response-template", "foo-transformer" ]
                    },
                    "priority" : 1
                }
                ''', wireMockStub)
        and:
            stubMappingIsValidWireMockStub(wireMockStub)
    }

Can you paste exactly how the WireMock JSON mapping should look like?

kubejm commented 6 years ago

This is how I would expect the WireMock JSON mapping to look like:

{
  "uuid" : "32912777-64f2-4382-9b55-4dc013a1cb28",
  "request" : {
    "urlPathPattern" : "/data/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "{\"jsonString\":\"{\\\"attribute\\\": \\\"value\\\"}\"}",
    "headers" : {
      "Content-Type" : "application/json;charset=UTF-8"
    }
  },
  "priority" : 1
}

The above mapping is what is being produced with the workaround we are currently utilizing in our project. Also, your assertion looks correct assuming that assertThatJsonsAreEqual does not do anything funny with escaping those characters.

Thank you for continuing to look into this! Sorry for not getting back to you sooner.

marcingrzejszczak commented 6 years ago

I gave it another try but still no luck. The problem is that all frameworks seem to treat the text in the json in a bizarre way. If I make some progress I'll update the issue.

OlgaMaciaszek commented 6 years ago

Since its an issue with the outputs of the Groovy Json Processing libraries, we can't really do a real fix, but I'm providing a workaround in this PR: https://github.com/spring-cloud/spring-cloud-contract/pull/750 .