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:
committed by
GitHub
parent
5d8015aa0f
commit
6c3caddb7b
@@ -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
12
pom.xml
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ class Headers {
|
||||
}
|
||||
}
|
||||
|
||||
void headers(Set<Header> headers) {
|
||||
entries.addAll(headers)
|
||||
}
|
||||
|
||||
void accept(String contentType) {
|
||||
header(accept(), matching(contentType))
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import groovy.transform.CompileStatic
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Tim Ysewyn
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 1.0.3
|
||||
*/
|
||||
@CompileStatic
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:'
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]" ?: ''
|
||||
|
||||
@@ -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}\")"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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]" ?: ''
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -23,7 +23,7 @@ package org.springframework.cloud.contract.verifier.util;
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface MethodBuffering {
|
||||
public interface MethodBuffering {
|
||||
|
||||
String method();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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("");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 "val'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);
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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(' ', '')
|
||||
}
|
||||
}
|
||||
@@ -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><test></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><test></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())
|
||||
}
|
||||
@@ -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"() {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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')]'''
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 "val'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>'"<>[]()</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 'quotes' 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><escape me></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 "quotes" 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)>")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user