From b0e33b3e90c3f67393358fdba8e3df8e972ab502 Mon Sep 17 00:00:00 2001 From: bono007 Date: Tue, 17 Nov 2020 01:31:53 -0600 Subject: [PATCH] Adds handling for empty list and empty map response body (#1556) Fixes gh-1423 --- .../builder/JsonBodyVerificationBuilder.java | 24 ++++++- .../util/JsonToJsonPathsConverter.groovy | 21 ++++--- .../SpringTestMethodBodyBuildersSpec.groovy | 60 ++++++++++++++++++ .../YamlMockMvcMethodBodyBuilderSpec.groovy | 62 +++++++++++++++++++ 4 files changed, 159 insertions(+), 8 deletions(-) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.java index b188e442ba..d842d10233 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.java @@ -17,6 +17,7 @@ package org.springframework.cloud.contract.verifier.builder; import java.math.BigDecimal; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; @@ -102,6 +103,12 @@ class JsonBodyVerificationBuilder implements BodyMethodGeneration, ClassVerifier Object copiedBody = cloneBody(convertedResponseBody); convertedResponseBody = JsonToJsonPathsConverter .removeMatchingJsonPaths(convertedResponseBody, bodyMatchers); + + // If it was a map or list where all elements covered by matchers - json paths + // should not include empty check + boolean includeEmptyCheck = !mapOrListBodyReducedToEmpty(copiedBody, + convertedResponseBody); + // remove quotes from fromRequest objects before picking json paths TestSideRequestTemplateModel templateModel = hasRequestBody() ? TestSideRequestTemplateModel.from(contract.getRequest()) : null; @@ -109,7 +116,8 @@ class JsonBodyVerificationBuilder implements BodyMethodGeneration, ClassVerifier returnReferencedEntries(templateModel), parsingClosure); JsonPaths jsonPaths = new JsonToJsonPathsConverter(assertJsonSize) .transformToJsonPathWithTestsSideValues(convertedResponseBody, - parsingClosure); + parsingClosure, includeEmptyCheck); + DocumentContext finalParsedRequestBody = parsedRequestBody; jsonPaths.forEach(it -> { String method = it.method(); @@ -123,6 +131,20 @@ class JsonBodyVerificationBuilder implements BodyMethodGeneration, ClassVerifier return convertedResponseBody; } + private boolean mapOrListBodyReducedToEmpty(Object originalBody, + Object convertedBody) { + int origSize = originalBody instanceof Map ? ((Map) originalBody).size() : -1; + int convertedSize = convertedBody instanceof Map ? ((Map) convertedBody).size() + : -1; + if (origSize > 0 && convertedSize == 0) { + return true; + } + origSize = originalBody instanceof List ? ((List) originalBody).size() : -1; + convertedSize = convertedBody instanceof List ? ((List) convertedBody).size() + : -1; + return (origSize > 0 && convertedSize == 0); + } + private boolean hasRequestBody() { return contract.getRequest() != null && contract.getRequest().getBody() != 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 da8ecc280d..8113c1333b 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 @@ -320,13 +320,15 @@ class JsonToJsonPathsConverter { } JsonPaths transformToJsonPathWithTestsSideValues(def json, - Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE) { - return transformToJsonPathWithValues(json, SERVER_SIDE, parsingClosure) + Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE, + boolean includeEmptyCheck = false) { + return transformToJsonPathWithValues(json, SERVER_SIDE, parsingClosure, includeEmptyCheck) } JsonPaths transformToJsonPathWithStubsSideValues(def json, - Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE) { - return transformToJsonPathWithValues(json, CLIENT_SIDE, parsingClosure) + Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE, + boolean includeEmptyCheck = false) { + return transformToJsonPathWithValues(json, CLIENT_SIDE, parsingClosure, includeEmptyCheck) } static JsonPaths transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(def json, @@ -336,11 +338,11 @@ class JsonToJsonPathsConverter { } private JsonPaths transformToJsonPathWithValues(def json, boolean clientSide, - Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE) { - if (!json) { + Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE, + boolean includeEmptyCheck = false) { + if (json == null || (!json && !includeEmptyCheck)) { return new JsonPaths() } - JsonPaths pathsAndValues = [] as Set Object convertedJson = MapConverter. getClientOrServerSideValues(json, clientSide, parsingClosure) Object jsonWithPatterns = ContentUtils. @@ -349,6 +351,11 @@ class JsonToJsonPathsConverter { new DelegatingJsonVerifiable(JsonAssertion. assertThat(JsonOutput.toJson(jsonWithPatterns)) .withoutThrowingException()) + JsonPaths pathsAndValues = [] as Set + if (isRootElement(methodBufferingJsonPathVerifiable) && !json) { + pathsAndValues.add(methodBufferingJsonPathVerifiable.isEmpty()) + return pathsAndValues + } traverseRecursivelyForKey(jsonWithPatterns, methodBufferingJsonPathVerifiable, { MethodBufferingJsonVerifiable key, Object value -> if (value instanceof ExecutionProperty || !(key instanceof FinishedDelegatingJsonVerifiable)) { diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy index 61cd073a92..d9a1639071 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy @@ -578,6 +578,66 @@ class SpringTestMethodBodyBuildersSpec extends Specification implements WireMock "webclient" | { properties.testMode = TestMode.WEBTESTCLIENT } } + @Issue('#1423') + def 'should generate assertions for a response body containing an empty list with #methodBuilderName'() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "/url" + } + response { + status OK() + body("[]") + } + } + methodBuilder() + when: + String test = singleTestGenerator(contractDsl) + then: + test.contains("""assertThatJson(parsedJson).isEmpty()""") + and: + stubMappingIsValidWireMockStub(contractDsl) + and: + SyntaxChecker.tryToCompile(methodBuilderName, test) + where: + methodBuilderName | methodBuilder + "spock" | { properties.testFramework = TestFramework.SPOCK } + "testng" | { properties.testFramework = TestFramework.TESTNG } + "mockmvc" | { properties.testMode = TestMode.MOCKMVC } + "webclient" | { properties.testMode = TestMode.WEBTESTCLIENT } + } + + @Issue('#1423') + def 'should generate assertions for a response body containing an empty map with #methodBuilderName'() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "/url" + } + response { + status OK() + body("{}") + } + } + methodBuilder() + when: + String test = singleTestGenerator(contractDsl) + then: + test.contains("""assertThatJson(parsedJson).isEmpty()""") + and: + stubMappingIsValidWireMockStub(contractDsl) + and: + SyntaxChecker.tryToCompile(methodBuilderName, test) + where: + methodBuilderName | methodBuilder + "spock" | { properties.testFramework = TestFramework.SPOCK } + "testng" | { properties.testFramework = TestFramework.TESTNG } + "mockmvc" | { properties.testMode = TestMode.MOCKMVC } + "webclient" | { properties.testMode = TestMode.WEBTESTCLIENT } + } + def 'should generate regex assertions for map objects in response body with #methodBuilderName'() { given: Contract contractDsl = Contract.make { diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/YamlMockMvcMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/YamlMockMvcMethodBodyBuilderSpec.groovy index 7007324aee..958985a73d 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/YamlMockMvcMethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/YamlMockMvcMethodBodyBuilderSpec.groovy @@ -434,6 +434,68 @@ response: "webclient" | { properties.testMode = TestMode.WEBTESTCLIENT } } + @Issue('#1423') + def 'should generate assertions for a response body containing an empty list with #methodBuilderName'() { + given: + String contract = """\ +--- +description: Returns an empty collection +request: + method: GET + urlPath: /url +response: + status: 200 + body: [] +""" + Contract contractDsl = fromYaml(contract) + methodBuilder() + when: + String test = singleTestGenerator(contractDsl) + then: + test.contains("""assertThatJson(parsedJson).isEmpty()""") + and: + stubMappingIsValidWireMockStub(contractDsl) + and: + SyntaxChecker.tryToCompile(methodBuilderName, test) + where: + methodBuilderName | methodBuilder + "spock" | { properties.testFramework = TestFramework.SPOCK } + "testng" | { properties.testFramework = TestFramework.TESTNG } + "mockmvc" | { properties.testMode = TestMode.MOCKMVC } + "webclient" | { properties.testMode = TestMode.WEBTESTCLIENT } + } + + @Issue('#1423') + def 'should generate assertions for a response body containing an empty map with #methodBuilderName'() { + given: + String contract = """\ +--- +description: Returns an empty map +request: + method: GET + urlPath: /url +response: + status: 200 + body: {} +""" + Contract contractDsl = fromYaml(contract) + methodBuilder() + when: + String test = singleTestGenerator(contractDsl) + then: + test.contains("""assertThatJson(parsedJson).isEmpty()""") + and: + stubMappingIsValidWireMockStub(contractDsl) + and: + SyntaxChecker.tryToCompile(methodBuilderName, test) + where: + methodBuilderName | methodBuilder + "spock" | { properties.testFramework = TestFramework.SPOCK } + "testng" | { properties.testFramework = TestFramework.TESTNG } + "mockmvc" | { properties.testMode = TestMode.MOCKMVC } + "webclient" | { properties.testMode = TestMode.WEBTESTCLIENT } + } + def 'should generate regex assertions for map objects in response body with #methodBuilderName'() { given: String contract = '''\