From 4b7e85cec1637dd9ef287c558bfaa2ad69c32331 Mon Sep 17 00:00:00 2001 From: bono007 Date: Fri, 8 Jan 2021 08:06:41 -0600 Subject: [PATCH] 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 --- .../asciidoc/_project-features-contract.adoc | 48 ++++ .../util/xml/XmlToXPathsConverter.groovy | 51 ++++- .../util/xml/ArrayValueAssertion.java | 6 + .../verifier/util/xml/XmlAsserter.java | 22 +- .../verifier/util/xml/XmlVerifiable.java | 11 + .../builder/XmlMethodBodyBuilderSpec.groovy | 209 ++++++++++++++++++ .../util/xml/XmlToXPathsConverterSpec.groovy | 204 +++++++++-------- .../util/ContractVerifierUtilTest.java | 177 ++++++++++----- 8 files changed, 568 insertions(+), 160 deletions(-) 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); + } + } + }