From ab7fe13aac30224a2979a3cc0059633dbc4adafe Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 8 Feb 2017 11:41:59 +0100 Subject: [PATCH] Adding an option to validate children nodes in an array (#219) without this change it's impossible to assert with response matchers all elements of an array with this change we're using AssertJs conditions in case the pattern contains [*] fixes #217 --- docs/src/main/asciidoc/verifier/contract.adoc | 2 +- .../spec/internal/MatchingType.groovy | 7 ++ .../spec/internal/MatchingTypeSpec.groovy | 22 ++++++ .../verifier/builder/BlockBuilder.groovy | 17 ++++- .../verifier/builder/MethodBodyBuilder.groovy | 33 ++++++-- .../util/JsonToJsonPathsConverter.groovy | 8 +- ...vcMethodBodyBuilderWithMatchersSpec.groovy | 75 ++++++++++++++++++- 7 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/MatchingTypeSpec.groovy diff --git a/docs/src/main/asciidoc/verifier/contract.adoc b/docs/src/main/asciidoc/verifier/contract.adoc index 68641ef9a2..6d1647a16d 100644 --- a/docs/src/main/asciidoc/verifier/contract.adoc +++ b/docs/src/main/asciidoc/verifier/contract.adoc @@ -342,7 +342,7 @@ assertions and the one from matchers with an `and` section): assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class); assertThat(parsedJson.read("$.valueWithMax", java.util.Collection.class).size()).isLessThanOrEqualTo(3); assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class); - assertThat(parsedJson.read("$.valueWithMinMax", java.util.Collection.class).size()).isStrictlyBetween(1, 3); + assertThat(parsedJson.read("$.valueWithMinMax", java.util.Collection.class).size()).isBetween(1, 3); ---- and the WireMock stub like this: diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingType.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingType.groovy index 16395c1aec..88704faff4 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingType.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MatchingType.groovy @@ -39,4 +39,11 @@ enum MatchingType { * provided regex */ REGEX + + static boolean regexRelated(MatchingType type) { + if (type == EQUALITY || type == TYPE ) { + return false + } + return true + } } diff --git a/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/MatchingTypeSpec.groovy b/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/MatchingTypeSpec.groovy new file mode 100644 index 0000000000..6673e46123 --- /dev/null +++ b/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/MatchingTypeSpec.groovy @@ -0,0 +1,22 @@ +package org.springframework.cloud.contract.spec.internal + +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +class MatchingTypeSpec extends Specification { + def "should return [#expected] for type [#type]"() { + expect: + MatchingType.regexRelated(type) == expected + where: + type | expected + MatchingType.EQUALITY | false + MatchingType.TYPE | false + MatchingType.REGEX | true + MatchingType.DATE | true + MatchingType.TIME | true + MatchingType.TIMESTAMP | true + + } +} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy index 08d35ccdec..c19d25c558 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy @@ -90,7 +90,11 @@ class BlockBuilder { } BlockBuilder addAtTheEnd(String toAdd) { - if (builder.charAt(builder.length() - 1) as String == '\n') { + String lastChar = builder.charAt(builder.length() - 1) as String + String secondLastChar = builder.length() >= 2 ? builder.charAt(builder.length() - 2) as String : "" + if (endsWithNewLine(lastChar) && aSpecialSign(secondLastChar, toAdd)) { + return this + } else if (endsWithNewLine(lastChar) && !aSpecialSign(secondLastChar, toAdd)) { builder.replace(builder.length() - 1, builder.length(), toAdd) builder << '\n' } else { @@ -99,6 +103,17 @@ class BlockBuilder { return this } + private boolean endsWithNewLine(String character) { + return character as String == '\n' + } + + private boolean aSpecialSign(String character, String toAdd) { + if (!character) { + return false + } + return character == "{" || character == toAdd + } + @Override String toString() { return builder.toString() diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy index e9dd894502..8a1e21a7ff 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy @@ -313,13 +313,17 @@ abstract class MethodBodyBuilder { // for the rest we'll do JsonPath matching in brute force bodyMatchers.jsonPathMatchers().each { if (it.value() || it.matchingType() == MatchingType.EQUALITY) { - String comparisonMethod = it.matchingType() == MatchingType.EQUALITY ? "isEqualTo" : "matches" + String path = quotedAndEscaped(it.path()) 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)) + if (path.contains("[*]") && MatchingType.regexRelated(it.matchingType())) { + buildCustomMatchingConditionForEachElement(bb, path, valueAsParam) + } else { + String comparisonMethod = it.matchingType() == MatchingType.EQUALITY ? "isEqualTo" : "matches" + String classToCastTo = "${retrievedValue.class.simpleName}.class" + String method = "assertThat(parsedJson.read(${path}, ${classToCastTo})).${comparisonMethod}(${valueAsParam})" + bb.addLine(postProcessJsonPathCall(method)) + } addColonIfRequired(bb) } else { Object elementFromBody = value(copiedBody, it) @@ -337,6 +341,23 @@ abstract class MethodBodyBuilder { processBodyElement(bb, "", convertedResponseBody) } + protected void buildCustomMatchingConditionForEachElement(BlockBuilder bb, String path, String valueAsParam) { + String method = "assertThat(parsedJson.read(${path}, java.util.Collection.class)).as(\"All elements match regex\").are(" + String newCondition = "new org.assertj.core.api.Condition() {" + String overriddenMethod = "@Override public boolean matches(Object o) {" + String matches = "return ((String)o).matches(${valueAsParam})" + String methodEnd = "}" + String classEnd = "})" + bb.addLine(postProcessJsonPathCall(method)) + bb.startBlock().startBlock().addLine(newCondition) + bb.startBlock().addLine(overriddenMethod) + bb.startBlock().addLine(postProcessJsonPathCall(matches)) + addColonIfRequired(bb) + bb.endBlock().addLine(methodEnd) + bb.endBlock().addLine(classEnd) + bb.endBlock().endBlock() + } + protected Object value(def body, BodyMatcher bodyMatcher) { if (bodyMatcher.matchingType() == MatchingType.EQUALITY || !bodyMatcher.value()) { return retrieveObjectByPath(body, bodyMatcher.path()) @@ -374,7 +395,7 @@ abstract class MethodBodyBuilder { protected String sizeCheckMethod(BodyMatcher bodyMatcher) { if (bodyMatcher.minTypeOccurrence() != null && bodyMatcher.maxTypeOccurrence() != null) { - return "isStrictlyBetween(${bodyMatcher.minTypeOccurrence()}, ${bodyMatcher.maxTypeOccurrence()})" + return "isBetween(${bodyMatcher.minTypeOccurrence()}, ${bodyMatcher.maxTypeOccurrence()})" } else if (bodyMatcher.minTypeOccurrence() != null ) { return "isGreaterThanOrEqualTo(${bodyMatcher.minTypeOccurrence()})" } else if (bodyMatcher.maxTypeOccurrence() != null) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy index c60638859e..1e4ae5a137 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy @@ -74,7 +74,13 @@ class JsonToJsonPathsConverter { DocumentContext context = JsonPath.parse(jsonCopy) if (bodyMatchers?.hasMatchers()) { bodyMatchers.jsonPathMatchers().each { BodyMatcher matcher -> - context.delete(matcher.path()) + try { + context.delete(matcher.path()) + } catch (RuntimeException e) { + if (log.isDebugEnabled()) { + log.debug("Exception occurred while trying to delete path [${matcher.path()}]", e) + } + } } } return jsonCopy diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy index 49ca4b00fd..08f66302f7 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy @@ -153,7 +153,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMax")).isInstanceOf(java.util.List.class)') test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMax", java.util.Collection.class).size()).isLessThanOrEqualTo(3)') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMinMax")).isInstanceOf(java.util.List.class)') - test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMinMax", java.util.Collection.class).size()).isStrictlyBetween(1, 3)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMinMax", java.util.Collection.class).size()).isBetween(1, 3)') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMinEmpty")).isInstanceOf(java.util.List.class)') test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMinEmpty", java.util.Collection.class).size()).isGreaterThanOrEqualTo(0)') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMaxEmpty")).isInstanceOf(java.util.List.class)') @@ -169,4 +169,77 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$' } + @Issue('#217') + def "should allow complex matchers for [#methodBuilderName]"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + url 'person' + } + response { + status 200 + body([ + "firstName": "Jane", + "lastName": "Doe", + "isAlive": true, + "address": [ + "postalCode": "98101", + ], + "phoneNumbers": [ + [ + "type": "home", + "number": "999 999-9999", + ] + ], + "gender": [ + "type": "female", + ], + "children": [ + [ + "firstName": "Kid", + "age": 55, + ] + ], + ]) + testMatchers { + jsonPath('$.phoneNumbers', byType { + minOccurrence(0) // min occurrence of 1 + maxOccurrence(4) // max occurrence of 3 + }) + jsonPath('$.phoneNumbers[*].number', byRegex("^[0-9]{3} [0-9]{3}-[0-9]{4}\$")) + jsonPath('$..number', byRegex("^[0-9]{3} [0-9]{3}-[0-9]{4}\$")) + } + + headers { + contentType('application/json') + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('assertThat(parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).as("All elements match regex").are(') + test.contains('new org.assertj.core.api.Condition() {') + test.contains('@Override public boolean matches(Object o) {') + test.contains('return ((String)o).matches("^[0-9]{3} [0-9]{3}-[0-9]{4}' + rootElement + '")') + test.contains('assertThat(parsedJson.read("' + rootElement + '..number", String.class)).matches("^[0-9]{3} [0-9]{3}-[0-9]{4}' + rootElement + '")') + !test.contains('cursor') + and: + try { + SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, blockBuilder.toString()) + } catch(NoClassDefFoundError error) { + // that's actually expected since we're creating an anonymous class + } + where: + methodBuilderName | methodBuilder | rootElement + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$' + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '$' + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$' + "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$' + } + }