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`.
This commit is contained in:
Olga Maciaszek-Sharma
2019-01-18 20:16:19 +01:00
committed by GitHub
parent 5d8015aa0f
commit 6c3caddb7b
67 changed files with 4606 additions and 757 deletions

View File

@@ -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

12
pom.xml
View File

@@ -48,6 +48,8 @@
<!-- For Takari plugin -->
<maven.version>3.2.1</maven.version>
<aether.version>1.1.0</aether.version>
<xpath2.processor.version>2.1.100</xpath2.processor.version>
<xerces.version>2.11.0</xerces.version>
</properties>
<modules>
@@ -405,6 +407,16 @@
<artifactId>aether-connector-basic</artifactId>
<version>${aether.version}</version>
</dependency>
<dependency>
<groupId>com.rackspace.eclipse.webtools.sourceediting</groupId>
<artifactId>org.eclipse.wst.xml.xpath2.processor</artifactId>
<version>${xpath2.processor.version}</version>
</dependency>
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>${xerces.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -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

View File

@@ -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<BodyMatcher> jsonPathRegexMatchers = []
protected final List<BodyMatcher> 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<BodyMatcher> jsonPathMatchers() {
return matchers()
}
boolean hasMatchers() {
return !this.jsonPathRegexMatchers.empty
return !this.matchers.empty
}
List<BodyMatcher> jsonPathMatchers() {
return this.jsonPathRegexMatchers
List<BodyMatcher> 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<BodyMatcher> thisMatchers = this.jsonPathRegexMatchers
List<BodyMatcher> thatMatchers = that.jsonPathRegexMatchers
List<BodyMatcher> thisMatchers = this.matchers
List<BodyMatcher> 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

View File

@@ -55,6 +55,10 @@ class Headers {
}
}
void headers(Set<Header> headers) {
entries.addAll(headers)
}
void accept(String contentType) {
header(accept(), matching(contentType))
}

View File

@@ -23,6 +23,7 @@ import groovy.transform.CompileStatic
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @author Olga Maciaszek-Sharma
* @since 1.0.3
*/
@CompileStatic

View File

@@ -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
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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) {

View File

@@ -72,6 +72,10 @@
<groupId>com.toomuchcoding.jsonassert</groupId>
<artifactId>jsonassert</artifactId>
</dependency>
<dependency>
<groupId>com.rackspace.eclipse.webtools.sourceediting</groupId>
<artifactId>org.eclipse.wst.xml.xpath2.processor</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
@@ -194,6 +198,11 @@
<version>2.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>

View File

@@ -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<String> lineSuffix, BlockBuilder blockBuilder) {
lineSuffix.ifPresent({
blockBuilder.addAtTheEnd(lineSuffix.get())
})
}
void addBodyMatchingBlock(List<BodyMatcher> 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:'
}
}

View File

@@ -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 <code>instanceof</code> 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
}
}
}

View File

@@ -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<String> lineSuffix() {
return Optional.of(JUNIT.lineSuffix)
}
@Override
protected String getPropertyInListString(String property, Integer listIndex) {
return "$property[$listIndex]" ?: ''

View File

@@ -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<String> lineSuffix() {
return Optional.of(JUNIT5.lineSuffix)
}
@Override
protected String getResponseBodyPropertyComparisonString(String property, String value) {
return "assertThat(responseBody${property}).isEqualTo(\"${value}\")"

View File

@@ -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'

View File

@@ -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<String> lineSuffix
private final Closure<String> 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<String> 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<Object> 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)
}
}
}

View File

@@ -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() {

View File

@@ -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<String> 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<Object> 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)
}
}

View File

@@ -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

View File

@@ -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)
}
/**

View File

@@ -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<String> lineSuffix() {
return Optional.empty()
}
@Override
protected String getResponseBodyPropertyComparisonString(String property, Object value) {
return ""

View File

@@ -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<String> lineSuffix() {
return Optional.empty()
}
@Override
protected String getPropertyInListString(String property, Integer listIndex) {
"$property[$listIndex]" ?: ''

View File

@@ -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<String> lineSuffix
XmlBodyVerificationBuilder(Contract contract, Optional<String> 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<BodyMatcher> 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.")
}
}

View File

@@ -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

View File

@@ -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<String> 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
}
}

View File

@@ -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<YamlContract.BodyTestMatcher> bodyMatchers) {
protected void setOutputBodyMatchers(DslProperty body,
List<YamlContract.BodyTestMatcher> 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)
}
}

View File

@@ -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 <predefined> remember to pass <type: by_regex>")
}
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 <predefined> remember to pass <type: by_regex>")
}
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 <predefined> remember to pass <type: by_regex>")
}
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 <predefined> remember to pass <type: by_regex>")
}
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<String, Object> headers) {
Set<Header> 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) {

View File

@@ -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
}

View File

@@ -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<BodyMatcher> 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<MatchingType> 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))
}
}

View File

@@ -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()) {

View File

@@ -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)
}
}
}

View File

@@ -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
}
}

View File

@@ -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)) {

View File

@@ -23,7 +23,7 @@ package org.springframework.cloud.contract.verifier.util;
*
* @since 1.0.0
*/
interface MethodBuffering {
public interface MethodBuffering {
String method();
}

View File

@@ -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<BodyMatcher> mapToMatchers(Object xml) {
DocumentBuilder documentBuilder = DocumentBuilderFactory
.newInstance()
.newDocumentBuilder()
Document parsedXml = documentBuilder
.parse(new InputSource(new StringReader(xml as String)))
List<List<Node>> valueNodes = getValueNodesWithParents(parsedXml)
List<BodyMatcher> matchers = []
List<NodePath> 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<NodePath> transformListEntries(List<List<Node>> nodeLists) {
List<PathOccurrenceCounter> pathOccurrenceCounters = []
List<NodePath> nodePaths = []
nodeLists.each { nodeList ->
List<Node> 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<String> nodeNames(List<Node> nodes) {
return nodes.stream()
.map({ it.getNodeName() })
.collect(toList())
}
static String buildXPath(List<Node> 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<List<Node>> getValueNodesWithParents(Node node) {
List<List<Node>> valueNodes = []
List<Node> attributes = []
addValueNodes(node, valueNodes, attributes)
attributes.each {
valueNodes << withParents(it)
}
return valueNodes
}
private static List<Node> addValueNodes(Node node, List<List<Node>> valueNodes, List<Node> 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<Node> getChildNodesAsList(Node node) {
NodeList nodeList = node.getChildNodes()
return getNodeCollectionElements(nodeList)
}
@CompileDynamic
private static List<Node> getAttributesAsList(Node node) {
NamedNodeMap nodeMap = node.getAttributes()
return nodeMap != null ? getNodeCollectionElements(nodeMap) : []
}
@CompileDynamic
private static List<Node> getNodeCollectionElements(def nodeCollection) {
return IntStream.range(0, nodeCollection.getLength())
.mapToObj({ nodeCollection.item(it) as Node })
.collect(toList())
}
private static List<Node> withParents(Node node) {
List<Node> nodeList = new ArrayList<>()
nodeList << node
return addParents(node, nodeList)
}
private static List<Node> withParents(Attr attribute) {
List<Node> nodeList = new ArrayList<>()
nodeList << attribute
Node ownerNode = attribute.getOwnerElement()
nodeList << ownerNode
return addParents(ownerNode, nodeList)
}
private static List<Node> addParents(Node node, List<Node> 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<Node> path
final int index
NodePath(List<Node> path, int index) {
this.path = path
this.index = index
}
List<Node> fromChildToParents() {
return path.reverse()
}
}
private static class PathOccurrenceCounter {
final List<Node> path
int counter
PathOccurrenceCounter(List<Node> path) {
this.path = path
counter = 1
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<String> xPathBuffer, LinkedList<String> specialCaseXPathBuffer,
Object arrayName, XmlAsserterConfiguration xmlAsserterConfiguration) {
super(cachedObjects, xPathBuffer, specialCaseXPathBuffer, arrayName, xmlAsserterConfiguration);
this.checkingPrimitiveType = true;
}
private ArrayValueAssertion(XmlCachedObjects cachedObjects, LinkedList<String> xPathBuffer, LinkedList<String> 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<Item> 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;
}
}

View File

@@ -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<String> xPathBuffer, LinkedList<String> specialCaseXPathBuffer,
Object value, XmlAsserterConfiguration xmlAsserterConfiguration) {
super(cachedObjects, xPathBuffer, specialCaseXPathBuffer, value, xmlAsserterConfiguration);
}
FieldAssertion(XmlAsserter asserter) {
super(asserter);
}
}

View File

@@ -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();
}

View File

@@ -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<String> xPathBuffer,
Object fieldName, XmlAsserterConfiguration xmlAsserterConfiguration) {
super(cachedObjects, xPathBuffer, new LinkedList<String>(), fieldName, xmlAsserterConfiguration);
}
@Override
protected boolean isReadyToCheck() {
return true;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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<String> xPathBuffer;
// for things like count(...)
final LinkedList<String> specialCaseXPathBuffer;
final Object fieldName;
final XmlAsserterConfiguration xmlAsserterConfiguration;
XmlAsserter(XmlCachedObjects cachedObjects, LinkedList<String> xPathBuffer, LinkedList<String> 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<String> buffer) {
LinkedList<String> queue = new LinkedList<String>(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<String> list = new LinkedList<String>();
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<String> 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);
}
}
}

View File

@@ -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;
}

View File

@@ -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<String> xPathBuffer = new LinkedList<>();
private final LinkedList<String> specialCaseXPathBuffer = new LinkedList<>();
private final XmlAsserterConfiguration xmlAsserterConfiguration = new XmlAsserterConfiguration();
private static final Map<String, XmlCachedObjects> 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("");
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}

View File

@@ -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
*
* <p>
*
* {@code
<?xml version="1.0" encoding="UTF-8" ?>
<some>
<nested>
<json>with &quot;val&apos;ue</json>
<anothervalue>4</anothervalue>
<withattr id="a" id2="b">foo</withattr>
<withlist>
<name>name1</name>
</withlist>
<withlist>
<name>name2</name>
</withlist>
<withlist>
8
</withlist>
<withlist>
<name id="10" surname="kowalski">name3</name>
</withlist>
</nested>
</some>
* }
*
* <p>
*
* In order to check the values of the attributes of the {@code withlist} element
* with value {@code name3} you'd have to call:
*
* <p>
*
* {@code assertThat(xml1).node("some").node("nested").array("withlist").contains("name").isEqualTo("name3").withAttribute("id", "10").withAttribute("surname", "kowalski")}
*
* <p>
*
* 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.
*
* <p>
*
* {@code
<list>
<element>foo</element>
<element>bar</element>
<complexElement>
<param>baz</param>
</complexElement>
</list>
* }
*
* <p>
* The code to check it would look like this:
* <p>
*
* {@code array("list").contains("element").isEqualTo("foo")}
* {@code array("list").contains("complexElement").node("param").isEqualTo("baz")}
*
* <p>
* The generated XPaths would be
* <p>
*
* {@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);
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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 """
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<key><complex>foo</complex></key>
</test>"""
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 """
<test>
<duck type='xtype'>123</duck>
</test>"""
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
}
}

View File

@@ -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

View File

@@ -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 = '''
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>
'''
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<Contract> contracts = [Contract.make {
request {
method 'GET'
url '/get'
headers {
contentType(applicationXml())
}
body """
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>"""
bodyMatchers {
xPath('/test/duck/text()', byRegex("[0-9]{3}"))
}
}
response {
status(OK())
body """
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>"""
bodyMatchers {
xPath('/test/duck/xxx', byNull())
}
}
}]
when:
Collection<YamlContract> 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)
]
}
}

View File

@@ -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 = '''
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>
'''
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<Contract> 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(' ', '')
}
}

View File

@@ -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":"<foo><name>Jozo</name><jobId>&lt;test&gt;</jobId></foo>"
}
]
},
"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": "<test>"
}
}
]
},
"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":"<user><name>Jozo</name><jobId>&lt;test&gt;</jobId></user>"
}
]
},
"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": "<test>"
}
}
]
},
"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())
}

View File

@@ -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"() {

View File

@@ -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())
}
}

View File

@@ -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 """
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>"""
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 """
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>"""
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 """
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<number>123</number>
</test>"""
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 """
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>"""
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<test>\\n<duck type='xtype'>123</duck>" +
"\\n<alpha>abc</alpha>\\n<list>\\n<elem>abc</elem>\\n<elem>def</elem>" +
"\\n<elem>ghi</elem>\\n</list>\\n<number>123</number>" +
"\\n<aBoolean>true</aBoolean>\\n<date>2017-01-01</date>" +
"\\n<dateTime>2017-01-01T01:23:45</dateTime>\\n<time>01:02:34</time>" +
"\\n<valueWithoutAMatcher>foo</valueWithoutAMatcher>" +
"\\n<valueWithTypeMatch>string</valueWithTypeMatch>" +
"\\n<key><complex>foo</complex></key>\\n</test>")
}
}

View File

@@ -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())
}

View File

@@ -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')]'''
}
}

View File

@@ -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 = '''<?xml version="1.0" encoding="UTF-8" ?>
<some>
<nested>
<json>with &quot;val&apos;ue</json>
<anothervalue>4</anothervalue>
<withattr id="a" id2="b">foo</withattr>
<withlist>
<name>name1</name>
</withlist>
<withlist>
<name>name2</name>
</withlist>
<withlist>
8
</withlist>
<withlist>
<name id="10" surname="kowalski">name3</name>
</withlist>
</nested>
</some>'''
@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 = '''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<property1>a</property1>
<property2>b</property2>
</root>
'''
@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 = '''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<property1>true</property1>
<property2 />
<property3>false</property3>
<property4>5</property4>
</root>
'''
@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 = '''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<property1>
<property2>test1</property2>
</property1>
<property1>
<property3>test2</property3>
</property1>
</root>
'''
@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 = """<?xml version="1.0" encoding="UTF-8" ?>
<root>
<property1>a</property1>
<property2>
<property3>b</property3>
</property2>
</root>
"""
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 = '''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<place>
<bounding_box>
<coordinates>-77.119759</coordinates>
<coordinates>38.995548</coordinates>
<coordinates>-76.909393</coordinates>
<coordinates>38.791645</coordinates>
</bounding_box>
</place>
</root>
'''
@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 = """<?xml version="1.0" encoding="UTF-8" ?>
<root>
<property1>a</property1>
<property2>
<property3>b</property3>
</property2>
</root>
"""
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 = """<?xml version="1.0" encoding="UTF-8" ?>
<root>
<property1>a</property1>
<property2>
<property3>b</property3>
</property2>
</root>
"""
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 = """<?xml version="1.0" encoding="UTF-8" ?>
<root>
<property1>a</property1>
<property2>
<property3>b</property3>
</property2>
</root>
"""
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 = """<?xml version="1.0" encoding="UTF-8" ?>
<root>
<property1>&apos;&quot;&lt;&gt;[]()</property1>
</root>"""
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 = """<?xml version="1.0" encoding="UTF-8" ?>
<root>
<path>/api/12</path>
<correlationId>123456</correlationId>
</root>"""
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 = """<?xml version="1.0" encoding="UTF-8" ?>
<root>
<text>text with &apos;quotes&apos; inside</text>
</root>
"""
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 = """<?xml version="1.0" encoding="UTF-8" ?>
<root>
<id>&lt;escape me&gt;</id>
</root>
"""
expect:
def verifiable = XmlAssertion.assertThatXml(xml).
node("root").node("id").isEqualTo("<escape me>")
verifiable.xPath() == '''/root[id='<escape me>']'''
}
def "should escape double quotes in a quoted string"() {
given:
String xml = """<?xml version="1.0" encoding="UTF-8" ?>
<root>
<text>text with &quot;quotes&quot; inside</text>
</root>
"""
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 =
'''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<element>
<some>
<nested>
<json>with value</json>
<anothervalue>4</anothervalue>
<withlist>
<name>name1</name>
</withlist>
<withlist>
<name>name2</name>
</withlist>
<withlist>
<anothernested>
<name>name3</name>
</anothernested>
</withlist>
</nested>
</some>
</element>
<element>
<someother>
<nested>
<json>true</json>
<anothervalue>4</anothervalue>
<withlist>
<name>name1</name>
</withlist>
<withlist>
<name>name2</name>
</withlist>
<withlist2>a</withlist2>
<withlist2>b</withlist2>
</nested>
</someother>
</element>
</root>'''
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 = '''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<first_name>existing</first_name>
<elements>
<partners>
<role>AGENT</role>
<payment_methods>BANK</payment_methods>
<payment_methods>CASH</payment_methods>
</partners>
</elements>
</root>
'''
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 = '''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<authorities>ROLE_ADMIN</authorities>
</root>
'''
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 = '''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<some_list>name1</some_list>
<some_list>name2</some_list>
</root>'''
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 = '''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<some_list>name1</some_list>
<some_list>name2</some_list>
</root>'''
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 = '''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<some_list>name1</some_list>
<some_list>name2</some_list>
</root>'''
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 = '''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<some_list>name1</some_list>
<some_list>name2</some_list>
</root>'''
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 <count(/root/some_list)>")
}
@Issue("#2")
def 'should return 0 if element is missing'() {
given:
String xml = '''<?xml version="1.0" encoding="UTF-8" ?>
<root>
<some_list>name1</some_list>
<some_list>name2</some_list>
</root>'''
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 <count(/root/foo)>")
}
}

View File

@@ -0,0 +1,68 @@
request:
method: GET
url: /getymlResponse
headers:
Content-Type: application/xml
body: |
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>
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: |
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>
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