diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy index 2060abdd65..173b70f901 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy @@ -21,8 +21,9 @@ import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.Headers import org.springframework.cloud.contract.spec.internal.MatchingType import org.springframework.cloud.contract.spec.internal.QueryParameters +import org.springframework.cloud.contract.verifier.util.JsonPaths +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter import org.springframework.cloud.contract.verifier.util.MapConverter - /** * Converter of JSON PACT file * @@ -36,6 +37,7 @@ class PactContractConverter implements ContractConverter { private static final String REGEX_KEY = "regex" private static final String MAX_KEY = "max" private static final String MIN_KEY = "min" + private static final String FULL_BODY = '$.body' @Override boolean isAccepted(File file) { @@ -90,7 +92,8 @@ class PactContractConverter implements ContractConverter { } if (requestResponseInteraction.request?.matchingRules) { stubMatchers { - requestResponseInteraction.request.matchingRules.each { String key, Map value -> + Map> rules = requestResponseInteraction.request.matchingRules + rules.each { String key, Map value -> String keyFromBody = toKeyStartingFromBody(key) if (value.containsKey(MATCH_KEY)) { MatchingType matchingType = MatchingType.valueOf((value.get(MATCH_KEY) as String).toUpperCase()) @@ -132,8 +135,19 @@ class PactContractConverter implements ContractConverter { } if (requestResponseInteraction.response?.matchingRules) { testMatchers { - requestResponseInteraction.response.matchingRules.each { String key, Map value -> + Map> rules = requestResponseInteraction.response.matchingRules + Map fullBodyCheck = rules.get(FULL_BODY) + if (fullBodyCheck != null) { + JsonPaths jsonPaths = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(requestResponseInteraction.request?.body?.value) + jsonPaths.each { + jsonPath(it.keyBeforeChecking(), byType()) + } + } + rules.each { String key, Map value -> String keyFromBody = toKeyStartingFromBody(key) + if (!keyFromBody) { + return + } if (value.containsKey(MATCH_KEY)) { MatchingType matchingType = MatchingType.valueOf((value.get(MATCH_KEY) as String).toUpperCase()) switch (matchingType) { @@ -185,7 +199,10 @@ class PactContractConverter implements ContractConverter { } protected String toKeyStartingFromBody(String key) { - return key.replace('$.body', '$') + if (key == FULL_BODY) { + return "" + } + return key.replace(FULL_BODY, '$') } @Override diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy index 0f6a5529b4..09531cd0e2 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy @@ -8,6 +8,7 @@ import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter import org.springframework.core.io.Resource import org.springframework.core.io.support.PathMatchingResourcePatternResolver +import spock.lang.Issue import spock.lang.Specification import spock.lang.Subject /** @@ -16,6 +17,7 @@ import spock.lang.Subject class PactContractConverterSpec extends Specification { File pactJson = new File(PactContractConverterSpec.getResource("/pact/pact.json").toURI()) + File pact509Json = new File(PactContractConverterSpec.getResource("/pact/pact_509.json").toURI()) @Subject PactContractConverter converter = new PactContractConverter() def "should accept json files that are pact files"() { @@ -76,6 +78,37 @@ class PactContractConverterSpec extends Specification { contracts == [expectedContract] } + @Issue("#509") + def "should convert from pact with matching rules to whole body to contract"() { + given: + Contract expectedContract = Contract.make { + description("a request to POST a person provider accepts a new person") + request { + method(POST()) + url("/user-service/users") + headers { + contentType(applicationJson()) + } + body(firstName: "Arthur", lastName: "Dent") + } + response { + status(201) + headers { + contentType(applicationJson()) + } + body(id: 42) + testMatchers { + jsonPath('''$.['lastName']''', byType()) + jsonPath('''$.['firstName']''', byType()) + } + } + } + when: + Collection contracts = converter.convertFrom(pact509Json) + then: + contracts == [expectedContract] + } + def "should convert from contract to pact"() { given: Collection inputContracts = [ diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_509.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_509.json new file mode 100644 index 0000000000..fb090543a0 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact_509.json @@ -0,0 +1,44 @@ +{ + "consumer": { + "name": "ui" + }, + "provider": { + "name": "userservice" + }, + "interactions": [ + { + "description": "a request to POST a person", + "providerState": "provider accepts a new person", + "request": { + "method": "POST", + "path": "/user-service/users", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "firstName": "Arthur", + "lastName": "Dent" + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json;charset=UTF-8" + }, + "body": { + "id": 42 + }, + "matchingRules": { + "$.body": { + "match": "type" + } + } + } + } + ], + "metadata": { + "pactSpecification": { + "version": "2.0.0" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java index 58ef30623d..3d73fc6e6e 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java @@ -71,7 +71,8 @@ class DelegatingJsonVerifiable implements MethodBufferingJsonVerifiable { @Override public MethodBufferingJsonVerifiable contains(Object value) { - DelegatingJsonVerifiable verifiable = new FinishedDelegatingJsonVerifiable(this.delegate.contains(value), this.methodsBuffer); + DelegatingJsonVerifiable verifiable = new FinishedDelegatingJsonVerifiable(this.delegate.jsonPath(), + this.delegate.contains(value), this.methodsBuffer); verifiable.appendMethodWithQuotedValue("contains", value); if (isAssertingAValueInArray()) { verifiable.methodsBuffer.offer(".value()"); @@ -145,7 +146,8 @@ class DelegatingJsonVerifiable implements MethodBufferingJsonVerifiable { @Override public MethodBufferingJsonVerifiable isEqualTo(String value) { - DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(this.delegate.isEqualTo(value), this.methodsBuffer); + DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(this.delegate.jsonPath(), + this.delegate.isEqualTo(value), this.methodsBuffer); if (this.delegate.isAssertingAValueInArray() && readyToCheck.methodsBuffer.peekLast().equals(".arrayField()")) { readyToCheck.appendMethodWithQuotedValue("isEqualTo", escapeJava(value)); readyToCheck.methodsBuffer.offer(".value()"); @@ -167,7 +169,8 @@ class DelegatingJsonVerifiable implements MethodBufferingJsonVerifiable { @Override public MethodBufferingJsonVerifiable isEqualTo(Number value) { - DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(this.delegate.isEqualTo(value), this.methodsBuffer); + DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(this.delegate.jsonPath(), + this.delegate.isEqualTo(value), this.methodsBuffer); // related to #271 - the problem is with asserting arrays of maps vs arrays of primitives String last = readyToCheck.methodsBuffer.peekLast(); boolean containsAMatcher = containsAnyMatcher(last); @@ -185,20 +188,23 @@ class DelegatingJsonVerifiable implements MethodBufferingJsonVerifiable { @Override public MethodBufferingJsonVerifiable isNull() { - DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(this.delegate.isNull(), this.methodsBuffer); + DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable( + this.delegate.jsonPath(), this.delegate.isNull(), this.methodsBuffer); readyToCheck.methodsBuffer.offer(".isNull()"); return readyToCheck; } @Override public MethodBufferingJsonVerifiable isEmpty() { - DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(this.delegate.isEmpty(), this.methodsBuffer); + DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable( + this.delegate.jsonPath(), this.delegate.isEmpty(), this.methodsBuffer); readyToCheck.methodsBuffer.offer(".isEmpty()"); return readyToCheck; } @Override public MethodBufferingJsonVerifiable matches(String value) { - DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(this.delegate.matches(value), this.methodsBuffer); + DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable( + this.delegate.jsonPath(), this.delegate.matches(value), this.methodsBuffer); if (this.delegate.isAssertingAValueInArray()) { readyToCheck.appendMethodWithQuotedValue("matches", escapedHackedJavaText(value)); readyToCheck.methodsBuffer.offer(".value()"); @@ -221,7 +227,8 @@ class DelegatingJsonVerifiable implements MethodBufferingJsonVerifiable { @Override public MethodBufferingJsonVerifiable isEqualTo(Boolean value) { - DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable(this.delegate.isEqualTo(value), this.methodsBuffer); + DelegatingJsonVerifiable readyToCheck = new FinishedDelegatingJsonVerifiable( + this.delegate.jsonPath(), this.delegate.isEqualTo(value), this.methodsBuffer); if (this.delegate.isAssertingAValueInArray()) { readyToCheck.methodsBuffer.offer(".value()"); } else { @@ -235,6 +242,10 @@ class DelegatingJsonVerifiable implements MethodBufferingJsonVerifiable { return new FinishedDelegatingJsonVerifiable(this.delegate, this.methodsBuffer); } + @Override public String keyBeforeChecking() { + return this.delegate.jsonPath(); + } + @Override public boolean assertsSize() { for (String s : this.methodsBuffer) { @@ -272,7 +283,8 @@ class DelegatingJsonVerifiable implements MethodBufferingJsonVerifiable { @Override public JsonVerifiable hasSize(int size) { - FinishedDelegatingJsonVerifiable verifiable = new FinishedDelegatingJsonVerifiable(this.delegate.hasSize(size), this.methodsBuffer); + FinishedDelegatingJsonVerifiable verifiable = new FinishedDelegatingJsonVerifiable( + this.delegate.jsonPath(), this.delegate.hasSize(size), this.methodsBuffer); verifiable.methodsBuffer.offer(".hasSize(" + size + ")"); return verifiable; } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/FinishedDelegatingJsonVerifiable.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/FinishedDelegatingJsonVerifiable.java index 6c3a3be898..ef8c79859b 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/FinishedDelegatingJsonVerifiable.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/FinishedDelegatingJsonVerifiable.java @@ -30,9 +30,21 @@ import com.toomuchcoding.jsonassert.JsonVerifiable; */ class FinishedDelegatingJsonVerifiable extends DelegatingJsonVerifiable { + final String keyBeforeChecking; + + FinishedDelegatingJsonVerifiable(String keyBeforeChecking, JsonVerifiable delegate, + LinkedList methodsBuffer) { + super(delegate, methodsBuffer); + this.keyBeforeChecking = keyBeforeChecking; + } + FinishedDelegatingJsonVerifiable(JsonVerifiable delegate, LinkedList methodsBuffer) { super(delegate, methodsBuffer); + this.keyBeforeChecking = delegate.jsonPath(); } + @Override public String keyBeforeChecking() { + return this.keyBeforeChecking; + } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBufferingJsonVerifiable.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBufferingJsonVerifiable.java index 49e0c82982..384343b6ca 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBufferingJsonVerifiable.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBufferingJsonVerifiable.java @@ -71,6 +71,8 @@ public interface MethodBufferingJsonVerifiable @Override MethodBufferingJsonVerifiable value(); + String keyBeforeChecking(); + boolean assertsSize(); boolean assertsConcreteValue();