From 6c3caddb7be927237300118d42ab5f3bc18c14ed Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Fri, 18 Jan 2019 20:16:19 +0100 Subject: [PATCH] Add xml support (#855) * Start working on generating stub requests from Xml. * Start implementing matching by xpath. * Handle xml request body matchers. * Merge branch 'master' into add-xml-support * Implement analysing xmls and converting nodes to matchers. * Handle attributes in xml analysis and generate stubs with xml body matchers. * Make XmlToXPathsConverter statically compiled. * Start extracting json test generation logic out of the MethodBodyBuilder. * Fix extracted JsonBodyVerificationBuilder. Fix processing attributes from xml while generating stubs. Fix and adjust tests to recent changes. * Merge branch 'master' into add-xml-support # Conflicts: # spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy # spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy # spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy # spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy # spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy * Implement test generation for XML body matchers. Start implementing handling XML lists in assertions. * Fix generating list element assertions and regex body matcher assertions. Move XML-assert lib content to project sources. Add tests. * Fix recognising XML content from String body. Minor refactoring. * Adjust yaml contracts to new xml handling. * Add tests for handling XML with yaml contracts. Handle incorrect request matchers processing. * Minor refactoring. * Add docs. * Fix docs. * Fixes after code review. * Switch to apache commons logger in `XmlAsserter`. --- docs/src/main/asciidoc/verifier_contract.adoc | 55 +- pom.xml | 12 + .../contract/spec/internal/BodyMatcher.groovy | 18 +- .../spec/internal/BodyMatchers.groovy | 73 +-- .../contract/spec/internal/Headers.groovy | 4 + .../spec/internal/MatchingType.groovy | 1 + .../spec/internal/PathBodyMatcher.groovy | 58 ++ .../camel/StubRunnerCamelPredicate.java | 2 +- .../StubRunnerIntegrationMessageSelector.java | 2 +- .../StubRunnerStreamMessageSelector.java | 2 +- .../spec/pact/MatchingRulesConverter.groovy | 4 +- spring-cloud-contract-verifier/pom.xml | 9 + .../builder/BodyMethodGeneration.groovy | 90 +++ .../verifier/builder/ClassVerifier.groovy | 41 ++ .../JUnitMessagingMethodBodyBuilder.groovy | 10 + .../builder/JUnitMethodBodyBuilder.groovy | 11 + .../verifier/builder/JavaTestGenerator.groovy | 12 +- .../JsonBodyVerificationBuilder.groovy | 315 ++++++++++ .../builder/MessagingMethodBodyBuilder.groovy | 12 +- .../verifier/builder/MethodBodyBuilder.groovy | 364 +++-------- .../verifier/builder/MethodBuilder.groovy | 2 +- .../RequestProcessingMethodBodyBuilder.groovy | 19 +- .../SpockMessagingMethodBodyBuilder.groovy | 10 + ...kMethodRequestProcessingBodyBuilder.groovy | 17 +- .../builder/XmlBodyVerificationBuilder.groovy | 102 +++ .../builder/imports/BaseImportProvider.groovy | 19 +- .../builder/imports/HttpImportProvider.groovy | 18 +- .../verifier/converter/ContractsToYaml.groovy | 68 +- .../verifier/converter/YamlToContracts.groovy | 77 ++- .../wiremock/BaseWireMockStubStrategy.groovy | 8 +- .../WireMockRequestStubStrategy.groovy | 121 +++- .../WireMockResponseStubStrategy.groovy | 12 +- ...cherToWireMockValuePatternConverter.groovy | 43 ++ .../verifier/util/ContentUtils.groovy | 87 +-- .../util/JsonToJsonPathsConverter.groovy | 5 +- .../verifier/util/MethodBuffering.java | 2 +- .../util/xml/XmlToXPathsConverter.groovy | 290 +++++++++ .../verifier/util/ContractVerifierUtil.java | 54 +- .../util/xml/ArrayValueAssertion.java | 137 ++++ .../verifier/util/xml/FieldAssertion.java | 31 + .../verifier/util/xml/IteratingOverArray.java | 38 ++ .../util/xml/ReadyToCheckAsserter.java | 32 + .../verifier/util/xml/XPathBuilder.java | 46 ++ .../verifier/util/xml/XmlArrayVerifiable.java | 40 ++ .../verifier/util/xml/XmlAsserter.java | 416 ++++++++++++ .../util/xml/XmlAsserterConfiguration.java | 28 + .../verifier/util/xml/XmlAssertion.java | 106 ++++ .../verifier/util/xml/XmlCachedObjects.java | 67 ++ .../contract/verifier/util/xml/XmlReader.java | 32 + .../verifier/util/xml/XmlVerifiable.java | 194 ++++++ .../JaxRsClientMethodBuilderSpec.groovy | 13 +- .../builder/MethodBodyBuilderSpec.groovy | 2 +- ...vcMethodBodyBuilderWithMatchersSpec.groovy | 11 +- .../builder/SingleTestGeneratorSpec.groovy | 52 +- .../SpringTestMethodBodyBuildersSpec.groovy | 13 +- .../builder/XmlMethodBodyBuilderSpec.groovy | 170 +++++ .../YamlMockMvcMethodBodyBuilderSpec.groovy | 15 +- .../DslToYamlContractConverterSpec.groovy | 109 +++- .../YamlContractConverterSpec.groovy | 441 +++++++------ .../WireMockGroovyDslSpec.groovy | 153 +++-- .../WireMockResponseStubStrategySpec.groovy | 19 +- .../WireMockStubVerifier.groovy | 12 +- .../WireMockXmlStubStrategySpec.groovy | 394 ++++++++++++ .../verifier/util/SyntaxChecker.groovy | 41 +- .../verifier/util/xml/XPathSpec.groovy | 40 ++ .../verifier/util/xml/XmlAssertionSpec.groovy | 594 ++++++++++++++++++ .../test/resources/yml/contract_rest_xml.yml | 68 ++ 67 files changed, 4606 insertions(+), 757 deletions(-) create mode 100644 spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PathBodyMatcher.groovy create mode 100644 spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BodyMethodGeneration.groovy create mode 100644 spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassVerifier.groovy create mode 100644 spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.groovy create mode 100644 spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/XmlBodyVerificationBuilder.groovy create mode 100644 spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/XPathBodyMatcherToWireMockValuePatternConverter.groovy create mode 100644 spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverter.groovy create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ArrayValueAssertion.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/FieldAssertion.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/IteratingOverArray.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ReadyToCheckAsserter.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XPathBuilder.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlArrayVerifiable.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserter.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserterConfiguration.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAssertion.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlCachedObjects.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlReader.java create mode 100644 spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlVerifiable.java create mode 100644 spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/XmlMethodBodyBuilderSpec.groovy rename spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/{ => wiremock}/WireMockGroovyDslSpec.groovy (96%) rename spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/{ => wiremock}/WireMockStubVerifier.groovy (87%) create mode 100644 spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockXmlStubStrategySpec.groovy create mode 100644 spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XPathSpec.groovy create mode 100644 spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlAssertionSpec.groovy create mode 100644 spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_xml.yml diff --git a/docs/src/main/asciidoc/verifier_contract.adoc b/docs/src/main/asciidoc/verifier_contract.adoc index cea2cec30f..406faab7c2 100644 --- a/docs/src/main/asciidoc/verifier_contract.adoc +++ b/docs/src/main/asciidoc/verifier_contract.adoc @@ -369,7 +369,7 @@ The WireMock stub is as follows: [source,json,indent=0] ---- -include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy[tags=multipartwiremock,indent=0] +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockGroovyDslSpec.groovy[tags=multipartwiremock,indent=0] ---- === Response @@ -1215,6 +1215,59 @@ include::{samples_url}/producer_webflux/src/test/java/com/example/BeerRestBase.j } ---- +=== XML Support for REST +For REST contracts, we also support XML request and response body. +The XML body has to be passed within the `body` element +as a `String` or `GString`. Also body matchers can be provided for +both request and response. In place of the `jsonPath(...)` method, the `org.springframework.cloud.contract.spec.internal.BodyMatchers.xPath` +method should be used, with the desired `xPath` provided as the first argument +and the appropriate `MatchingType` as second. All the body matchers apart from `byType()` are supported. + +Here is an example of a Groovy DSL contract with XML response body: + +[source,groovy,indent=0] +---- +include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/XmlMethodBodyBuilderSpec.groovy[tags=xmlgroovy] +---- + +And below is an example of a YAML contract with XML request and response bodies: + +[source,yaml,indent=0] +---- +include::{verifier_core_path}/src/test/resources/yml/contract_rest_xml.yml +---- + +Here is an example of an automatically generated test for XML response body: + +[source,java,indent=0] +---- +@Test +public void validate_xmlMatches() throws Exception { + // given: + MockMvcRequestSpecification request = given() + .header("Content-Type", "application/xml"); + + // when: + ResponseOptions response = given().spec(request).get("/get"); + + // then: + assertThat(response.statusCode()).isEqualTo(200); + // and: + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance() + .newDocumentBuilder(); + Document parsedXml = documentBuilder.parse(new InputSource( + new StringReader(response.getBody().asString()))); + // and: + assertThat(valueFromXPath(parsedXml, "/test/list/elem/text()")).isEqualTo("abc"); + assertThat(valueFromXPath(parsedXml,"/test/list/elem[2]/text()")).isEqualTo("def"); + assertThat(valueFromXPath(parsedXml, "/test/duck/text()")).matches("[0-9]{3}"); + assertThat(nodeFromXPath(parsedXml, "/test/duck/xxx")).isNull(); + assertThat(valueFromXPath(parsedXml, "/test/alpha/text()")).matches("[\\p{L}]*"); + assertThat(valueFromXPath(parsedXml, "/test/*/complex/text()")).isEqualTo("foo"); + assertThat(valueFromXPath(parsedXml, "/test/duck/@type")).isEqualTo("xtype"); + } +---- + === Messaging Top-Level Elements The DSL for messaging looks a little bit different than the one that focuses on HTTP. The diff --git a/pom.xml b/pom.xml index 28e5be4b42..01922e09a8 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,8 @@ 3.2.1 1.1.0 + 2.1.100 + 2.11.0 @@ -405,6 +407,16 @@ aether-connector-basic ${aether.version} + + com.rackspace.eclipse.webtools.sourceediting + org.eclipse.wst.xml.xpath2.processor + ${xpath2.processor.version} + + + xerces + xercesImpl + ${xerces.version} + diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatcher.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatcher.groovy index b835d8cb12..fa67876b5f 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatcher.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatcher.groovy @@ -1,7 +1,23 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.cloud.contract.spec.internal /** - * A jsonPathMatchers for the given path. + * Matchers for the given path. * * @author Marcin Grzejszczak * @since 1.0.3 diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy index d618796976..79773607d3 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2013-2018 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,24 +25,42 @@ import groovy.transform.ToString * Matching strategy of dynamic parts of the body. * * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma * @since 1.0.3 */ -@CompileStatic @ToString(includeFields = true, includePackage = false) class BodyMatchers { private final RegexPatterns regexPatterns = new RegexPatterns() - protected final List jsonPathRegexMatchers = [] + protected final List matchers = [] void jsonPath(String path, MatchingTypeValue matchingType) { - this.jsonPathRegexMatchers << new JsonPathBodyMatcher(path, matchingType) + this.matchers << new PathBodyMatcher(path, matchingType) + } + + /** + * Adds xPath matcher; even though same implementation as in {@link BodyMatchers#jsonPath(java.lang.String, org.springframework.cloud.contract.spec.internal.MatchingTypeValue)}, + * added for logical coherence in xml + * @param xPath the xPath used to find the element to match + * @param matchingTypeValue to match the element found by the xPath against + */ + void xPath(String xPath, MatchingTypeValue matchingTypeValue) { + matchers << new PathBodyMatcher(xPath, matchingTypeValue) + } + + /** + * @deprecated use {@link #matchers()} + */ + @Deprecated + List jsonPathMatchers() { + return matchers() } boolean hasMatchers() { - return !this.jsonPathRegexMatchers.empty + return !this.matchers.empty } - List jsonPathMatchers() { - return this.jsonPathRegexMatchers + List matchers() { + return this.matchers } MatchingTypeValue byDate() { @@ -86,49 +104,15 @@ class BodyMatchers { if (this.is(o)) return true if (this.getClass() != o.class) return false BodyMatchers that = (BodyMatchers) o - List thisMatchers = this.jsonPathRegexMatchers - List thatMatchers = that.jsonPathRegexMatchers + List thisMatchers = this.matchers + List thatMatchers = that.matchers if (thisMatchers.size() != thatMatchers.size()) return false if (new HashSet<>(thisMatchers) != new HashSet(thatMatchers)) return false return true } int hashCode() { - return (this.jsonPathRegexMatchers != null ? this.jsonPathRegexMatchers.hashCode() : 0) - } -} - -@ToString(includePackage = false) -@EqualsAndHashCode -@Canonical -@CompileStatic -class JsonPathBodyMatcher implements BodyMatcher { - String jsonPath - MatchingTypeValue matchingTypeValue - - @Override - MatchingType matchingType() { - return this.matchingTypeValue.type - } - - @Override - String path() { - return this.jsonPath - } - - @Override - Object value() { - return this.matchingTypeValue.value - } - - @Override - Integer minTypeOccurrence() { - return this.matchingTypeValue.minTypeOccurrence - } - - @Override - Integer maxTypeOccurrence() { - return this.matchingTypeValue.maxTypeOccurrence + return (this.matchers != null ? this.matchers.hashCode() : 0) } } @@ -192,7 +176,6 @@ class RegexMatchingTypeValue extends MatchingTypeValue { */ @Canonical @ToString(includePackage = false) -@EqualsAndHashCode class MatchingTypeValue { MatchingType type diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy index 907673c221..03cc579b5e 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy @@ -55,6 +55,10 @@ class Headers { } } + void headers(Set
headers) { + entries.addAll(headers) + } + void accept(String contentType) { header(accept(), matching(contentType)) } 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 d0983aedb3..57051697d8 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 @@ -23,6 +23,7 @@ import groovy.transform.CompileStatic * * @author Marcin Grzejszczak * @author Tim Ysewyn + * @author Olga Maciaszek-Sharma * @since 1.0.3 */ @CompileStatic diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PathBodyMatcher.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PathBodyMatcher.groovy new file mode 100644 index 0000000000..96da82362f --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PathBodyMatcher.groovy @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.Canonical +import groovy.transform.CompileStatic +import groovy.transform.ToString + +/** + * @author Marcin Grzejszczak + */ +@ToString(includePackage = false) +@Canonical +@CompileStatic +class PathBodyMatcher implements BodyMatcher { + + String path + MatchingTypeValue matchingTypeValue + + @Override + MatchingType matchingType() { + return this.matchingTypeValue.type + } + + @Override + String path() { + return this.path + } + + @Override + Object value() { + return this.matchingTypeValue.value + } + + @Override + Integer minTypeOccurrence() { + return this.matchingTypeValue.minTypeOccurrence + } + + @Override + Integer maxTypeOccurrence() { + return this.matchingTypeValue.maxTypeOccurrence + } +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java index 691e10400a..e9934ed4ff 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java @@ -172,7 +172,7 @@ class StubRunnerCamelPredicate implements Predicate { matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath()); } if (matchers != null && matchers.hasMatchers()) { - for (BodyMatcher matcher : matchers.jsonPathMatchers()) { + for (BodyMatcher matcher : matchers.matchers()) { String jsonPath = JsonToJsonPathsConverter .convertJsonPathAndRegexToAJsonPath(matcher, dslBody); matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath); diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java index 657d4ec9d1..982fb63f2b 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java @@ -190,7 +190,7 @@ class StubRunnerIntegrationMessageSelector implements MessageSelector { matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath()); } if (matchers != null && matchers.hasMatchers()) { - for (BodyMatcher matcher : matchers.jsonPathMatchers()) { + for (BodyMatcher matcher : matchers.matchers()) { String jsonPath = JsonToJsonPathsConverter .convertJsonPathAndRegexToAJsonPath(matcher, dslBody); matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath); diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java index a9a940e506..b0980cc55e 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java @@ -189,7 +189,7 @@ class StubRunnerStreamMessageSelector implements MessageSelector { matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath()); } if (matchers != null && matchers.hasMatchers()) { - for (BodyMatcher matcher : matchers.jsonPathMatchers()) { + for (BodyMatcher matcher : matchers.matchers()) { String jsonPath = JsonToJsonPathsConverter .convertJsonPathAndRegexToAJsonPath(matcher, dslBody); matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath); diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.groovy index 53fbc95975..9276b2a805 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2013-2018 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ class MatchingRulesConverter { private static Category matchingRulesFor(String categoryName, BodyMatchers bodyMatchers) { Category category = new Category(categoryName) - bodyMatchers.jsonPathMatchers().forEach({ BodyMatcher it -> + bodyMatchers.matchers().forEach({ BodyMatcher it -> String key = getMatcherKey(it.path()) MatchingType matchingType = it.matchingType() switch (matchingType) { diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index 97b53157bb..6e8cacd1bc 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -72,6 +72,10 @@ com.toomuchcoding.jsonassert jsonassert + + com.rackspace.eclipse.webtools.sourceediting + org.eclipse.wst.xml.xpath2.processor + org.codehaus.groovy groovy @@ -194,6 +198,11 @@ 2.1 test + + xerces + xercesImpl + test + diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BodyMethodGeneration.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BodyMethodGeneration.groovy new file mode 100644 index 0000000000..e9579cc4c2 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BodyMethodGeneration.groovy @@ -0,0 +1,90 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + + +import org.apache.commons.text.StringEscapeUtils + +import org.springframework.cloud.contract.spec.internal.BodyMatcher +import org.springframework.cloud.contract.spec.internal.MatchingType +import org.springframework.util.SerializationUtils + +/** + * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma + * @since 2.1.0 + */ +trait BodyMethodGeneration { + + // Doing a clone doesn't work for nested lists... + Object cloneBody(Object object) { + if (object instanceof List || object instanceof Map) { + byte[] serializedObject = SerializationUtils.serialize(object) + return SerializationUtils.deserialize(serializedObject) + } + try { + return object.clone() + } + catch (CloneNotSupportedException ignored) { + return object + } + } + + void addColonIfRequired(Optional lineSuffix, BlockBuilder blockBuilder) { + lineSuffix.ifPresent({ + blockBuilder.addAtTheEnd(lineSuffix.get()) + }) + } + + void addBodyMatchingBlock(List matchers, BlockBuilder blockBuilder, + Object responseBody, boolean shouldCommentOutBDDBlocks) { + blockBuilder.endBlock() + blockBuilder.addLine(getAssertionJoiner(shouldCommentOutBDDBlocks)) + blockBuilder.startBlock() + matchers.each { + if (it.matchingType() == MatchingType.NULL) { + methodForNullCheck(it, blockBuilder) + } + else if (MatchingType.regexRelated(it.matchingType()) || it + .matchingType() == MatchingType.EQUALITY) { + methodForEqualityCheck(it, blockBuilder, responseBody) + } + else if (it.matchingType() == MatchingType.COMMAND) { + methodForCommandExecution(it, blockBuilder, responseBody) + } + else { + methodForTypeCheck(it, blockBuilder, responseBody) + } + } + } + + String quotedAndEscaped(String string) { + return '"' + StringEscapeUtils.escapeJava(string) + '"' + } + + abstract void methodForNullCheck(BodyMatcher bodyMatcher, BlockBuilder bb) + + abstract void methodForEqualityCheck(BodyMatcher bodyMatcher, BlockBuilder bb, Object body) + + abstract void methodForCommandExecution(BodyMatcher bodyMatcher, BlockBuilder bb, Object body) + + abstract void methodForTypeCheck(BodyMatcher bodyMatcher, BlockBuilder bb, Object body) + + String getAssertionJoiner(boolean shouldCommentOutBDDBlocks) { + return shouldCommentOutBDDBlocks ? '// and:' : 'and:' + } +} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassVerifier.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassVerifier.groovy new file mode 100644 index 0000000000..63e72e3f6e --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassVerifier.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +/** + * Used to return the {@link Class} against which the type of the element should be verified + * using instanceof in generated response assertions. + * + * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma + * @since 2.1.0 + */ +trait ClassVerifier { + + Class classToCheck(Object elementFromBody) { + switch (elementFromBody.getClass()) { + case List: + return List + case Set: + return Set + case Map: + return Map + default: + return elementFromBody.class + } + } +} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy index 0989886f20..a1e89acc55 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy @@ -179,12 +179,22 @@ class JUnitMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { return "// $baseString" } + @Override + protected boolean shouldCommentOutBDDBlocks() { + return true + } + @Override protected BlockBuilder addColonIfRequired(BlockBuilder blockBuilder) { blockBuilder.addAtTheEnd(JUNIT.lineSuffix) return blockBuilder } + @Override + protected Optional lineSuffix() { + return Optional.of(JUNIT.lineSuffix) + } + @Override protected String getPropertyInListString(String property, Integer listIndex) { return "$property[$listIndex]" ?: '' diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy index 9ff3bc8658..01a3b88473 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy @@ -35,6 +35,7 @@ import org.springframework.cloud.contract.verifier.util.RegexpBuilders import static groovy.json.StringEscapeUtils.escapeJava import static org.springframework.cloud.contract.verifier.config.TestFramework.JUNIT +import static org.springframework.cloud.contract.verifier.config.TestFramework.JUNIT5 import static org.springframework.cloud.contract.verifier.util.ContentUtils.getJavaMultipartFileParameterContent /** * Root class for JUnit method building @@ -65,12 +66,22 @@ abstract class JUnitMethodBodyBuilder extends RequestProcessingMethodBodyBuilder return "// $baseString" } + @Override + protected boolean shouldCommentOutBDDBlocks() { + return true + } + @Override protected BlockBuilder addColonIfRequired(BlockBuilder blockBuilder) { blockBuilder.addAtTheEnd(JUNIT.lineSuffix) return blockBuilder } + @Override + protected Optional lineSuffix() { + return Optional.of(JUNIT5.lineSuffix) + } + @Override protected String getResponseBodyPropertyComparisonString(String property, String value) { return "assertThat(responseBody${property}).isEqualTo(\"${value}\")" diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JavaTestGenerator.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JavaTestGenerator.groovy index 9fa20f6200..1e3fc89990 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JavaTestGenerator.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JavaTestGenerator.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2013-2016 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,7 +74,9 @@ class JavaTestGenerator implements SingleTestGenerator { clazz.addImports(configProperties.testFramework.getOrderAnnotationImports()) clazz.addClassLevelAnnotation(configProperties.testFramework.getOrderAnnotation()) } + // FIXME: change during Hoxton refactoring: we should only add either Json or Xml imports and not both addJsonPathRelatedImports(clazz) + addXPathRelatedImports(clazz) processContractFiles(listOfFiles, configProperties, clazz, generatedClassData) return clazz.build() } @@ -187,6 +189,14 @@ class JavaTestGenerator implements SingleTestGenerator { } } + private void addXPathRelatedImports(ClassBuilder clazz) { + clazz.addImports(['javax.xml.parsers.DocumentBuilder', + 'javax.xml.parsers.DocumentBuilderFactory', + 'org.w3c.dom.Document', + 'org.xml.sax.InputSource', + 'java.io.StringReader']) + } + private void addMessagingRelatedEntries(ClassBuilder clazz) { clazz.addField(['@Inject ContractVerifierMessaging contractVerifierMessaging', '@Inject ContractVerifierObjectMapper contractVerifierObjectMapper' diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.groovy new file mode 100644 index 0000000000..3e8d2b1ed6 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.groovy @@ -0,0 +1,315 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + + +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import com.jayway.jsonpath.PathNotFoundException +import groovy.json.JsonOutput +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import org.apache.commons.beanutils.PropertyUtilsBean + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.ContractTemplate +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.RegexProperty +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.template.TemplateProcessor +import org.springframework.cloud.contract.verifier.util.JsonPaths +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter +import org.springframework.cloud.contract.verifier.util.MapConverter + +/** + * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma + * @since 2.1.0 + */ + +@PackageScope +@CompileStatic +class JsonBodyVerificationBuilder implements BodyMethodGeneration, ClassVerifier { + + private static final String FROM_REQUEST_PREFIX = 'request.' + private static final String FROM_REQUEST_BODY = 'escapejsonbody' + private static final String FROM_REQUEST_PATH = 'path' + + private final ContractVerifierConfigProperties configProperties + private final TemplateProcessor templateProcessor + private final ContractTemplate contractTemplate + private final Contract contract + private final Optional lineSuffix + private final Closure postProcessJsonPathCall + + // FIXME + // Passing way more arguments here than I would like to, but since we are planning a major + // refactoring of this module for Hoxton release, leaving it this way for now + JsonBodyVerificationBuilder(ContractVerifierConfigProperties configProperties, + TemplateProcessor templateProcessor, + ContractTemplate contractTemplate, + Contract contract, + Optional lineSuffix, + Closure postProcessJsonPathCall) { + this.configProperties = configProperties + this.templateProcessor = templateProcessor + this.contractTemplate = contractTemplate + this.contract = contract + this.lineSuffix = lineSuffix + this.postProcessJsonPathCall = postProcessJsonPathCall + } + + Object addJsonResponseBodyCheck(BlockBuilder bb, Object convertedResponseBody, + BodyMatchers bodyMatchers, + String responseString, + boolean shouldCommentOutBDDBlocks) { + appendJsonPath(bb, responseString) + DocumentContext parsedRequestBody + if (contract.request?.body) { + def testSideRequestBody = MapConverter + .getTestSideValues(contract.request.body) + parsedRequestBody = JsonPath.parse(testSideRequestBody) + if (convertedResponseBody instanceof String && ! + textContainsJsonPathTemplate(convertedResponseBody)) { + convertedResponseBody = templateProcessor + .transform(contract.request, convertedResponseBody.toString()) + } + } + Object copiedBody = cloneBody(convertedResponseBody) + convertedResponseBody = JsonToJsonPathsConverter + .removeMatchingJsonPaths(convertedResponseBody, bodyMatchers) + // remove quotes from fromRequest objects before picking json paths + TestSideRequestTemplateModel templateModel = contract.request?.body ? + TestSideRequestTemplateModel.from(contract.request) : null + convertedResponseBody = MapConverter.transformValues(convertedResponseBody, + returnReferencedEntries(templateModel)) + JsonPaths jsonPaths = new JsonToJsonPathsConverter(configProperties). + transformToJsonPathWithTestsSideValues(convertedResponseBody) + jsonPaths.each { + String method = it.method() + method = processIfTemplateIsPresent(method, parsedRequestBody) + String postProcessedMethod = templateProcessor + .containsJsonPathTemplateEntry(method) ? + method : postProcessJsonPathCall(method) + bb.addLine("assertThatJson(parsedJson)" + postProcessedMethod) + addColonIfRequired(lineSuffix, bb) + } + doBodyMatchingIfPresent(bodyMatchers, bb, copiedBody, shouldCommentOutBDDBlocks) + return convertedResponseBody + } + + 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)) + addColonIfRequired(lineSuffix, bb) + } + + // we want to make the type more generic (e.g. not ArrayList but List) + @CompileDynamic + protected String sizeCheckMethod(BodyMatcher bodyMatcher, String quotedAndEscaptedPath) { + String prefix = sizeCheckPrefix(bodyMatcher, quotedAndEscaptedPath) + if (bodyMatcher.minTypeOccurrence() != null && bodyMatcher + .maxTypeOccurrence() != null) { + return "${prefix}Between(${bodyMatcher.minTypeOccurrence()}, ${bodyMatcher.maxTypeOccurrence()})" + } + else if (bodyMatcher.minTypeOccurrence() != null) { + return "${prefix}GreaterThanOrEqualTo(${bodyMatcher.minTypeOccurrence()})" + } + else if (bodyMatcher.maxTypeOccurrence() != null) { + return "${prefix}LessThanOrEqualTo(${bodyMatcher.maxTypeOccurrence()})" + } + return prefix + } + + + protected void buildCustomMatchingConditionForEachElement(BlockBuilder bb, String path, String valueAsParam) { + String method = "assertThat((java.lang.Iterable) parsedJson.read(${path}, java.util.Collection.class)).as(${path}).allElementsMatch(${valueAsParam})" + bb.addLine(postProcessJsonPathCall(method)) + } + + @Override + void methodForEqualityCheck(BodyMatcher bodyMatcher, BlockBuilder bb, Object copiedBody) { + String path = quotedAndEscaped(bodyMatcher.path()) + Object retrievedValue = value(copiedBody, bodyMatcher) + retrievedValue = retrievedValue instanceof RegexProperty ? + ((RegexProperty) retrievedValue).getPattern().pattern() : retrievedValue + String valueAsParam = retrievedValue instanceof String ? + quotedAndEscaped(retrievedValue.toString()) : retrievedValue.toString() + if (arrayRelated(path) && MatchingType.regexRelated(bodyMatcher.matchingType())) { + buildCustomMatchingConditionForEachElement(bb, path, valueAsParam) + } + else { + String comparisonMethod = bodyMatcher. + 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(lineSuffix, bb) + } + + protected String processIfTemplateIsPresent(String method, DocumentContext parsedRequestBody) { + if (textContainsJsonPathTemplate(method) && contract.request?.body) { + // Unquoting the values of non strings + String jsonPathEntry = templateProcessor.jsonPathFromTemplateEntry(method) + Object object = parsedRequestBody.read(jsonPathEntry) + if (!(object instanceof String)) { + return method + .replace('"' + contractTemplate. + escapedOpeningTemplate(), contractTemplate. + escapedOpeningTemplate()) + .replace(contractTemplate. + escapedClosingTemplate() + '"', contractTemplate. + escapedClosingTemplate()) + .replace('"' + contractTemplate. + openingTemplate(), contractTemplate.openingTemplate()) + .replace(contractTemplate. + closingTemplate() + '"', contractTemplate.closingTemplate()) + } + } + return method + } + + @Override + void methodForCommandExecution(BodyMatcher bodyMatcher, BlockBuilder bb, Object copiedBody) { + String path = quotedAndEscaped(bodyMatcher.path()) + // assert that path exists + retrieveObjectByPath(copiedBody, bodyMatcher.path()) + ExecutionProperty property = bodyMatcher.value() as ExecutionProperty + bb.addLine( + postProcessJsonPathCall(property.insertValue("parsedJson.read(${path})"))) + addColonIfRequired(lineSuffix, bb) + } + + @Override + void methodForNullCheck(BodyMatcher bodyMatcher, BlockBuilder bb) { + String quotedAndEscapedPath = quotedAndEscaped(bodyMatcher.path()) + String method = "assertThat((Object) parsedJson.read(${quotedAndEscapedPath})).isNull()" + bb.addLine(postProcessJsonPathCall(method)) + addColonIfRequired(lineSuffix, bb) + } + + protected boolean arrayRelated(String path) { + return path.contains("[*]") || path.contains("..") + } + + @Override + void methodForTypeCheck(BodyMatcher bodyMatcher, BlockBuilder bb, Object copiedBody) { + Object elementFromBody = value(copiedBody, bodyMatcher) + if (bodyMatcher.minTypeOccurrence() != null || bodyMatcher + .maxTypeOccurrence() != null) { + checkType(bb, bodyMatcher, elementFromBody) + String quotedAndEscaptedPath = quotedAndEscaped(bodyMatcher.path()) + String method = "assertThat((java.lang.Iterable) parsedJson.read(${quotedAndEscaptedPath}, java.util.Collection.class)).${sizeCheckMethod(bodyMatcher, quotedAndEscaptedPath)}" + bb.addLine(postProcessJsonPathCall(method)) + addColonIfRequired(lineSuffix, bb) + } + else { + checkType(bb, bodyMatcher, elementFromBody) + } + } + + + private static Object value(Object body, BodyMatcher bodyMatcher) { + if (bodyMatcher.matchingType() == MatchingType.EQUALITY || !bodyMatcher.value()) { + return retrieveObjectByPath(body, bodyMatcher.path()) + } + return bodyMatcher.value() + } + + private static Object retrieveObjectByPath(Object 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) + } + } + + @CompileDynamic + private Closure returnReferencedEntries(TestSideRequestTemplateModel templateModel) { + return { entry -> + if (!(entry instanceof String) || !templateModel) { + return entry + } + String entryAsString = (String) entry + if (templateProcessor.containsTemplateEntry(entryAsString) && + !templateProcessor.containsJsonPathTemplateEntry(entryAsString)) { + // TODO: HANDLEBARS LEAKING VIA request. + String justEntry = entryAsString - contractTemplate. + escapedOpeningTemplate() - + contractTemplate.openingTemplate() - + contractTemplate.escapedClosingTemplate() - + contractTemplate.closingTemplate() - FROM_REQUEST_PREFIX + if (justEntry == FROM_REQUEST_BODY) { + // the body should be transformed by standard mechanism + return contractTemplate. + escapedOpeningTemplate() + FROM_REQUEST_PREFIX + + "escapedBody" + contractTemplate.escapedClosingTemplate() + } + try { + Object result = new PropertyUtilsBean() + .getProperty(templateModel, justEntry) + // Path from the Test model is an object and we'd like to return its String representation + if (justEntry == FROM_REQUEST_PATH) { + return result.toString() + } + return result + } + catch (Exception ignored) { + return entry + } + } + return entry + } + } + + protected boolean textContainsJsonPathTemplate(String method) { + return templateProcessor.containsTemplateEntry(method) && + templateProcessor.containsJsonPathTemplateEntry(method) + } + + /** + * Appends to {@link BlockBuilder} parsing of the JSON Path document + */ + protected void appendJsonPath(BlockBuilder blockBuilder, String json) { + blockBuilder.addLine(("DocumentContext parsedJson = JsonPath.parse($json)")) + addColonIfRequired(lineSuffix, blockBuilder) + } + + private String sizeCheckPrefix(BodyMatcher bodyMatcher, String quotedAndEscaptedPath) { + String description = "as(" + quotedAndEscaptedPath + ")." + String prefix = description + "has" + if (arrayRelated(bodyMatcher.path())) { + prefix = prefix + "Flattened" + } + return prefix + "Size" + } + + private void doBodyMatchingIfPresent(BodyMatchers bodyMatchers, BlockBuilder bb, + Object responseBody, + boolean shouldCommentOutBDDBlocks) { + if (bodyMatchers?.hasMatchers()) { + addBodyMatchingBlock(bodyMatchers. + matchers(), bb, responseBody, shouldCommentOutBDDBlocks) + } + } +} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy index ececd3fcff..65bb68dfb7 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2013-2018 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,8 +29,7 @@ import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.ContentUtils import static org.apache.commons.text.StringEscapeUtils.escapeJava -import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent -import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader +import static org.springframework.cloud.contract.verifier.util.ContentUtils.evaluateContentType /** * Root class for messaging method building. @@ -112,11 +111,8 @@ abstract class MessagingMethodBodyBuilder extends MethodBodyBuilder { } protected ContentType getResponseContentType() { - ContentType contentType = recognizeContentTypeFromHeader(outputMessage.headers) - if (contentType == ContentType.UNKNOWN) { - contentType = recognizeContentTypeFromContent(outputMessage.body.serverValue) - } - return contentType + return evaluateContentType(outputMessage?.headers, + outputMessage?.body?.serverValue) } protected String getBodyAsString() { 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 7e7ceca7ef..5d2783f313 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2018 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,18 +18,12 @@ package org.springframework.cloud.contract.verifier.builder import java.util.regex.Pattern -import com.jayway.jsonpath.DocumentContext -import com.jayway.jsonpath.JsonPath -import com.jayway.jsonpath.PathNotFoundException -import groovy.json.JsonOutput import groovy.transform.PackageScope import groovy.transform.TypeChecked -import org.apache.commons.beanutils.PropertyUtilsBean import org.apache.commons.text.StringEscapeUtils import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.ContractTemplate -import org.springframework.cloud.contract.spec.internal.BodyMatcher import org.springframework.cloud.contract.spec.internal.BodyMatchers import org.springframework.cloud.contract.spec.internal.Cookie import org.springframework.cloud.contract.spec.internal.DslProperty @@ -37,7 +31,6 @@ import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.FromFileProperty import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.MatchingStrategy -import org.springframework.cloud.contract.spec.internal.MatchingType import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.OptionalProperty import org.springframework.cloud.contract.spec.internal.QueryParameter @@ -47,12 +40,13 @@ import org.springframework.cloud.contract.verifier.template.HandlebarsTemplatePr import org.springframework.cloud.contract.verifier.template.TemplateProcessor import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.ContentUtils -import org.springframework.cloud.contract.verifier.util.JsonPaths -import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter import org.springframework.cloud.contract.verifier.util.MapConverter -import org.springframework.util.SerializationUtils import org.springframework.util.StringUtils +import static org.springframework.cloud.contract.verifier.util.ContentType.FORM +import static org.springframework.cloud.contract.verifier.util.ContentType.JSON +import static org.springframework.cloud.contract.verifier.util.ContentType.TEXT +import static org.springframework.cloud.contract.verifier.util.ContentType.XML import static org.springframework.cloud.contract.verifier.util.ContentUtils.extractValue /** * Main class for building method body. @@ -66,25 +60,33 @@ import static org.springframework.cloud.contract.verifier.util.ContentUtils.extr */ @TypeChecked @PackageScope -abstract class MethodBodyBuilder { +abstract class MethodBodyBuilder implements ClassVerifier { private static final Closure GET_SERVER_VALUE = { it instanceof DslProperty ? it.serverValue : it } - private static final String FROM_REQUEST_PREFIX = "request." - private static final String FROM_REQUEST_BODY = "escapejsonbody" - private static final String FROM_REQUEST_PATH = "path" protected final ContractVerifierConfigProperties configProperties protected final TemplateProcessor templateProcessor protected final ContractTemplate contractTemplate protected final Contract contract protected final GeneratedClassDataForMethod classDataForMethod + private final JsonBodyVerificationBuilder jsonBodyVerificationBuilder + private final XmlBodyVerificationBuilder xmlBodyVerificationBuilder - protected MethodBodyBuilder(ContractVerifierConfigProperties configProperties, Contract contract, GeneratedClassDataForMethod classDataForMethod) { + protected MethodBodyBuilder(ContractVerifierConfigProperties configProperties, + Contract contract, + GeneratedClassDataForMethod classDataForMethod) { this.configProperties = configProperties this.templateProcessor = processor() this.contractTemplate = template() this.contract = contract this.classDataForMethod = classDataForMethod + this.jsonBodyVerificationBuilder = new JsonBodyVerificationBuilder(this.configProperties, + templateProcessor, contractTemplate, this.contract, + lineSuffix(), { String jsonPath -> + postProcessJsonPathCall(jsonPath) + }) + this.xmlBodyVerificationBuilder = new XmlBodyVerificationBuilder(contract, + lineSuffix()) } private String byteBodyToAFileForTestMethod(FromFileProperty property, CommunicationType side) { @@ -148,11 +150,21 @@ abstract class MethodBodyBuilder { */ protected abstract String addCommentSignIfRequired(String baseString) + /** + * Returns true if the BDD-syntax blocks should be commented out for a given framework + */ + protected abstract boolean shouldCommentOutBDDBlocks() + /** * Adds a colon sign at the end of each line if necessary */ protected abstract BlockBuilder addColonIfRequired(BlockBuilder blockBuilder) + /** + * Returns line suffix appropriate for test builder if required + */ + protected abstract Optional lineSuffix() + /** * Builds the code that for the given {@code property} will compare it to * the given Object {@code value} @@ -321,6 +333,13 @@ abstract class MethodBodyBuilder { */ protected abstract boolean hasGivenSection() + /** + * Post processing of each JSON path entry + */ + protected String postProcessJsonPathCall(String jsonPath) { + return jsonPath + } + /** * Builds the test contents and appends them to {@link BlockBuilder} */ @@ -394,266 +413,39 @@ abstract class MethodBodyBuilder { if (convertedResponseBody instanceof GString) { convertedResponseBody = extractValue(convertedResponseBody as GString, contentType, { Object o -> o instanceof DslProperty ? o.serverValue : o }) } - if (contentType != ContentType.TEXT && contentType != ContentType.FORM) { + if (TEXT != contentType && FORM != contentType) { convertedResponseBody = MapConverter.getTestSideValues(convertedResponseBody) } else { convertedResponseBody = StringEscapeUtils.escapeJava(convertedResponseBody.toString()) } - if (contentType == ContentType.JSON) { - addJsonResponseBodyCheck(bb, convertedResponseBody, bodyMatchers) - } else if (contentType == ContentType.XML) { - bb.addLine(getParsedXmlResponseBodyString(getResponseAsString())) - addColonIfRequired(bb) - // TODO xml validation + if (JSON == contentType) { + addJsonBodyVerification(bb, convertedResponseBody, bodyMatchers) + } + else if (XML == contentType) { + xmlBodyVerificationBuilder.addXmlResponseBodyCheck(bb, convertedResponseBody, + bodyMatchers, getResponseAsString(), shouldCommentOutBDDBlocks()) } else { simpleTextResponseBodyCheck(bb, convertedResponseBody) } } - private void byteResponseBodyCheck(BlockBuilder bb, FromFileProperty convertedResponseBody) { - processText(bb, "", convertedResponseBody) - addColonIfRequired(bb) - } - private void simpleTextResponseBodyCheck(BlockBuilder bb, convertedResponseBody) { - bb.addLine(getSimpleResponseBodyString(getResponseAsString())) - processText(bb, "", convertedResponseBody) - addColonIfRequired(bb) - } - - private void addJsonResponseBodyCheck(BlockBuilder bb, convertedResponseBody, BodyMatchers bodyMatchers) { - appendJsonPath(bb, getResponseAsString()) - DocumentContext parsedRequestBody - if (contract.request?.body) { - def testSideRequestBody = MapConverter.getTestSideValues(contract.request.body) - parsedRequestBody = JsonPath.parse(testSideRequestBody) - if (convertedResponseBody instanceof String && !textContainsJsonPathTemplate(convertedResponseBody)) { - convertedResponseBody = templateProcessor.transform(contract.request, convertedResponseBody.toString()) - } - } - Object copiedBody = cloneBody(convertedResponseBody) - convertedResponseBody = JsonToJsonPathsConverter.removeMatchingJsonPaths(convertedResponseBody, bodyMatchers) - // remove quotes from fromRequest objects before picking json paths - TestSideRequestTemplateModel templateModel = contract.request?.body ? - TestSideRequestTemplateModel.from(contract.request) : null - convertedResponseBody = MapConverter.transformValues(convertedResponseBody, returnReferencedEntries(templateModel)) - JsonPaths jsonPaths = new JsonToJsonPathsConverter(configProperties).transformToJsonPathWithTestsSideValues(convertedResponseBody) - jsonPaths.each { - String method = it.method() - method = processIfTemplateIsPresent(method, parsedRequestBody) - String postProcessedMethod = templateProcessor.containsJsonPathTemplateEntry(method) ? - method : postProcessJsonPathCall(method) - bb.addLine("assertThatJson(parsedJson)" + postProcessedMethod) - addColonIfRequired(bb) - } - doBodyMatchingIfPresent(bodyMatchers, bb, copiedBody) + private void addJsonBodyVerification(BlockBuilder bb, Object responseBody, BodyMatchers bodyMatchers) { + Object convertedResponseBody = jsonBodyVerificationBuilder + .addJsonResponseBodyCheck(bb, responseBody, + bodyMatchers, getResponseAsString(), shouldCommentOutBDDBlocks()) if (!(convertedResponseBody instanceof Map || convertedResponseBody instanceof List)) { simpleTextResponseBodyCheck(bb, convertedResponseBody) } processBodyElement(bb, "", "", convertedResponseBody) } - private void doBodyMatchingIfPresent(BodyMatchers bodyMatchers, BlockBuilder bb, copiedBody) { - if (bodyMatchers?.hasMatchers()) { - bb.endBlock() - bb.addLine(addCommentSignIfRequired('and:')) - bb.startBlock() - // for the rest we'll do JsonPath matching in brute force - bodyMatchers.jsonPathMatchers().each { - if (it.matchingType() == MatchingType.NULL) { - methodForNullCheck(it, bb) - } else if (MatchingType.regexRelated(it.matchingType()) || it.matchingType() == MatchingType.EQUALITY) { - methodForEqualityCheck(it, bb, copiedBody) - } else if (it.matchingType() == MatchingType.COMMAND) { - methodForCommandExecution(it, bb, copiedBody) - } else { - methodForTypeCheck(it, bb, copiedBody) - } - } - } - } - - private Closure returnReferencedEntries(TestSideRequestTemplateModel templateModel) { - return { entry -> - if (!(entry instanceof String) || !templateModel) { - return entry - } - String entryAsString = (String) entry - if (templateProcessor.containsTemplateEntry(entryAsString) && - !templateProcessor.containsJsonPathTemplateEntry(entryAsString)) { - // TODO: HANDLEBARS LEAKING VIA request. - String justEntry = entryAsString - contractTemplate.escapedOpeningTemplate() - - contractTemplate.openingTemplate() - - contractTemplate.escapedClosingTemplate() - - contractTemplate.closingTemplate() - FROM_REQUEST_PREFIX - if (justEntry == FROM_REQUEST_BODY) { - // the body should be transformed by standard mechanism - return contractTemplate.escapedOpeningTemplate() + FROM_REQUEST_PREFIX + - "escapedBody" + contractTemplate.escapedClosingTemplate() - } - try { - Object result = new PropertyUtilsBean().getProperty(templateModel, justEntry) - // Path from the Test model is an object and we'd like to return its String representation - if (justEntry == FROM_REQUEST_PATH) { - return result.toString() - } - return result - } catch (Exception e) { - return entry - } - } - return entry - } - } - - protected String processIfTemplateIsPresent(String method, DocumentContext parsedRequestBody) { - if (textContainsJsonPathTemplate(method) && contract.request?.body) { - // Unquoting the values of non strings - String jsonPathEntry = templateProcessor.jsonPathFromTemplateEntry(method) - Object object = parsedRequestBody.read(jsonPathEntry) - if (!(object instanceof String)) { - return method - .replace('"' + contractTemplate.escapedOpeningTemplate(), contractTemplate.escapedOpeningTemplate()) - .replace(contractTemplate.escapedClosingTemplate() + '"', contractTemplate.escapedClosingTemplate()) - .replace('"' + contractTemplate.openingTemplate(), contractTemplate.openingTemplate()) - .replace(contractTemplate.closingTemplate() + '"', contractTemplate.closingTemplate()) - } - } - return method - } - - protected boolean textContainsJsonPathTemplate(String method) { - return templateProcessor.containsTemplateEntry(method) && - templateProcessor.containsJsonPathTemplateEntry(method) - } - - protected void methodForEqualityCheck(BodyMatcher bodyMatcher, BlockBuilder bb, Object copiedBody) { - String path = quotedAndEscaped(bodyMatcher.path()) - Object retrievedValue = value(copiedBody, bodyMatcher) - retrievedValue = retrievedValue instanceof RegexProperty ? ((RegexProperty) retrievedValue).getPattern().pattern() : retrievedValue - String valueAsParam = retrievedValue instanceof String ? quotedAndEscaped(retrievedValue.toString()) : retrievedValue.toString() - if (arrayRelated(path) && MatchingType.regexRelated(bodyMatcher.matchingType())) { - buildCustomMatchingConditionForEachElement(bb, path, valueAsParam) - } else { - String comparisonMethod = bodyMatcher.matchingType() == MatchingType.EQUALITY ? "isEqualTo" : "matches" - String classToCastTo = "${retrievedValue.class.simpleName}.class" - String method = "assertThat(parsedJson.read(${path}, ${classToCastTo})).${comparisonMethod}(${valueAsParam})" - bb.addLine(postProcessJsonPathCall(method)) - } + private void simpleTextResponseBodyCheck(BlockBuilder bb, convertedResponseBody) { + bb.addLine(getSimpleResponseBodyString(getResponseAsString())) + processText(bb, "", convertedResponseBody) addColonIfRequired(bb) } - protected void methodForCommandExecution(BodyMatcher bodyMatcher, BlockBuilder bb, Object copiedBody) { - String path = quotedAndEscaped(bodyMatcher.path()) - // assert that path exists - retrieveObjectByPath(copiedBody, bodyMatcher.path()) - ExecutionProperty property = bodyMatcher.value() as ExecutionProperty - bb.addLine(postProcessJsonPathCall(property.insertValue("parsedJson.read(${path})"))) - addColonIfRequired(bb) - } - - protected void methodForNullCheck(BodyMatcher bodyMatcher, BlockBuilder bb) { - String quotedAndEscaptedPath = quotedAndEscaped(bodyMatcher.path()) - String method = "assertThat((Object) parsedJson.read(${quotedAndEscaptedPath})).isNull()" - bb.addLine(postProcessJsonPathCall(method)) - addColonIfRequired(bb) - } - - protected void methodForTypeCheck(BodyMatcher bodyMatcher, BlockBuilder bb, Object copiedBody) { - Object elementFromBody = value(copiedBody, bodyMatcher) - if (bodyMatcher.minTypeOccurrence() != null || bodyMatcher.maxTypeOccurrence() != null) { - checkType(bb, bodyMatcher, elementFromBody) - String quotedAndEscaptedPath = quotedAndEscaped(bodyMatcher.path()) - String method = "assertThat((java.lang.Iterable) parsedJson.read(${quotedAndEscaptedPath}, java.util.Collection.class)).${sizeCheckMethod(bodyMatcher, quotedAndEscaptedPath)}" - bb.addLine(postProcessJsonPathCall(method)) - addColonIfRequired(bb) - } else { - checkType(bb, bodyMatcher, elementFromBody) - } - } - - protected boolean arrayRelated(String path) { - return path.contains("[*]") || path.contains("..") - } - - protected void buildCustomMatchingConditionForEachElement(BlockBuilder bb, String path, String valueAsParam) { - String method = "assertThat((java.lang.Iterable) parsedJson.read(${path}, java.util.Collection.class)).as(${path}).allElementsMatch(${valueAsParam})" - bb.addLine(postProcessJsonPathCall(method)) - } - - // Doing a clone doesn't work for nested lists... - private Object cloneBody(Object object) { - if (object instanceof List || object instanceof Map) { - byte[] serializedObject = SerializationUtils.serialize(object) - return SerializationUtils.deserialize(serializedObject) - } - try { - return object.clone() - } catch (CloneNotSupportedException e) { - return object - } - } - - 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)) - addColonIfRequired(bb) - } - - // we want to make the type more generic (e.g. not ArrayList but List) - protected Class classToCheck(Object elementFromBody) { - switch (elementFromBody.getClass()) { - case List: - return List - case Set: - return Set - case Map: - return Map - default: - return elementFromBody.class - } - } - - protected String sizeCheckMethod(BodyMatcher bodyMatcher, String quotedAndEscaptedPath) { - String prefix = sizeCheckPrefix(bodyMatcher, quotedAndEscaptedPath) - if (bodyMatcher.minTypeOccurrence() != null && bodyMatcher.maxTypeOccurrence() != null) { - return "${prefix}Between(${bodyMatcher.minTypeOccurrence()}, ${bodyMatcher.maxTypeOccurrence()})" - } else if (bodyMatcher.minTypeOccurrence() != null ) { - return "${prefix}GreaterThanOrEqualTo(${bodyMatcher.minTypeOccurrence()})" - } else if (bodyMatcher.maxTypeOccurrence() != null) { - return "${prefix}LessThanOrEqualTo(${bodyMatcher.maxTypeOccurrence()})" - } - return prefix - } - - private String sizeCheckPrefix(BodyMatcher bodyMatcher, String quotedAndEscaptedPath) { - String description = "as(" + quotedAndEscaptedPath + ")." - String prefix = description + "has" - if (arrayRelated(bodyMatcher.path())) { - prefix = prefix + "Flattened" - } - return prefix + "Size" - } - - protected String quotedAndEscaped(String string) { - return '"' + StringEscapeUtils.escapeJava(string) + '"' - } - protected String trailingKey(String key) { if (key.startsWith(".")) { return key.substring(1) @@ -669,38 +461,6 @@ abstract class MethodBodyBuilder { return remindingKey } - /** - * Post processing of each JSON path entry - */ - protected String postProcessJsonPathCall(String jsonPath) { - return jsonPath - } - - /** - * Appends to {@link BlockBuilder} parsing of the JSON Path document - */ - protected void appendJsonPath(BlockBuilder blockBuilder, String json) { - blockBuilder.addLine(("DocumentContext parsedJson = JsonPath.parse($json)")) - addColonIfRequired(blockBuilder) - } - - /** - * Appends to {@link BlockBuilder} processing of the given String value. - */ - protected void processText(BlockBuilder blockBuilder, String property, Object value) { - if (value instanceof String && (value as String).startsWith('$')) { - String newValue = stripFirstChar((value as String)).replaceAll('\\$value', "responseBody$property") - blockBuilder.addLine(newValue) - addColonIfRequired(blockBuilder) - } else { - blockBuilder.addLine(getResponseBodyPropertyComparisonString(property, value)) - } - } - - private String stripFirstChar(String s) { - return s.substring(1) - } - /** * Appends to the {@link BlockBuilder} the assertion for the given header path */ @@ -821,4 +581,30 @@ abstract class MethodBodyBuilder { processBodyElement(blockBuilder, property, prop, listElement) } } + + /** + * Appends to {@link BlockBuilder} processing of the given String value. + */ + protected void processText(BlockBuilder blockBuilder, String property, Object value) { + if (value instanceof String && (value as String).startsWith('$')) { + String newValue = stripFirstChar((value as String)). + replaceAll('\\$value', "responseBody$property") + blockBuilder.addLine(newValue) + addColonIfRequired(blockBuilder) + } + else { + blockBuilder.addLine(getResponseBodyPropertyComparisonString(property, value)) + } + } + + private void byteResponseBodyCheck(BlockBuilder bb, + FromFileProperty convertedResponseBody) { + processText(bb, "", convertedResponseBody) + addColonIfRequired(bb) + } + + private String stripFirstChar(String s) { + return s.substring(1) + } + } \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy index fb5ca1d3ae..7d7eef432c 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilder.groovy @@ -79,7 +79,7 @@ class MethodBuilder { } else if (contract.convertedContract.size() > 1) { int index = contract.convertedContract.findIndexOf { it == stubContent} String name = "${camelCasedMethodFromFileName(stubsFile)}_${index}" - if (log.isDebugEnabled()) { + if (log.isDebugEnabled()) { log.debug("Scenario found. The method name will be [" + name + "]") } return name diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy index 502e3d5222..9d76d4699e 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2013-2018 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import groovy.json.JsonOutput import groovy.transform.PackageScope import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode + import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.internal.BodyMatchers import org.springframework.cloud.contract.spec.internal.Cookie @@ -37,8 +38,8 @@ import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.ContentUtils import org.springframework.cloud.contract.verifier.util.MapConverter -import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent -import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader +import static org.springframework.cloud.contract.verifier.util.ContentUtils.evaluateContentType + /** * An abstraction for creating a test method that includes processing of an HTTP request * @@ -229,11 +230,7 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { @Override protected ContentType getResponseContentType() { - ContentType contentType = recognizeContentTypeFromHeader(response.headers) - if (contentType == ContentType.UNKNOWN) { - contentType = recognizeContentTypeFromContent(response.body.serverValue) - } - return contentType + return evaluateContentType(response?.headers, response?.body?.serverValue) } @Override @@ -269,11 +266,7 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { * Maps the {@link Request} into a {@link ContentType} */ protected ContentType getRequestContentType() { - ContentType contentType = recognizeContentTypeFromHeader(request.headers) - if (contentType == ContentType.UNKNOWN) { - contentType = recognizeContentTypeFromContent(request.body.serverValue) - } - return contentType + return evaluateContentType(request?.headers, request?.body?.serverValue) } /** diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy index dfb0231800..4fdced029a 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy @@ -154,11 +154,21 @@ class SpockMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { return baseString } + @Override + protected boolean shouldCommentOutBDDBlocks() { + return false + } + @Override protected BlockBuilder addColonIfRequired(BlockBuilder blockBuilder) { return blockBuilder } + @Override + protected Optional lineSuffix() { + return Optional.empty() + } + @Override protected String getResponseBodyPropertyComparisonString(String property, Object value) { return "" diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy index 70bc3e6ad4..380d64d632 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy @@ -16,23 +16,24 @@ package org.springframework.cloud.contract.verifier.builder +import java.util.regex.Pattern + import groovy.json.StringEscapeUtils import groovy.transform.PackageScope import groovy.transform.TypeChecked + import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.internal.Cookie +import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.FromFileProperty import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.RegexProperty import org.springframework.cloud.contract.spec.internal.Request -import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties import org.springframework.cloud.contract.verifier.util.ContentUtils import org.springframework.cloud.contract.verifier.util.RegexpBuilders -import java.util.regex.Pattern - import static org.apache.commons.text.StringEscapeUtils.escapeJava import static org.springframework.cloud.contract.verifier.util.ContentUtils.getGroovyMultipartFileParameterContent @@ -88,11 +89,21 @@ abstract class SpockMethodRequestProcessingBodyBuilder extends RequestProcessing return baseString } + @Override + protected boolean shouldCommentOutBDDBlocks() { + return false + } + @Override protected BlockBuilder addColonIfRequired(BlockBuilder blockBuilder) { return blockBuilder } + @Override + protected Optional lineSuffix() { + return Optional.empty() + } + @Override protected String getPropertyInListString(String property, Integer listIndex) { "$property[$listIndex]" ?: '' diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/XmlBodyVerificationBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/XmlBodyVerificationBuilder.groovy new file mode 100644 index 0000000000..949def4a78 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/XmlBodyVerificationBuilder.groovy @@ -0,0 +1,102 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope + +import org.springframework.cloud.contract.spec.Contract +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.verifier.util.xml.XmlToXPathsConverter + +/** + * @author Olga Maciaszek-Sharma + * @since 2.1.0 + */ +@PackageScope +@CompileStatic +class XmlBodyVerificationBuilder implements BodyMethodGeneration { + + private final Contract contract + private final Optional lineSuffix + + XmlBodyVerificationBuilder(Contract contract, Optional lineSuffix) { + this.contract = contract + this.lineSuffix = lineSuffix + } + + void addXmlResponseBodyCheck(BlockBuilder blockBuilder, Object responseBody, + BodyMatchers bodyMatchers, String responseString, + boolean shouldCommentOutBDDBlocks) { + addXmlProcessingLines(blockBuilder, responseString) + Object processedBody = XmlToXPathsConverter + .removeMatchingXPaths(responseBody, bodyMatchers) + List matchers = new XmlToXPathsConverter() + .mapToMatchers(processedBody) + if (bodyMatchers?.hasMatchers()) { + matchers.addAll(bodyMatchers.matchers()) + } + addBodyMatchingBlock(matchers, blockBuilder, responseBody, shouldCommentOutBDDBlocks) + } + + private void addXmlProcessingLines(BlockBuilder blockBuilder, String responseString) { + ['DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()', + "Document parsedXml = documentBuilder.parse(new InputSource(new StringReader($responseString)))"] + .each { + blockBuilder.addLine(it as String) + addColonIfRequired(lineSuffix, blockBuilder) + } + } + + @Override + void methodForNullCheck(BodyMatcher bodyMatcher, BlockBuilder bb) { + String quotedAndEscapedPath = quotedAndEscaped(bodyMatcher.path()) + String method = "assertThat(nodeFromXPath(parsedXml, ${quotedAndEscapedPath})).isNull()" + bb.addLine(method.replace('$', '\\$')) + addColonIfRequired(lineSuffix, bb) + } + + @Override + void methodForEqualityCheck(BodyMatcher bodyMatcher, BlockBuilder bb, Object body) { + Object retrievedValue = + quotedAndEscaped(XmlToXPathsConverter. + retrieveValue(bodyMatcher, body)) + String comparisonMethod = bodyMatcher + .matchingType() == MatchingType.EQUALITY ? 'isEqualTo' : 'matches' + String method = "assertThat(valueFromXPath(parsedXml, ${quotedAndEscaped(bodyMatcher.path())})).$comparisonMethod(${retrievedValue})" + bb.addLine(method.replace('$', '\\$')) + addColonIfRequired(lineSuffix, bb) + } + + @Override + void methodForCommandExecution(BodyMatcher bodyMatcher, BlockBuilder bb, Object body) { + Object retrievedValue = + quotedAndEscaped(XmlToXPathsConverter + .retrieveValueFromBody(bodyMatcher.path(), body)) + ExecutionProperty property = bodyMatcher.value() as ExecutionProperty + bb.addLine(property.insertValue(retrievedValue.replace('$', '\\$'))) + addColonIfRequired(lineSuffix, bb) + } + + @Override + void methodForTypeCheck(BodyMatcher bodyMatcher, BlockBuilder bb, Object copiedBody) { + throw new UnsupportedOperationException("The `getNodeValue()` method in `org.w3c.dom.Node` always returns String.") + } +} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/imports/BaseImportProvider.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/imports/BaseImportProvider.groovy index 8bb12a0e49..1717662f2f 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/imports/BaseImportProvider.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/imports/BaseImportProvider.groovy @@ -1,3 +1,19 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.cloud.contract.verifier.builder.imports import groovy.transform.CompileStatic @@ -20,7 +36,8 @@ import static org.springframework.cloud.contract.verifier.config.TestFramework.S class BaseImportProvider { private static final ImportDefinitions GENERAL_IMPORTS = new ImportDefinitions([], [ - 'org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat' + 'org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat', + 'org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*' ]) private static diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/imports/HttpImportProvider.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/imports/HttpImportProvider.groovy index acd6129c0e..73c1135095 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/imports/HttpImportProvider.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/imports/HttpImportProvider.groovy @@ -1,3 +1,19 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.cloud.contract.verifier.builder.imports import org.springframework.cloud.contract.verifier.config.TestFramework @@ -81,6 +97,6 @@ class HttpImportProvider { */ List getStaticImports(TestFramework testFramework, TestMode testMode) { return TEST_MODE_SPECIFIC_IMPORTS.get(testMode).staticImports + - FRAMEWORK_AND_TEST_MODE_SPECIFIC_IMPORTS.get(new Tuple2(testFramework, testMode)).staticImports + ['org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes'] + FRAMEWORK_AND_TEST_MODE_SPECIFIC_IMPORTS.get(new Tuple2(testFramework, testMode)).staticImports } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy index 5a58de2fd6..359da5bfc3 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy @@ -1,3 +1,19 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.cloud.contract.verifier.converter import java.util.regex.Pattern @@ -18,11 +34,17 @@ import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.NotToEscapePattern import org.springframework.cloud.contract.spec.internal.RegexProperty import org.springframework.cloud.contract.verifier.converter.YamlContract.RegexType +import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.JsonPaths import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter import org.springframework.cloud.contract.verifier.util.MapConverter + +import static org.springframework.cloud.contract.verifier.util.ContentType.XML +import static org.springframework.cloud.contract.verifier.util.ContentUtils.evaluateContentType + /** * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma */ @PackageScope @CompileStatic @@ -50,11 +72,14 @@ class ContractsToYaml { if (!contract.outputMessage) { return } + ContentType contentType = evaluateContentType(contract.response?.headers, + contract.response?.body) yamlContract.outputMessage = new YamlContract.OutputMessage() yamlContract.outputMessage.sentTo = MapConverter.getStubSideValues(contract.outputMessage.sentTo) yamlContract.outputMessage.headers = (contract.outputMessage?.headers as Headers)?.asStubSideMap() - yamlContract.outputMessage.body = MapConverter.getStubSideValues(contract.outputMessage?.body) - contract.outputMessage?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> + yamlContract.outputMessage.body = MapConverter.getStubSideValues( + contract.outputMessage?.body) + contract.outputMessage?.bodyMatchers?.matchers()?.each { BodyMatcher matcher -> yamlContract.outputMessage.matchers.body << new YamlContract.BodyTestMatcher( path: matcher.path(), type: testMatcherType(matcher.matchingType()), @@ -63,28 +88,36 @@ class ContractsToYaml { maxOccurrence: matcher.maxTypeOccurrence() ) } - setOutputBodyMatchers(contract.outputMessage?.body, yamlContract.outputMessage.matchers.body) - setOutputHeadersMatchers(contract.outputMessage?.headers, yamlContract.outputMessage.matchers.headers) + if (XML != contentType) { + setOutputBodyMatchers(contract.outputMessage?.body, + yamlContract.outputMessage.matchers.body) + } + setOutputHeadersMatchers(contract.outputMessage?.headers, + yamlContract.outputMessage.matchers.headers) } protected void input(Contract contract, YamlContract yamlContract) { if (!contract.input) { return } + ContentType contentType = evaluateContentType(contract.input?.messageHeaders, + contract.input?.messageBody) yamlContract.input = new YamlContract.Input() yamlContract.input.assertThat = MapConverter.getTestSideValues(contract.input?.assertThat?.toString()) yamlContract.input.triggeredBy = MapConverter.getTestSideValues(contract.input?.triggeredBy?.toString()) yamlContract.input.messageHeaders = (contract.input?.messageHeaders as Headers)?.asTestSideMap() yamlContract.input.messageBody = MapConverter.getTestSideValues(contract.input?.messageBody) yamlContract.input.messageFrom = MapConverter.getTestSideValues(contract.input?.messageFrom) - contract.input?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> + contract.input?.bodyMatchers?.matchers()?.each { BodyMatcher matcher -> yamlContract.input.matchers.body << new YamlContract.BodyStubMatcher( path: matcher.path(), type: stubMatcherType(matcher.matchingType()), value: matcher.value()?.toString() ) } - setInputBodyMatchers(contract.input?.messageBody, yamlContract.input.matchers.body) + if (XML != contentType) { + setInputBodyMatchers(contract.input?.messageBody, yamlContract.input.matchers.body) + } setInputHeadersMatchers(contract.input?.messageHeaders as Headers, yamlContract.input.matchers.headers) } @@ -92,6 +125,8 @@ class ContractsToYaml { if (!contract.request) { return } + ContentType requestContentType = evaluateContentType(contract.request.headers, + contract.request.body) yamlContract.request = new YamlContract.Request() yamlContract.request.with { YamlContract.Request request -> request.method = contract.request?.method?.serverValue @@ -133,7 +168,7 @@ class ContractsToYaml { } } request.matchers = new YamlContract.StubMatchers() - contract.request?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> + contract.request?.bodyMatchers?.matchers()?.each { BodyMatcher matcher -> request.matchers.body << new YamlContract.BodyStubMatcher( path: matcher.path(), type: stubMatcherType(matcher.matchingType()), @@ -181,7 +216,9 @@ class ContractsToYaml { } } // TODO: Cookie matchers - including absent - setInputBodyMatchers(contract.request?.body, request.matchers.body) + if (XML != requestContentType) { + setInputBodyMatchers(contract.request?.body, request.matchers.body) + } setInputHeadersMatchers(contract.request?.headers as Headers, yamlContract.request.matchers.headers) } } @@ -235,7 +272,8 @@ class ContractsToYaml { } } - protected void setOutputBodyMatchers(DslProperty body, List bodyMatchers) { + protected void setOutputBodyMatchers(DslProperty body, + List bodyMatchers) { def testSideValues = MapConverter.getTestSideValues(body) JsonPaths paths = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(body) paths?.findAll { it.valueBeforeChecking() instanceof Pattern }?.each { @@ -259,6 +297,8 @@ class ContractsToYaml { if (!contract.response) { return } + ContentType contentType = evaluateContentType(contract.response?.headers, + contract.response?.body) yamlContract.response = new YamlContract.Response() yamlContract.response.with { YamlContract.Response response -> response.async = contract.response.async @@ -276,7 +316,7 @@ class ContractsToYaml { } else { response.body = MapConverter.getStubSideValues(contract.response?.body) } - contract.response?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> + contract.response?.bodyMatchers?.matchers()?.each { BodyMatcher matcher -> response.matchers.body << new YamlContract.BodyTestMatcher( path: matcher.path(), type: testMatcherType(matcher.matchingType()), @@ -285,8 +325,12 @@ class ContractsToYaml { maxOccurrence: matcher.maxTypeOccurrence() ) } - setOutputBodyMatchers(contract.response?.body, yamlContract.response.matchers.body) - setOutputHeadersMatchers(contract.response?.headers, yamlContract.response.matchers.headers) + if (XML != contentType) { + setOutputBodyMatchers(contract.response?.body, + yamlContract.response.matchers.body) + } + setOutputHeadersMatchers(contract.response?.headers, + yamlContract.response.matchers.headers) } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy index 24018ecda6..c50a314cb9 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy @@ -1,3 +1,19 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.cloud.contract.verifier.converter import java.nio.file.Files @@ -12,15 +28,23 @@ import org.yaml.snakeyaml.Yaml import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.internal.DslProperty import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.Header +import org.springframework.cloud.contract.spec.internal.Headers import org.springframework.cloud.contract.spec.internal.MatchingTypeValue import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.RegexPatterns import org.springframework.cloud.contract.spec.internal.Request +import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.NamesUtil import org.springframework.util.StringUtils +import static java.util.stream.Collectors.toSet +import static org.springframework.cloud.contract.verifier.util.ContentType.XML +import static org.springframework.cloud.contract.verifier.util.ContentUtils.evaluateContentType + /** * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma */ @CompileStatic @PackageScope @@ -185,6 +209,10 @@ class YamlToContracts { } bodyMatchers { yamlContract.request.matchers?.body?.each { YamlContract.BodyStubMatcher matcher -> + ContentType contentType = + evaluateContentType( + yamlHeadersToContractHeaders(yamlContract.request?.headers), + yamlContract.request?.body) MatchingTypeValue value = null switch (matcher.type) { case YamlContract.StubMatcherType.by_date: @@ -219,7 +247,12 @@ class YamlToContracts { throw new UnsupportedOperationException("The type [" + matcher.type + "] is unsupported. Hint: If you're using remember to pass ") } if (value) { - jsonPath(matcher.path, value) + if (XML == contentType) { + xPath(matcher.path, value) + } + else { + jsonPath(matcher.path, value) + } } } } @@ -268,6 +301,10 @@ class YamlToContracts { if (yamlContract.response.fixedDelayMilliseconds) fixedDelayMilliseconds(yamlContract.response.fixedDelayMilliseconds) bodyMatchers { yamlContract.response?.matchers?.body?.each { YamlContract.BodyTestMatcher testMatcher -> + ContentType contentType = + evaluateContentType( + yamlHeadersToContractHeaders(yamlContract.response?.headers), + yamlContract.response?.body) MatchingTypeValue value = null switch (testMatcher.type) { case YamlContract.TestMatcherType.by_date: @@ -305,7 +342,12 @@ class YamlToContracts { throw new UnsupportedOperationException("The type [" + testMatcher.type + "] is unsupported. Hint: If you're using remember to pass ") } if (testMatcher.path) { - jsonPath(testMatcher.path, value) + if (XML == contentType) { + xPath(testMatcher.path, value) + } + else { + jsonPath(testMatcher.path, value) + } } } } @@ -327,6 +369,10 @@ class YamlToContracts { if (yamlContract.input.messageBodyFromFileAsBytes) messageBody(fileAsBytes(yamlContract.input.messageBodyFromFileAsBytes)) bodyMatchers { yamlContract.input.matchers.body?.each { YamlContract.BodyStubMatcher matcher -> + ContentType contentType = + evaluateContentType( + yamlHeadersToContractHeaders(yamlContract.input?.messageHeaders), + yamlContract.input?.messageBody) MatchingTypeValue value = null switch (matcher.type) { case YamlContract.StubMatcherType.by_date: @@ -351,7 +397,12 @@ class YamlToContracts { default: throw new UnsupportedOperationException("The type [" + matcher.type + "] is unsupported. Hint: If you're using remember to pass ") } - jsonPath(matcher.path, value) + if (XML == contentType) { + xPath(matcher.path, value) + } + else { + jsonPath(matcher.path, value) + } } } } @@ -374,6 +425,10 @@ class YamlToContracts { if (outputMsg.matchers) { bodyMatchers { yamlContract.outputMessage?.matchers?.body?.each { YamlContract.BodyTestMatcher testMatcher -> + ContentType contentType = + evaluateContentType( + yamlHeadersToContractHeaders(yamlContract.outputMessage?.headers), + yamlContract.outputMessage?.body) MatchingTypeValue value = null switch (testMatcher.type) { case YamlContract.TestMatcherType.by_date: @@ -410,7 +465,12 @@ class YamlToContracts { default: throw new UnsupportedOperationException("The type [" + testMatcher.type + "] is unsupported. Hint: If you're using remember to pass ") } - jsonPath(testMatcher.path, value) + if (XML == contentType) { + xPath(testMatcher.path, value) + } + else { + jsonPath(testMatcher.path, value) + } } } } @@ -422,6 +482,15 @@ class YamlToContracts { return contracts } + private Headers yamlHeadersToContractHeaders(Map headers) { + Set
convertedHeaders = headers.keySet().stream() + .map({ new Header(it, headers.get(it)) }) + .collect(toSet()) + Headers contractHeaders = new Headers() + contractHeaders.headers(convertedHeaders) + return contractHeaders + } + protected DslProperty urlValue(String url, YamlContract.KeyValueMatcher urlMatcher) { if (urlMatcher) { if (urlMatcher.command) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy index 25954f620c..cbfac035fb 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy @@ -34,7 +34,9 @@ import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.ContentUtils import org.springframework.cloud.contract.verifier.util.MapConverter +import static org.springframework.cloud.contract.verifier.util.ContentType.UNKNOWN import static org.springframework.cloud.contract.verifier.util.ContentUtils.extractValue +import static org.springframework.cloud.contract.verifier.util.ContentUtils.getClientContentType import static org.springframework.cloud.contract.verifier.util.MapConverter.transformValues /** @@ -195,11 +197,11 @@ abstract class BaseWireMockStubStrategy { */ protected ContentType tryToGetContentType(Object body, Headers headers) { ContentType contentType = ContentUtils.recognizeContentTypeFromHeader(headers) - if (contentType == ContentType.UNKNOWN) { + if (UNKNOWN == contentType) { if (!body) { - return ContentType.UNKNOWN + return UNKNOWN } - return ContentUtils.getClientContentType(body) + return getClientContentType(body) } return contentType } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy index 690bf0f85f..8ab79e7e13 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2013-2018 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,11 +34,14 @@ import groovy.util.logging.Commons import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.internal.Body +import org.springframework.cloud.contract.spec.internal.BodyMatcher import org.springframework.cloud.contract.spec.internal.DslProperty import org.springframework.cloud.contract.spec.internal.FromFileProperty import org.springframework.cloud.contract.spec.internal.MatchingStrategy +import org.springframework.cloud.contract.spec.internal.MatchingType import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.OptionalProperty +import org.springframework.cloud.contract.spec.internal.PathBodyMatcher import org.springframework.cloud.contract.spec.internal.QueryParameters import org.springframework.cloud.contract.spec.internal.RegexPatterns import org.springframework.cloud.contract.spec.internal.RegexProperty @@ -48,14 +51,25 @@ import org.springframework.cloud.contract.verifier.util.ContentUtils import org.springframework.cloud.contract.verifier.util.JsonPaths import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter import org.springframework.cloud.contract.verifier.util.MapConverter +import org.springframework.cloud.contract.verifier.util.xml.XmlToXPathsConverter +import static org.springframework.cloud.contract.spec.internal.MatchingStrategy.Type.BINARY_EQUAL_TO +import static org.springframework.cloud.contract.spec.internal.MatchingType.COMMAND +import static org.springframework.cloud.contract.spec.internal.MatchingType.EQUALITY +import static org.springframework.cloud.contract.spec.internal.MatchingType.NULL +import static org.springframework.cloud.contract.spec.internal.MatchingType.TYPE +import static org.springframework.cloud.contract.verifier.util.ContentType.FORM +import static org.springframework.cloud.contract.verifier.util.ContentUtils.getEqualsTypeFromContentType import static org.springframework.cloud.contract.verifier.util.RegexpBuilders.buildGStringRegexpForStubSide import static org.springframework.cloud.contract.verifier.util.RegexpBuilders.buildJSONRegexpMatch +import static org.springframework.cloud.contract.verifier.util.xml.XmlToXPathsConverter.retrieveValue + /** * Converts a {@link Request} into {@link RequestPattern} * * @author Marcin Grzejszczak * @author Tim Ysewyn + * @author Olga Maciaszek-Sharma * @since 1.0.0 */ @TypeChecked @@ -64,10 +78,13 @@ import static org.springframework.cloud.contract.verifier.util.RegexpBuilders.bu class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { private final Request request + private final ContentType contentType WireMockRequestStubStrategy(Contract groovyDsl) { super(groovyDsl) this.request = groovyDsl.request + this.contentType = + tryToGetContentType(request?.body?.clientValue, request?.headers) } @PackageScope @@ -76,11 +93,10 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { return null } RequestPatternBuilder requestPatternBuilder = appendMethodAndUrl() - ContentType contentType = tryToGetContentType(request?.body?.clientValue, request?.headers) - appendCookies(requestPatternBuilder, contentType) - appendHeaders(requestPatternBuilder, contentType) - appendQueryParameters(requestPatternBuilder, contentType) - appendBody(requestPatternBuilder, contentType) + appendCookies(requestPatternBuilder) + appendHeaders(requestPatternBuilder) + appendQueryParameters(requestPatternBuilder) + appendBody(requestPatternBuilder) appendMultipart(requestPatternBuilder) return requestPatternBuilder.build() } @@ -94,7 +110,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { return RequestPatternBuilder.newRequestPattern(requestMethod, urlPattern) } - private void appendBody(RequestPatternBuilder requestPattern, ContentType contentType) { + private void appendBody(RequestPatternBuilder requestPattern) { if (!request.body) { return } @@ -104,30 +120,67 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { def body = JsonToJsonPathsConverter.removeMatchingJsonPaths(originalBody, request.bodyMatchers) JsonPaths values = JsonToJsonPathsConverter.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(body) if ((values.empty && !request.bodyMatchers?.hasMatchers()) || onlySizeAssertionsArePresent(values)) { - requestPattern.withRequestBody(WireMock.equalToJson(JsonOutput.toJson(getMatchingStrategy(request.body.clientValue).clientValue), false, false)) + requestPattern.withRequestBody(WireMock.equalToJson(JsonOutput.toJson( + getMatchingStrategy(request.body.clientValue).clientValue), + false, false)) } else { values.findAll{ !it.assertsSize() }.each { requestPattern.withRequestBody(WireMock.matchingJsonPath(it.jsonPath().replace("\\\\", "\\"))) } } - if (request.bodyMatchers?.hasMatchers()) { - request.bodyMatchers.jsonPathMatchers().each { + request.bodyMatchers?.matchers()?.each { String newPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(it, originalBody) requestPattern.withRequestBody(WireMock.matchingJsonPath(newPath.replace("\\\\", "\\"))) - } } - } else if (contentType == ContentType.XML) { - requestPattern.withRequestBody(WireMock.equalToXml(getMatchingStrategy(request.body.clientValue).clientValue.toString())) + } + else if (contentType == ContentType.XML) { + Object originalBody = matchingStrategy?.clientValue + Object body = XmlToXPathsConverter + .removeMatchingXPaths(originalBody, request.bodyMatchers) + List byEqualityMatchersFromXml = new XmlToXPathsConverter() + .mapToMatchers(body) + byEqualityMatchersFromXml.each { + addWireMockStubMatchingSection(it, requestPattern, originalBody) + } + request.bodyMatchers?.matchers()?.each { + addWireMockStubMatchingSection(it, requestPattern, originalBody) + } } else if (containsPattern(request?.body)) { - requestPattern.withRequestBody(convertToValuePattern(appendBodyRegexpMatchPattern(request.body), contentType)) + requestPattern.withRequestBody( + convertToValuePattern(appendBodyRegexpMatchPattern(request.body))) } else { - requestBodyGuessedFromMatchingStrategy(requestPattern, contentType) + requestBodyGuessedFromMatchingStrategy(requestPattern) } } - private RequestPatternBuilder requestBodyGuessedFromMatchingStrategy(RequestPatternBuilder requestPattern, ContentType contentType) { + private RequestPatternBuilder requestBodyGuessedFromMatchingStrategy(RequestPatternBuilder requestPattern) { return requestPattern.withRequestBody(convertToValuePattern( - getMatchingStrategy(request.body.clientValue), contentType)) + getMatchingStrategy(request.body.clientValue))) + } + + private static void addWireMockStubMatchingSection(BodyMatcher matcher, + RequestPatternBuilder requestPattern, + Object body) { + Set matchingTypesUnsupportedForRequest = [NULL, COMMAND, TYPE] as Set + if (!matcher instanceof PathBodyMatcher) { + throw new IllegalArgumentException("Only jsonPath and XPath matchers can be processed.") + } + String retrievedValue = Optional.ofNullable(matcher.value()).orElseGet({ + if (matchingTypesUnsupportedForRequest.contains(matcher.matchingType())) { + throw new IllegalArgumentException("Null, Command and Type matchers are not supported in requests.") + } + if (EQUALITY == matcher.matchingType()) { + return retrieveValue(matcher, body) + } + else { + return '' + } + }) + PathBodyMatcher pathMatcher = matcher as PathBodyMatcher + requestPattern.withRequestBody(WireMock.matchingXPath(pathMatcher.path(), + XPathBodyMatcherToWireMockValuePatternConverter + .mapToPattern(pathMatcher.matchingType(), + String.valueOf(retrievedValue)))) } private boolean onlySizeAssertionsArePresent(JsonPaths values) { @@ -152,21 +205,23 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { } } - private void appendHeaders(RequestPatternBuilder requestPattern, ContentType contentType) { + private void appendHeaders(RequestPatternBuilder requestPattern) { if(!request.headers) { return } request.headers.entries.each { - requestPattern.withHeader(it.name, (StringValuePattern) convertToValuePattern(it.clientValue, contentType)) + requestPattern.withHeader(it.name, (StringValuePattern) + convertToValuePattern(it.clientValue)) } } - private void appendCookies(RequestPatternBuilder requestPattern, ContentType contentType) { + private void appendCookies(RequestPatternBuilder requestPattern) { if(!request.cookies) { return } request.cookies.entries.each { - requestPattern.withCookie(it.key, (StringValuePattern) convertToValuePattern(it.clientValue, contentType)) + requestPattern.withCookie(it.key, (StringValuePattern) + convertToValuePattern(it.clientValue)) } } @@ -216,15 +271,16 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { return clientSide } - private void appendQueryParameters(RequestPatternBuilder requestPattern, ContentType contentType) { + private void appendQueryParameters(RequestPatternBuilder requestPattern) { QueryParameters queryParameters = request?.urlPath?.queryParameters ?: request?.url?.queryParameters queryParameters?.parameters?.each { - requestPattern.withQueryParam(it.name, (StringValuePattern) convertToValuePattern(it.clientValue, contentType)) + requestPattern.withQueryParam(it.name, (StringValuePattern) + convertToValuePattern(it.clientValue)) } } @TypeChecked(TypeCheckingMode.SKIP) - private static ContentPattern convertToValuePattern(Object object, ContentType contentType) { + private ContentPattern convertToValuePattern(Object object) { switch (object) { case Pattern: case RegexProperty: @@ -253,7 +309,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { } protected static Object clientBody(Object bodyValue, ContentType contentType) { - if (contentType == ContentType.FORM) { + if (FORM == contentType) { if (bodyValue instanceof Map) { // [a:3, b:4] == "a=3&b=4" return ((Map) bodyValue).collect { @@ -265,7 +321,8 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { StringEscapeUtils.unescapeJavaScript(it.toString()) }.join("&") } - } else if (bodyValue instanceof FromFileProperty) { + } + else if (bodyValue instanceof FromFileProperty) { return bodyValue.isByte() ? bodyValue.asBytes() : bodyValue.asString() } return bodyValue @@ -301,11 +358,12 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { } private MatchingStrategy getMatchingStrategy(FromFileProperty bodyValue) { - return new MatchingStrategy(bodyValue, MatchingStrategy.Type.BINARY_EQUAL_TO) + return new MatchingStrategy(bodyValue, BINARY_EQUAL_TO) } private MatchingStrategy tryToFindMachingStrategy(Object bodyValue) { - return new MatchingStrategy(MapConverter.transformToClientValues(bodyValue), getEqualsTypeFromContentTypeHeader()) + return new MatchingStrategy(MapConverter.transformToClientValues(bodyValue), + getEqualsTypeFromContentType(contentType)) } private MatchingStrategy getMatchingStrategyIncludingContentType(MatchingStrategy matchingStrategy) { @@ -314,7 +372,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { ContentType contentType = ContentUtils.recognizeContentTypeFromMatchingStrategy(type) if (contentType == ContentType.UNKNOWN && type == MatchingStrategy.Type.EQUAL_TO) { contentType = ContentUtils.recognizeContentTypeFromContent(value) - type = ContentUtils.getEqualsTypeFromContentType(contentType) + type = getEqualsTypeFromContentType(contentType) } return new MatchingStrategy(parseBody(value, contentType), type) } @@ -369,9 +427,4 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { private boolean containsPattern(Object o) { return false } - - private MatchingStrategy.Type getEqualsTypeFromContentTypeHeader() { - return ContentUtils.getEqualsTypeFromContentType(ContentUtils.recognizeContentTypeFromHeader(request.headers)) - } - } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy index ee93ce4f99..1a322a81b8 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy @@ -32,11 +32,14 @@ import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.MapConverter import org.springframework.core.io.support.SpringFactoriesLoader -import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent -import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader +import static org.springframework.cloud.contract.verifier.util.ContentUtils.evaluateContentType + /** * Converts a {@link Request} into {@link ResponseDefinition} * + * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma + * * @since 1.0.0 */ @TypeChecked @@ -86,10 +89,7 @@ class WireMockResponseStubStrategy extends BaseWireMockStubStrategy { private void appendBody(ResponseDefinitionBuilder builder) { if (response.body) { Object body = MapConverter.getStubSideValues(response.body) - ContentType contentType = recognizeContentTypeFromHeader(response.headers) - if (contentType == ContentType.UNKNOWN) { - contentType = recognizeContentTypeFromContent(body) - } + ContentType contentType = evaluateContentType(response.headers, body) if (body instanceof byte[]) { builder.withBody(body) } else if (body instanceof FromFileProperty && body.isByte()) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/XPathBodyMatcherToWireMockValuePatternConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/XPathBodyMatcherToWireMockValuePatternConverter.groovy new file mode 100644 index 0000000000..114841b899 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/XPathBodyMatcherToWireMockValuePatternConverter.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.dsl.wiremock + +import com.github.tomakehurst.wiremock.client.WireMock +import com.github.tomakehurst.wiremock.matching.StringValuePattern +import groovy.transform.CompileStatic +import groovy.transform.PackageScope + +import org.springframework.cloud.contract.spec.internal.MatchingType + +import static org.springframework.cloud.contract.spec.internal.MatchingType.EQUALITY + +/** + * @author Olga Maciaszek-Sharma + * @since 2.1.0 + */ +@CompileStatic +@PackageScope +class XPathBodyMatcherToWireMockValuePatternConverter { + + static StringValuePattern mapToPattern(MatchingType type, String value) { + switch (type) { + case EQUALITY: return WireMock.equalTo(value) + default: return WireMock.matching(value) + } + } + +} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy index d159cc7f31..c523012e5b 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy @@ -40,9 +40,15 @@ import static org.apache.commons.text.StringEscapeUtils.escapeJava import static org.apache.commons.text.StringEscapeUtils.escapeJson import static org.apache.commons.text.StringEscapeUtils.escapeXml11 import static org.apache.commons.text.StringEscapeUtils.unescapeXml +import static org.springframework.cloud.contract.verifier.util.ContentType.JSON +import static org.springframework.cloud.contract.verifier.util.ContentType.UNKNOWN + /** * A utility class that can operate on a message body basing on the provided Content Type. * + * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma + * * @since 1.0.0 */ @CompileStatic @@ -84,7 +90,7 @@ class ContentUtils { if (contentType == ContentType.TEXT || contentType == ContentType.FORM) { return extractValueForText(bodyAsValue, valueProvider) } - if (contentType == ContentType.JSON) { + if (JSON == contentType) { return extractValueForJSON(bodyAsValue, valueProvider) } if (contentType == ContentType.XML) { @@ -109,14 +115,14 @@ class ContentUtils { static ContentType getClientContentType(GString bodyAsValue) { try { extractValueForJSON(bodyAsValue, GET_STUB_SIDE) - return ContentType.JSON + return JSON } catch(JsonException e) { try { new XmlSlurper().parseText(extractValueForXML(bodyAsValue, GET_STUB_SIDE).toString()) return ContentType.XML - } catch (Exception exception) { + } catch (Exception ignored) { extractValueForGString(bodyAsValue, GET_STUB_SIDE) - return ContentType.UNKNOWN + return UNKNOWN } } } @@ -124,13 +130,13 @@ class ContentUtils { static ContentType getClientContentType(String bodyAsValue) { try { new JsonSlurper().parseText(bodyAsValue) - return ContentType.JSON + return JSON } catch(JsonException e) { try { new XmlSlurper().parseText(bodyAsValue) return ContentType.XML - } catch (Exception exception) { - return ContentType.UNKNOWN + } catch (Exception ignored) { + return UNKNOWN } } } @@ -145,9 +151,9 @@ class ContentUtils { } else if (bodyAsValue instanceof List) { return getClientContentType((List) bodyAsValue) } else if (bodyAsValue instanceof MatchingStrategy) { - return ContentType.UNKNOWN + return UNKNOWN } else if (bodyAsValue instanceof FromFileProperty) { - return ContentType.UNKNOWN + return UNKNOWN } return tryToGuessContentType(bodyAsValue) } @@ -158,17 +164,18 @@ class ContentUtils { log.debug("No content type passed, will try to guess the type of payload") } return getClientContentType(JsonOutput.toJson(bodyAsValue)) - } catch (Exception ex) { + } + catch (Exception ignored) { if (log.isTraceEnabled()) { log.trace("Failed to assume that body [" + bodyAsValue + "] is json") } } - return ContentType.UNKNOWN + return UNKNOWN } static ContentType getClientContentType(Object bodyAsValue, Headers headers) { ContentType contentType = recognizeContentTypeFromHeader(headers) - if (contentType == ContentType.UNKNOWN) { + if (contentType == UNKNOWN) { return getClientContentType(bodyAsValue) } return contentType; @@ -177,18 +184,18 @@ class ContentUtils { static ContentType getClientContentType(Map bodyAsValue) { try { JsonOutput.toJson(bodyAsValue) - return ContentType.JSON + return JSON } catch (Exception ignore) { - return ContentType.UNKNOWN + return UNKNOWN } } static ContentType getClientContentType(List bodyAsValue) { try { JsonOutput.toJson(bodyAsValue) - return ContentType.JSON + return JSON } catch (Exception ignore) { - return ContentType.UNKNOWN + return UNKNOWN } } @@ -200,7 +207,7 @@ class ContentUtils { } static Object extractValue(GString bodyAsValue, Closure valueProvider) { - return extractValue(bodyAsValue, ContentType.UNKNOWN, valueProvider) + return extractValue(bodyAsValue, UNKNOWN, valueProvider) } private static String extractValueForText(GString bodyAsValue, Closure valueProvider) { @@ -341,7 +348,7 @@ class ContentUtils { it.name == "contentType" } String content = closure(header)?.toString() if (content?.contains("json")) { - return ContentType.JSON + return JSON } if (content?.contains("xml")) { return ContentType.XML @@ -352,7 +359,7 @@ class ContentUtils { if (content?.contains("form-urlencoded")) { return ContentType.FORM } - return ContentType.UNKNOWN + return UNKNOWN } static ContentType recognizeContentTypeFromHeader(Headers headers) { @@ -365,7 +372,7 @@ class ContentUtils { static MatchingStrategy.Type getEqualsTypeFromContentType(ContentType contentType) { switch (contentType) { - case ContentType.JSON: + case JSON: return MatchingStrategy.Type.EQUAL_TO_JSON case ContentType.XML: return MatchingStrategy.Type.EQUAL_TO_XML @@ -375,32 +382,35 @@ class ContentUtils { static ContentType recognizeContentTypeFromContent(GString gstring) { if (isJsonType(gstring)) { - return ContentType.JSON + return JSON } if (isXmlType(gstring)) { return ContentType.XML } - return ContentType.UNKNOWN + return UNKNOWN } static ContentType recognizeContentTypeFromContent(Map jsonMap) { - return ContentType.JSON + return JSON } static ContentType recognizeContentTypeFromContent(byte[] bytes) { - return ContentType.UNKNOWN + return UNKNOWN } static ContentType recognizeContentTypeFromContent(List jsonList) { - return ContentType.JSON + return JSON } static ContentType recognizeContentTypeFromContent(String string) { try { new JsonSlurper().parseText(string) - return ContentType.JSON - } catch (Exception e){ - return ContentType.UNKNOWN + return JSON + } catch (Exception ignored){ + if (isXmlType("$string")) { + return ContentType.XML + } + return UNKNOWN } } @@ -422,7 +432,7 @@ class ContentUtils { } else if (object instanceof Number) { return recognizeContentTypeFromContent((Number) object) } - return ContentType.UNKNOWN + return UNKNOWN } static boolean isJsonType(GString gstring) { @@ -444,17 +454,17 @@ class ContentUtils { return false } - static boolean isXmlType(GString gstring) { + static boolean isXmlType(GString gString) { GString stringWithoutValues = new GStringImpl( - gstring.values.collect({ + gString.values.collect({ it instanceof String || it instanceof GString ? it.toString() : escapeXml11(it.toString()) }) as Object[], - gstring.strings.clone() as String[] + gString.strings.clone() as String[] ) try { new XmlSlurper().parseText(stringWithoutValues.toString()) return true - } catch (Exception e) { + } catch (Exception ignored) { // Not XML } return false @@ -465,9 +475,9 @@ class ContentUtils { case MatchingStrategy.Type.EQUAL_TO_XML: return ContentType.XML case MatchingStrategy.Type.EQUAL_TO_JSON: - return ContentType.JSON + return JSON } - return ContentType.UNKNOWN + return UNKNOWN } static String getGroovyMultipartFileParameterContent(String propertyName, NamedProperty propertyValue, @@ -528,4 +538,11 @@ class ContentUtils { return quote + escapeJava(property.value.serverValue.toString()) + quote + ".getBytes()" } + static ContentType evaluateContentType(Headers contractHeaders, Object body) { + ContentType contentType = recognizeContentTypeFromHeader(contractHeaders) + if (UNKNOWN == contentType) { + contentType = recognizeContentTypeFromContent(body) + } + return contentType + } } 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 e80b01e4be..ba6b032c9e 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 @@ -82,7 +82,7 @@ class JsonToJsonPathsConverter { def jsonCopy = cloneBody(json) DocumentContext context = JsonPath.parse(jsonCopy) if (bodyMatchers?.hasMatchers()) { - bodyMatchers.jsonPathMatchers().each { BodyMatcher matcher -> + bodyMatchers.matchers().each { BodyMatcher matcher -> try { context.delete(matcher.path()) removeTrailingContainers(matcher, context) @@ -254,7 +254,8 @@ class JsonToJsonPathsConverter { Object convertedJson = MapConverter.getClientOrServerSideValues(json, clientSide) Object jsonWithPatterns = ContentUtils.convertDslPropsToTemporaryRegexPatterns(convertedJson) MethodBufferingJsonVerifiable methodBufferingJsonPathVerifiable = - new DelegatingJsonVerifiable(JsonAssertion.assertThat(JsonOutput.toJson(jsonWithPatterns)).withoutThrowingException()) + new DelegatingJsonVerifiable(JsonAssertion.assertThat(JsonOutput.toJson(jsonWithPatterns)) + .withoutThrowingException()) traverseRecursivelyForKey(jsonWithPatterns, methodBufferingJsonPathVerifiable) { MethodBufferingJsonVerifiable key, Object value -> if (value instanceof ExecutionProperty || !(key instanceof FinishedDelegatingJsonVerifiable)) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBuffering.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBuffering.java index 071f6bce5c..4c6c6a791c 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBuffering.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MethodBuffering.java @@ -23,7 +23,7 @@ package org.springframework.cloud.contract.verifier.util; * * @since 1.0.0 */ -interface MethodBuffering { +public interface MethodBuffering { String method(); } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverter.groovy new file mode 100644 index 0000000000..75f42b3eb4 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverter.groovy @@ -0,0 +1,290 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml + + +import java.util.stream.IntStream + +import javax.xml.parsers.DocumentBuilder +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.Transformer +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult +import javax.xml.xpath.XPath +import javax.xml.xpath.XPathFactory + +import groovy.transform.CompileDynamic +import org.w3c.dom.Attr +import org.w3c.dom.Document +import org.w3c.dom.NamedNodeMap +import org.w3c.dom.Node +import org.w3c.dom.NodeList +import org.xml.sax.InputSource + +import org.springframework.cloud.contract.spec.internal.BodyMatcher +import org.springframework.cloud.contract.spec.internal.BodyMatchers +import org.springframework.cloud.contract.spec.internal.MatchingType +import org.springframework.cloud.contract.spec.internal.MatchingTypeValue +import org.springframework.cloud.contract.spec.internal.PathBodyMatcher + +import static java.util.stream.Collectors.toList +import static javax.xml.xpath.XPathConstants.NODE +import static org.apache.commons.lang3.StringUtils.isBlank +import static org.w3c.dom.Node.ATTRIBUTE_NODE +import static org.w3c.dom.Node.CDATA_SECTION_NODE +import static org.w3c.dom.Node.COMMENT_NODE +import static org.w3c.dom.Node.DOCUMENT_NODE +import static org.w3c.dom.Node.DOCUMENT_TYPE_NODE +import static org.w3c.dom.Node.NOTATION_NODE +import static org.w3c.dom.Node.PROCESSING_INSTRUCTION_NODE +import static org.w3c.dom.Node.TEXT_NODE + +/** + * @author Olga Maciaszek-Sharma + * @since 2.1.0 + */ +class XmlToXPathsConverter { + + static Object removeMatchingXPaths(Object body, BodyMatchers bodyMatchers) { + XPath xPath = XPathFactory.newInstance().newXPath() + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + Document parsedXml = documentBuilder + .parse(new InputSource(new StringReader(body as String))) + bodyMatchers?.matchers()?.each({ + Node node = xPath.evaluate(it.path(), parsedXml.documentElement, NODE) as Node + removeNode(node) + }) + parsedXml.normalizeDocument() + return xmlToString(parsedXml) + } + + static String retrieveValue(BodyMatcher matcher, Object body) { + if (matcher.matchingType() == MatchingType.EQUALITY || !matcher.value()) { + return retrieveValueFromBody(matcher.path(), body) + } + return matcher.value() + } + + static String retrieveValueFromBody(String path, Object body) { + return getNodeValue(path, body) + } + + private static String getNodeValue(String path, Object body) { + XPath xPath = XPathFactory.newInstance().newXPath() + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + Document parsedXml = documentBuilder. + parse(new InputSource(new StringReader(body as String))) + return xPath.evaluate(path, parsedXml.documentElement) + } + + private static void removeNode(Node node) { + Optional.ofNullable(node).ifPresent() { + if (isValueNode(node as Node)) { + node.getParentNode().removeChild(node) + } + else removeNode(node.getParentNode()) + } + } + + private static boolean isValueNode(Node node) { + return [TEXT_NODE, + CDATA_SECTION_NODE, + COMMENT_NODE, + DOCUMENT_TYPE_NODE, + PROCESSING_INSTRUCTION_NODE, + NOTATION_NODE].contains(node.nodeType) + } + + private static boolean isAttributeNode(Node node) { + return ATTRIBUTE_NODE == node.nodeType + } + + private static String xmlToString(Node parsedXml) { + Transformer transformer = TransformerFactory.newInstance().newTransformer() + StringWriter writer = new StringWriter() + StreamResult result = new StreamResult(writer) + transformer.transform(new DOMSource(parsedXml), result) + return writer.toString() + } + + static List mapToMatchers(Object xml) { + DocumentBuilder documentBuilder = DocumentBuilderFactory + .newInstance() + .newDocumentBuilder() + Document parsedXml = documentBuilder + .parse(new InputSource(new StringReader(xml as String))) + List> valueNodes = getValueNodesWithParents(parsedXml) + List matchers = [] + List valueNodePaths = transformListEntries(valueNodes) + valueNodePaths.each { + matchers << new PathBodyMatcher( + buildXPath(it.fromChildToParents(), it.index), + new MatchingTypeValue(MatchingType.EQUALITY, + it.path.get(0).nodeValue)) + } + return matchers + } + + static List transformListEntries(List> nodeLists) { + List pathOccurrenceCounters = [] + List nodePaths = [] + nodeLists.each { nodeList -> + List parentNodesList = nodeList.subList(1, nodeList.size()) + int elementIndex = pathOccurrenceCounters.stream() + .map({ it }) + .filter({ + nodeNames(it.path) == nodeNames(parentNodesList) + }).findFirst() + .map({ ++it.counter }) + .orElseGet({ + PathOccurrenceCounter pathCounter = new PathOccurrenceCounter(parentNodesList) + pathOccurrenceCounters << pathCounter + return pathCounter.counter + }) + nodePaths << new NodePath(nodeList, elementIndex) + } + return nodePaths + } + + private static List nodeNames(List nodes) { + return nodes.stream() + .map({ it.getNodeName() }) + .collect(toList()) + } + + static String buildXPath(List nodes, int index = 1) { + XmlVerifiable xmlVerifiable = XPathBuilder.builder() + if (!nodes) { + return xmlVerifiable.xPath() + } + nodes.subList(0, nodes.size() - 1).each { + xmlVerifiable = processNode(xmlVerifiable, it) + } + Node closingNode = nodes.get(nodes.size() - 1) + xmlVerifiable = processClosingNode(xmlVerifiable, closingNode, index) + return xmlVerifiable.xPath() + } + + private static XmlVerifiable processNode(XmlVerifiable xmlVerifiable, Node node) { + return xmlVerifiable.node(node.nodeName) + } + + private static XmlVerifiable processNode(XmlVerifiable xmlVerifiable, Attr attribute) { + return xmlVerifiable.withAttribute(attribute.nodeName) + } + + private static XmlVerifiable processClosingNode(XmlVerifiable xmlVerifiable, Node node, int index) { + return index != 1 ? xmlVerifiable.index(index).text() : xmlVerifiable.text() + } + + private static XmlVerifiable processClosingNode(XmlVerifiable xmlVerifiable, Attr attribute, int index) { + if (index != 1) { + xmlVerifiable.index(index) + } + return processNode(xmlVerifiable, attribute) + } + + private static List> getValueNodesWithParents(Node node) { + List> valueNodes = [] + List attributes = [] + addValueNodes(node, valueNodes, attributes) + attributes.each { + valueNodes << withParents(it) + } + return valueNodes + } + + private static List addValueNodes(Node node, List> valueNodes, List attributes) { + getChildNodesAsList(node).each { + attributes.addAll(getAttributesAsList(node)) + if (isValueNode(it) && !isBlank(it.nodeValue)) { + valueNodes << withParents(it) + } + else { + addValueNodes(it, valueNodes, attributes) + } + } + } + + private static List getChildNodesAsList(Node node) { + NodeList nodeList = node.getChildNodes() + return getNodeCollectionElements(nodeList) + } + + @CompileDynamic + private static List getAttributesAsList(Node node) { + NamedNodeMap nodeMap = node.getAttributes() + return nodeMap != null ? getNodeCollectionElements(nodeMap) : [] + } + + @CompileDynamic + private static List getNodeCollectionElements(def nodeCollection) { + return IntStream.range(0, nodeCollection.getLength()) + .mapToObj({ nodeCollection.item(it) as Node }) + .collect(toList()) + } + + private static List withParents(Node node) { + List nodeList = new ArrayList<>() + nodeList << node + return addParents(node, nodeList) + } + + private static List withParents(Attr attribute) { + List nodeList = new ArrayList<>() + nodeList << attribute + Node ownerNode = attribute.getOwnerElement() + nodeList << ownerNode + return addParents(ownerNode, nodeList) + } + + private static List addParents(Node node, List nodeList) { + Node parentNode = node.getParentNode() + if (parentNode != null && DOCUMENT_NODE != parentNode.nodeType) { + nodeList << parentNode + return addParents(parentNode, nodeList) + } + return nodeList + } + + private static class NodePath { + final List path + final int index + + NodePath(List path, int index) { + this.path = path + this.index = index + } + + List fromChildToParents() { + return path.reverse() + } + } + + private static class PathOccurrenceCounter { + final List path + int counter + + PathOccurrenceCounter(List path) { + this.path = path + counter = 1 + } + } +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtil.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtil.java index c2480b254e..5400744dbd 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtil.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2018 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,20 +24,34 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + /** * Helper class for the generated tests * * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma * @since 2.1.0 */ public class ContractVerifierUtil { + private static final Log LOG = LogFactory.getLog(ContractVerifierUtil.class); + /** * Helper method to convert a file to bytes * * @param testClass - test class relative to which the file is stored * @param relativePath - relative path to the file * @return bytes of the file + * @since 2.1.0 */ public static byte[] fileToBytes(Object testClass, String relativePath) { try { @@ -52,4 +66,42 @@ public class ContractVerifierUtil { } } + /** + * Helper method to retrieve XML node value with provided xPath + * + * @param parsedXml - a {@link Document} object with parsed XML content + * @param path - the xPath expression to retrieve the value with + * @return {@link String} value of the XML node + * @since 2.1.0 + */ + public static String valueFromXPath(Document parsedXml, String path) { + XPath xPath = XPathFactory.newInstance().newXPath(); + try { + return xPath.evaluate(path, parsedXml.getDocumentElement()); + } + catch (XPathExpressionException exception) { + LOG.error("Incorrect xpath provided: " + path, exception); + throw new IllegalArgumentException(); + } + } + + /** + * Helper method to retrieve XML {@link Node} with provided xPath + * + * @param parsedXml - a {@link Document} object with parsed XML content + * @param path - the xPath expression to retrieve the value with + * @return XML {@link Node} object + * @since 2.1.0 + */ + public static Node nodeFromXPath(Document parsedXml, String path) { + XPath xPath = XPathFactory.newInstance().newXPath(); + try { + return (Node) xPath.evaluate(path, parsedXml.getDocumentElement(), + XPathConstants.NODE); + } + catch (XPathExpressionException exception) { + LOG.error("Incorrect xpath provided: " + path, exception); + throw new IllegalArgumentException(); + } + } } diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ArrayValueAssertion.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ArrayValueAssertion.java new file mode 100644 index 0000000000..a00930f5dc --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ArrayValueAssertion.java @@ -0,0 +1,137 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + +import java.util.Iterator; +import java.util.LinkedList; + +import org.eclipse.wst.xml.xpath2.api.Item; +import org.eclipse.wst.xml.xpath2.api.ResultSequence; + +class ArrayValueAssertion extends FieldAssertion implements XmlArrayVerifiable { + + private final boolean checkingPrimitiveType; + + ArrayValueAssertion(XmlCachedObjects cachedObjects, LinkedList xPathBuffer, LinkedList specialCaseXPathBuffer, + Object arrayName, XmlAsserterConfiguration xmlAsserterConfiguration) { + super(cachedObjects, xPathBuffer, specialCaseXPathBuffer, arrayName, xmlAsserterConfiguration); + this.checkingPrimitiveType = true; + } + + private ArrayValueAssertion(XmlCachedObjects cachedObjects, LinkedList xPathBuffer, LinkedList specialCaseXPathBuffer, + Object arrayName, XmlAsserterConfiguration xmlAsserterConfiguration, + boolean checkingPrimitiveType) { + super(cachedObjects, xPathBuffer, specialCaseXPathBuffer, arrayName, xmlAsserterConfiguration); + this.checkingPrimitiveType = checkingPrimitiveType; + } + + private ArrayValueAssertion(XmlAsserter asserter, boolean checkingPrimitiveType) { + super(asserter); + this.checkingPrimitiveType = checkingPrimitiveType; + } + + @Override + public XmlArrayVerifiable contains(String value) { + return new ArrayValueAssertion(cachedObjects, xPathBuffer, specialCaseXPathBuffer, value, + xmlAsserterConfiguration, false); + } + + @Override + public XmlArrayVerifiable hasSize(int size) { + String xPath = "count(" + createXPathString() + ")"; + ArrayValueAssertion verifiable = new ArrayValueAssertion(this, this.checkingPrimitiveType); + verifiable.specialCaseXPathBuffer.clear(); + verifiable.specialCaseXPathBuffer.add(xPath); + String xPathString = verifiable.createSpecialCaseXPathString(); + ResultSequence sequence = verifiable.resultSequence(xPathString); + Iterator iterator = sequence.iterator(); + if (!iterator.hasNext()) { + throw new IllegalStateException("Parsed XML [" + cachedObjects.xmlAsString + "] doesn't match the XPath <" + xPathString + ">"); + } + int retrievedSize = Integer.valueOf(iterator.next().getStringValue()); + if (retrievedSize != size) { + throw new IllegalStateException("Parsed XML [" + cachedObjects.xmlAsString + "] has size [" + retrievedSize + "] and not [" + size + "] for XPath <" + xPathString + "> "); + } + return verifiable; + } + + @Override + public FieldAssertion node(String value) { + FieldAssertion assertion = super.node(value); + return new ArrayValueAssertion(assertion, false); + } + + @Override + public FieldAssertion node(String... nodeNames) { + FieldAssertion assertion = super.node(nodeNames); + return new ArrayValueAssertion(assertion, false); + } + + @Override + protected void removeLastFieldElement(XmlAsserter readyToCheck) { + readyToCheck.xPathBuffer.removeLast(); + } + + @Override + public XmlVerifiable isEqualTo(String value) { + if (!checkingPrimitiveType) { + return super.isEqualTo(value); + } + return equalityOnAPrimitive("[text()=" + escapeText(value) + "]"); + } + + @Override + public XmlVerifiable isEqualTo(Number value) { + if (!checkingPrimitiveType) { + return super.isEqualTo(value); + } + return equalityOnAPrimitive("[number()=" + String.valueOf(value) + "]"); + } + + private XmlVerifiable equalityOnAPrimitive(String xPath) { + ReadyToCheckAsserter readyToCheck = new ReadyToCheckAsserter(cachedObjects, + xPathBuffer, fieldName, xmlAsserterConfiguration); + readyToCheck.xPathBuffer.removeLast(); + readyToCheck.xPathBuffer.offer(xPath); + readyToCheck.checkBufferedXPathString(); + return readyToCheck; + } + + @Override + public XmlVerifiable matches(String value) { + if (!checkingPrimitiveType) { + return super.matches(value); + } + return equalityOnAPrimitive( + "[matches(text(), " + + escapeText(escapeRegex(value)) + ")]"); + } + + @Override + public XmlVerifiable isEqualTo(Boolean value) { + if (!checkingPrimitiveType) { + return super.isEqualTo(value); + } + return isEqualTo(String.valueOf(value)); + } + + @Override + public boolean isAssertingAValueInArray() { + return true; + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/FieldAssertion.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/FieldAssertion.java new file mode 100644 index 0000000000..d48f33946e --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/FieldAssertion.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + +import java.util.LinkedList; + +class FieldAssertion extends XmlAsserter { + + FieldAssertion(XmlCachedObjects cachedObjects, LinkedList xPathBuffer, LinkedList specialCaseXPathBuffer, + Object value, XmlAsserterConfiguration xmlAsserterConfiguration) { + super(cachedObjects, xPathBuffer, specialCaseXPathBuffer, value, xmlAsserterConfiguration); + } + + FieldAssertion(XmlAsserter asserter) { + super(asserter); + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/IteratingOverArray.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/IteratingOverArray.java new file mode 100644 index 0000000000..fe4a8cb5cf --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/IteratingOverArray.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + +/** + * Helper interface describing the process of current iteration + * + * @author Marcin Grzejszczak + * + * @since 2.1.0 + */ +public interface IteratingOverArray { + + /** + * True if is in progress of iterating over an array + */ + boolean isIteratingOverArray(); + + /** + * True if current element is a particular value on which concrete assertion will take place + */ + boolean isAssertingAValueInArray(); + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ReadyToCheckAsserter.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ReadyToCheckAsserter.java new file mode 100644 index 0000000000..f7e8f6fe62 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ReadyToCheckAsserter.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + +import java.util.LinkedList; + +class ReadyToCheckAsserter extends XmlAsserter { + + public ReadyToCheckAsserter(XmlCachedObjects cachedObjects, LinkedList xPathBuffer, + Object fieldName, XmlAsserterConfiguration xmlAsserterConfiguration) { + super(cachedObjects, xPathBuffer, new LinkedList(), fieldName, xmlAsserterConfiguration); + } + + @Override + protected boolean isReadyToCheck() { + return true; + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XPathBuilder.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XPathBuilder.java new file mode 100644 index 0000000000..624051f24f --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XPathBuilder.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + +/** + * Builder of XPaths. + * + * @author Marcin Grzejszczak + * @since 2.1.0 + * + * @see XmlVerifiable + * @see XmlAssertion + */ +public class XPathBuilder { + + /** + * Returns a builder of {@link XmlVerifiable} with which you can build your + * XPath. Once finished just call {@link XmlVerifiable#xPath()} to get + * XPath as String. + */ + public static XmlVerifiable builder() { + return XmlAssertion.assertThat("").withoutThrowingException(); + } + + /** + * Using a XPath builder for the given XML you can read its value. + */ + public static XmlVerifiable builder(String xml) { + return XmlAssertion.assertThat(xml).withoutThrowingException(); + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlArrayVerifiable.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlArrayVerifiable.java new file mode 100644 index 0000000000..8784b3ab21 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlArrayVerifiable.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + + +/** + * Contract to match an array in a parsed XML via XPath + * + * @author Marcin Grzejszczak + * + * @since 2.1.0 + */ +public interface XmlArrayVerifiable extends XmlVerifiable { + + /** + * When you want to assert a node with a name in an array. + */ + XmlArrayVerifiable contains(String nodeName); + + /** + * When you want to assert if an array is of given size + * @since 0.0.2 + */ + XmlArrayVerifiable hasSize(int size); + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserter.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserter.java new file mode 100644 index 0000000000..eea0b6e322 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserter.java @@ -0,0 +1,416 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Pattern; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.wst.xml.xpath2.api.ResultSequence; +import org.eclipse.wst.xml.xpath2.api.XPath2Expression; +import org.eclipse.wst.xml.xpath2.processor.Engine; +import org.eclipse.wst.xml.xpath2.processor.internal.types.ElementType; +import org.eclipse.wst.xml.xpath2.processor.util.DynamicContextBuilder; + +class XmlAsserter implements XmlVerifiable { + + private static final Log log = LogFactory.getLog(XmlAsserter.class); + + private final static Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\].+*?^$\\\\|]"); + + final XmlCachedObjects cachedObjects; + final LinkedList xPathBuffer; + // for things like count(...) + final LinkedList specialCaseXPathBuffer; + final Object fieldName; + final XmlAsserterConfiguration xmlAsserterConfiguration; + + XmlAsserter(XmlCachedObjects cachedObjects, LinkedList xPathBuffer, LinkedList specialCaseXPathBuffer, + Object fieldName, XmlAsserterConfiguration xmlAsserterConfiguration) { + this.cachedObjects = cachedObjects; + this.xPathBuffer = new LinkedList<>(xPathBuffer); + this.specialCaseXPathBuffer = new LinkedList<>(specialCaseXPathBuffer); + this.fieldName = fieldName; + this.xmlAsserterConfiguration = xmlAsserterConfiguration; + } + + XmlAsserter(XmlAsserter asserter) { + this.cachedObjects = asserter.cachedObjects; + this.xPathBuffer = new LinkedList<>(asserter.xPathBuffer); + this.specialCaseXPathBuffer = new LinkedList<>(asserter.specialCaseXPathBuffer); + this.fieldName = asserter.fieldName; + this.xmlAsserterConfiguration = asserter.xmlAsserterConfiguration; + } + + @Override + public FieldAssertion node(final String value) { + FieldAssertion asserter = new FieldAssertion(cachedObjects, xPathBuffer, specialCaseXPathBuffer, value, + xmlAsserterConfiguration); + asserter.xPathBuffer.offer(String.valueOf(value)); + asserter.xPathBuffer.offer("/"); + return asserter; + } + + @Override + public XmlVerifiable withAttribute(String attribute, String attributeValue) { + FieldAssertion asserter = new FieldAssertion(cachedObjects, xPathBuffer, + specialCaseXPathBuffer, fieldName, + xmlAsserterConfiguration); + if (asserter.xPathBuffer.peekLast().equals("/")) { + asserter.xPathBuffer.removeLast(); + } + if (isReadyToCheck()) { + asserter.xPathBuffer.offer("/" + fieldName); + } + asserter.xPathBuffer.offer("[@" + String.valueOf(attribute) + "=" + escapeText(attributeValue) + "]"); + updateCurrentBuffer(asserter); + asserter.checkBufferedXPathString(); + return asserter; + } + + @Override + public XmlVerifiable withAttribute(String attribute) { + FieldAssertion asserter = new FieldAssertion(cachedObjects, xPathBuffer, + specialCaseXPathBuffer, fieldName, + xmlAsserterConfiguration); + asserter.xPathBuffer.offer("@" + String.valueOf(attribute)); + updateCurrentBuffer(asserter); + asserter.checkBufferedXPathString(); + return asserter; + } + + @Override + public XmlVerifiable text() { + FieldAssertion asserter = new FieldAssertion(cachedObjects, xPathBuffer, + specialCaseXPathBuffer, fieldName, + xmlAsserterConfiguration); + asserter.xPathBuffer.offer("text()"); + return asserter; + } + + @Override + public XmlVerifiable index(int index) { + FieldAssertion asserter = new FieldAssertion(cachedObjects, xPathBuffer, + specialCaseXPathBuffer, fieldName, + xmlAsserterConfiguration); + if (asserter.xPathBuffer.peekLast().equals("/")) { + asserter.xPathBuffer.removeLast(); + } + asserter.xPathBuffer.offer("[" + index + "]"); + asserter.xPathBuffer.offer("/"); + return asserter; + } + + @Override + public FieldAssertion node(String... nodeNames) { + FieldAssertion assertion = null; + for(String field : nodeNames) { + assertion = assertion == null ? node(field) : assertion.node(field); + } + return assertion; + } + + @Override + public XmlArrayVerifiable array(final String value) { + ArrayValueAssertion asserter = new ArrayValueAssertion(cachedObjects, xPathBuffer, specialCaseXPathBuffer, + value, xmlAsserterConfiguration); + asserter.xPathBuffer.offer(String.valueOf(value)); + asserter.xPathBuffer.offer("/"); + return asserter; + } + + @Override + public XmlVerifiable isEqualTo(String value) { + if (value == null) { + return isNull(); + } + ReadyToCheckAsserter readyToCheck = new ReadyToCheckAsserter(cachedObjects, + xPathBuffer, fieldName, xmlAsserterConfiguration); + removeLastFieldElement(readyToCheck); + readyToCheck.xPathBuffer.offer("[" + fieldName + "=" + escapeText(value) + "]"); + updateCurrentBuffer(readyToCheck); + readyToCheck.checkBufferedXPathString(); + return readyToCheck; + } + + private void updateCurrentBuffer(XmlAsserter readyToCheck) { + xPathBuffer.clear(); + xPathBuffer.addAll(readyToCheck.xPathBuffer); + } + + @Override + public XmlVerifiable isEqualTo(Object value) { + if (value == null) { + return isNull(); + } + if (value instanceof Number) { + return isEqualTo((Number) value); + } else if (value instanceof Boolean) { + return isEqualTo((Boolean) value); + } else if (value instanceof Pattern) { + return matches(((Pattern) value).pattern()); + } + return isEqualTo(value.toString()); + } + + @Override + public XmlVerifiable isEqualTo(Number value) { + if (value == null) { + return isNull(); + } + return xmlVerifiableFromObject(value); + } + + private XmlVerifiable xmlVerifiableFromObject(Object value) { + ReadyToCheckAsserter readyToCheck = new ReadyToCheckAsserter(cachedObjects, + xPathBuffer, fieldName, xmlAsserterConfiguration); + removeLastFieldElement(readyToCheck); + readyToCheck.xPathBuffer.offer("[" + fieldName + "=" + String.valueOf(value) + "]"); + // and finally '/foo/bar[baz='sth'] + updateCurrentBuffer(readyToCheck); + readyToCheck.checkBufferedXPathString(); + return readyToCheck; + } + + protected void removeLastFieldElement(XmlAsserter readyToCheck) { + // assuming /foo/bar/baz/ + // remove '/' + readyToCheck.xPathBuffer.removeLast(); + // remove field name ('baz') + readyToCheck.xPathBuffer.removeLast(); + // remove '/' + readyToCheck.xPathBuffer.removeLast(); + // and then we get '/foo/bar' + } + + @Override + public XmlVerifiable isNull() { + ReadyToCheckAsserter readyToCheck = new ReadyToCheckAsserter(cachedObjects, + xPathBuffer, fieldName, xmlAsserterConfiguration); + String xpath = createXPathString(); + readyToCheck.xPathBuffer.clear(); + readyToCheck.xPathBuffer.offer("not(boolean(" + xpath + "/text()[1]))"); + updateCurrentBuffer(readyToCheck); + readyToCheck.checkBufferedXPathString(); + return readyToCheck; + } + + @Override + public XmlVerifiable matches(String value) { + if (value == null) { + return isNull(); + } + ReadyToCheckAsserter readyToCheck = new ReadyToCheckAsserter(cachedObjects, + xPathBuffer, fieldName, xmlAsserterConfiguration); + removeLastFieldElement(readyToCheck); + readyToCheck.xPathBuffer.offer("[matches(" + fieldName + ", " + + escapeText(escapeRegex(value)) + ")]"); + updateCurrentBuffer(readyToCheck); + readyToCheck.checkBufferedXPathString(); + return readyToCheck; + } + + @Override + public XmlVerifiable isEqualTo(Boolean value) { + if (value == null) { + return isNull(); + } + return isEqualTo(String.valueOf(value)); + } + + @Override + public XmlVerifiable withoutThrowingException() { + xmlAsserterConfiguration.ignoreXPathException = true; + return this; + } + + protected void check(String xPathString) { + if (xmlAsserterConfiguration.ignoreXPathException) { + log.trace("WARNING!!! Overriding verification of the XPath. Your tests may pass even though they shouldn't"); + return; + } + ResultSequence expr = resultSequence(xPathString); + boolean xpathMatched = !expr.empty(); + if (!xpathMatched) { + throw new IllegalStateException("Parsed XML [" + cachedObjects.xmlAsString + "] doesn't match the XPath <" + xPathString + ">"); + } + } + + ResultSequence resultSequence(String xPathString) { + return xPathExpression(xPathString); + } + + private ResultSequence xPathExpression(String xPathString) { + try { + XPath2Expression expr = new Engine().parseExpression(xPathString, cachedObjects.xpathBuilder); + return expr.evaluate(new DynamicContextBuilder(cachedObjects.xpathBuilder), + new Object[] { cachedObjects.document }); + } catch (Exception e) { + throw new XmlAsserterXpathException(xPath(), cachedObjects.xmlAsString, e); + } + } + + void checkBufferedXPathString() { + check(createXPathString()); + } + + String createXPathString() { + return createXPathString(xPathBuffer); + } + + String createSpecialCaseXPathString() { + return createXPathString(specialCaseXPathBuffer); + } + + String createXPathString(LinkedList buffer) { + LinkedList queue = new LinkedList(buffer); + StringBuilder stringBuffer = new StringBuilder(); + while (!queue.isEmpty()) { + String value = queue.remove(); + if (!(queue.isEmpty() && value.equals("/"))) { + stringBuffer.append(value); + } + } + return stringBuffer.toString(); + } + + @Override + public String xPath() { + if (!specialCaseXPathBuffer.isEmpty()) { + return createSpecialCaseXPathString(); + } + return createXPathString(); + } + + @Override + public void matchesXPath(String xPath) { + check(xPath); + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + XmlAsserter that = (XmlAsserter) o; + if (!xPathBuffer.equals(that.xPathBuffer)) + return false; + return fieldName != null ? fieldName.equals(that.fieldName) : that.fieldName == null; + + } + + @Override + public int hashCode() { + int result = xPathBuffer.hashCode(); + result = 31 * result + (fieldName != null ? fieldName.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "\\nAsserter{\n " + "xPathBuffer=" + String.valueOf(xPathBuffer) + + "\n}"; + } + + @Override + public boolean isIteratingOverArray() { + return false; + } + + @Override + public boolean isAssertingAValueInArray() { + return false; + } + + static String escapeText(Object object) { + String string = String.valueOf(object); + if (!string.contains("'")) { + return wrapValueWithSingleQuotes(string); + } + String[] split = string.split("'"); + LinkedList list = new LinkedList(); + list.add("concat("); + for (String splitString : split) { + list.add("'" + splitString + "'"); + list.add(","); + list.add("\"'\""); + list.add(","); + } + // will remove the last ,', entries + // removing last colon + list.removeLast(); + // removing last escaped apostrophe + list.removeLast(); + // removing last colon + list.removeLast(); + list.add(")"); + return buildStringFromList(list); + } + + static String escapeRegex(Object object) { + return String.valueOf(object); + } + + private static String escapeSpecialRegexChars(String str) { + return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0"); + } + + private static String buildStringFromList(List list) { + StringBuilder builder = new StringBuilder(); + for (String string : list) { + builder.append(string); + } + return builder.toString(); + } + + private static String wrapValueWithSingleQuotes(Object value) { + return value instanceof String ? + "'" + value + "'" : + value.toString(); + } + + @Override + public String read() { + String xpath = xPath(); + ResultSequence expr = resultSequence(xpath); + if (expr.empty()) { + throw new XmlAsserterXpathException(xPath(), cachedObjects.xmlAsString); + } + if (expr instanceof ElementType) { + return ((ElementType) expr).getStringValue(); + } + throw new UnsupportedOperationException("Can't return values of complex types"); + } + + protected boolean isReadyToCheck() { + return false; + } + + private static class XmlAsserterXpathException extends RuntimeException { + XmlAsserterXpathException(String xPath, String xmlAsString) { + super("Exception occurred while trying to evaluate " + + "XPath [" + xPath + "] from XML [" + xmlAsString + "]"); + } + + XmlAsserterXpathException(String xPath, String xmlAsString, Exception e) { + super("Exception occurred while trying to evaluate " + + "XPath [" + xPath + "] from XML [" + xmlAsString + "]", e); + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserterConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserterConfiguration.java new file mode 100644 index 0000000000..bcf792aa42 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserterConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + +/** + * Assertion configuration + * + * @author Marcin Grzejszczak + * + * @since 2.1.0 + */ +class XmlAsserterConfiguration { + boolean ignoreXPathException = false; +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAssertion.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAssertion.java new file mode 100644 index 0000000000..5b1cc0931f --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAssertion.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.wst.xml.xpath2.processor.DOMLoader; +import org.eclipse.wst.xml.xpath2.processor.XercesLoader; +import org.w3c.dom.Document; + +/** + * Entry point for assertions. Use the static factory method and you're ready to go! + * + * @author Marcin Grzejszczak + * + * @since 2.1.0 + * + * @see XmlVerifiable + */ +public class XmlAssertion { + private final XmlCachedObjects cachedObjects; + private final LinkedList xPathBuffer = new LinkedList<>(); + private final LinkedList specialCaseXPathBuffer = new LinkedList<>(); + private final XmlAsserterConfiguration xmlAsserterConfiguration = new XmlAsserterConfiguration(); + private static final Map CACHE = new ConcurrentHashMap<>(); + + private XmlAssertion(Document parsedXml) { + this.cachedObjects = new XmlCachedObjects(parsedXml); + } + + private XmlAssertion(String xml) { + XmlCachedObjects cachedObjects = CACHE.get(xml); + if (cachedObjects == null && !empty(xml)) { + try { + InputStream inputXml = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); + DOMLoader loader = new XercesLoader(); + Document document = loader.load(inputXml); + cachedObjects = new XmlCachedObjects(document, xml); + } catch (Exception e) { + throw new IllegalStateException("Exception occurred while trying to parse the XML", e); + } + CACHE.put(xml, cachedObjects); + } + this.cachedObjects = cachedObjects; + } + + private boolean empty(String text) { + return text == null || text.length() == 0 || text.matches("^\\s*$"); + } + + /** + * Starts assertions for the XML provided as {@link String} + */ + public static XmlVerifiable assertThat(String xml) { + return new XmlAssertion(xml).root(); + } + + /** + * Starts assertions for the XML provided as {@link Document} + */ + public static XmlVerifiable assertThat(Document parsedXml) { + return new XmlAssertion(parsedXml).root(); + } + + /** + * Helper method so that there are no clashes with other static methods of that name + * + * @see XmlAssertion#assertThat(String) + */ + public static XmlVerifiable assertThatXml(String body) { + return assertThat(body); + } + + /** + * Helper method so that there are no clashes with other static methods of that name + * + * @see XmlAssertion#assertThat(Document) + */ + public static XmlVerifiable assertThatXml(Document parsedXml) { + return assertThat(parsedXml); + } + + private XmlVerifiable root() { + return new FieldAssertion(cachedObjects, xPathBuffer, specialCaseXPathBuffer, "", xmlAsserterConfiguration).node(""); + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlCachedObjects.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlCachedObjects.java new file mode 100644 index 0000000000..951807bb25 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlCachedObjects.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + +import java.io.StringWriter; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.eclipse.wst.xml.xpath2.processor.util.StaticContextBuilder; +import org.w3c.dom.Document; + +/** + * Contains cached objects that are memory consuming + * + * @author Marcin Grzejszczak + * + * @since 2.1.0 + */ +class XmlCachedObjects { + final Document document; + final StaticContextBuilder xpathBuilder; + final String xmlAsString; + + XmlCachedObjects(Document document) { + this.document = document; + this.xpathBuilder = new StaticContextBuilder(); + this.xmlAsString = xmlAsString(); + } + + XmlCachedObjects(Document document, String xmlAsString) { + this.document = document; + this.xpathBuilder = new StaticContextBuilder(); + this.xmlAsString = xmlAsString; + } + + private String xmlAsString() { + try { + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + StringWriter writer = new StringWriter(); + transformer.transform(new DOMSource(document), new StreamResult(writer)); + return writer.getBuffer().toString().replaceAll("\n|\r", ""); + } catch (TransformerException e) { + throw new RuntimeException("Exception occured while trying to convert XML Document to String", e); + } + } +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlReader.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlReader.java new file mode 100644 index 0000000000..81f950fa29 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlReader.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + +/** + * Contract to read the value from a XML basing on it. + * + * @author Marcin Grzejszczak + * + * @since 2.1.0 + */ +public interface XmlReader { + + /** + * Returns the value from the XML, based on the created XPath. + */ + String read(); +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlVerifiable.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlVerifiable.java new file mode 100644 index 0000000000..4ac43ff356 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlVerifiable.java @@ -0,0 +1,194 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml; + +/** + * Contract to match a parsed XML via XPath + * + * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma + * + * @since 2.1.0 + */ +public interface XmlVerifiable extends IteratingOverArray, XmlReader { + + /** + * Field assertion. Adds a XPath entry for a single node. + */ + XmlVerifiable node(String nodeName); + + /** + * Field assertion. Adds a attribute to the currently checked node. + * NOTE: If you want to both check equality and attributes you have to + * first check the equality and then attributes. E.g. having such an XML + * + *

+ * + * {@code + + + + with "val'ue + 4 + foo + + name1 + + + name2 + + + 8 + + + name3 + + + + * } + * + *

+ * + * In order to check the values of the attributes of the {@code withlist} element + * with value {@code name3} you'd have to call: + * + *

+ * + * {@code assertThat(xml1).node("some").node("nested").array("withlist").contains("name").isEqualTo("name3").withAttribute("id", "10").withAttribute("surname", "kowalski")} + * + *

+ * + * The following XPath would be created: + {@code /some/nested/withlist[name='name3']/name[@id='10'][@surname='kowalski'] } + * + */ + XmlVerifiable withAttribute(String attribute, String attributeValue); + + /** + * Adds attribute query to xPath without comparing with any provided value + * @param attribute AttributeName + * @return new {@code XmlVerifiable} + */ + XmlVerifiable withAttribute(String attribute); + + /** + * Adds a {@code text()} call to xPath + * @return new {@code XmlVerifiable} + */ + XmlVerifiable text(); + + /** + * Adds an index to xPath + * @return new {@code XmlVerifiable} + */ + XmlVerifiable index(int index); + + /** + * Field assertions. Traverses through the list of nodes and + * adds a XPath entry for each one. + */ + XmlVerifiable node(String... nodeNames); + + /** + * When you want to assert values in a array with a given name, e.g. + * + *

+ * + * {@code + + foo + bar + + baz + + + * } + * + *

+ * The code to check it would look like this: + *

+ * + * {@code array("list").contains("element").isEqualTo("foo")} + * {@code array("list").contains("complexElement").node("param").isEqualTo("baz")} + * + *

+ * The generated XPaths would be + *

+ * + * {@code /list/element[text()='foo']} + * {@code /list/complexElement[param='baz']} + */ + XmlArrayVerifiable array(String value); + + /** + * Equality comparison with String + * + * @throws IllegalStateException - if XPath is not matched for the parsed XML + */ + XmlVerifiable isEqualTo(String value) throws IllegalStateException; + + /** + * Equality comparison with any object + * + * @throws IllegalStateException - if XPath is not matched for the parsed XML + */ + XmlVerifiable isEqualTo(Object value) throws IllegalStateException; + + /** + * Equality comparison with a Number + * + * @throws IllegalStateException - if XPath is not matched for the parsed XML + */ + XmlVerifiable isEqualTo(Number value) throws IllegalStateException; + + /** + * Equality comparison to null + * + * @throws IllegalStateException - if XPath is not matched for the parsed XML + */ + XmlVerifiable isNull() throws IllegalStateException; + + /** + * Regex matching for strings + * + * @throws IllegalStateException - if XPath is not matched for the parsed XML + */ + XmlVerifiable matches(String value) throws IllegalStateException; + + /** + * Equality comparison with a Boolean + * + * @throws IllegalStateException - if XPath is not matched for the parsed XML + */ + XmlVerifiable isEqualTo(Boolean value) throws IllegalStateException; + + /** + * Calling this method will setup the fluent interface to ignore any XPath verification + */ + XmlVerifiable withoutThrowingException(); + + /** + * Returns current XPath expression + */ + String xPath(); + + /** + * Checks if the parsed document matches given XPath + */ + void matchesXPath(String xPath); + +} diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy index 73aae01682..5f196240a6 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy @@ -18,18 +18,19 @@ package org.springframework.cloud.contract.verifier.builder import com.jayway.jsonpath.DocumentContext import com.jayway.jsonpath.JsonPath -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties -import org.springframework.cloud.contract.verifier.dsl.WireMockStubVerifier -import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubStrategy -import org.springframework.cloud.contract.verifier.file.ContractMetadata -import org.springframework.cloud.contract.verifier.util.SyntaxChecker import spock.lang.Issue import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll import spock.util.environment.RestoreSystemProperties +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubStrategy +import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubVerifier +import org.springframework.cloud.contract.verifier.file.ContractMetadata +import org.springframework.cloud.contract.verifier.util.SyntaxChecker + class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStubVerifier { @Shared ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(assertJsonSize: true) diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy index 2f78b184c0..8bbc00e2fd 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy @@ -26,7 +26,7 @@ import spock.lang.Specification import org.springframework.boot.test.rule.OutputCapture import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties -import org.springframework.cloud.contract.verifier.dsl.WireMockStubVerifier +import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubVerifier import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter import org.springframework.cloud.contract.verifier.util.SyntaxChecker 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 293755f3d8..d991ba35cc 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 @@ -17,15 +17,16 @@ package org.springframework.cloud.contract.verifier.builder import org.junit.Rule -import org.springframework.boot.test.rule.OutputCapture -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties -import org.springframework.cloud.contract.verifier.dsl.WireMockStubVerifier -import org.springframework.cloud.contract.verifier.util.SyntaxChecker import spock.lang.Issue import spock.lang.Shared import spock.lang.Specification +import org.springframework.boot.test.rule.OutputCapture +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubVerifier +import org.springframework.cloud.contract.verifier.util.SyntaxChecker + class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements WireMockStubVerifier { @Rule diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy index 74ca558dea..3cd96db5bf 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy @@ -184,13 +184,13 @@ class SingleTestGeneratorSpec extends Specification { and: asserter(clazz) where: - testFramework | order | mode | classStrings | asserter - JUNIT | 2 | MOCKMVC | mockMvcJUnitRestAssured3ClassStrings | JAVA_ASSERTER - JUNIT | 2 | TestMode.EXPLICIT | explicitJUnitRestAssured3ClassStrings | JAVA_ASSERTER - JUNIT5 | null | MOCKMVC | mockMvcJUnit5RestAssured3ClassStrings | JAVA_ASSERTER - JUNIT5 | null | TestMode.EXPLICIT | explicitJUnit5RestAssured3ClassStrings | JAVA_ASSERTER - SPOCK | 2 | MOCKMVC | spockClassRestAssured3Strings | GROOVY_ASSERTER - SPOCK | 2 | TestMode.EXPLICIT | explicitSpockRestAssured3ClassStrings | GROOVY_ASSERTER + testFramework | order | mode | classStrings | asserter + JUNIT | 2 | MOCKMVC | mockMvcJUnitRestAssured3ClassStrings | JAVA_ASSERTER + JUNIT | 2 | EXPLICIT | explicitJUnitRestAssured3ClassStrings | JAVA_ASSERTER + JUNIT5 | null | MOCKMVC | mockMvcJUnit5RestAssured3ClassStrings | JAVA_ASSERTER + JUNIT5 | null | EXPLICIT | explicitJUnit5RestAssured3ClassStrings | JAVA_ASSERTER + SPOCK | 2 | MOCKMVC | spockClassRestAssured3Strings | GROOVY_ASSERTER + SPOCK | 2 | EXPLICIT | explicitSpockRestAssured3ClassStrings | GROOVY_ASSERTER } def 'should build test class for #testFramework when the path contains bizarre signs'() { @@ -214,13 +214,13 @@ class SingleTestGeneratorSpec extends Specification { size > 0 asserter(new File(newFolder.parent, '/org/springframework/cloud/contract/verifier/tests/com_uscm/dale_api44_spec/_0_1_0_dev_1_uncommitted_d1174dd/' + testName).text) where: - testFramework | mode | asserter | testName - JUNIT | MOCKMVC | JAVA_ASSERTER | 'ContractsTest.java' - JUNIT | TestMode.EXPLICIT | JAVA_ASSERTER | 'ContractsTest.java' - JUNIT5 | MOCKMVC | JAVA_ASSERTER | 'ContractsTest.java' - JUNIT5 | TestMode.EXPLICIT | JAVA_ASSERTER | 'ContractsTest.java' - SPOCK | MOCKMVC | GROOVY_ASSERTER | 'ContractsSpec.groovy' - SPOCK | TestMode.EXPLICIT | GROOVY_ASSERTER | 'ContractsSpec.groovy' + testFramework | mode | asserter | testName + JUNIT | MOCKMVC | JAVA_ASSERTER | 'ContractsTest.java' + JUNIT | EXPLICIT | JAVA_ASSERTER | 'ContractsTest.java' + JUNIT5 | MOCKMVC | JAVA_ASSERTER | 'ContractsTest.java' + JUNIT5 | EXPLICIT | JAVA_ASSERTER | 'ContractsTest.java' + SPOCK | MOCKMVC | GROOVY_ASSERTER | 'ContractsSpec.groovy' + SPOCK | EXPLICIT | GROOVY_ASSERTER | 'ContractsSpec.groovy' } def "should build test class for #testFramework with Rest Assured 2x"() { @@ -247,13 +247,13 @@ class SingleTestGeneratorSpec extends Specification { !clazz.contains('io.restassured') where: - testFramework | order | mode | classStrings - JUNIT | 2 | MOCKMVC | mockMvcJUnitRestAssured2ClassStrings - JUNIT | 2 | TestMode.EXPLICIT | explicitJUnitRestAssured2ClassStrings - JUNIT5 | null | MOCKMVC | mockMvcJUnit5RestAssured2ClassStrings - JUNIT5 | null | TestMode.EXPLICIT | explicitJUnit5RestAssured2ClassStrings - SPOCK | 2 | MOCKMVC | spockClassRestAssured2Strings - SPOCK | 2 | TestMode.EXPLICIT | explicitSpockRestAssured2ClassStrings + testFramework | order | mode | classStrings + JUNIT | 2 | MOCKMVC | mockMvcJUnitRestAssured2ClassStrings + JUNIT | 2 | EXPLICIT | explicitJUnitRestAssured2ClassStrings + JUNIT5 | null | MOCKMVC | mockMvcJUnit5RestAssured2ClassStrings + JUNIT5 | null | EXPLICIT | explicitJUnit5RestAssured2ClassStrings + SPOCK | 2 | MOCKMVC | spockClassRestAssured2Strings + SPOCK | 2 | EXPLICIT | explicitSpockRestAssured2ClassStrings } def 'should build test class for #testFramework and mode #mode with two files'() { @@ -322,17 +322,17 @@ class SingleTestGeneratorSpec extends Specification { where: testFramework | mode | classStrings | asserter | textAssertion JUNIT | MOCKMVC | mockMvcJUnitRestAssured3ClassStrings | JAVA_ASSERTER | {String test -> StringUtils.countOccurrencesOf(test, '\t\t\tMockMvcRequestSpecification') == 2} - JUNIT | TestMode.EXPLICIT | explicitJUnitRestAssured3ClassStrings | JAVA_ASSERTER | {String test -> StringUtils.countOccurrencesOf(test, '\t\t\tMockMvcRequestSpecification') == 2} + JUNIT | EXPLICIT | explicitJUnitRestAssured3ClassStrings | JAVA_ASSERTER | {String test -> StringUtils.countOccurrencesOf(test, '\t\t\tMockMvcRequestSpecification') == 2} JUNIT5 | MOCKMVC | mockMvcJUnit5RestAssured3ClassStrings | JAVA_ASSERTER | {String test -> StringUtils.countOccurrencesOf(test, '\t\t\tMockMvcRequestSpecification') == 2} - JUNIT5 | TestMode.EXPLICIT | explicitJUnit5RestAssured3ClassStrings | JAVA_ASSERTER | {String test -> StringUtils.countOccurrencesOf(test, '\t\t\tMockMvcRequestSpecification') == 2} + JUNIT5 | EXPLICIT | explicitJUnit5RestAssured3ClassStrings | JAVA_ASSERTER | {String test -> StringUtils.countOccurrencesOf(test, '\t\t\tMockMvcRequestSpecification') == 2} SPOCK | MOCKMVC | spockClassRestAssured3Strings | GROOVY_ASSERTER | {String test -> StringUtils.countOccurrencesOf(test, '\t\t\tdef request') == 2} - SPOCK | TestMode.EXPLICIT | explicitSpockRestAssured2ClassStrings | GROOVY_ASSERTER | {String test -> StringUtils.countOccurrencesOf(test, '\t\t\tdef request') == 2} + SPOCK | EXPLICIT | explicitSpockRestAssured2ClassStrings | GROOVY_ASSERTER | {String test -> StringUtils.countOccurrencesOf(test, '\t\t\tdef request') == 2} } def 'should build JaxRs test class for #testFramework'() { given: ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties() - properties.testMode = TestMode.JAXRSCLIENT + properties.testMode = JAXRSCLIENT properties.testFramework =testFramework ContractMetadata contract = new ContractMetadata(file.toPath(), true, 1, null, convertAsCollection(new File('/'), file)) contract.ignored >> true @@ -485,7 +485,7 @@ class SingleTestGeneratorSpec extends Specification { and: ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties() properties.testFramework =JUNIT - properties.testMode = TestMode.EXPLICIT + properties.testMode = EXPLICIT properties.baseClassForTests = 'test.ContextPathTestingBaseClass' and: ContractMetadata contract = new ContractMetadata(file.toPath(), false, 1, 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 df92befc26..dd8d166582 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 @@ -16,19 +16,20 @@ package org.springframework.cloud.contract.verifier.builder +import java.util.regex.Pattern + import org.codehaus.groovy.control.MultipleCompilationErrorsException import org.junit.Rule -import org.springframework.boot.test.rule.OutputCapture -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties -import org.springframework.cloud.contract.verifier.dsl.WireMockStubVerifier -import org.springframework.cloud.contract.verifier.util.SyntaxChecker import spock.lang.Issue import spock.lang.Shared import spock.lang.Specification import spock.util.environment.RestoreSystemProperties -import java.util.regex.Pattern +import org.springframework.boot.test.rule.OutputCapture +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubVerifier +import org.springframework.cloud.contract.verifier.util.SyntaxChecker /** * @author Jakub Kubrynski, codearte.io diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/XmlMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/XmlMethodBodyBuilderSpec.groovy new file mode 100644 index 0000000000..03666c4b95 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/XmlMethodBodyBuilderSpec.groovy @@ -0,0 +1,170 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder + +import org.junit.Rule +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.boot.test.rule.OutputCapture +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.util.SyntaxChecker + +/** + * @author Olga Maciaszek-Sharma + * @since 2.1.0 + */ +class XmlMethodBodyBuilderSpec extends Specification { + + @Rule + OutputCapture capture = new OutputCapture() + + @Shared + GeneratedClassDataForMethod classDataForMethod = new GeneratedClassDataForMethod( + new SingleTestGenerator.GeneratedClassData("ClassName", "com.example", + new File("target/test.java").toPath()), + "some_method" + ) + + @Shared + ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties( + assertJsonSize: true, generatedTestSourcesDir: new File("."), + generatedTestResourcesDir: new File(".") + ) + + @Unroll + def 'should generate correct verification from xml with body matchers [#methodBuilderName]'() { + given: + Contract contractDsl = + // tag::xmlgroovy[] + Contract.make { + request { + method GET() + urlPath '/get' + headers { + contentType(applicationXml()) + } + } + response { + status(OK()) + headers { + contentType(applicationXml()) + } + body """ + +123 +abc + +abc +def +ghi + +123 +true +2017-01-01 +2017-01-01T01:23:45 + +foo +foo +""" + bodyMatchers { + xPath('/test/duck/text()', byRegex("[0-9]{3}")) + xPath('/test/duck/text()', byCommand('test($it)')) + xPath('/test/duck/xxx', byNull()) + xPath('/test/duck/text()', byEquality()) + xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode())) + xPath('/test/alpha/text()', byEquality()) + xPath('/test/number/text()', byRegex(number())) + xPath('/test/date/text()', byDate()) + xPath('/test/dateTime/text()', byTimestamp()) + xPath('/test/time/text()', byTime()) + xPath('/test/*/complex/text()', byEquality()) + xPath('/test/duck/@type', byEquality()) + } + } + } + // end::xmlgroovy[] + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(' ') + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('assertThat(valueFromXPath(parsedXml, "/test/list/elem/text()")).isEqualTo("abc")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/list/elem[2]/text()")).isEqualTo("def")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/list/elem[3]/text()")).isEqualTo("ghi")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/aBoolean/text()")).isEqualTo("true")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/valueWithoutAMatcher/text()")).isEqualTo("foo")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/duck/text()")).matches("[0-9]{3}")') + test.contains('test("123")') + test.contains('assertThat(nodeFromXPath(parsedXml, "/test/duck/xxx")).isNull()') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/duck/text()")).isEqualTo("123")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/alpha/text()")).matches("[\\\\p{L}]*")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/alpha/text()")).isEqualTo("abc")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/number/text()")).matches("-?(\\\\d*\\\\.\\\\d+|\\\\d+)")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/date/text()")).matches("(\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/dateTime/text()")).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])")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/time/text()")).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/*/complex/text()")).isEqualTo("foo")') + test.contains('assertThat(valueFromXPath(parsedXml, "/test/duck/@type")).isEqualTo("xtype")') + and: + SyntaxChecker.tryToCompile(methodBuilderName, blockBuilder. + toString()) + where: + methodBuilderName | methodBuilder + HttpSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties, classDataForMethod) } + MockMvcJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties, classDataForMethod) } + JaxRsClientSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties, classDataForMethod) } + JaxRsClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties, classDataForMethod) } + WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(dsl, properties, classDataForMethod) } + } + + def 'should throw exception for verification by type'() { + given: + Contract contractDsl = Contract.make { + request { + method GET() + urlPath '/get' + headers { + contentType(applicationXml()) + } + } + response { + status(OK()) + headers { + contentType(applicationXml()) + } + body """ + +123 +""" + bodyMatchers { + xPath('/test/duck/text()', byType()) + } + } + } + MethodBodyBuilder builder = new MockMvcJUnitMethodBodyBuilder(contractDsl, properties, classDataForMethod) + BlockBuilder blockBuilder = new BlockBuilder(' ') + when: + builder.appendTo(blockBuilder) + blockBuilder.toString() + then: + thrown UnsupportedOperationException + } +} 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 2bd0b654b4..1d573c539e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2018 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,17 +18,18 @@ package org.springframework.cloud.contract.verifier.builder import org.codehaus.groovy.control.MultipleCompilationErrorsException import org.junit.Rule -import org.springframework.boot.test.rule.OutputCapture -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties -import org.springframework.cloud.contract.verifier.converter.YamlContractConverter -import org.springframework.cloud.contract.verifier.dsl.WireMockStubVerifier -import org.springframework.cloud.contract.verifier.util.SyntaxChecker import spock.lang.Issue import spock.lang.Shared import spock.lang.Specification import spock.util.environment.RestoreSystemProperties +import org.springframework.boot.test.rule.OutputCapture +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.converter.YamlContractConverter +import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubVerifier +import org.springframework.cloud.contract.verifier.util.SyntaxChecker + /** * @author Jakub Kubrynski, codearte.io * @author Tim Ysewyn diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/DslToYamlContractConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/DslToYamlContractConverterSpec.groovy index f1faba9202..c29f23383c 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/DslToYamlContractConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/DslToYamlContractConverterSpec.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2013-2018 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,30 @@ import org.springframework.cloud.contract.spec.Contract /** * @author Marcin Grzejszczak * @author Tim Ysewyn + * @author Olga Maciaszek-Sharma */ class DslToYamlContractConverterSpec extends Specification { + String xmlContractBody = ''' + +123 +abc + +abc +def +ghi + +123 +true +2017-01-01 +2017-01-01T01:23:45 + +foo +string +foo + +''' + YamlContractConverter converter = new YamlContractConverter() def "should convert rest DSL to YAML"() { @@ -466,4 +487,90 @@ class DslToYamlContractConverterSpec extends Specification { ] yamlContract.response.status == 200 } + + def "should convert REST XML DSL to YAML"() { + given: + List contracts = [Contract.make { + request { + method 'GET' + url '/get' + headers { + contentType(applicationXml()) + } + body """ + +123 +abc + +abc +def +ghi + +123 +true +2017-01-01 +2017-01-01T01:23:45 + +foo +string +foo +""" + bodyMatchers { + xPath('/test/duck/text()', byRegex("[0-9]{3}")) + } + } + response { + status(OK()) + body """ + +123 +abc + +abc +def +ghi + +123 +true +2017-01-01 +2017-01-01T01:23:45 + +foo +string +foo +""" + bodyMatchers { + xPath('/test/duck/xxx', byNull()) + } + } + }] + when: + Collection yamlContracts = converter.convertTo(contracts) + then: + yamlContracts.size() == 1 + YamlContract yamlContract = yamlContracts.first() + yamlContract.request.method == 'GET' + yamlContract.request.url == '/get' + yamlContract.request.body.replaceAll("\n", "") + .replaceAll(' ', '') == xmlContractBody.replaceAll("\n", "") + .replaceAll(' ', '') + yamlContract.request.headers == [ + "Content-Type": "application/xml" + ] + yamlContract.request.matchers.body == [ + new YamlContract.BodyStubMatcher( + path: '/test/duck/text()', + type: YamlContract.StubMatcherType.by_regex, + value: '[0-9]{3}'), + ] + yamlContract.response.status == 200 + yamlContract.response.body.replaceAll("\n", "") + .replaceAll(' ', '') == xmlContractBody.replaceAll("\n", "") + .replaceAll(' ', '') + yamlContract.response.matchers.body == [ + new YamlContract.BodyTestMatcher( + path: '/test/duck/xxx', + type: YamlContract.TestMatcherType.by_null) + ] + } } \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy index a89997abb6..55fe2d679b 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2013-2018 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,16 +26,26 @@ import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.FromFileProperty import org.springframework.cloud.contract.spec.internal.MatchingStrategy -import org.springframework.cloud.contract.spec.internal.MatchingType import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.QueryParameters import org.springframework.cloud.contract.spec.internal.RegexPatterns import org.springframework.cloud.contract.spec.internal.Url import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter import org.springframework.cloud.contract.verifier.util.MapConverter + +import static org.springframework.cloud.contract.spec.internal.MatchingType.COMMAND +import static org.springframework.cloud.contract.spec.internal.MatchingType.DATE +import static org.springframework.cloud.contract.spec.internal.MatchingType.EQUALITY +import static org.springframework.cloud.contract.spec.internal.MatchingType.NULL +import static org.springframework.cloud.contract.spec.internal.MatchingType.REGEX +import static org.springframework.cloud.contract.spec.internal.MatchingType.TIME +import static org.springframework.cloud.contract.spec.internal.MatchingType.TIMESTAMP +import static org.springframework.cloud.contract.spec.internal.MatchingType.TYPE + /** * @author Marcin Grzejszczak * @author Tim Ysewyn + * @author Olga Maciaszek-Sharma */ class YamlContractConverterSpec extends Specification { @@ -69,7 +79,29 @@ class YamlContractConverterSpec extends Specification { File groovyBytes = new File(groovyBytesUrl.toURI()) URL ymlMessagingBytesUrl = YamlContractConverterSpec.getResource("/yml/contract_messaging_pdf.yml") File ymlMessagingBytes = new File(ymlMessagingBytesUrl.toURI()) + URL ymlRestXmlFile = YamlContractConverterSpec. + getResource("/yml/contract_rest_xml.yml") + File ymlRestXml = new File(ymlRestXmlFile.toURI()) YamlContractConverter converter = new YamlContractConverter() + String xmlContractBody = ''' + +123 +abc + +abc +def +ghi + +123 +true +2017-01-01 +2017-01-01T01:23:45 + +foo +string +foo + +''' def "should convert YAML with Cookies to DSL"() { given: @@ -122,9 +154,9 @@ class YamlContractConverterSpec extends Specification { contract.request.headers.entries.find { it.name == "fooReq" && it.serverValue == "baz" } contract.request.body.clientValue == [foo: "bar"] - contract.request.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.foo' - contract.request.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX - contract.request.bodyMatchers.jsonPathRegexMatchers[0].value().pattern() == 'bar' + contract.request.bodyMatchers.matchers[0].path() == '$.foo' + contract.request.bodyMatchers.matchers[0].matchingType() == REGEX + contract.request.bodyMatchers.matchers[0].value().pattern() == 'bar' and: contract.response.status.clientValue == 200 if (yamlFile == ymlWithRest) contract.response.delay.clientValue == 1000 else !contract.response.delay @@ -135,15 +167,15 @@ class YamlContractConverterSpec extends Specification { contract.response.headers.entries.find { it.name == "fooRes" && it.clientValue == "baz" } contract.response.body.clientValue == [foo2: "bar", foo3: "baz", nullValue: null] - contract.response.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.foo2' - contract.response.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX - contract.response.bodyMatchers.jsonPathRegexMatchers[0].value().pattern() == 'bar' - contract.response.bodyMatchers.jsonPathRegexMatchers[1].path() == '$.foo3' - contract.response.bodyMatchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.COMMAND - contract.response.bodyMatchers.jsonPathRegexMatchers[1].value() == new ExecutionProperty('executeMe($it)') - contract.response.bodyMatchers.jsonPathRegexMatchers[2].path() == '$.nullValue' - contract.response.bodyMatchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.NULL - contract.response.bodyMatchers.jsonPathRegexMatchers[2].value() == null + contract.response.bodyMatchers.matchers[0].path() == '$.foo2' + contract.response.bodyMatchers.matchers[0].matchingType() == REGEX + contract.response.bodyMatchers.matchers[0].value().pattern() == 'bar' + contract.response.bodyMatchers.matchers[1].path() == '$.foo3' + contract.response.bodyMatchers.matchers[1].matchingType() == COMMAND + contract.response.bodyMatchers.matchers[1].value() == new ExecutionProperty('executeMe($it)') + contract.response.bodyMatchers.matchers[2].path() == '$.nullValue' + contract.response.bodyMatchers.matchers[2].matchingType() == NULL + contract.response.bodyMatchers.matchers[2].value() == null where: yamlFile << [ymlWithRest, ymlWithRest2, ymlWithRest3] } @@ -214,93 +246,93 @@ class YamlContractConverterSpec extends Specification { MatchingStrategy.Type.MATCHING, "John.*") assertQueryParam(queryParameters, "hello", true, MatchingStrategy.Type.ABSENT, null) - contract.request.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.duck' - contract.request.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX - contract.request.bodyMatchers.jsonPathRegexMatchers[0].value().pattern() == '[0-9]{3}' - contract.request.bodyMatchers.jsonPathRegexMatchers[1].path() == '$.duck' - contract.request.bodyMatchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.EQUALITY - contract.request.bodyMatchers.jsonPathRegexMatchers[2].path() == '$.alpha' - contract.request.bodyMatchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.REGEX - contract.request.bodyMatchers.jsonPathRegexMatchers[2].value().pattern() == patterns.onlyAlphaUnicode().pattern() - contract.request.bodyMatchers.jsonPathRegexMatchers[3].path() == '$.alpha' - contract.request.bodyMatchers.jsonPathRegexMatchers[3].matchingType() == MatchingType.EQUALITY - contract.request.bodyMatchers.jsonPathRegexMatchers[4].path() == '$.number' - contract.request.bodyMatchers.jsonPathRegexMatchers[4].matchingType() == MatchingType.REGEX - contract.request.bodyMatchers.jsonPathRegexMatchers[4].value().pattern() == patterns.number().pattern() - contract.request.bodyMatchers.jsonPathRegexMatchers[5].path() == '$.aBoolean' - contract.request.bodyMatchers.jsonPathRegexMatchers[5].matchingType() == MatchingType.REGEX - contract.request.bodyMatchers.jsonPathRegexMatchers[5].value().pattern() == patterns.anyBoolean().pattern() - contract.request.bodyMatchers.jsonPathRegexMatchers[6].path() == '$.date' - contract.request.bodyMatchers.jsonPathRegexMatchers[6].matchingType() == MatchingType.DATE - contract.request.bodyMatchers.jsonPathRegexMatchers[6].value().pattern() == patterns.isoDate().pattern() - contract.request.bodyMatchers.jsonPathRegexMatchers[7].path() == '$.dateTime' - contract.request.bodyMatchers.jsonPathRegexMatchers[7].matchingType() == MatchingType.TIMESTAMP - contract.request.bodyMatchers.jsonPathRegexMatchers[7].value().pattern() == patterns.isoDateTime().pattern() - contract.request.bodyMatchers.jsonPathRegexMatchers[8].path() == '$.time' - contract.request.bodyMatchers.jsonPathRegexMatchers[8].matchingType() == MatchingType.TIME - contract.request.bodyMatchers.jsonPathRegexMatchers[8].value().pattern() == patterns.isoTime().pattern() - contract.request.bodyMatchers.jsonPathRegexMatchers[9].path() == "\$.['key'].['complex.key']" - contract.request.bodyMatchers.jsonPathRegexMatchers[9].matchingType() == MatchingType.EQUALITY - contract.request.bodyMatchers.jsonPathRegexMatchers[10].path() == '$.valueWithMin' - contract.request.bodyMatchers.jsonPathRegexMatchers[10].matchingType() == MatchingType.TYPE - contract.request.bodyMatchers.jsonPathRegexMatchers[10].minTypeOccurrence() == 1 - contract.request.bodyMatchers.jsonPathRegexMatchers[11].path() == '$.valueWithMax' - contract.request.bodyMatchers.jsonPathRegexMatchers[11].matchingType() == MatchingType.TYPE - contract.request.bodyMatchers.jsonPathRegexMatchers[11].maxTypeOccurrence() == 3 - contract.request.bodyMatchers.jsonPathRegexMatchers[12].path() == '$.valueWithMinMax' - contract.request.bodyMatchers.jsonPathRegexMatchers[12].matchingType() == MatchingType.TYPE - contract.request.bodyMatchers.jsonPathRegexMatchers[12].minTypeOccurrence() == 1 - contract.request.bodyMatchers.jsonPathRegexMatchers[12].maxTypeOccurrence() == 3 + contract.request.bodyMatchers.matchers[0].path() == '$.duck' + contract.request.bodyMatchers.matchers[0].matchingType() == REGEX + contract.request.bodyMatchers.matchers[0].value().pattern() == '[0-9]{3}' + contract.request.bodyMatchers.matchers[1].path() == '$.duck' + contract.request.bodyMatchers.matchers[1].matchingType() == EQUALITY + contract.request.bodyMatchers.matchers[2].path() == '$.alpha' + contract.request.bodyMatchers.matchers[2].matchingType() == REGEX + contract.request.bodyMatchers.matchers[2].value().pattern() == patterns.onlyAlphaUnicode().pattern() + contract.request.bodyMatchers.matchers[3].path() == '$.alpha' + contract.request.bodyMatchers.matchers[3].matchingType() == EQUALITY + contract.request.bodyMatchers.matchers[4].path() == '$.number' + contract.request.bodyMatchers.matchers[4].matchingType() == REGEX + contract.request.bodyMatchers.matchers[4].value().pattern() == patterns.number().pattern() + contract.request.bodyMatchers.matchers[5].path() == '$.aBoolean' + contract.request.bodyMatchers.matchers[5].matchingType() == REGEX + contract.request.bodyMatchers.matchers[5].value().pattern() == patterns.anyBoolean().pattern() + contract.request.bodyMatchers.matchers[6].path() == '$.date' + contract.request.bodyMatchers.matchers[6].matchingType() == DATE + contract.request.bodyMatchers.matchers[6].value().pattern() == patterns.isoDate().pattern() + contract.request.bodyMatchers.matchers[7].path() == '$.dateTime' + contract.request.bodyMatchers.matchers[7].matchingType() == TIMESTAMP + contract.request.bodyMatchers.matchers[7].value().pattern() == patterns.isoDateTime().pattern() + contract.request.bodyMatchers.matchers[8].path() == '$.time' + contract.request.bodyMatchers.matchers[8].matchingType() == TIME + contract.request.bodyMatchers.matchers[8].value().pattern() == patterns.isoTime().pattern() + contract.request.bodyMatchers.matchers[9].path() == "\$.['key'].['complex.key']" + contract.request.bodyMatchers.matchers[9].matchingType() == EQUALITY + contract.request.bodyMatchers.matchers[10].path() == '$.valueWithMin' + contract.request.bodyMatchers.matchers[10].matchingType() == TYPE + contract.request.bodyMatchers.matchers[10].minTypeOccurrence() == 1 + contract.request.bodyMatchers.matchers[11].path() == '$.valueWithMax' + contract.request.bodyMatchers.matchers[11].matchingType() == TYPE + contract.request.bodyMatchers.matchers[11].maxTypeOccurrence() == 3 + contract.request.bodyMatchers.matchers[12].path() == '$.valueWithMinMax' + contract.request.bodyMatchers.matchers[12].matchingType() == TYPE + contract.request.bodyMatchers.matchers[12].minTypeOccurrence() == 1 + contract.request.bodyMatchers.matchers[12].maxTypeOccurrence() == 3 contract.request.cookies.entries.find { it.key == "foo" }.clientValue instanceof Pattern contract.request.cookies.entries.find { it.key == "bar" }.serverValue == new ExecutionProperty('equals($it)') and: contract.response.status.clientValue == 200 - contract.response.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.duck' - contract.response.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX - contract.response.bodyMatchers.jsonPathRegexMatchers[0].value().pattern() == '[0-9]{3}' - contract.response.bodyMatchers.jsonPathRegexMatchers[1].path() == '$.duck' - contract.response.bodyMatchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.EQUALITY - contract.response.bodyMatchers.jsonPathRegexMatchers[2].path() == '$.alpha' - contract.response.bodyMatchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.REGEX - contract.response.bodyMatchers.jsonPathRegexMatchers[2].value().pattern() == patterns.onlyAlphaUnicode().pattern() - contract.response.bodyMatchers.jsonPathRegexMatchers[3].path() == '$.alpha' - contract.response.bodyMatchers.jsonPathRegexMatchers[3].matchingType() == MatchingType.EQUALITY - contract.response.bodyMatchers.jsonPathRegexMatchers[4].path() == '$.number' - contract.response.bodyMatchers.jsonPathRegexMatchers[4].matchingType() == MatchingType.REGEX - contract.response.bodyMatchers.jsonPathRegexMatchers[4].value().pattern() == patterns.number().pattern() - contract.response.bodyMatchers.jsonPathRegexMatchers[5].path() == '$.aBoolean' - contract.response.bodyMatchers.jsonPathRegexMatchers[5].matchingType() == MatchingType.REGEX - contract.response.bodyMatchers.jsonPathRegexMatchers[5].value().pattern() == patterns.anyBoolean().pattern() - contract.response.bodyMatchers.jsonPathRegexMatchers[6].path() == '$.date' - contract.response.bodyMatchers.jsonPathRegexMatchers[6].matchingType() == MatchingType.DATE - contract.response.bodyMatchers.jsonPathRegexMatchers[6].value().pattern() == patterns.isoDate().pattern() - contract.response.bodyMatchers.jsonPathRegexMatchers[7].path() == '$.dateTime' - contract.response.bodyMatchers.jsonPathRegexMatchers[7].matchingType() == MatchingType.TIMESTAMP - contract.response.bodyMatchers.jsonPathRegexMatchers[7].value().pattern() == patterns.isoDateTime().pattern() - contract.response.bodyMatchers.jsonPathRegexMatchers[8].path() == '$.time' - contract.response.bodyMatchers.jsonPathRegexMatchers[8].matchingType() == MatchingType.TIME - contract.response.bodyMatchers.jsonPathRegexMatchers[8].value().pattern() == patterns.isoTime().pattern() - contract.response.bodyMatchers.jsonPathRegexMatchers[9].path() == '$.valueWithTypeMatch' - contract.response.bodyMatchers.jsonPathRegexMatchers[9].matchingType() == MatchingType.TYPE - contract.response.bodyMatchers.jsonPathRegexMatchers[10].path() == '$.valueWithMin' - contract.response.bodyMatchers.jsonPathRegexMatchers[10].matchingType() == MatchingType.TYPE - contract.response.bodyMatchers.jsonPathRegexMatchers[10].minTypeOccurrence() == 1 - contract.response.bodyMatchers.jsonPathRegexMatchers[11].path() == '$.valueWithMax' - contract.response.bodyMatchers.jsonPathRegexMatchers[11].matchingType() == MatchingType.TYPE - contract.response.bodyMatchers.jsonPathRegexMatchers[11].maxTypeOccurrence() == 3 - contract.response.bodyMatchers.jsonPathRegexMatchers[12].path() == '$.valueWithMinMax' - contract.response.bodyMatchers.jsonPathRegexMatchers[12].matchingType() == MatchingType.TYPE - contract.response.bodyMatchers.jsonPathRegexMatchers[12].minTypeOccurrence() == 1 - contract.response.bodyMatchers.jsonPathRegexMatchers[12].maxTypeOccurrence() == 3 - contract.response.bodyMatchers.jsonPathRegexMatchers[13].path() == '$.valueWithMinEmpty' - contract.response.bodyMatchers.jsonPathRegexMatchers[13].matchingType() == MatchingType.TYPE - contract.response.bodyMatchers.jsonPathRegexMatchers[13].minTypeOccurrence() == 0 - contract.response.bodyMatchers.jsonPathRegexMatchers[14].path() == '$.valueWithMaxEmpty' - contract.response.bodyMatchers.jsonPathRegexMatchers[14].matchingType() == MatchingType.TYPE - contract.response.bodyMatchers.jsonPathRegexMatchers[14].maxTypeOccurrence() == 0 - contract.response.bodyMatchers.jsonPathRegexMatchers[15].path() == '$.duck' - contract.response.bodyMatchers.jsonPathRegexMatchers[15].matchingType() == MatchingType.COMMAND - contract.response.bodyMatchers.jsonPathRegexMatchers[15].value() == new ExecutionProperty('assertThatValueIsANumber($it)') + contract.response.bodyMatchers.matchers[0].path() == '$.duck' + contract.response.bodyMatchers.matchers[0].matchingType() == REGEX + contract.response.bodyMatchers.matchers[0].value().pattern() == '[0-9]{3}' + contract.response.bodyMatchers.matchers[1].path() == '$.duck' + contract.response.bodyMatchers.matchers[1].matchingType() == EQUALITY + contract.response.bodyMatchers.matchers[2].path() == '$.alpha' + contract.response.bodyMatchers.matchers[2].matchingType() == REGEX + contract.response.bodyMatchers.matchers[2].value().pattern() == patterns.onlyAlphaUnicode().pattern() + contract.response.bodyMatchers.matchers[3].path() == '$.alpha' + contract.response.bodyMatchers.matchers[3].matchingType() == EQUALITY + contract.response.bodyMatchers.matchers[4].path() == '$.number' + contract.response.bodyMatchers.matchers[4].matchingType() == REGEX + contract.response.bodyMatchers.matchers[4].value().pattern() == patterns.number().pattern() + contract.response.bodyMatchers.matchers[5].path() == '$.aBoolean' + contract.response.bodyMatchers.matchers[5].matchingType() == REGEX + contract.response.bodyMatchers.matchers[5].value().pattern() == patterns.anyBoolean().pattern() + contract.response.bodyMatchers.matchers[6].path() == '$.date' + contract.response.bodyMatchers.matchers[6].matchingType() == DATE + contract.response.bodyMatchers.matchers[6].value().pattern() == patterns.isoDate().pattern() + contract.response.bodyMatchers.matchers[7].path() == '$.dateTime' + contract.response.bodyMatchers.matchers[7].matchingType() == TIMESTAMP + contract.response.bodyMatchers.matchers[7].value().pattern() == patterns.isoDateTime().pattern() + contract.response.bodyMatchers.matchers[8].path() == '$.time' + contract.response.bodyMatchers.matchers[8].matchingType() == TIME + contract.response.bodyMatchers.matchers[8].value().pattern() == patterns.isoTime().pattern() + contract.response.bodyMatchers.matchers[9].path() == '$.valueWithTypeMatch' + contract.response.bodyMatchers.matchers[9].matchingType() == TYPE + contract.response.bodyMatchers.matchers[10].path() == '$.valueWithMin' + contract.response.bodyMatchers.matchers[10].matchingType() == TYPE + contract.response.bodyMatchers.matchers[10].minTypeOccurrence() == 1 + contract.response.bodyMatchers.matchers[11].path() == '$.valueWithMax' + contract.response.bodyMatchers.matchers[11].matchingType() == TYPE + contract.response.bodyMatchers.matchers[11].maxTypeOccurrence() == 3 + contract.response.bodyMatchers.matchers[12].path() == '$.valueWithMinMax' + contract.response.bodyMatchers.matchers[12].matchingType() == TYPE + contract.response.bodyMatchers.matchers[12].minTypeOccurrence() == 1 + contract.response.bodyMatchers.matchers[12].maxTypeOccurrence() == 3 + contract.response.bodyMatchers.matchers[13].path() == '$.valueWithMinEmpty' + contract.response.bodyMatchers.matchers[13].matchingType() == TYPE + contract.response.bodyMatchers.matchers[13].minTypeOccurrence() == 0 + contract.response.bodyMatchers.matchers[14].path() == '$.valueWithMaxEmpty' + contract.response.bodyMatchers.matchers[14].matchingType() == TYPE + contract.response.bodyMatchers.matchers[14].maxTypeOccurrence() == 0 + contract.response.bodyMatchers.matchers[15].path() == '$.duck' + contract.response.bodyMatchers.matchers[15].matchingType() == COMMAND + contract.response.bodyMatchers.matchers[15].value() == new ExecutionProperty('assertThatValueIsANumber($it)') } protected Object assertQueryParam(QueryParameters queryParameters, String queryParamName, Object serverValue, @@ -326,80 +358,80 @@ class YamlContractConverterSpec extends Specification { RegexPatterns patterns = new RegexPatterns() contract.input.messageHeaders.entries.find { it.name == "contentType" && ((Pattern) it.clientValue).pattern == "application/json.*" && it.serverValue == "application/json" } - contract.input.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.duck' - contract.input.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX - contract.input.bodyMatchers.jsonPathRegexMatchers[0].value().pattern() == '[0-9]{3}' - contract.input.bodyMatchers.jsonPathRegexMatchers[1].path() == '$.duck' - contract.input.bodyMatchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.EQUALITY - contract.input.bodyMatchers.jsonPathRegexMatchers[2].path() == '$.alpha' - contract.input.bodyMatchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.REGEX - contract.input.bodyMatchers.jsonPathRegexMatchers[2].value().pattern() == patterns.onlyAlphaUnicode().pattern() - contract.input.bodyMatchers.jsonPathRegexMatchers[3].path() == '$.alpha' - contract.input.bodyMatchers.jsonPathRegexMatchers[3].matchingType() == MatchingType.EQUALITY - contract.input.bodyMatchers.jsonPathRegexMatchers[4].path() == '$.number' - contract.input.bodyMatchers.jsonPathRegexMatchers[4].matchingType() == MatchingType.REGEX - contract.input.bodyMatchers.jsonPathRegexMatchers[4].value().pattern() == patterns.number().pattern() - contract.input.bodyMatchers.jsonPathRegexMatchers[5].path() == '$.aBoolean' - contract.input.bodyMatchers.jsonPathRegexMatchers[5].matchingType() == MatchingType.REGEX - contract.input.bodyMatchers.jsonPathRegexMatchers[5].value().pattern() == patterns.anyBoolean().pattern() - contract.input.bodyMatchers.jsonPathRegexMatchers[6].path() == '$.date' - contract.input.bodyMatchers.jsonPathRegexMatchers[6].matchingType() == MatchingType.DATE - contract.input.bodyMatchers.jsonPathRegexMatchers[6].value().pattern() == patterns.isoDate().pattern() - contract.input.bodyMatchers.jsonPathRegexMatchers[7].path() == '$.dateTime' - contract.input.bodyMatchers.jsonPathRegexMatchers[7].matchingType() == MatchingType.TIMESTAMP - contract.input.bodyMatchers.jsonPathRegexMatchers[7].value().pattern() == patterns.isoDateTime().pattern() - contract.input.bodyMatchers.jsonPathRegexMatchers[8].path() == '$.time' - contract.input.bodyMatchers.jsonPathRegexMatchers[8].matchingType() == MatchingType.TIME - contract.input.bodyMatchers.jsonPathRegexMatchers[8].value().pattern() == patterns.isoTime().pattern() - contract.input.bodyMatchers.jsonPathRegexMatchers[9].path() == "\$.['key'].['complex.key']" - contract.input.bodyMatchers.jsonPathRegexMatchers[9].matchingType() == MatchingType.EQUALITY + contract.input.bodyMatchers.matchers[0].path() == '$.duck' + contract.input.bodyMatchers.matchers[0].matchingType() == REGEX + contract.input.bodyMatchers.matchers[0].value().pattern() == '[0-9]{3}' + contract.input.bodyMatchers.matchers[1].path() == '$.duck' + contract.input.bodyMatchers.matchers[1].matchingType() == EQUALITY + contract.input.bodyMatchers.matchers[2].path() == '$.alpha' + contract.input.bodyMatchers.matchers[2].matchingType() == REGEX + contract.input.bodyMatchers.matchers[2].value().pattern() == patterns.onlyAlphaUnicode().pattern() + contract.input.bodyMatchers.matchers[3].path() == '$.alpha' + contract.input.bodyMatchers.matchers[3].matchingType() == EQUALITY + contract.input.bodyMatchers.matchers[4].path() == '$.number' + contract.input.bodyMatchers.matchers[4].matchingType() == REGEX + contract.input.bodyMatchers.matchers[4].value().pattern() == patterns.number().pattern() + contract.input.bodyMatchers.matchers[5].path() == '$.aBoolean' + contract.input.bodyMatchers.matchers[5].matchingType() == REGEX + contract.input.bodyMatchers.matchers[5].value().pattern() == patterns.anyBoolean().pattern() + contract.input.bodyMatchers.matchers[6].path() == '$.date' + contract.input.bodyMatchers.matchers[6].matchingType() == DATE + contract.input.bodyMatchers.matchers[6].value().pattern() == patterns.isoDate().pattern() + contract.input.bodyMatchers.matchers[7].path() == '$.dateTime' + contract.input.bodyMatchers.matchers[7].matchingType() == TIMESTAMP + contract.input.bodyMatchers.matchers[7].value().pattern() == patterns.isoDateTime().pattern() + contract.input.bodyMatchers.matchers[8].path() == '$.time' + contract.input.bodyMatchers.matchers[8].matchingType() == TIME + contract.input.bodyMatchers.matchers[8].value().pattern() == patterns.isoTime().pattern() + contract.input.bodyMatchers.matchers[9].path() == "\$.['key'].['complex.key']" + contract.input.bodyMatchers.matchers[9].matchingType() == EQUALITY and: - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.duck' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[0].value().pattern() == '[0-9]{3}' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[1].path() == '$.duck' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.EQUALITY - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[2].path() == '$.alpha' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[2].matchingType() == MatchingType.REGEX - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[2].value().pattern() == patterns.onlyAlphaUnicode().pattern() - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[3].path() == '$.alpha' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[3].matchingType() == MatchingType.EQUALITY - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[4].path() == '$.number' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[4].matchingType() == MatchingType.REGEX - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[4].value().pattern() == patterns.number().pattern() - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[5].path() == '$.aBoolean' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[5].matchingType() == MatchingType.REGEX - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[5].value().pattern() == patterns.anyBoolean().pattern() - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[6].path() == '$.date' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[6].matchingType() == MatchingType.DATE - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[6].value().pattern() == patterns.isoDate().pattern() - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[7].path() == '$.dateTime' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[7].matchingType() == MatchingType.TIMESTAMP - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[7].value().pattern() == patterns.isoDateTime().pattern() - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[8].path() == '$.time' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[8].matchingType() == MatchingType.TIME - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[8].value().pattern() == patterns.isoTime().pattern() - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[9].path() == '$.valueWithTypeMatch' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[9].matchingType() == MatchingType.TYPE - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[10].path() == '$.valueWithMin' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[10].matchingType() == MatchingType.TYPE - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[10].minTypeOccurrence() == 1 - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[11].path() == '$.valueWithMax' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[11].matchingType() == MatchingType.TYPE - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[11].maxTypeOccurrence() == 3 - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[12].path() == '$.valueWithMinMax' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[12].matchingType() == MatchingType.TYPE - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[12].minTypeOccurrence() == 1 - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[12].maxTypeOccurrence() == 3 - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[13].path() == '$.valueWithMinEmpty' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[13].matchingType() == MatchingType.TYPE - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[13].minTypeOccurrence() == 0 - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[14].path() == '$.valueWithMaxEmpty' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[14].matchingType() == MatchingType.TYPE - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[14].maxTypeOccurrence() == 0 - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[15].path() == '$.duck' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[15].matchingType() == MatchingType.COMMAND - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[15].value() == new ExecutionProperty('assertThatValueIsANumber($it)') + contract.outputMessage.bodyMatchers.matchers[0].path() == '$.duck' + contract.outputMessage.bodyMatchers.matchers[0].matchingType() == REGEX + contract.outputMessage.bodyMatchers.matchers[0].value().pattern() == '[0-9]{3}' + contract.outputMessage.bodyMatchers.matchers[1].path() == '$.duck' + contract.outputMessage.bodyMatchers.matchers[1].matchingType() == EQUALITY + contract.outputMessage.bodyMatchers.matchers[2].path() == '$.alpha' + contract.outputMessage.bodyMatchers.matchers[2].matchingType() == REGEX + contract.outputMessage.bodyMatchers.matchers[2].value().pattern() == patterns.onlyAlphaUnicode().pattern() + contract.outputMessage.bodyMatchers.matchers[3].path() == '$.alpha' + contract.outputMessage.bodyMatchers.matchers[3].matchingType() == EQUALITY + contract.outputMessage.bodyMatchers.matchers[4].path() == '$.number' + contract.outputMessage.bodyMatchers.matchers[4].matchingType() == REGEX + contract.outputMessage.bodyMatchers.matchers[4].value().pattern() == patterns.number().pattern() + contract.outputMessage.bodyMatchers.matchers[5].path() == '$.aBoolean' + contract.outputMessage.bodyMatchers.matchers[5].matchingType() == REGEX + contract.outputMessage.bodyMatchers.matchers[5].value().pattern() == patterns.anyBoolean().pattern() + contract.outputMessage.bodyMatchers.matchers[6].path() == '$.date' + contract.outputMessage.bodyMatchers.matchers[6].matchingType() == DATE + contract.outputMessage.bodyMatchers.matchers[6].value().pattern() == patterns.isoDate().pattern() + contract.outputMessage.bodyMatchers.matchers[7].path() == '$.dateTime' + contract.outputMessage.bodyMatchers.matchers[7].matchingType() == TIMESTAMP + contract.outputMessage.bodyMatchers.matchers[7].value().pattern() == patterns.isoDateTime().pattern() + contract.outputMessage.bodyMatchers.matchers[8].path() == '$.time' + contract.outputMessage.bodyMatchers.matchers[8].matchingType() == TIME + contract.outputMessage.bodyMatchers.matchers[8].value().pattern() == patterns.isoTime().pattern() + contract.outputMessage.bodyMatchers.matchers[9].path() == '$.valueWithTypeMatch' + contract.outputMessage.bodyMatchers.matchers[9].matchingType() == TYPE + contract.outputMessage.bodyMatchers.matchers[10].path() == '$.valueWithMin' + contract.outputMessage.bodyMatchers.matchers[10].matchingType() == TYPE + contract.outputMessage.bodyMatchers.matchers[10].minTypeOccurrence() == 1 + contract.outputMessage.bodyMatchers.matchers[11].path() == '$.valueWithMax' + contract.outputMessage.bodyMatchers.matchers[11].matchingType() == TYPE + contract.outputMessage.bodyMatchers.matchers[11].maxTypeOccurrence() == 3 + contract.outputMessage.bodyMatchers.matchers[12].path() == '$.valueWithMinMax' + contract.outputMessage.bodyMatchers.matchers[12].matchingType() == TYPE + contract.outputMessage.bodyMatchers.matchers[12].minTypeOccurrence() == 1 + contract.outputMessage.bodyMatchers.matchers[12].maxTypeOccurrence() == 3 + contract.outputMessage.bodyMatchers.matchers[13].path() == '$.valueWithMinEmpty' + contract.outputMessage.bodyMatchers.matchers[13].matchingType() == TYPE + contract.outputMessage.bodyMatchers.matchers[13].minTypeOccurrence() == 0 + contract.outputMessage.bodyMatchers.matchers[14].path() == '$.valueWithMaxEmpty' + contract.outputMessage.bodyMatchers.matchers[14].matchingType() == TYPE + contract.outputMessage.bodyMatchers.matchers[14].maxTypeOccurrence() == 0 + contract.outputMessage.bodyMatchers.matchers[15].path() == '$.duck' + contract.outputMessage.bodyMatchers.matchers[15].matchingType() == COMMAND + contract.outputMessage.bodyMatchers.matchers[15].value() == new ExecutionProperty('assertThatValueIsANumber($it)') } def "should convert YAML with REST with response from request"() { @@ -459,9 +491,9 @@ class YamlContractConverterSpec extends Specification { contract.input.messageHeaders.entries.find { it.name == "foo" && ((Pattern) it.clientValue).pattern == "bar" && it.serverValue == "bar" } contract.input.messageBody.clientValue == [foo: "bar"] - contract.input.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.bar' - contract.input.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX - contract.input.bodyMatchers.jsonPathRegexMatchers[0].value().pattern() == 'bar' + contract.input.bodyMatchers.matchers[0].path() == '$.bar' + contract.input.bodyMatchers.matchers[0].matchingType() == REGEX + contract.input.bodyMatchers.matchers[0].value().pattern() == 'bar' and: contract.outputMessage.assertThat.toString() == "baz()" contract.outputMessage.headers.entries.find { it.name == "foo2" && @@ -471,12 +503,12 @@ class YamlContractConverterSpec extends Specification { contract.outputMessage.headers.entries.find { it.name == "fooRes" && it.clientValue == "baz" } contract.outputMessage.body.clientValue == [foo2: "bar", foo3: "baz"] - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.foo2' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[0].value().pattern() == 'bar' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[1].path() == '$.foo3' - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[1].matchingType() == MatchingType.COMMAND - contract.outputMessage.bodyMatchers.jsonPathRegexMatchers[1].value() == new ExecutionProperty('executeMe($it)') + contract.outputMessage.bodyMatchers.matchers[0].path() == '$.foo2' + contract.outputMessage.bodyMatchers.matchers[0].matchingType() == REGEX + contract.outputMessage.bodyMatchers.matchers[0].value().pattern() == 'bar' + contract.outputMessage.bodyMatchers.matchers[1].path() == '$.foo3' + contract.outputMessage.bodyMatchers.matchers[1].matchingType() == COMMAND + contract.outputMessage.bodyMatchers.matchers[1].value() == new ExecutionProperty('executeMe($it)') } def "should convert YAML with messaging triggered by a method to DSL"() { @@ -1184,4 +1216,55 @@ ignored: false yamlContract.request.body == null yamlContract.request.bodyFromFileAsBytes != null } + + def "should convert REST YAML with XML request and response to DSL"() { + given: + assert converter.isAccepted(ymlRestXml) + when: + Collection contracts = converter.convertFrom(ymlRestXml) + then: + contracts.size() == 1 + Contract contract = contracts.first() + RegexPatterns patterns = new RegexPatterns() + contract.request.headers.entries.find({ + it.name == 'Content-Type' && it.clientValue == "application/xml" && it.serverValue == "application/xml" + }) + contract.request.bodyMatchers.matchers[0].path() == '/test/duck/text()' + contract.request.bodyMatchers.matchers[0].matchingType() == REGEX + contract.request.bodyMatchers.matchers[0].value().pattern() == '[0-9]{10}' + contract.request.bodyMatchers.matchers[1].path() == '/test/duck/text()' + contract.request.bodyMatchers.matchers[1].matchingType() == EQUALITY + contract.request.bodyMatchers.matchers[2].path() == '/test/time/text()' + contract.request.bodyMatchers.matchers[2].matchingType() == TIME + contract.request.bodyMatchers.matchers[2] + .value().pattern() == patterns.isoTime().pattern() + contract.request.body.clientValue.replaceAll("\n", ""). + replaceAll(' ', '') == xmlContractBody.replaceAll("\n", ""). + replaceAll(' ', '') + contract.request.body.serverValue.replaceAll("\n", ""). + replaceAll(' ', '') == xmlContractBody.replaceAll("\n", ""). + replaceAll(' ', '') + and: + contract.response.bodyMatchers.matchers[0].path() == '/test/duck/text()' + contract.response.bodyMatchers.matchers[0].matchingType() == REGEX + contract.response.bodyMatchers.matchers[0].value().pattern() == '[0-9]{10}' + contract.response.bodyMatchers.matchers[1].path() == '/test/duck/text()' + contract.response.bodyMatchers.matchers[1].matchingType() == COMMAND + contract.response.bodyMatchers.matchers[1]. + value().executionCommand == 'test($it)' + contract.response.bodyMatchers.matchers[2].path() == '/test/duck/xxx' + contract.response.bodyMatchers.matchers[2].matchingType() == NULL + contract.response.bodyMatchers.matchers[3].path() == '/test/duck/text()' + contract.response.bodyMatchers.matchers[3].matchingType() == EQUALITY + contract.response.bodyMatchers.matchers[4].path() == '/test/time/text()' + contract.response.bodyMatchers.matchers[4].matchingType() == TIME + contract.response.bodyMatchers.matchers[4] + .value().pattern() == patterns.isoTime().pattern() + contract.response.body.clientValue.replaceAll("\n", "") + .replaceAll(' ', '') == xmlContractBody + .replaceAll("\n", "").replaceAll(' ', '') + contract.response.body.serverValue.replaceAll("\n", "") + .replaceAll(' ', '') == xmlContractBody + .replaceAll("\n", "").replaceAll(' ', '') + } } \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockGroovyDslSpec.groovy similarity index 96% rename from spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy rename to spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockGroovyDslSpec.groovy index 27cd2d7fb3..2a1dd82006 100755 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockGroovyDslSpec.groovy @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.contract.verifier.dsl +package org.springframework.cloud.contract.verifier.dsl.wiremock import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.core.WireMockConfiguration @@ -29,8 +29,6 @@ import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsEscapeHelper import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsJsonPathHelper -import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubMapping -import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubStrategy import org.springframework.cloud.contract.verifier.file.ContractMetadata import org.springframework.cloud.contract.verifier.util.AssertionUtil import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter @@ -392,7 +390,7 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie stubMappingIsValidWireMockStub(json) } - def 'should use equalToXml when content type ends with xml'() { + def 'should use xml matchers when content type ends with xml'() { given: org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { request { @@ -414,31 +412,43 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie then: AssertionUtil.assertThatJsonsAreEqual((''' { - "request": { - "method": "GET", - "url": "/users", - "headers": { - "Content-Type": { - "equalTo": "customtype/xml" - } - }, - "bodyPatterns": [ - { - "equalToXml":"Jozo<test>" - } - ] - }, - "response": { - "status": 200, - "transformers" : [ "response-template", "foo-transformer" ] - } - } + "request": { + "url": "/users", + "method": "GET", + "headers": { + "Content-Type": { + "equalTo": "customtype/xml" + } + }, + "bodyPatterns": [ + { + "matchesXPath": { + "expression": "/foo/name/text()", + "equalTo": "Jozo" + } + }, + { + "matchesXPath": { + "expression": "/foo/jobId/text()", + "equalTo": "" + } + } + ] + }, + "response": { + "status": 200, + "transformers": [ + "response-template", + "foo-transformer" + ] + } +} '''), json) and: stubMappingIsValidWireMockStub(json) } - def 'should use equalToXml when content type is parsable xml'() { + def 'should use xml matchers when content type is parsable xml'() { given: org.springframework.cloud.contract.spec.Contract groovyDsl = org.springframework.cloud.contract.spec.Contract.make { request { @@ -456,21 +466,33 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie String json = toWireMockClientJsonStub(groovyDsl) then: AssertionUtil.assertThatJsonsAreEqual((''' - { - "request": { - "method": "GET", - "url": "/users", - "bodyPatterns": [ - { - "equalToXml":"Jozo<test>" - } - ] - }, - "response": { - "status": 200, - "transformers" : [ "response-template", "foo-transformer" ] - } - } +{ + "request": { + "url": "/users", + "method": "GET", + "bodyPatterns": [ + { + "matchesXPath": { + "expression": "/user/name/text()", + "equalTo": "Jozo" + } + }, + { + "matchesXPath": { + "expression": "/user/jobId/text()", + "equalTo": "" + } + } + ] + }, + "response": { + "status": 200, + "transformers": [ + "response-template", + "foo-transformer" + ] + } +} '''), json) and: stubMappingIsValidWireMockStub(json) @@ -2428,6 +2450,59 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie stubMappingIsValidWireMockStub(wireMockStub) } + def should_generate_stubs_with_request_body_matchers() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/get' + 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', + key : [ + 'complex.key': 'foo' + ] + ]) + bodyMatchers { + jsonPath('$.duck', byRegex("[0-9]{3}")) + jsonPath('$.duck', byEquality()) + jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.alpha', byEquality()) + jsonPath('$.number', byRegex(number())) + jsonPath('$.aBoolean', byRegex(anyBoolean())) + jsonPath('$.date', byDate()) + jsonPath('$.dateTime', byTimestamp()) + jsonPath('$.time', byTime()) + jsonPath("\$.['key'].['complex.key']", byEquality()) + } + headers { + contentType(applicationJson()) + } + } + response { + status(200) + headers { + contentType(applicationJsonUtf8()) + } + body("true") + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", + new ContractMetadata(null, false, 0, null, contractDsl), contractDsl) + .toWireMockClientStub() + + then: + stubMappingIsValidWireMockStub(wireMockStub) + } + WireMockConfiguration config() { return new WireMockConfiguration().extensions(responseTemplateTransformer()) } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy index 18a401c9ee..35c82c4695 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy @@ -1,10 +1,27 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.cloud.contract.verifier.dsl.wiremock import groovy.json.JsonSlurper -import org.springframework.cloud.contract.spec.Contract import spock.lang.Issue import spock.lang.Specification +import org.springframework.cloud.contract.spec.Contract + class WireMockResponseStubStrategySpec extends Specification { def "should not quote floating point numbers"() { diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockStubVerifier.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubVerifier.groovy similarity index 87% rename from spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockStubVerifier.groovy rename to spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubVerifier.groovy index a0fc0ab772..ce16a4fb4a 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockStubVerifier.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubVerifier.groovy @@ -14,16 +14,15 @@ * limitations under the License. */ -package org.springframework.cloud.contract.verifier.dsl +package org.springframework.cloud.contract.verifier.dsl.wiremock + +import java.util.regex.Pattern import com.github.tomakehurst.wiremock.matching.RegexPattern import com.github.tomakehurst.wiremock.stubbing.StubMapping -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubMapping -import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubStrategy -import org.springframework.cloud.contract.verifier.file.ContractMetadata -import java.util.regex.Pattern +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.file.ContractMetadata trait WireMockStubVerifier { @@ -38,5 +37,4 @@ trait WireMockStubVerifier { void stubMappingIsValidWireMockStub(Contract contractDsl) { stubMappingIsValidWireMockStub(new WireMockStubStrategy("Test", new ContractMetadata(null, false, 0, null, contractDsl), contractDsl).toWireMockClientStub()) } - } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockXmlStubStrategySpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockXmlStubStrategySpec.groovy new file mode 100644 index 0000000000..3c14c21ec7 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockXmlStubStrategySpec.groovy @@ -0,0 +1,394 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.dsl.wiremock + + +import spock.lang.Specification + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.file.ContractMetadata + +/** + * @author Olga Maciaszek-Sharma + */ +class WireMockXmlStubStrategySpec extends Specification implements WireMockStubVerifier { + + def 'should generate stubs with plain xml request body'() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/get' + body """ + +123 +abc + +abc +def +ghi + +123 +true +2017-01-01 +2017-01-01T01:23:45 + +foo +string +foo +""" + headers { + contentType(applicationXml()) + } + } + response { + status(OK()) + headers { + contentType(applicationXml()) + } + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", + new ContractMetadata(null, false, 0, null, contractDsl), contractDsl) + .toWireMockClientStub() + then: + stubMappingIsValidWireMockStub(wireMockStub) + wireMockStub + .replaceAll("\n", "") + .replaceAll(' ', '') + .contains( + """ +"bodyPatterns": [ + { + "matchesXPath": { + "expression": "/test/duck/text()", + "equalTo": "123" + } + }, + { + "matchesXPath": { + "expression": "/test/alpha/text()", + "equalTo": "abc" + } + }, + { + "matchesXPath": { + "expression": "/test/list/elem/text()", + "equalTo": "abc" + } + }, + { + "matchesXPath": { + "expression": "/test/list/elem[2]/text()", + "equalTo": "def" + } + }, + { + "matchesXPath": { + "expression": "/test/list/elem[3]/text()", + "equalTo": "ghi" + } + }, + { + "matchesXPath": { + "expression": "/test/number/text()", + "equalTo": "123" + } + }, + { + "matchesXPath": { + "expression": "/test/aBoolean/text()", + "equalTo": "true" + } + }, + { + "matchesXPath": { + "expression": "/test/date/text()", + "equalTo": "2017-01-01" + } + }, + { + "matchesXPath": { + "expression": "/test/dateTime/text()", + "equalTo": "2017-01-01T01:23:45" + } + }, + { + "matchesXPath": { + "expression": "/test/time/text()", + "equalTo": "01:02:34" + } + }, + { + "matchesXPath": { + "expression": "/test/valueWithoutAMatcher/text()", + "equalTo": "foo" + } + }, + { + "matchesXPath": { + "expression": "/test/valueWithTypeMatch/text()", + "equalTo": "string" + } + }, + { + "matchesXPath": { + "expression": "/test/key/complex/text()", + "equalTo": "foo" + } + }, + { + "matchesXPath": { + "expression": "/test/duck/@type", + "equalTo": "xtype" + } + }] +""".replaceAll("\n", "").replaceAll(' ', '')) + } + + def 'should generate stubs with request body matchers'() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/get' + body """ + +123 +abc +123 +true +2017-01-01 +2017-01-01T01:23:45 + +foo +string +foo +""" + bodyMatchers { + xPath('/test/duck/text()', byRegex("[0-9]{3}")) + xPath('/test/duck/text()', byEquality()) + xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode())) + xPath('/test/alpha/text()', byEquality()) + xPath('/test/number/text()', byRegex(number())) + xPath('/test/aBoolean/text()', byRegex(anyBoolean())) + xPath('/test/date/text()', byDate()) + xPath('/test/dateTime/text()', byTimestamp()) + xPath('/test/time/text()', byTime()) + xPath('/test/*/complex/text()', byEquality()) + xPath('/test/duck/@type', byEquality()) + } + headers { + contentType(applicationXml()) + } + } + response { + status(OK()) + headers { + contentType(applicationXml()) + } + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", + new ContractMetadata(null, false, 0, null, contractDsl), contractDsl) + .toWireMockClientStub() + then: + stubMappingIsValidWireMockStub(wireMockStub) + wireMockStub.replaceAll("\n", '').replaceAll(' ', '') + .contains(""" + matchesXPath" : { + "expression" : "/test/duck/text()", + "matches" : "[0-9]{3}" + } + }, { + "matchesXPath" : { + "expression" : "/test/duck/text()", + "equalTo" : "123" + } + }, { + "matchesXPath" : { + "expression" : "/test/alpha/text()", + "matches" : "[\\\\p{L}]*" + } + }, { + "matchesXPath" : { + "expression" : "/test/alpha/text()", + "equalTo" : "abc" + } + }, { + "matchesXPath" : { + "expression" : "/test/number/text()", + "matches" : "-?(\\\\d*\\\\.\\\\d+|\\\\d+)" + } + }, { + "matchesXPath" : { + "expression" : "/test/aBoolean/text()", + "matches" : "(true|false)" + } + }, { + "matchesXPath" : { + "expression" : "/test/date/text()", + "matches" : "(\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])" + } + }, { + "matchesXPath" : { + "expression" : "/test/dateTime/text()", + "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])" + } + }, { + "matchesXPath" : { + "expression" : "/test/time/text()", + "matches" : "(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])" + } + }, { + "matchesXPath" : { + "expression" : "/test/*/complex/text()", + "equalTo" : "foo" + } + }, { + "matchesXPath" : { + "expression" : "/test/duck/@type", + "equalTo" : "xtype" + } + }""".replaceAll("\n", + "").replaceAll(' ', '')) + } + + def 'should generate stubs with both xml and body matchers in request'() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/get' + body """ + +123 +abc +123 +""" + bodyMatchers { + xPath('/test/duck/text()', byEquality()) + xPath('/test/number/text()', byRegex(number())) + } + headers { + contentType(applicationXml()) + } + } + response { + status(OK()) + headers { + contentType(applicationXml()) + } + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", + new ContractMetadata(null, false, 0, null, contractDsl), contractDsl) + .toWireMockClientStub() + then: + stubMappingIsValidWireMockStub(wireMockStub) + wireMockStub.replaceAll("\n", "") + .replaceAll(' ', '') + .contains(""" + "bodyPatterns" : [ { + "matchesXPath": { + "expression": "/test/alpha/text()", + "equalTo": "abc" + } + }, { + "matchesXPath": { + "expression": "/test/duck/text()", + "equalTo": "123" + } + }, { + "matchesXPath": { + "expression": "/test/number/text()", + "matches" : "-?(\\\\d*\\\\.\\\\d+|\\\\d+)" + } + } + ] +} +""".replaceAll("\n", "").replaceAll(' ', '')) + } + + + def 'should generate stubs with response body matchers'() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/get' + headers { + contentType(applicationXml()) + } + } + response { + status(OK()) + headers { + contentType(applicationXml()) + } + body """ + +123 +abc + +abc +def +ghi + +123 +true +2017-01-01 +2017-01-01T01:23:45 + +foo +string +foo +""" + bodyMatchers { + xPath('/test/duck/text()', byRegex("[0-9]{3}")) + xPath('/test/duck/text()', byEquality()) + xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode())) + xPath('/test/alpha/text()', byEquality()) + xPath('/test/number/text()', byRegex(number())) + xPath('/test/aBoolean/text()', byRegex(anyBoolean())) + xPath('/test/date/text()', byDate()) + xPath('/test/dateTime/text()', byTimestamp()) + xPath('/test/time/text()', byTime()) + xPath('/test/*/complex/text()', byEquality()) + xPath('/test/duck/@type', byEquality()) + } + } + } + when: + String wireMockStub = new WireMockStubStrategy("Test", + new ContractMetadata(null, false, 0, null, contractDsl), contractDsl) + .toWireMockClientStub() + then: + wireMockStub.contains("\\n\\n123" + + "\\nabc\\n\\nabc\\ndef" + + "\\nghi\\n\\n123" + + "\\ntrue\\n2017-01-01" + + "\\n2017-01-01T01:23:45\\n" + + "\\nfoo" + + "\\nstring" + + "\\nfoo\\n") + } +} + diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy index 5dd38775dc..8b5969941a 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy @@ -1,5 +1,14 @@ package org.springframework.cloud.contract.verifier.util +import java.lang.reflect.Method + +import javax.inject.Inject +import javax.ws.rs.client.Entity +import javax.ws.rs.client.WebTarget +import javax.ws.rs.core.Response +import javax.xml.parsers.DocumentBuilder +import javax.xml.parsers.DocumentBuilderFactory + import com.jayway.jsonpath.DocumentContext import com.jayway.jsonpath.JsonPath import com.toomuchcoding.jsonassert.JsonAssertion @@ -16,6 +25,9 @@ import org.codehaus.groovy.control.customizers.ImportCustomizer import org.junit.Rule import org.junit.Test import org.mdkt.compiler.InMemoryJavaCompiler +import org.w3c.dom.Document +import org.xml.sax.InputSource + import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage @@ -24,19 +36,12 @@ import org.springframework.cloud.contract.verifier.messaging.internal.ContractVe import org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil import org.springframework.util.ReflectionUtils -import javax.inject.Inject -import javax.ws.rs.client.Entity -import javax.ws.rs.client.WebTarget -import javax.ws.rs.core.Response -import java.lang.reflect.Method - /** * checking the syntax of produced scripts */ @CompileStatic class SyntaxChecker { - WebTarget webTarget Entity entity private static final String[] DEFAULT_IMPORTS = [ @@ -55,7 +60,12 @@ class SyntaxChecker { WebTarget.name, Response.name, WebTestClientRequestSpecification.name, - WebTestClientResponse.name + WebTestClientResponse.name, + DocumentBuilder.name, + DocumentBuilderFactory.name, + Document.name, + InputSource.name, + StringReader.name ] private static final String DEFAULT_IMPORTS_AS_STRING = DEFAULT_IMPORTS.collect { @@ -67,21 +77,26 @@ class SyntaxChecker { "${RestAssuredMockMvc.name}.when", "${RestAssured.name}.*", "${Entity.name}.*", - "${ContractVerifierUtil.name}.fileToBytes", + "${ContractVerifierUtil.name}.*", "${ContractVerifierMessagingUtil.name}.headers", "${JsonAssertion.name}.assertThatJson", - "${SpringCloudContractAssertions.name}.assertThat" + "${SpringCloudContractAssertions.name}.assertThat", ].collect { "import static ${it};"}.join("\n") private static final String WEB_TEST_CLIENT_STATIC_IMPORTS = [ "${RestAssuredWebTestClient.name}.*", "${Entity.name}.*", - "${ContractVerifierUtil.name}.fileToBytes", + "${ContractVerifierUtil.name}.*", "${ContractVerifierMessagingUtil.name}.headers", "${JsonAssertion.name}.assertThatJson", "${SpringCloudContractAssertions.name}.assertThat" ].collect { "import static ${it};" }.join("\n") + private static final String dummyMethod = ''' +private void test(String test) { +\t\tassertThat(test).isEqualTo("123"); +\t}''' + static void tryToCompile(String builderName, String test) { if (builderName.toLowerCase().contains("spock")) { tryToCompileGroovy(builderName, test) @@ -125,6 +140,7 @@ class SyntaxChecker { sourceCode.append("WebTarget webTarget") sourceCode.append("\n") sourceCode.append(test) + sourceCode.append(dummyMethod) return new GroovyShell(SyntaxChecker.classLoader, configuration).parse(sourceCode.toString()) } @@ -150,9 +166,10 @@ class SyntaxChecker { sourceCode.append("\n") sourceCode.append(" WebTarget webTarget;") sourceCode.append("\n") - sourceCode.append(" public void method() {\n") + sourceCode.append(" public void method() throws Exception {\n") sourceCode.append(" ${test}\n") sourceCode.append(" }\n") + sourceCode.append(dummyMethod) sourceCode.append("}") return InMemoryJavaCompiler.compile(fqnClassName, sourceCode.toString()) } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XPathSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XPathSpec.groovy new file mode 100644 index 0000000000..6bd0349200 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XPathSpec.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml + +import spock.lang.Specification +import spock.lang.Unroll + +/** + * @author Marcin Grzejszczak + */ +class XPathSpec extends Specification { + + @Unroll + def "should generate [#expectedXPath] for XPath [#xPath]"() { + expect: + xPath == expectedXPath + where: + xPath || expectedXPath + XPathBuilder.builder().node("some").node("nested").node("anothervalue").isEqualTo(4).xPath() || '''/some/nested[anothervalue=4]''' + XPathBuilder.builder().node("some").node("nested").array("withlist").contains("name").isEqualTo("name1").xPath() || '''/some/nested/withlist[name='name1']''' + XPathBuilder.builder().node("some").node("nested").array("withlist").contains("name").isEqualTo("name2").xPath() || '''/some/nested/withlist[name='name2']''' + XPathBuilder.builder().node("some").node("nested").node("json").isEqualTo("with \"val'ue").xPath() || '''/some/nested[json=concat('with "val',"'",'ue')]''' + XPathBuilder.builder().node("some", "nested", "json").isEqualTo("with \"val'ue").xPath() || '''/some/nested[json=concat('with "val',"'",'ue')]''' + } + +} diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlAssertionSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlAssertionSpec.groovy new file mode 100644 index 0000000000..0d819b8ce3 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlAssertionSpec.groovy @@ -0,0 +1,594 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.util.xml + +import groovy.xml.MarkupBuilder +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +class XmlAssertionSpec extends Specification { + + @Shared + String xml1 = ''' + + + with "val'ue + 4 + foo + + name1 + + + name2 + + + 8 + + + name3 + + + ''' + + @Unroll + def 'should convert an xml with a map as root to a map of path to value '() { + expect: + verifiable.xPath() == expectedXPath + where: + verifiable || expectedXPath + XmlAssertion.assertThat(xml1).node("some"). + node("nested").node("anothervalue").isEqualTo(4) || '''/some/nested[anothervalue=4]''' + XmlAssertion.assertThat(xml1).node("some"). + node("nested").node("anothervalue") || '''/some/nested/anothervalue''' + XmlAssertion. + assertThat(xml1).node("some").text() || '''/some/text()''' + XmlAssertion.assertThat(xml1).node("some"). + node("nested").node("withattr").withAttribute("id", "a"). + withAttribute("id2", "b") || '''/some/nested/withattr[@id='a'][@id2='b']''' + XmlAssertion.assertThat(xml1).node("some"). + node("nested").node("withattr").withAttribute("id") || '''/some/nested/withattr/@id''' + XmlAssertion.assertThat(xml1).node("some"). + node("nested").node("withattr").isEqualTo("foo"). + withAttribute("id", "a").withAttribute("id2", "b") || '''/some/nested[withattr='foo']/withattr[@id='a'][@id2='b']''' + XmlAssertion.assertThatXml(xml1).node("some"). + node("nested").node("anothervalue").isEqualTo(4) || '''/some/nested[anothervalue=4]''' + XmlAssertion.assertThat(xml1).node("some"). + node("nested").array("withlist").contains("name"). + isEqualTo("name1") || '''/some/nested/withlist[name='name1']''' + XmlAssertion.assertThat(xml1).node("some"). + node("nested").array("withlist").contains("name"). + isEqualTo("name2") || '''/some/nested/withlist[name='name2']''' + XmlAssertion.assertThat(xml1).node("some"). + node("nested").array("withlist").contains("name").isEqualTo("name3"). + withAttribute("id", "10").withAttribute("surname", "kowalski") || '''/some/nested/withlist[name='name3']/name[@id='10'][@surname='kowalski']''' + XmlAssertion.assertThat(xml1).node("some"). + node("nested").array("withlist").isEqualTo(8) || '''/some/nested/withlist[number()=8]''' + XmlAssertion.assertThat(xml1).node("some"). + node("nested").node("json").isEqualTo("with \"val'ue") || '''/some/nested[json=concat('with "val',"'",'ue')]''' + XmlAssertion.assertThat(xml1). + node("some", "nested", "json").isEqualTo("with \"val'ue") || '''/some/nested[json=concat('with "val',"'",'ue')]''' + } + + @Shared + String xml2 = ''' + + a + b + +''' + + @Unroll + def "should generate assertions for simple response body"() { + expect: + verifiable.xPath() == expectedXPath + where: + verifiable || expectedXPath + XmlAssertion.assertThat(xml2).node("root"). + node("property1").isEqualTo("a") || '''/root[property1='a']''' + XmlAssertion.assertThat(xml2).node("root"). + node("property2").isEqualTo("b") || '''/root[property2='b']''' + } + + @Shared + String xml3 = ''' + + true + + false + 5 + +''' + + @Unroll + def "should generate assertions for null and boolean values"() { + expect: + verifiable.xPath() == expectedXPath + where: + verifiable || expectedXPath + XmlAssertion.assertThat(xml3).node("root"). + node("property1").isEqualTo("true") || '''/root[property1='true']''' + XmlAssertion.assertThat(xml3).node("root"). + node("property2").isNull() || '''not(boolean(/root/property2/text()[1]))''' + XmlAssertion.assertThat(xml3).node("root"). + node("property3").isEqualTo(false) || '''/root[property3='false']''' + XmlAssertion.assertThat(xml3).node("root"). + node("property4").isEqualTo(5) || '''/root[property4=5]''' + } + + @Shared + StringWriter xml4 = new StringWriter() + @Shared + def root4 = new MarkupBuilder(xml4).root { + property1('a') + property2 { + a('sth') + b('sthElse') + } + } + + @Unroll + def "should generate assertions for simple response body constructed from map with a list"() { + expect: + verifiable.xPath() == expectedXPath + where: + verifiable || expectedXPath + XmlAssertion.assertThat(xml4.toString()). + node("root").node("property1"). + isEqualTo("a") || '''/root[property1='a']''' + XmlAssertion.assertThat(xml4.toString()). + node("root").array("property2").contains("a"). + isEqualTo("sth") || '''/root/property2[a='sth']''' + XmlAssertion.assertThat(xml4.toString()). + node("root").array("property2").contains("b"). + isEqualTo("sthElse") || '''/root/property2[b='sthElse']''' + } + + @Shared + String xml7 = ''' + + + test1 + + + test2 + + +''' + + @Unroll + def "should generate assertions for array inside response body element"() { + expect: + verifiable.xPath() == expectedXPath + where: + verifiable || expectedXPath + XmlAssertion.assertThat(xml7).node("root"). + array("property1").contains("property2").isEqualTo("test1") || '''/root/property1[property2='test1']''' + XmlAssertion.assertThat(xml7).node("root"). + array("property1").contains("property3").isEqualTo("test2") || '''/root/property1[property3='test2']''' + } + + @Shared + String xml8 = """ + + a + + b + + +""" + + def "should generate assertions for nested objects in response body"() { + expect: + verifiable.xPath() == expectedXPath + where: + verifiable || expectedXPath + XmlAssertion.assertThat(xml8).node("root"). + node("property2").node("property3").isEqualTo("b") || '''/root/property2[property3='b']''' + XmlAssertion.assertThat(xml8).node("root"). + node("property1").isEqualTo("a") || '''/root[property1='a']''' + } + + @Shared + StringWriter xml9 = new StringWriter() + @Shared + def root9 = new MarkupBuilder(xml9).root { + property1('a') + property2(123) + } + + @Unroll + def "should generate regex assertions for map objects in response body"() { + expect: + verifiable.xPath() == expectedXPath + where: + verifiable || expectedXPath + XmlAssertion.assertThat(xml9.toString()). + node("root").node("property2").matches("[0-9]{3}") || '''/root[matches(property2, '[0-9]{3}')]''' + XmlAssertion.assertThat(xml9.toString()). + node("root").node("property1").isEqualTo("a") || '''/root[property1='a']''' + } + + def "should generate escaped regex assertions for string objects in response body"() { + given: + StringWriter xml = new StringWriter() + def root = new MarkupBuilder(xml).root { + property2(123123) + } + expect: + def verifiable = XmlAssertion. + assertThat(xml.toString()).node("root").node("property2"). + matches("\\d+") + verifiable.xPath() == '''/root[matches(property2, '\\d+')]''' + } + + @Shared + StringWriter xml10 = new StringWriter() + @Shared + def root10 = new MarkupBuilder(xml10).root { + errors { + property('bank_account_number') + message('incorrect_format') + } + } + + @Unroll + def "should work with more complex stuff and xpaths"() { + expect: + verifiable.xPath() == expectedXPath + where: + verifiable || expectedXPath + XmlAssertion.assertThat(xml10.toString()). + node("root").array("errors").contains("property"). + isEqualTo("bank_account_number") || '''/root/errors[property='bank_account_number']''' + XmlAssertion.assertThat(xml10.toString()). + node("root").array("errors").contains("message"). + isEqualTo("incorrect_format") || '''/root/errors[message='incorrect_format']''' + } + + @Shared + String xml11 = ''' + + + + -77.119759 + 38.995548 + -76.909393 + 38.791645 + + + +''' + + @Unroll + def "should manage to parse a double array"() { + expect: + verifiable.xPath() == expectedXPath + where: + verifiable || expectedXPath + XmlAssertion.assertThat(xml11).node("root"). + node("place").node("bounding_box").array("coordinates"). + isEqualTo(38.995548) || '''/root/place/bounding_box/coordinates[number()=38.995548]''' + XmlAssertion.assertThat(xml11).node("root"). + node("place").node("bounding_box").array("coordinates"). + isEqualTo(-77.119759) || '''/root/place/bounding_box/coordinates[number()=-77.119759]''' + XmlAssertion.assertThat(xml11).node("root"). + node("place").node("bounding_box").array("coordinates"). + isEqualTo(-76.909393) || '''/root/place/bounding_box/coordinates[number()=-76.909393]''' + XmlAssertion.assertThat(xml11).node("root"). + node("place").node("bounding_box").array("coordinates"). + isEqualTo(38.791645) || '''/root/place/bounding_box/coordinates[number()=38.791645]''' + + } + + def "should run XPath when provided manually"() { + given: + String xml = """ + + a + + b + + +""" + and: + String xPath = '''/root/property2[property3='b']''' + expect: + XmlAssertion.assertThat(xml).matchesXPath(xPath) + } + + def "should throw exception when XPath is not matched"() { + given: + String xml = """ + + a + + b + + +""" + and: + String xPath = '''/root/property2[property3='non-existing']''' + when: + XmlAssertion.assertThat(xml).matchesXPath(xPath) + then: + IllegalStateException illegalStateException = thrown(IllegalStateException) + illegalStateException.message.contains("Parsed XML") + illegalStateException.message.contains("doesn't match the XPath") + } + + def "should not throw exception when json path is not matched and system prop overrides the check"() { + given: + String xml = """ + + a + + b + + + """ + and: + String xPath = '''/root/property2[property3='non-existing']''' + when: + XmlAssertion.assertThat(xml). + withoutThrowingException().matchesXPath(xPath) + then: + noExceptionThrown() + } + + def "should generate escaped regex assertions for text with regular expression values"() { + given: + // '"<>[]() + String xml = """ + + '"<>[]() + """ + expect: + def verifiable = XmlAssertion.assertThat(xml). + node("root").node("property1").matches('\'"<>\\[\\]\\(\\)') + verifiable.xPath() == '''/root[matches(property1, concat('',"'",'"<>\\[\\]\\(\\)'))]''' + } + + def "should escape regular expression properly"() { + given: + String xml = """ + + /api/12 + 123456 + """ + expect: + def verifiable = XmlAssertion.assertThatXml(xml). + node("root").node("path").matches("^/api/[0-9]{2}\$") + verifiable.xPath() == '''/root[matches(path, '^/api/[0-9]{2}$')]''' + } + + def "should escape single quotes in a quoted string"() { + given: + String xml = """ + + text with 'quotes' inside + + """ + expect: + def verifiable = XmlAssertion.assertThatXml(xml). + node("root").node("text").isEqualTo("text with 'quotes' inside") + verifiable.xPath() == '''/root[text=concat('text with ',"'",'quotes',"'",' inside')]''' + } + + def "should escape brackets in a string"() { + given: + String xml = """ + + <escape me> + + """ + expect: + def verifiable = XmlAssertion.assertThatXml(xml). + node("root").node("id").isEqualTo("") + verifiable.xPath() == '''/root[id='']''' + } + + def "should escape double quotes in a quoted string"() { + given: + String xml = """ + + text with "quotes" inside + + """ + expect: + def verifiable = XmlAssertion.assertThatXml(xml). + node("root").node("text").isEqualTo('''text with "quotes" inside''') + verifiable.xPath() == '''/root[text='text with "quotes" inside']''' + } + + def 'should resolve the value of XML via XPath'() { + given: + String xml = + ''' + + + + + with value + 4 + + name1 + + + name2 + + + + name3 + + + + + + + + + true + 4 + + name1 + + + name2 + + a + b + + + + ''' + expect: + XPathBuilder.builder(xml).node("root"). + array("element").node("some").node("nested").node("json"). + read() == 'with value' + XPathBuilder.builder(xml).node("root"). + array("element").node("some").node("nested").node("anothervalue"). + read() == 4.toString() + // assertThat(xml).node("root").array("element").node("some").node("nested").array("withlist").node("name").read() == ['name1', 'name2'].toString() + //assertThat(xml).node("root").array("element").node("someother").node("nested").array("withlist2").read() == ['a', 'b'].toString() + XmlAssertion.assertThat(xml).node("root"). + array("element").node("someother").node("nested").node("json"). + read() == true.toString() + } + + def 'should match array containing an array of primitives'() { + given: + String xml = ''' + + existing + + + AGENT + BANK + CASH + + + +''' + expect: + def verifiable = XmlAssertion.assertThatXml(xml). + node("root").array("elements").array("partners"). + contains("payment_methods").isEqualTo("BANK") + verifiable.xPath() == '''/root/elements/partners[payment_methods='BANK']''' + } + + def 'should match pattern in array'() { + given: + String xml = ''' + + ROLE_ADMIN + + ''' + + expect: + def verifiable = XmlAssertion.assertThatXml(xml). + node("root").array("authorities").matches("^[a-zA-Z0-9_\\- ]+\$") + verifiable.xPath() == '''/root/authorities[matches(text(), '^[a-zA-Z0-9_\\- ]+$')]''' + } + + def 'should manage to parse array with string values'() { + given: + String xml = ''' + + name1 + name2 + ''' + + expect: + def v1 = XmlAssertion.assertThat(xml). + node("root").array("some_list").isEqualTo("name1") + def v2 = XmlAssertion.assertThat(xml). + node("root").array("some_list").isEqualTo("name2") + and: + v1.xPath() == '''/root/some_list[text()='name1']''' + v2.xPath() == '''/root/some_list[text()='name2']''' + } + + @Issue("#2") + def 'should allow nested calls with counting the elements size'() { + given: + String xml = ''' + + name1 + name2 + ''' + + expect: + def v1 = XmlAssertion.assertThat(xml). + node("root").array("some_list").hasSize(2).isEqualTo("name1") + and: + v1.xPath() == '''/root/some_list[text()='name1']''' + } + + @Issue("#2") + def 'should count the elements size'() { + given: + String xml = ''' + + name1 + name2 + ''' + + expect: + def v1 = XmlAssertion.assertThat(xml). + node("root").array("some_list").hasSize(2) + and: + v1.xPath() == '''count(/root/some_list)''' + } + + @Issue("#2") + def 'should throw exception if size is wrong'() { + given: + String xml = ''' + + name1 + name2 + ''' + + when: + XmlAssertion.assertThat(xml).node("root"). + array("some_list").hasSize(1) + then: + IllegalStateException e = thrown(IllegalStateException) + e.message. + contains("has size [2] and not [1] for XPath ") + } + + @Issue("#2") + def 'should return 0 if element is missing'() { + given: + String xml = ''' + + name1 + name2 + ''' + + when: + XmlAssertion.assertThat(xml).node("root"). + array("foo").hasSize(1) + then: + IllegalStateException e = thrown(IllegalStateException) + e.message.contains("has size [0] and not [1] for XPath ") + } + +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_xml.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_xml.yml new file mode 100644 index 0000000000..47ad9f2fe6 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_xml.yml @@ -0,0 +1,68 @@ +request: + method: GET + url: /getymlResponse + headers: + Content-Type: application/xml + body: | + + 123 + abc + + abc + def + ghi + + 123 + true + 2017-01-01 + 2017-01-01T01:23:45 + + foo + string + foo + + matchers: + body: + - path: /test/duck/text() + type: by_regex + value: "[0-9]{10}" + - path: /test/duck/text() + type: by_equality + - path: /test/time/text() + type: by_time +response: + status: 200 + headers: + Content-Type: application/xml + body: | + + 123 + abc + + abc + def + ghi + + 123 + true + 2017-01-01 + 2017-01-01T01:23:45 + + foo + string + foo + + matchers: + body: + - path: /test/duck/text() + type: by_regex + value: "[0-9]{10}" + - path: /test/duck/text() + type: by_command + value: "test($it)" + - path: /test/duck/xxx + type: by_null + - path: /test/duck/text() + type: by_equality + - path: /test/time/text() + type: by_time \ No newline at end of file