Merge branch '1.0.x'

This commit is contained in:
Marcin Grzejszczak
2017-01-11 19:13:59 +01:00
12 changed files with 151 additions and 76 deletions

View File

@@ -263,8 +263,8 @@ part of your contract. The other is called `testMatchers` which is present in th
Currently we support only JSON Path based matchers with the following matching possibilities.
For `stubMatchers`:
- `byValue(...)` - the value taken from the response via the provided JSON Path needs
to be equal to the provided value
- `byEquality()` - the value taken from the response via the provided JSON Path needs
to be equal to the provided value in the contract
- `byRegex(...)` - the value taken from the response via the provided JSON Path needs
to match the regex
- `byDate()` - the value taken from the response via the provided JSON Path needs to
@@ -276,8 +276,8 @@ match the regex for ISO Time
For `testMatchers`:
- `byValue(...)` - the value taken from the response via the provided JSON Path needs
to be equal to the provided value
- `byEquality()` - the value taken from the response via the provided JSON Path needs
to be equal to the provided value in the contract
- `byRegex(...)` - the value taken from the response via the provided JSON Path needs
to match the regex
- `byDate()` - the value taken from the response via the provided JSON Path needs to
@@ -319,38 +319,38 @@ assertions and the one from matchers with an `and` section):
[source,java,indent=0]
----
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/json")
.body("{\"duck\":123,\"alpha\":\"abc\",\"number\":123,\"aBoolean\":true,\"date\":\"2017-01-01\",\"dateTime\":\"2017-01-01T01:23:45\",\"time\":\"01:02:34\",\"valueWithoutAMatcher\":\"foo\",\"valueWithTypeMatch\":\"string\"}");
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/json")
.body("{\"duck\":123,\"alpha\":\"abc\",\"number\":123,\"aBoolean\":true,\"date\":\"2017-01-01\",\"dateTime\":\"2017-01-01T01:23:45\",\"time\":\"01:02:34\",\"valueWithoutAMatcher\":\"foo\",\"valueWithTypeMatch\":\"string\"}");
// when:
ResponseOptions response = given().spec(request)
.get("/get");
// when:
ResponseOptions response = given().spec(request)
.get("/get");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("valueWithoutAMatcher").isEqualTo("foo");
// and:
assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}");
assertThat(parsedJson.read("$.duck", Integer.class)).isEqualTo(123);
assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*");
assertThat(parsedJson.read("$.alpha", String.class)).isEqualTo("abc");
assertThat(parsedJson.read("$.number", String.class)).matches("-?\\d*(\\.\\d+)?");
assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)");
assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])");
assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(java.lang.String.class);
assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMin", java.util.Collection.class).size()).isLessThanOrEqualTo(1);
assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMax", java.util.Collection.class).size()).isGreaterThanOrEqualTo(3);
assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMinMax", java.util.Collection.class).size()).isStrictlyBetween(1, 3);
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("valueWithoutAMatcher").isEqualTo("foo");
// and:
assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}");
assertThat(parsedJson.read("$.duck", Integer.class)).isEqualTo(123);
assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*");
assertThat(parsedJson.read("$.alpha", String.class)).isEqualTo("abc");
assertThat(parsedJson.read("$.number", String.class)).matches("-?\\d*(\\.\\d+)?");
assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)");
assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])");
assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(java.lang.String.class);
assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMin", java.util.Collection.class).size()).isLessThanOrEqualTo(1);
assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMax", java.util.Collection.class).size()).isGreaterThanOrEqualTo(3);
assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMinMax", java.util.Collection.class).size()).isStrictlyBetween(1, 3);
----
and the WireMock stub like this:

View File

@@ -46,9 +46,8 @@ class BodyMatchers {
return new MatchingTypeValue(MatchingType.REGEX, regex)
}
MatchingTypeValue byValue(Object value) {
assert value
return new MatchingTypeValue(MatchingType.EQUALITY, value)
MatchingTypeValue byEquality() {
return new MatchingTypeValue(MatchingType.EQUALITY, null)
}
boolean equals(o) {

View File

@@ -11,5 +11,32 @@ import groovy.transform.CompileStatic
*/
@CompileStatic
enum MatchingType {
EQUALITY, TYPE, DATE, TIME, TIMESTAMP, REGEX
/**
* The default approach - ensures that the value received
* in a body for the given path is as presented in the contract body
*/
EQUALITY,
/**
* Verification by type - is the type received in a body
* for the given path is of the same type. If it's a collection
* you can verify number of occurrences
*/
TYPE,
/**
* Special version of regex for date check
*/
DATE,
/**
* Special version of regex for time check
*/
TIME,
/**
* Special version of regex for timestamp check
*/
TIMESTAMP,
/**
* Verification if the value for the given path matches the
* provided regex
*/
REGEX
}

View File

@@ -77,7 +77,7 @@ class StubRunnerCamelPredicate implements Predicate {
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.jsonPathMatchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher);
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(parsedJson, jsonPath);
}
}

View File

@@ -76,7 +76,7 @@ class StubRunnerIntegrationMessageSelector implements MessageSelector {
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.jsonPathMatchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher);
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(parsedJson, jsonPath);
}
}

View File

@@ -76,7 +76,7 @@ class StubRunnerStreamMessageSelector implements MessageSelector {
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.jsonPathMatchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher);
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(parsedJson, jsonPath);
}
}

View File

@@ -425,9 +425,9 @@ class DslToWireMockClientConverterSpec extends Specification {
])
stubMatchers {
jsonPath('$.duck', byRegex("[0-9]{3}"))
jsonPath('$.duck', byValue(123))
jsonPath('$.duck', byEquality())
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
jsonPath('$.alpha', byValue("abc"))
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()))
jsonPath('$.aBoolean', byRegex(anyBoolean()))
jsonPath('$.date', byDate())
@@ -464,10 +464,10 @@ class DslToWireMockClientConverterSpec extends Specification {
testMatchers {
// asserts the jsonpath value against manual regex
jsonPath('$.duck', byRegex("[0-9]{3}"))
jsonPath('$.duck', byValue(123))
jsonPath('$.duck', byEquality())
// asserts the jsonpath value against some default regex
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
jsonPath('$.alpha', byValue("abc"))
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()))
jsonPath('$.aBoolean', byRegex(anyBoolean()))
// asserts vs inbuilt time related regex

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.contract.verifier.builder
import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.PathNotFoundException
import groovy.json.JsonOutput
import groovy.transform.PackageScope
import groovy.transform.TypeChecked
@@ -311,19 +312,17 @@ abstract class MethodBodyBuilder {
bb.startBlock()
// for the rest we'll do JsonPath matching in brute force
bodyMatchers.jsonPathMatchers().each {
if (it.value()) {
if (it.value() || it.matchingType() == MatchingType.EQUALITY) {
String comparisonMethod = it.matchingType() == MatchingType.EQUALITY ? "isEqualTo" : "matches"
String valueAsParam = it.value() instanceof String ? quotedAndEscaped(it.value().toString()) : it.value().toString()
String classToCastTo = "${it.value().class.simpleName}.class"
Object retrievedValue = value(copiedBody, it)
String valueAsParam = retrievedValue instanceof String ? quotedAndEscaped(retrievedValue.toString()) : retrievedValue.toString()
String classToCastTo = "${retrievedValue.class.simpleName}.class"
String path = quotedAndEscaped(it.path())
String method = "assertThat(parsedJson.read(${path}, ${classToCastTo})).${comparisonMethod}(${valueAsParam})"
bb.addLine(postProcessJsonPathCall(method))
addColonIfRequired(bb)
} else {
Object elementFromBody = JsonPath.parse(copiedBody).read(it.path())
if (!elementFromBody) {
throw new IllegalStateException("Entry for the provided JSON path [${it.path()}] doesn't exist in the body [${JsonOutput.toJson(copiedBody)}]")
}
Object elementFromBody = value(copiedBody, it)
if (it.minTypeOccurrence() || it.maxTypeOccurrence()) {
checkType(bb, it, elementFromBody)
String method = "assertThat(parsedJson.read(${quotedAndEscaped(it.path())}, java.util.Collection.class).size()).${sizeCheckMethod(it)}"
@@ -338,6 +337,21 @@ abstract class MethodBodyBuilder {
processBodyElement(bb, "", convertedResponseBody)
}
protected Object value(def body, BodyMatcher bodyMatcher) {
if (bodyMatcher.matchingType() == MatchingType.EQUALITY || !bodyMatcher.value()) {
return retrieveObjectByPath(body, bodyMatcher.path())
}
return bodyMatcher.value()
}
protected Object retrieveObjectByPath(def body, String path) {
try {
return JsonPath.parse(body).read(path)
} catch (PathNotFoundException e) {
throw new IllegalStateException("Entry for the provided JSON path [${path}] doesn't exist in the body [${JsonOutput.toJson(body)}]", e)
}
}
protected void checkType(BlockBuilder bb, BodyMatcher it, Object elementFromBody) {
String method = "assertThat((Object) parsedJson.read(${quotedAndEscaped(it.path())})).isInstanceOf(${classToCheck(elementFromBody).name}.class)"
bb.addLine(postProcessJsonPathCall(method))

View File

@@ -75,8 +75,8 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
}
ContentType contentType = tryToGetContentType(request.body.clientValue, request.headers)
if (contentType == ContentType.JSON) {
def body = getMatchingStrategyFromBody(request.body)?.clientValue
body = JsonToJsonPathsConverter.removeMatchingJsonPaths(body, request.matchers)
def originalBody = getMatchingStrategyFromBody(request.body)?.clientValue
def body = JsonToJsonPathsConverter.removeMatchingJsonPaths(originalBody, request.matchers)
JsonPaths values = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(body)
if (values.empty && !request.matchers?.hasMatchers()) {
requestPattern.withRequestBody(WireMock.equalToJson(JsonOutput.toJson(getMatchingStrategy(request.body.clientValue).clientValue), false, false))
@@ -87,7 +87,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
}
if (request.matchers?.hasMatchers()) {
request.matchers.jsonPathMatchers().each {
String newPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(it)
String newPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(it, originalBody)
requestPattern.withRequestBody(WireMock.matchingJsonPath(newPath.replace("\\\\", "\\")))
}
}

View File

@@ -16,19 +16,18 @@
package org.springframework.cloud.contract.verifier.util
import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.PathNotFoundException
import com.toomuchcoding.jsonassert.JsonAssertion
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Slf4j
import org.springframework.cloud.contract.spec.internal.BodyMatcher
import org.springframework.cloud.contract.spec.internal.BodyMatchers
import org.springframework.cloud.contract.spec.internal.ExecutionProperty
import org.springframework.cloud.contract.spec.internal.MatchingType
import org.springframework.cloud.contract.spec.internal.OptionalProperty
import org.springframework.cloud.contract.spec.internal.*
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
import java.util.regex.Pattern
/**
* I would like to apologize to anyone who is reading this class. Since JSON is a hectic structure
* this class is also hectic. The idea is to traverse the JSON structure and build a set of
@@ -71,12 +70,14 @@ class JsonToJsonPathsConverter {
* @return json with removed entries
*/
static def removeMatchingJsonPaths(def json, BodyMatchers bodyMatchers) {
def jsonCopy = json.clone()
DocumentContext context = JsonPath.parse(jsonCopy)
if (bodyMatchers?.hasMatchers()) {
bodyMatchers.jsonPathMatchers().each { BodyMatcher matcher ->
JsonPath.parse(json).delete(matcher.path())
context.delete(matcher.path())
}
}
return json
return jsonCopy
}
/**
@@ -86,23 +87,31 @@ class JsonToJsonPathsConverter {
* @param bodyMatcher
* @return JSON path that checks the regex for its last element
*/
static String convertJsonPathAndRegexToAJsonPath(BodyMatcher bodyMatcher) {
static String convertJsonPathAndRegexToAJsonPath(BodyMatcher bodyMatcher, def body = null) {
String path = bodyMatcher.path()
Object value = bodyMatcher.value()
if (!value) {
if (value == null && bodyMatcher.matchingType() != MatchingType.EQUALITY) {
return path
}
int lastIndexOfDot = path.lastIndexOf(".")
String toLastDot = path.substring(0, lastIndexOfDot)
String fromLastDot = path.substring(lastIndexOfDot + 1)
String comparison = createComparison(bodyMatcher, value)
String comparison = createComparison(bodyMatcher, value, body)
return "${toLastDot}[?(@.${fromLastDot} ${comparison})]"
}
private static String createComparison(BodyMatcher bodyMatcher, Object value) {
private static String createComparison(BodyMatcher bodyMatcher, Object value, def body) {
if (bodyMatcher.matchingType() == MatchingType.EQUALITY) {
String wrappedValue = value instanceof Number ? value : "'${value.toString()}'"
return "== ${wrappedValue}"
if (!body) {
throw new IllegalStateException("Body hasn't been passed")
}
try {
Object retrievedValue = JsonPath.parse(body).read(bodyMatcher.path())
String wrappedValue = retrievedValue instanceof Number ? retrievedValue : "'${retrievedValue.toString()}'"
return "== ${wrappedValue}"
} catch (PathNotFoundException e) {
throw new IllegalStateException("Value [${bodyMatcher.path()}] not found in JSON [${JsonOutput.toJson(body)}]", e)
}
} else {
return "=~ /(${value})/"
}

View File

@@ -51,9 +51,9 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
])
stubMatchers {
jsonPath('$.duck', byRegex("[0-9]{3}"))
jsonPath('$.duck', byValue(123))
jsonPath('$.duck', byEquality())
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
jsonPath('$.alpha', byValue("abc"))
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()))
jsonPath('$.aBoolean', byRegex(anyBoolean()))
jsonPath('$.date', byDate())
@@ -90,10 +90,10 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
// asserts the jsonpath value against manual regex
jsonPath('$.duck', byRegex("[0-9]{3}"))
// asserts the jsonpath value against the provided value
jsonPath('$.duck', byValue(123))
jsonPath('$.duck', byEquality())
// asserts the jsonpath value against some default regex
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
jsonPath('$.alpha', byValue("abc"))
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()))
jsonPath('$.aBoolean', byRegex(anyBoolean()))
// asserts vs inbuilt time related regex

View File

@@ -766,17 +766,19 @@ class JsonToJsonPathsConverterSpec extends Specification {
def "should convert a json path with value to a equality checking json path without quotes for numbers"() {
given:
String jsonPath = '$.a.b.c.d'
Integer value = 1234
and:
def body = [ a: [ b: [ c: [ d: 1234 ] ] ] ]
expect:
'$.a.b.c[?(@.d == 1234)]' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.EQUALITY, jsonPath, value))
'$.a.b.c[?(@.d == 1234)]' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.EQUALITY, jsonPath, null), body)
}
def "should convert a json path with value to a equality checking json path with quotes for strings"() {
given:
String jsonPath = '$.a.b.c.d'
String value = "foo"
and:
def body = [ a: [ b: [ c: [ d: "foo" ] ] ] ]
expect:
'$.a.b.c[?(@.d == \'foo\')]' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.EQUALITY, jsonPath, value))
'$.a.b.c[?(@.d == \'foo\')]' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.EQUALITY, jsonPath, null), body)
}
def "should return the path if no value is provided"() {
@@ -786,6 +788,30 @@ class JsonToJsonPathsConverterSpec extends Specification {
'$.a.b.c.d' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.REGEX, jsonPath, null))
}
def "should throw an exception when null body is passed to check for equality"() {
given:
String jsonPath = '$.a.b.c.d'
and:
def body = null
when:
JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.EQUALITY, jsonPath, null), body)
then:
IllegalStateException e = thrown(IllegalStateException)
e.message.contains("Body")
}
def "should throw an exception when nonexisting jsonpath is passed to check for equality"() {
given:
String jsonPath = '$.a.b.c.d'
and:
def body = [ foo: "bar" ]
when:
JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.EQUALITY, jsonPath, null), body)
then:
IllegalStateException e = thrown(IllegalStateException)
e.message.contains("not found")
}
private BodyMatcher matcher(final MatchingType matchingType, final String jsonPath, final Object value) {
return new BodyMatcher() {
@Override