diff --git a/docs/src/main/asciidoc/_project-features-contract.adoc b/docs/src/main/asciidoc/_project-features-contract.adoc
index dd08e3684e..e20fe4db12 100644
--- a/docs/src/main/asciidoc/_project-features-contract.adoc
+++ b/docs/src/main/asciidoc/_project-features-contract.adoc
@@ -1843,6 +1843,54 @@ public void validate_xmlMatches() throws Exception {
----
====
+==== XML Support for Namespaces
+Namespaced XML is supported. However, any XPath expresssions used to select namespaced content must be updated.
+
+Consider the following explicitly namespaced XML document:
+
+[source,xml,indent=0]
+----
+
+ customer@test.com
+
+----
+The XPath expression to select the email address is: `/ns1:customer/email/text()`.
+
+WARNING: Beware as the unqualified expression (`/customer/email/text()`) results in `""`.
+
+For content that uses an unqualified namespace, the expression is more verbose. Consider the following XML document that
+uses an unqualified namespace:
+
+[source,xml,indent=0]
+----
+
+ customer@test.com
+
+----
+The XPath expression to select the email address is
+```
+*/[local-name()='customer' and namespace-uri()='http://demo.com/customer']/*[local-name()='email']/text()
+```
+WARNING: Beware, as the unqualified expressions (`/customer/email/text()` or `*/[local-name()='customer' and namespace-uri()='http://demo.com/customer']/email/text()`)
+result in `""`. Even the child elements have to be referenced with the `local-name` syntax.
+
+====== General Namespaced Node Expression Syntax
+- Node using qualified namespace:
+```
+/
+```
+- Node using and defining an unqualified namespace:
+```
+/*[local-name=()='' and namespace-uri=()='']
+```
+NOTE: In some cases, you can omit the `namespace_uri` portion, but doing so may lead to ambiguity.
+
+- Node using an unqualified namespace (one of its ancestor's defines the xmlns attribute):
+```
+/*[local-name=()='']
+```
+
+
[[contract-dsl-multiple]]
=== Multiple Contracts in One File
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverter.groovy
index 17d692c09e..9c1bb2392f 100644
--- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverter.groovy
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverter.groovy
@@ -54,6 +54,7 @@ import static org.w3c.dom.Node.PROCESSING_INSTRUCTION_NODE
import static org.w3c.dom.Node.TEXT_NODE
/**
* @author Olga Maciaszek-Sharma
+ * @author Chris Bono
* @since 2.1.0
*/
class XmlToXPathsConverter {
@@ -131,23 +132,26 @@ class XmlToXPathsConverter {
}
static List mapToMatchers(Object xml) {
- DocumentBuilder documentBuilder = DocumentBuilderFactory
- .newInstance()
- .newDocumentBuilder()
- Document parsedXml = documentBuilder
- .parse(new InputSource(new StringReader(xml as String)))
+ DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance()
+ builderFactory.setNamespaceAware(true)
+ DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder()
+ Document parsedXml = documentBuilder.parse(new InputSource(new StringReader(xml as String)))
List> valueNodes = getValueNodesWithParents(parsedXml)
List matchers = []
List valueNodePaths = transformListEntries(valueNodes)
- valueNodePaths.each {
+ valueNodePaths.findAll{ !isPathToDefaultXmlnsAttributeNode(it) }.each {
matchers << new PathBodyMatcher(
buildXPath(it.fromChildToParents(), it.index),
- new MatchingTypeValue(MatchingType.EQUALITY,
- it.path.get(0).nodeValue))
+ new MatchingTypeValue(MatchingType.EQUALITY, it.path.get(0).nodeValue))
}
return matchers
}
+ static boolean isPathToDefaultXmlnsAttributeNode(NodePath nodePath) {
+ Node node = nodePath?.path?.first()
+ return node && isAttributeNode(node) && node.getNodeName() == 'xmlns'
+ }
+
static List transformListEntries(List> nodeLists) {
List pathOccurrenceCounters = []
List nodePaths = []
@@ -189,9 +193,40 @@ class XmlToXPathsConverter {
}
private static XmlVerifiable processNode(XmlVerifiable xmlVerifiable, Node node) {
+ // If node has explicit namespace (eg '') no special processing needed
+ if (nodeUsesExplicitNamespace(node)) {
+ return xmlVerifiable.node(node.nodeName)
+ }
+ // If node directly declares default ns (eg. 'xmlns=') then use local name and namespace uri syntax
+ String defaultXmlns = getDefaultXmlnsDeclarationOnNodeIfExists(node)
+ if (defaultXmlns != null) {
+ return xmlVerifiable.nodeWithDefaultNamespace(node.nodeName, defaultXmlns)
+ }
+ // If node indirectly declares (via ancestor) default ns then use local name syntax
+ if (getDefaultXmlnsDeclarationOnAncestorsIfExists(node) != null) {
+ return xmlVerifiable.nodeWithDefaultNamespace(node.nodeName, null)
+ }
return xmlVerifiable.node(node.nodeName)
}
+ private static boolean nodeUsesExplicitNamespace(Node node) {
+ return node?.getNodeName()?.contains(":")
+ }
+
+ private static String getDefaultXmlnsDeclarationOnNodeIfExists(Node node) {
+ return node?.getAttributes()?.getNamedItem("xmlns")?.getTextContent()
+ }
+
+ private static String getDefaultXmlnsDeclarationOnAncestorsIfExists(Node node) {
+ while ((node = node?.getParentNode()) != null) {
+ String defaultXmlns = getDefaultXmlnsDeclarationOnNodeIfExists(node)
+ if (defaultXmlns != null) {
+ return defaultXmlns
+ }
+ }
+ return null
+ }
+
private static XmlVerifiable processNode(XmlVerifiable xmlVerifiable, Attr attribute) {
return xmlVerifiable.withAttribute(attribute.nodeName)
}
diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ArrayValueAssertion.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ArrayValueAssertion.java
index e1976397d1..9557525c17 100644
--- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ArrayValueAssertion.java
+++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/ArrayValueAssertion.java
@@ -78,6 +78,12 @@ class ArrayValueAssertion extends FieldAssertion implements XmlArrayVerifiable {
return new ArrayValueAssertion(assertion, false);
}
+ @Override
+ public FieldAssertion nodeWithDefaultNamespace(String value, String defaultNamespace) {
+ FieldAssertion assertion = super.nodeWithDefaultNamespace(value, defaultNamespace);
+ return new ArrayValueAssertion(assertion, false);
+ }
+
@Override
public FieldAssertion node(String... nodeNames) {
FieldAssertion assertion = super.node(nodeNames);
diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserter.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserter.java
index 34430481a5..c91778e90e 100644
--- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserter.java
+++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlAsserter.java
@@ -28,6 +28,8 @@ 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;
+import org.springframework.util.StringUtils;
+
class XmlAsserter implements XmlVerifiable {
private static final Log log = LogFactory.getLog(XmlAsserter.class);
@@ -117,6 +119,20 @@ class XmlAsserter implements XmlVerifiable {
return asserter;
}
+ @Override
+ public FieldAssertion nodeWithDefaultNamespace(final String value, String defaultNamespace) {
+ FieldAssertion asserter = new FieldAssertion(this.cachedObjects, this.xPathBuffer, this.specialCaseXPathBuffer,
+ value, this.xmlAsserterConfiguration);
+ String path = String.format("*[local-name()='%s'", value);
+ if (StringUtils.hasText(defaultNamespace)) {
+ path += String.format(" and namespace-uri()='%s'", defaultNamespace);
+ }
+ path += "]";
+ asserter.xPathBuffer.offer(path);
+ asserter.xPathBuffer.offer("/");
+ return asserter;
+ }
+
@Override
public XmlVerifiable withAttribute(String attribute, String attributeValue) {
FieldAssertion asserter = new FieldAssertion(this.cachedObjects, this.xPathBuffer, this.specialCaseXPathBuffer,
@@ -137,7 +153,11 @@ class XmlAsserter implements XmlVerifiable {
public XmlVerifiable withAttribute(String attribute) {
FieldAssertion asserter = new FieldAssertion(this.cachedObjects, this.xPathBuffer, this.specialCaseXPathBuffer,
this.fieldName, this.xmlAsserterConfiguration);
- asserter.xPathBuffer.offer("@" + String.valueOf(attribute));
+ String path = "@" + attribute;
+ if (attribute.startsWith("xmlns:")) {
+ path = "namespace::" + attribute.substring("xmlns:".length());
+ }
+ asserter.xPathBuffer.offer(path);
updateCurrentBuffer(asserter);
asserter.checkBufferedXPathString();
return asserter;
diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlVerifiable.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlVerifiable.java
index 6e8b44d7ca..ce7c89564e 100644
--- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlVerifiable.java
+++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/xml/XmlVerifiable.java
@@ -21,6 +21,7 @@ package org.springframework.cloud.contract.verifier.util.xml;
*
* @author Marcin Grzejszczak
* @author Olga Maciaszek-Sharma
+ * @author Chris Bono
* @since 2.1.0
*/
public interface XmlVerifiable extends IteratingOverArray, XmlReader {
@@ -32,6 +33,16 @@ public interface XmlVerifiable extends IteratingOverArray, XmlReader {
*/
XmlVerifiable node(String nodeName);
+ /**
+ * Field assertion. Adds a XPath entry for a single node that uses a default
+ * namespace.
+ * @param nodeName local name of the node
+ * @param namespace the default namespace uri or null if one of the node ancestor
+ * declares the namespace
+ * @return new {@code XmlVerifiable}
+ */
+ XmlVerifiable nodeWithDefaultNamespace(String nodeName, String namespace);
+
/**
* 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
diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/XmlMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/XmlMethodBodyBuilderSpec.groovy
index 0ec66e4b48..16a7e97896 100644
--- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/XmlMethodBodyBuilderSpec.groovy
+++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/XmlMethodBodyBuilderSpec.groovy
@@ -31,6 +31,7 @@ import org.springframework.cloud.contract.verifier.util.SyntaxChecker
/**
* @author Olga Maciaszek-Sharma
+ * @author Chris Bono
* @since 2.1.0
*/
class XmlMethodBodyBuilderSpec extends Specification {
@@ -179,6 +180,214 @@ class XmlMethodBodyBuilderSpec extends Specification {
}
}
+ @Unroll
+ def 'should generate correct verification from named 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 """
+
+
+
+ duck5150
+
+
+
+"""
+ bodyMatchers {
+ xPath('/test/duck/text()', byRegex("[0-9]{3}"))
+ xPath('/test/duck/text()', byCommand('equals($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/duck/@type', byEquality())
+ }
+ }
+ }
+ // end::xmlgroovy[]
+ methodBuilder()
+ when:
+ String test = singleTestGenerator(contractDsl)
+ then:
+ test.contains('assertThat(valueFromXPath(parsedXml, "/ns1:test/ns1:header/duck-bucket/duck/text()")).isEqualTo("duck5150")')
+ test.contains('assertThat(valueFromXPath(parsedXml, "/ns1:test/namespace::ns1")).isEqualTo("http://demo.com/testns")')
+ test.contains('assertThat(valueFromXPath(parsedXml, "/ns1:test/ns1:header/duck-bucket/@type")).isEqualTo("bigbucket")')
+ and:
+ SyntaxChecker.tryToCompile(methodBuilderName, test)
+ where:
+ methodBuilderName | methodBuilder
+ "spock" | {
+ properties.testFramework = TestFramework.SPOCK
+ }
+ "testng" | {
+ properties.testFramework = TestFramework.TESTNG
+ }
+ "junit" | {
+ properties.testMode = TestMode.MOCKMVC
+ }
+ "jaxrs-spock" | {
+ properties.testFramework = TestFramework.SPOCK; properties.testMode = TestMode.JAXRSCLIENT
+ }
+ "jaxrs" | {
+ properties.testFramework = TestFramework.JUNIT; properties.testMode = TestMode.JAXRSCLIENT
+ }
+ "webclient" | {
+ properties.testMode = TestMode.WEBTESTCLIENT
+ }
+ }
+
+ @Unroll
+ def 'should generate correct verification from complex named 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 """
+
+
+
+ 1234
+
+
+
+"""
+ bodyMatchers {
+ xPath('//*[local-name()=\'RsHeader\' and namespace-uri()=\'http://schemas.xmlsoap.org/soap/custom\']/*[local-name()=\'MsgSeqId\']/text()', byEquality())
+ }
+ }
+ }
+ // end::xmlgroovy[]
+ methodBuilder()
+ when:
+ String test = singleTestGenerator(contractDsl)
+ then:
+ test.contains('assertThat(valueFromXPath(parsedXml, "//*[local-name()=\'RsHeader\' and namespace-uri()=\'http://schemas.xmlsoap.org/soap/custom\']/*[local-name()=\'MsgSeqId\']/text()")).isEqualTo("1234")')
+ test.contains('assertThat(valueFromXPath(parsedXml, "/SOAP-ENV:Envelope/namespace::SOAP-ENV")).isEqualTo("http://schemas.xmlsoap.org/soap/envelope/")')
+ !test.contains('assertThat(valueFromXPath(parsedXml, "/SOAP-ENV:Envelope/SOAP-ENV:Header/*[local-name()=\'RsHeader\' and namespace-uri()=\'http://schemas.xmlsoap.org/soap/custom\']/@xmlns")).isEqualTo"')
+ and:
+ SyntaxChecker.tryToCompile(methodBuilderName, test)
+ where:
+ methodBuilderName | methodBuilder
+ "spock" | {
+ properties.testFramework = TestFramework.SPOCK
+ }
+ "testng" | {
+ properties.testFramework = TestFramework.TESTNG
+ }
+ "junit" | {
+ properties.testMode = TestMode.MOCKMVC
+ }
+ "jaxrs-spock" | {
+ properties.testFramework = TestFramework.SPOCK; properties.testMode = TestMode.JAXRSCLIENT
+ }
+ "jaxrs" | {
+ properties.testFramework = TestFramework.JUNIT; properties.testMode = TestMode.JAXRSCLIENT
+ }
+ "webclient" | {
+ properties.testMode = TestMode.WEBTESTCLIENT
+ }
+ }
+
+ @Unroll
+ def 'should generate correct verification from very complex named xml without 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 """
+
+ customer@test.com
+
+ Krombopulous
+
+
+ 51
+ 50
+
+
+
+
+"""
+ }
+ }
+ // end::xmlgroovy[]
+ methodBuilder()
+ when:
+ String test = singleTestGenerator(contractDsl)
+ then:
+ test.contains('assertThat(valueFromXPath(parsedXml, "/ns1:customer/namespace::ns1")).isEqualTo("http://demo.com/customer")')
+ test.contains('assertThat(valueFromXPath(parsedXml, "/ns1:customer/namespace::addr")).isEqualTo("http://demo.com/address")')
+ test.contains('assertThat(valueFromXPath(parsedXml, "/ns1:customer/email/text()")).isEqualTo("customer@test.com")')
+ test.contains('assertThat(valueFromXPath(parsedXml, "/ns1:customer/*[local-name()=\'contact-info\' and namespace-uri()=\'http://demo.com/contact-info\']/*[local-name()=\'name\']/text()")).isEqualTo("Krombopulous")')
+ test.contains('assertThat(valueFromXPath(parsedXml, "/ns1:customer/*[local-name()=\'contact-info\' and namespace-uri()=\'http://demo.com/contact-info\']/*[local-name()=\'address\']/addr:gps/*[local-name()=\'lat\']/text()")).isEqualTo("51")')
+ test.contains('assertThat(valueFromXPath(parsedXml, "/ns1:customer/*[local-name()=\'contact-info\' and namespace-uri()=\'http://demo.com/contact-info\']/*[local-name()=\'address\']/addr:gps/addr:lon/text()")).isEqualTo("50")')
+ !test.contains('assertThat(valueFromXPath(parsedXml,"/ns1:customer/*[local-name()=\'contact-info\' and namespace-uri()=\'http://demo.com/contact-info\']/@xmlns")).isEqualTo"')
+ and:
+ SyntaxChecker.tryToCompile(methodBuilderName, test)
+ where:
+ methodBuilderName | methodBuilder
+ "spock" | {
+ properties.testFramework = TestFramework.SPOCK
+ }
+ "testng" | {
+ properties.testFramework = TestFramework.TESTNG
+ }
+ "junit" | {
+ properties.testMode = TestMode.MOCKMVC
+ }
+ "jaxrs-spock" | {
+ properties.testFramework = TestFramework.SPOCK; properties.testMode = TestMode.JAXRSCLIENT
+ }
+ "jaxrs" | {
+ properties.testFramework = TestFramework.JUNIT; properties.testMode = TestMode.JAXRSCLIENT
+ }
+ "webclient" | {
+ properties.testMode = TestMode.WEBTESTCLIENT
+ }
+ }
+
def 'should throw exception for verification by type'() {
given:
Contract contractDsl = Contract.make {
diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverterSpec.groovy
index 22ca5ea8e2..73b579ba2b 100644
--- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverterSpec.groovy
+++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/xml/XmlToXPathsConverterSpec.groovy
@@ -1,94 +1,110 @@
-package org.springframework.cloud.contract.verifier.util.xml
-
-import javax.xml.xpath.XPathExpressionException
-
-import spock.lang.Issue
-import spock.lang.Shared
-import spock.lang.Specification
-import spock.lang.Unroll
-
-import org.springframework.cloud.contract.spec.internal.BodyMatchers
-
-class XmlToXPathsConverterSpec extends Specification {
- @Shared
- String namedXml = '''
- customer@test.com
-
- '''
- @Shared
- String unnamedXml = '''
- customer@test.com
-
- '''
- @Shared
- String attributesInChildXml = '''
- customer@test.com
-
-
- '''
-
- @Unroll
- def "should generate [#expectedValue] for xPath [#value]"() {
- expect:
- value == expectedValue
- where:
- value || expectedValue
- XmlToXPathsConverter.retrieveValueFromBody("/ns1:customer/email/text()", namedXml) || '''customer@test.com'''
- XmlToXPathsConverter.retrieveValueFromBody("/customer/email/text()", namedXml) || ''''''
- XmlToXPathsConverter.retrieveValueFromBody("/customer/email/text()", unnamedXml) || '''customer@test.com'''
- }
-
- @Unroll
- def "should throw exception when searching for in existent name space"() {
- when:
- XmlToXPathsConverter.retrieveValueFromBody("/ns1:customer/email/text()", unnamedXml)
- then:
- def e = thrown(XPathExpressionException)
- e.message.contains('Prefix must resolve to a namespace: ns1')
- }
-
- @Unroll
- def "should generate matched path [#expectedValue] for xPath [#value]"() {
- expect:
- value == expectedValue
- where:
- value || expectedValue
- XmlToXPathsConverter.mapToMatchers(attributesInChildXml).get(0).path() || '''/customer/email/text()'''
- XmlToXPathsConverter.mapToMatchers(attributesInChildXml).get(1).path() || '''/customer/@first_custom_attribute'''
- XmlToXPathsConverter.mapToMatchers(attributesInChildXml).get(2).path() || '''/customer/email/@second_custom_attribute'''
- XmlToXPathsConverter.mapToMatchers(attributesInChildXml).get(3).path() || '''/customer/address/@third_custom_attribute'''
- }
-
- @Unroll
- def "should remove elements to [#expectedValue] for xPath [#value]"() {
- given:
- BodyMatchers m = new BodyMatchers()
- m.xPath(xpath, m.byEquality());
- expect:
- result == XmlToXPathsConverter.removeMatchingXPaths(xml, m)
- where:
- xpath || xml || result
- "/ns1:customer/email/text()" || namedXml || '''\n \n '''
- "/customer/email/text()" || namedXml || '''\n customer@test.com\n '''
- "/customer/email/text()" || unnamedXml || '''\n \n '''
- }
-
- @Issue("#1546")
- def "should remove multiple elements when xpath matches them"() {
- given:
- String test = '''\
-
-
- 123
-
-
- 234
-
-
-'''
- BodyMatchers m = new BodyMatchers()
- m.xPath("/root/*/id/text()", m.byEquality());
- expect:
- XmlToXPathsConverter.removeMatchingXPaths(test, m) == '''\n \n \n \n \n \n \n'''
- }
-}
+package org.springframework.cloud.contract.verifier.util.xml
+
+import javax.xml.xpath.XPathExpressionException
+
+import spock.lang.Issue
+import spock.lang.Shared
+import spock.lang.Specification
+import spock.lang.Unroll
+
+import org.springframework.cloud.contract.spec.internal.BodyMatchers
+
+/**
+ * @author Chris Bono
+ * @since 2.1.0
+ */
+class XmlToXPathsConverterSpec extends Specification {
+ @Shared
+ String namedXml = '''
+ customer@test.com
+
+ '''
+ @Shared
+ String namedDefaultNamespaceXml = '''
+ customer@test.com
+
+ '''
+ @Shared
+ String unnamedXml = '''
+ customer@test.com
+
+ '''
+ @Shared
+ String attributesInChildXml = '''
+ customer@test.com
+
+
+ '''
+
+ @Unroll
+ def "should generate [#expectedValue] for xPath [#value]"() {
+ expect:
+ value == expectedValue
+ where:
+ value || expectedValue
+ XmlToXPathsConverter.retrieveValueFromBody("/customer/email/text()", unnamedXml) || '''customer@test.com'''
+ XmlToXPathsConverter.retrieveValueFromBody("/ns1:customer/email/text()", namedXml) || '''customer@test.com'''
+ XmlToXPathsConverter.retrieveValueFromBody("//email/text()", namedXml) || '''customer@test.com'''
+ XmlToXPathsConverter.retrieveValueFromBody("/customer/email/text()", namedXml) || ''''''
+ XmlToXPathsConverter.retrieveValueFromBody("/*[local-name()='customer' and namespace-uri()='http://demo.com/testns']/*[local-name()='email']/text()", namedDefaultNamespaceXml) || '''customer@test.com'''
+ XmlToXPathsConverter.retrieveValueFromBody("//*[local-name()='email']/text()", namedDefaultNamespaceXml) || '''customer@test.com'''
+ XmlToXPathsConverter.retrieveValueFromBody("/*[local-name()='customer']/*[local-name()='email']/text()", namedDefaultNamespaceXml) || '''customer@test.com'''
+ XmlToXPathsConverter.retrieveValueFromBody("/*[local-name()='customer' and namespace-uri()='http://demo.com/testns']/email/text()", namedDefaultNamespaceXml) || ''''''
+ XmlToXPathsConverter.retrieveValueFromBody("/customer/email/text()", namedDefaultNamespaceXml) || ''''''
+ }
+
+ @Unroll
+ def "should throw exception when searching for in existent name space"() {
+ when:
+ XmlToXPathsConverter.retrieveValueFromBody("/ns1:customer/email/text()", unnamedXml)
+ then:
+ def e = thrown(XPathExpressionException)
+ e.message.contains('Prefix must resolve to a namespace: ns1')
+ }
+
+ @Unroll
+ def "should generate matched path [#expectedValue] for xPath [#value]"() {
+ expect:
+ value == expectedValue
+ where:
+ value || expectedValue
+ XmlToXPathsConverter.mapToMatchers(attributesInChildXml).get(0).path() || '''/customer/email/text()'''
+ XmlToXPathsConverter.mapToMatchers(attributesInChildXml).get(1).path() || '''/customer/@first_custom_attribute'''
+ XmlToXPathsConverter.mapToMatchers(attributesInChildXml).get(2).path() || '''/customer/email/@second_custom_attribute'''
+ XmlToXPathsConverter.mapToMatchers(attributesInChildXml).get(3).path() || '''/customer/address/@third_custom_attribute'''
+ }
+
+ @Unroll
+ def "should remove elements to [#expectedValue] for xPath [#value]"() {
+ given:
+ BodyMatchers m = new BodyMatchers()
+ m.xPath(xpath, m.byEquality())
+ expect:
+ result == XmlToXPathsConverter.removeMatchingXPaths(xml, m)
+ where:
+ xpath || xml || result
+ "/customer/email/text()" || unnamedXml || '''\n \n '''
+ "/customer/email/text()" || namedXml || '''\n customer@test.com\n '''
+ "/ns1:customer/email/text()" || namedXml || '''\n \n '''
+ "/*[local-name()='customer' and namespace-uri()='http://demo.com/testns']/*[local-name()='email']/text()" || namedDefaultNamespaceXml || '''\n \n '''
+ }
+
+ @Issue("#1546")
+ def "should remove multiple elements when xpath matches them"() {
+ given:
+ String test = '''\
+
+
+ 123
+
+
+ 234
+
+
+'''
+ BodyMatchers m = new BodyMatchers()
+ m.xPath("/root/*/id/text()", m.byEquality())
+ expect:
+ XmlToXPathsConverter.removeMatchingXPaths(test, m) == '''\n \n \n \n \n \n \n'''
+ }
+}
diff --git a/spring-cloud-contract-verifier/src/test/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtilTest.java b/spring-cloud-contract-verifier/src/test/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtilTest.java
index 617d807a6f..70f454a1fc 100644
--- a/spring-cloud-contract-verifier/src/test/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtilTest.java
+++ b/spring-cloud-contract-verifier/src/test/java/org/springframework/cloud/contract/verifier/util/ContractVerifierUtilTest.java
@@ -31,21 +31,23 @@ import org.xml.sax.SAXException;
import static org.assertj.core.api.Assertions.assertThat;
+/**
+ * @author Chris Bono
+ * @since 2.1.0
+ */
public class ContractVerifierUtilTest {
- private final String unnamedXml = "\r\n" + " customer@test.com\r\n"
- + " ";
+ private final String unnamedXml = "\n" + "customer@test.com\n" + "";
- private final String namedXml = "\r\n"
- + " customer@test.com\r\n" + " ";
+ private final String namedComplexXml = "\n"
+ + "customer@test.com\n" + "\n"
+ + "Krombopulous" + "" + "" + "51" + "50"
+ + "" + "" + "\n" + "";
@Test
- public void shouldGetValueFromXPath() throws ParserConfigurationException, IOException, SAXException {
+ public void shouldGetValueFromXPath() {
// Given
- DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
- builderFactory.setNamespaceAware(true);
- DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
- Document parsedXml = documentBuilder.parse(new InputSource(new StringReader(unnamedXml)));
+ Document parsedXml = parsedXml(unnamedXml);
// When
String value = ContractVerifierUtil.valueFromXPath(parsedXml, "/customer/email/text()");
// Then
@@ -53,38 +55,17 @@ public class ContractVerifierUtilTest {
}
@Test(expected = IllegalArgumentException.class)
- public void shouldThrowExceptionOnIllegalValueFromXPath()
- throws ParserConfigurationException, IOException, SAXException {
+ public void shouldThrowExceptionOnIllegalValueFromXPath() {
// Given
- DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
- builderFactory.setNamespaceAware(true);
- DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
- Document parsedXml = documentBuilder.parse(new InputSource(new StringReader(unnamedXml)));
+ Document parsedXml = parsedXml(unnamedXml);
// When
ContractVerifierUtil.valueFromXPath(parsedXml, "/ns1:customer/email/text()");
}
@Test
- public void shouldGetValueFromXPathWithNamespace() throws ParserConfigurationException, IOException, SAXException {
+ public void shouldGetEmptyValueFromXPathWithNamespace() {
// Given
- DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
- builderFactory.setNamespaceAware(true);
- DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
- Document parsedXml = documentBuilder.parse(new InputSource(new StringReader(namedXml)));
- // When
- String value = ContractVerifierUtil.valueFromXPath(parsedXml, "/ns1:customer/email/text()");
- // Then
- assertThat(value).isEqualTo("customer@test.com");
- }
-
- @Test
- public void shouldGetEmptyValueFromXPathWithNamespace()
- throws ParserConfigurationException, IOException, SAXException {
- // Given
- DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
- builderFactory.setNamespaceAware(true);
- DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
- Document parsedXml = documentBuilder.parse(new InputSource(new StringReader(namedXml)));
+ Document parsedXml = parsedXml(namedComplexXml);
// When
String value = ContractVerifierUtil.valueFromXPath(parsedXml, "/customer/email/text()");
// Then
@@ -92,12 +73,83 @@ public class ContractVerifierUtilTest {
}
@Test
- public void shouldGetNodeFromXPath() throws ParserConfigurationException, IOException, SAXException {
+ public void shouldGetValueFromXPathWithNamespace() {
// Given
- DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
- builderFactory.setNamespaceAware(true);
- DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
- Document parsedXml = documentBuilder.parse(new InputSource(new StringReader(unnamedXml)));
+ Document parsedXml = parsedXml(namedComplexXml);
+ // When
+ String value = ContractVerifierUtil.valueFromXPath(parsedXml, "/ns1:customer/email/text()");
+ // Then
+ assertThat(value).isEqualTo("customer@test.com");
+ }
+
+ @Test
+ public void shouldGetNamespaceValueFromXPathWithNamespace() {
+ // Given
+ Document parsedXml = parsedXml(namedComplexXml);
+ // When
+ String value = ContractVerifierUtil.valueFromXPath(parsedXml, "/ns1:customer/namespace::ns1");
+ // Then
+ assertThat(value).isEqualTo("http://demo.com/customer");
+ }
+
+ @Test
+ public void shouldGetValueFromXPathWithDefaultNamespace() {
+ // Given
+ Document parsedXml = parsedXml(namedComplexXml);
+ // When
+ String value = ContractVerifierUtil.valueFromXPath(parsedXml,
+ "/ns1:customer/*[local-name()='contact-info' and namespace-uri()='http://demo.com/contact-info']/*[local-name()='name']/text()");
+ // Then
+ assertThat(value).isEqualTo("Krombopulous");
+ }
+
+ @Test
+ public void shouldGetValueFromXPathWithNestedNamespaces1() {
+ // Given
+ Document parsedXml = parsedXml(namedComplexXml);
+ // When
+ String value = ContractVerifierUtil.valueFromXPath(parsedXml,
+ "/ns1:customer/*[local-name()='contact-info' and namespace-uri()='http://demo.com/contact-info']/*[local-name()='address']/addr:gps/*[local-name()='lat']/text()");
+ // Then
+ assertThat(value).isEqualTo("51");
+ }
+
+ @Test
+ public void shouldGetValueFromXPathWithNestedNamespaces2() {
+ // Given
+ Document parsedXml = parsedXml(namedComplexXml);
+ // When
+ String value = ContractVerifierUtil.valueFromXPath(parsedXml,
+ "/ns1:customer/*[local-name()='contact-info' and namespace-uri()='http://demo.com/contact-info']/*[local-name()='address']/addr:gps/addr:lon/text()");
+ // Then
+ assertThat(value).isEqualTo("50");
+ }
+
+ @Test
+ public void shouldGetEmptyValueFromXPathWithDefaultNamespace() {
+ // Given
+ Document parsedXml = parsedXml(namedComplexXml);
+ // When
+ String value = ContractVerifierUtil.valueFromXPath(parsedXml, "/ns1:customer/contact-info/name/text()");
+ // Then
+ assertThat(value).isEqualTo("");
+ }
+
+ @Test
+ public void shouldGetEmptyValueFromXPathWithNestedDefaultNamespace() {
+ // Given
+ Document parsedXml = parsedXml(namedComplexXml);
+ // When
+ String value = ContractVerifierUtil.valueFromXPath(parsedXml,
+ "/ns1:customer/*[local-name()='contact-info' and namespace-uri()='http://demo.com/contact-info']/name/text()");
+ // Then
+ assertThat(value).isEqualTo("");
+ }
+
+ @Test
+ public void shouldGetNodeFromXPath() {
+ // Given
+ Document parsedXml = parsedXml(unnamedXml);
// When
Node node = ContractVerifierUtil.nodeFromXPath(parsedXml, "/customer/email/text()");
// Then
@@ -105,24 +157,17 @@ public class ContractVerifierUtilTest {
}
@Test(expected = IllegalArgumentException.class)
- public void shouldThrowExceptionOnIllegalNodeFromXPath()
- throws ParserConfigurationException, IOException, SAXException {
+ public void shouldThrowExceptionOnIllegalNodeFromXPath() {
// Given
- DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
- builderFactory.setNamespaceAware(true);
- DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
- Document parsedXml = documentBuilder.parse(new InputSource(new StringReader(unnamedXml)));
+ Document parsedXml = parsedXml(unnamedXml);
// When
ContractVerifierUtil.nodeFromXPath(parsedXml, "/ns1:customer/email/text()");
}
@Test
- public void shouldGetNodeFromXPathWithNamespace() throws ParserConfigurationException, IOException, SAXException {
+ public void shouldGetNodeFromXPathWithNamespace() {
// Given
- DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
- builderFactory.setNamespaceAware(true);
- DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
- Document parsedXml = documentBuilder.parse(new InputSource(new StringReader(namedXml)));
+ Document parsedXml = parsedXml(namedComplexXml);
// When
Node node = ContractVerifierUtil.nodeFromXPath(parsedXml, "/ns1:customer/email/text()");
// Then
@@ -130,13 +175,19 @@ public class ContractVerifierUtilTest {
}
@Test
- public void shouldGetEmptyNodeFromXPathWithNamespace()
- throws ParserConfigurationException, IOException, SAXException {
+ public void shouldGetNamespaceNodeFromXPathWithNamespace() {
// Given
- DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
- builderFactory.setNamespaceAware(true);
- DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
- Document parsedXml = documentBuilder.parse(new InputSource(new StringReader(namedXml)));
+ Document parsedXml = parsedXml(namedComplexXml);
+ // When
+ Node node = ContractVerifierUtil.nodeFromXPath(parsedXml, "/ns1:customer/namespace::ns1");
+ // Then
+ assertThat(node.getTextContent()).isEqualTo("http://demo.com/customer");
+ }
+
+ @Test
+ public void shouldGetEmptyNodeFromXPathWithNamespace() {
+ // Given
+ Document parsedXml = parsedXml(namedComplexXml);
// When
Node node = ContractVerifierUtil.nodeFromXPath(parsedXml, "/customer/email/text()");
// Then
@@ -144,11 +195,23 @@ public class ContractVerifierUtilTest {
}
@Test
- public void should_set_path_without_prefix_and_with_suffix() {
+ public void shouldSetPathWithoutPrefixAndWithSuffix() {
assertThat(ContractVerifierUtil.fromRelativePath("validate_foo()")).isEqualTo("foo.yml");
assertThat(ContractVerifierUtil.fromRelativePath("validate_foo")).isEqualTo("foo.yml");
assertThat(ContractVerifierUtil.fromRelativePath("foo")).isEqualTo("foo.yml");
assertThat(ContractVerifierUtil.fromRelativePath("foo.yml")).isEqualTo("foo.yml");
}
+ private Document parsedXml(String inputXml) {
+ DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
+ builderFactory.setNamespaceAware(true);
+ try {
+ DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
+ return documentBuilder.parse(new InputSource(new StringReader(inputXml)));
+ }
+ catch (ParserConfigurationException | IOException | SAXException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
}