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:
bono007
2021-01-08 08:06:41 -06:00
committed by GitHub
parent 119d9bdc48
commit 4b7e85cec1
8 changed files with 568 additions and 160 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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