Adds support for unqualified namespaced XML content (#1590)
Adds support for unqualified namespaced XML content and fixes how qualified namespace URIs values are retrieved. Fixes gh-1571
This commit is contained in:
@@ -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]
|
||||
----
|
||||
<ns1:customer xmlns:ns1="http://demo.com/customer">
|
||||
<email>customer@test.com</email>
|
||||
</ns1:customer>
|
||||
----
|
||||
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 xmlns="http://demo.com/customer">
|
||||
<email>customer@test.com</email>
|
||||
</customer>
|
||||
----
|
||||
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-name>
|
||||
```
|
||||
- Node using and defining an unqualified namespace:
|
||||
```
|
||||
/*[local-name=()='<node-name>' and namespace-uri=()='<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=()='<node-name>']
|
||||
```
|
||||
|
||||
|
||||
[[contract-dsl-multiple]]
|
||||
=== Multiple Contracts in One File
|
||||
|
||||
|
||||
@@ -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<BodyMatcher> 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<List<Node>> valueNodes = getValueNodesWithParents(parsedXml)
|
||||
List<BodyMatcher> matchers = []
|
||||
List<NodePath> 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<NodePath> transformListEntries(List<List<Node>> nodeLists) {
|
||||
List<PathOccurrenceCounter> pathOccurrenceCounters = []
|
||||
List<NodePath> nodePaths = []
|
||||
@@ -189,9 +193,40 @@ class XmlToXPathsConverter {
|
||||
}
|
||||
|
||||
private static XmlVerifiable processNode(XmlVerifiable xmlVerifiable, Node node) {
|
||||
// If node has explicit namespace (eg '<prefix:name>') no special processing needed
|
||||
if (nodeUsesExplicitNamespace(node)) {
|
||||
return xmlVerifiable.node(node.nodeName)
|
||||
}
|
||||
// If node directly declares default ns (eg. 'xmlns=<namespace_uri>') 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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 """
|
||||
<ns1:test xmlns:ns1="http://demo.com/testns">
|
||||
<ns1:header>
|
||||
<duck-bucket type='bigbucket'>
|
||||
<duck>duck5150</duck>
|
||||
</duck-bucket>
|
||||
</ns1:header>
|
||||
</ns1:test>
|
||||
"""
|
||||
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 """
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<SOAP-ENV:Header>
|
||||
<RsHeader xmlns="http://schemas.xmlsoap.org/soap/custom">
|
||||
<MsgSeqId>1234</MsgSeqId>
|
||||
</RsHeader>
|
||||
</SOAP-ENV:Header>
|
||||
</SOAP-ENV:Envelope>
|
||||
"""
|
||||
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 """
|
||||
<ns1:customer xmlns:ns1="http://demo.com/customer" xmlns:addr="http://demo.com/address">
|
||||
<email>customer@test.com</email>
|
||||
<contact-info xmlns="http://demo.com/contact-info">
|
||||
<name>Krombopulous</name>
|
||||
<address>
|
||||
<addr:gps>
|
||||
<lat>51</lat>
|
||||
<addr:lon>50</addr:lon>
|
||||
</addr:gps>
|
||||
</address>
|
||||
</contact-info>
|
||||
</ns1:customer>
|
||||
"""
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
|
||||
@@ -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 = '''<ns1:customer xmlns:ns1="http://demo.com/testns">
|
||||
<email>customer@test.com</email>
|
||||
</ns1:customer>
|
||||
'''
|
||||
@Shared
|
||||
String unnamedXml = '''<customer>
|
||||
<email>customer@test.com</email>
|
||||
</customer>
|
||||
'''
|
||||
@Shared
|
||||
String attributesInChildXml = '''<customer first_custom_attribute="first_value">
|
||||
<email second_custom_attribute="second_value" >customer@test.com</email>
|
||||
<address third_custom_attribute="third_value"/>
|
||||
</customer>
|
||||
'''
|
||||
|
||||
@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 || '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><ns1:customer xmlns:ns1="http://demo.com/testns">\n <email/>\n </ns1:customer>'''
|
||||
"/customer/email/text()" || namedXml || '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><ns1:customer xmlns:ns1="http://demo.com/testns">\n <email>customer@test.com</email>\n </ns1:customer>'''
|
||||
"/customer/email/text()" || unnamedXml || '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><customer>\n <email/>\n </customer>'''
|
||||
}
|
||||
|
||||
@Issue("#1546")
|
||||
def "should remove multiple elements when xpath matches them"() {
|
||||
given:
|
||||
String test = '''\
|
||||
<root>
|
||||
<childOne>
|
||||
<id>123</id>
|
||||
</childOne>
|
||||
<childTwo>
|
||||
<id>234</id>
|
||||
</childTwo>
|
||||
</root>
|
||||
'''
|
||||
BodyMatchers m = new BodyMatchers()
|
||||
m.xPath("/root/*/id/text()", m.byEquality());
|
||||
expect:
|
||||
XmlToXPathsConverter.removeMatchingXPaths(test, m) == '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><root>\n <childOne>\n <id/>\n </childOne>\n <childTwo>\n <id/>\n </childTwo>\n</root>'''
|
||||
}
|
||||
}
|
||||
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 = '''<ns1:customer xmlns:ns1="http://demo.com/testns">
|
||||
<email>customer@test.com</email>
|
||||
</ns1:customer>
|
||||
'''
|
||||
@Shared
|
||||
String namedDefaultNamespaceXml = '''<customer xmlns="http://demo.com/testns">
|
||||
<email>customer@test.com</email>
|
||||
</customer>
|
||||
'''
|
||||
@Shared
|
||||
String unnamedXml = '''<customer>
|
||||
<email>customer@test.com</email>
|
||||
</customer>
|
||||
'''
|
||||
@Shared
|
||||
String attributesInChildXml = '''<customer first_custom_attribute="first_value">
|
||||
<email second_custom_attribute="second_value" >customer@test.com</email>
|
||||
<address third_custom_attribute="third_value"/>
|
||||
</customer>
|
||||
'''
|
||||
|
||||
@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 || '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><customer>\n <email/>\n </customer>'''
|
||||
"/customer/email/text()" || namedXml || '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><ns1:customer xmlns:ns1="http://demo.com/testns">\n <email>customer@test.com</email>\n </ns1:customer>'''
|
||||
"/ns1:customer/email/text()" || namedXml || '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><ns1:customer xmlns:ns1="http://demo.com/testns">\n <email/>\n </ns1:customer>'''
|
||||
"/*[local-name()='customer' and namespace-uri()='http://demo.com/testns']/*[local-name()='email']/text()" || namedDefaultNamespaceXml || '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><customer xmlns="http://demo.com/testns">\n <email/>\n </customer>'''
|
||||
}
|
||||
|
||||
@Issue("#1546")
|
||||
def "should remove multiple elements when xpath matches them"() {
|
||||
given:
|
||||
String test = '''\
|
||||
<root>
|
||||
<childOne>
|
||||
<id>123</id>
|
||||
</childOne>
|
||||
<childTwo>
|
||||
<id>234</id>
|
||||
</childTwo>
|
||||
</root>
|
||||
'''
|
||||
BodyMatchers m = new BodyMatchers()
|
||||
m.xPath("/root/*/id/text()", m.byEquality())
|
||||
expect:
|
||||
XmlToXPathsConverter.removeMatchingXPaths(test, m) == '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><root>\n <childOne>\n <id/>\n </childOne>\n <childTwo>\n <id/>\n </childTwo>\n</root>'''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = "<customer>\r\n" + " <email>customer@test.com</email>\r\n"
|
||||
+ " </customer>";
|
||||
private final String unnamedXml = "<customer>\n" + "<email>customer@test.com</email>\n" + "</customer>";
|
||||
|
||||
private final String namedXml = "<ns1:customer xmlns:ns1=\"http://demo.com/testns\">\r\n"
|
||||
+ " <email>customer@test.com</email>\r\n" + " </ns1:customer>";
|
||||
private final String namedComplexXml = "<ns1:customer xmlns:ns1=\"http://demo.com/customer\" xmlns:addr=\"http://demo.com/address\">\n"
|
||||
+ "<email>customer@test.com</email>\n" + "<contact-info xmlns=\"http://demo.com/contact-info\">\n"
|
||||
+ "<name>Krombopulous</name>" + "<address>" + "<addr:gps>" + "<lat>51</lat>" + "<addr:lon>50</addr:lon>"
|
||||
+ "</addr:gps>" + "</address>" + "</contact-info>\n" + "</ns1:customer>";
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user