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 b01af2283f..a27acccea1 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 @@ -329,7 +329,7 @@ abstract class MethodBodyBuilder { Object elementFromBody = value(copiedBody, it) if (it.minTypeOccurrence() != null || it.maxTypeOccurrence() != null) { checkType(bb, it, elementFromBody) - String method = "assertThat(parsedJson.read(${quotedAndEscaped(it.path())}, java.util.Collection.class).size()).${sizeCheckMethod(it)}" + String method = "assertThat(parsedJson.read(${quotedAndEscaped(it.path())}, java.util.Collection.class)).${sizeCheckMethod(it)}" bb.addLine(postProcessJsonPathCall(method)) addColonIfRequired(bb) } else { @@ -382,15 +382,24 @@ abstract class MethodBodyBuilder { } protected String sizeCheckMethod(BodyMatcher bodyMatcher) { + String prefix = sizeCheckPrefix(bodyMatcher) if (bodyMatcher.minTypeOccurrence() != null && bodyMatcher.maxTypeOccurrence() != null) { - return "isBetween(${bodyMatcher.minTypeOccurrence()}, ${bodyMatcher.maxTypeOccurrence()})" + return "${prefix}Between(${bodyMatcher.minTypeOccurrence()}, ${bodyMatcher.maxTypeOccurrence()})" } else if (bodyMatcher.minTypeOccurrence() != null ) { - return "isGreaterThanOrEqualTo(${bodyMatcher.minTypeOccurrence()})" + return "${prefix}GreaterThanOrEqualTo(${bodyMatcher.minTypeOccurrence()})" } else if (bodyMatcher.maxTypeOccurrence() != null) { - return "isLessThanOrEqualTo(${bodyMatcher.maxTypeOccurrence()})" + return "${prefix}LessThanOrEqualTo(${bodyMatcher.maxTypeOccurrence()})" } } + private String sizeCheckPrefix(BodyMatcher bodyMatcher) { + String prefix = "has" + if (bodyMatcher.path().contains("[*]")) { + prefix = prefix + "Flattened" + } + return prefix + "Size" + } + protected String quotedAndEscaped(String string) { return '"' + StringEscapeUtils.escapeJava(string) + '"' } diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/assertion/CollectionAssert.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/assertion/CollectionAssert.java index 013ad19494..5d6b746318 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/assertion/CollectionAssert.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/assertion/CollectionAssert.java @@ -1,6 +1,8 @@ package org.springframework.cloud.contract.verifier.assertion; +import java.util.Collection; import java.util.Iterator; +import java.util.Map; import org.assertj.core.api.IterableAssert; @@ -35,4 +37,114 @@ public class CollectionAssert extends IterableAssert { } return this; } + + /** + * Flattens the collection and checks whether size is greater than or equal to the provided value + * @param size - the flattened collection should have size greater than or equal to this value + * @return this + */ + public CollectionAssert hasFlattenedSizeGreaterThanOrEqualTo(int size) { + isNotNull(); + int flattenedSize = flattenedSize(0, this.actual); + if (!(flattenedSize >= size)) { + failWithMessage("The flattened size <%s> is not greater or equal to <%s>", flattenedSize, size); + } + return this; + } + + /** + * Flattens the collection and checks whether size is less than or equal to the provided value + * @param size - the flattened collection should have size less than or equal to this value + * @return this + */ + public CollectionAssert hasFlattenedSizeLessThanOrEqualTo(int size) { + isNotNull(); + int flattenedSize = flattenedSize(0, this.actual); + if (!(flattenedSize <= size)) { + failWithMessage("The flattened size <%s> is not less or equal to <%s>", flattenedSize, size); + } + return this; + } + + /** + * Flattens the collection and checks whether size is between the provided value + * @param lowerBound - the flattened collection should have size greater than or equal to this value + * @param higherBound - the flattened collection should have size less than or equal to this value + * @return this + */ + public CollectionAssert hasFlattenedSizeBetween(int lowerBound, int higherBound) { + isNotNull(); + int flattenedSize = flattenedSize(0, this.actual); + if (!(flattenedSize >= lowerBound && flattenedSize <= higherBound)) { + failWithMessage("The flattened size <%s> is not between <%s> and <%s>", flattenedSize, lowerBound, higherBound); + } + return this; + } + + /** + * Checks whether size is greater than or equal to the provided value + * @param size - the collection should have size greater than or equal to this value + * @return this + */ + public CollectionAssert hasSizeGreaterThanOrEqualTo(int size) { + isNotNull(); + int actualSize = size(this.actual); + if (!(actualSize >= size)) { + failWithMessage("The size <%s> is not greater or equal to <%s>", actualSize, size); + } + return this; + } + + /** + * Checks whether size is less than or equal to the provided value + * @param size - the collection should have size less than or equal to this value + * @return this + */ + public CollectionAssert hasSizeLessThanOrEqualTo(int size) { + isNotNull(); + int actualSize = size(this.actual); + if (!(actualSize <= size)) { + failWithMessage("The size <%s> is not less or equal to <%s>", actualSize, size); + } + return this; + } + + /** + * Checks whether size is between the provided value + * @param lowerBound - the collection should have size greater than or equal to this value + * @param higherBound - the collection should have size less than or equal to this value + * @return this + */ + public CollectionAssert hasSizeBetween(int lowerBound, int higherBound) { + isNotNull(); + int size = size(this.actual); + if (!(size >= lowerBound && size <= higherBound)) { + failWithMessage("The size <%s> is not between <%s> and <%s>", size, lowerBound, higherBound); + } + return this; + } + + private int flattenedSize(int counter, Object object) { + if (object instanceof Map) { + return counter + ((Map) object).size(); + } else if (object instanceof Iterator) { + Iterator iterator = ((Iterator) object); + while (iterator.hasNext()) { + Object next = iterator.next(); + counter = flattenedSize(counter, next); + } + return counter; + } else if (object instanceof Collection) { + return flattenedSize(counter, ((Collection) object).iterator()); + } + return counter; + } + + private int size(Iterable iterable) { + int size = 0; + for (Object value : iterable) { + size++; + } + return size; + } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/assertion/CollectionAssertTests.java b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/assertion/CollectionAssertTests.java index f8854e232a..ef6b74ad09 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/assertion/CollectionAssertTests.java +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/assertion/CollectionAssertTests.java @@ -2,7 +2,9 @@ package org.springframework.cloud.contract.verifier.assertion; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.assertj.core.api.Assertions; import org.junit.Test; @@ -55,6 +57,206 @@ public class CollectionAssertTests { } } + @Test + public void should_not_throw_an_exception_when_flattened_size_is_greater_than_or_equal_to_provided_size() { + Collection collection = nestedCollection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasFlattenedSizeGreaterThanOrEqualTo(0) + .hasFlattenedSizeGreaterThanOrEqualTo(4); + } + + @Test + public void should_throw_an_exception_when_flattened_size_is_not_greater_than_or_equal_to_provided_size() { + Collection collection = nestedCollection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeGreaterThanOrEqualTo(5); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The flattened size <4> is not greater or equal to <5>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_flattened_greater_than_or_equal() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeGreaterThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + + @Test + public void should_not_throw_an_exception_when_flattened_size_is_less_than_or_equal_to_provided_size() { + Collection collection = nestedCollection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasFlattenedSizeLessThanOrEqualTo(5) + .hasFlattenedSizeLessThanOrEqualTo(4); + } + + @Test + public void should_throw_an_exception_when_flattened_size_is_not_less_than_or_equal_to_provided_size() { + Collection collection = nestedCollection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeLessThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The flattened size <4> is not less or equal to <1>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_flattened_less_than_or_equal() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeLessThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + + + @Test + public void should_not_throw_an_exception_when_flattened_size_is_between_the_provided_sizes() { + Collection collection = nestedCollection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasFlattenedSizeBetween(1, 5) + .hasFlattenedSizeBetween(4, 4); + } + + @Test + public void should_throw_an_exception_when_flattened_size_is_not_between_the_provided_sizes() { + Collection collection = nestedCollection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeBetween(5, 7); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The flattened size <4> is not between <5> and <7>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_flattened_between() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeBetween(1, 2); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + + @Test + public void should_not_throw_an_exception_when_size_is_greater_than_or_equal_to_provided_size() { + Collection collection = collection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasSizeGreaterThanOrEqualTo(0) + .hasSizeGreaterThanOrEqualTo(3); + } + + @Test + public void should_throw_an_exception_when_size_is_not_greater_than_or_equal_to_provided_size() { + Collection collection = collection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeGreaterThanOrEqualTo(5); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The size <3> is not greater or equal to <5>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_greater_than_or_equal() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeGreaterThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + + @Test + public void should_not_throw_an_exception_when_size_is_less_than_or_equal_to_provided_size() { + Collection collection = collection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasSizeLessThanOrEqualTo(4) + .hasSizeLessThanOrEqualTo(3); + } + + @Test + public void should_throw_an_exception_when_size_is_not_less_than_or_equal_to_provided_size() { + Collection collection = collection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeLessThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The size <3> is not less or equal to <1>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_less_than_or_equal() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeLessThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + + + @Test + public void should_not_throw_an_exception_when_size_is_between_the_provided_sizes() { + Collection collection = collection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasSizeBetween(1, 4) + .hasSizeBetween(3, 3); + } + + @Test + public void should_throw_an_exception_when_size_is_not_between_the_provided_sizes() { + Collection collection = collection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeBetween(5, 7); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The size <3> is not between <5> and <7>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_between() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeBetween(1, 2); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + private Collection collection() { List list = new ArrayList<>(); list.add("a"); @@ -63,4 +265,21 @@ public class CollectionAssertTests { return list; } + private Collection nestedCollection() { + List list = new ArrayList<>(); + List list1 = new ArrayList<>(); + Map map1 = new HashMap<>(); + map1.put("a", "1"); + map1.put("b", "2"); + map1.put("c", "3"); + List list2 = new ArrayList<>(); + Map map2 = new HashMap<>(); + map2.put("d", "4"); + list.add(list1); + list.add(list2); + list1.add(map1); + list2.add(map2); + return list; + } + } \ No newline at end of file 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 800a53b98c..4bf74dc0e4 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 @@ -149,15 +149,15 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements test.contains('assertThat(parsedJson.read("' + rootElement + '.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])")') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithTypeMatch")).isInstanceOf(java.lang.String.class)') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMin")).isInstanceOf(java.util.List.class)') - test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMin", java.util.Collection.class).size()).isGreaterThanOrEqualTo(1)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMin", java.util.Collection.class)).hasSizeGreaterThanOrEqualTo(1)') 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(parsedJson.read("' + rootElement + '.valueWithMax", java.util.Collection.class)).hasSizeLessThanOrEqualTo(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()).isBetween(1, 3)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMinMax", java.util.Collection.class)).hasSizeBetween(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(parsedJson.read("' + rootElement + '.valueWithMinEmpty", java.util.Collection.class)).hasSizeGreaterThanOrEqualTo(0)') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMaxEmpty")).isInstanceOf(java.util.List.class)') - test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMaxEmpty", java.util.Collection.class).size()).isLessThanOrEqualTo(0)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMaxEmpty", java.util.Collection.class)).hasSizeLessThanOrEqualTo(0)') !test.contains('cursor') and: SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, blockBuilder.toString()) @@ -238,4 +238,58 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$' } + + @Issue('#217') + def "should use the flattened assertions when jsonpath contains [*] for [#methodBuilderName]"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + url 'person' + } + response { + status 200 + body([ + "phoneNumbers": [ + number: "foo" + ] + ]) + testMatchers { + jsonPath('$.phoneNumbers[*].number', byType { + minOccurrence(0) + maxOccurrence(4) + }) + jsonPath('$.phoneNumbers[*].number', byType { + minOccurrence(0) + }) + jsonPath('$.phoneNumbers[*].number', byType { + maxOccurrence(4) + }) + } + } + } + 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)).hasFlattenedSizeBetween(0, 4)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).hasFlattenedSizeGreaterThanOrEqualTo(0)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).hasFlattenedSizeLessThanOrEqualTo(4)') + !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) } | '$' + } + }