bug: handle xml with namespaces for validations #1427 (#1446)

fixes gh-1446
This commit is contained in:
Javier Alejandro Miño
2020-07-28 06:40:30 -05:00
committed by Marcin Grzejszczak
parent 242c8208a9
commit 90be2f0b50
9 changed files with 358 additions and 13 deletions

View File

@@ -45,7 +45,9 @@ class XmlBodyVerificationBuilder implements BodyMethodGeneration {
private void addXmlProcessingLines(final BlockBuilder blockBuilder,
String responseString) {
Arrays.asList(
"DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()",
"DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance()",
"builderFactory.setNamespaceAware(true)",
"DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder()",
"Document parsedXml = documentBuilder.parse(new InputSource(new StringReader("
+ responseString + ")))")
.forEach(it -> {

View File

@@ -16,9 +16,6 @@
package org.springframework.cloud.contract.verifier.util.xml
import com.sun.org.apache.xml.internal.security.utils.DOMNamespaceContext
import javax.xml.namespace.NamespaceContext
import java.util.stream.IntStream
import javax.xml.parsers.DocumentBuilder
@@ -64,10 +61,13 @@ class XmlToXPathsConverter {
static Object removeMatchingXPaths(Object body, BodyMatchers bodyMatchers) {
XPath xPath = XPathFactory.newInstance().newXPath()
DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance()
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance()
builderFactory.setNamespaceAware(true)
DocumentBuilder documentBuilder = builderFactory
.newDocumentBuilder()
Document parsedXml = documentBuilder
.parse(new InputSource(new StringReader(body as String)))
xPath.setNamespaceContext(new DOMNamespaceContext(parsedXml.documentElement))
bodyMatchers?.matchers()?.each({
Node node = xPath.evaluate(it.path(), parsedXml.documentElement, NODE) as Node
removeNode(node)

View File

@@ -35,8 +35,11 @@ import javax.xml.xpath.XPathFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.springframework.cloud.contract.verifier.util.xml.DOMNamespaceContext;
/**
* Helper class for the generated tests.
*
@@ -81,8 +84,10 @@ public final class ContractVerifierUtil {
*/
public static String valueFromXPath(Document parsedXml, String path) {
XPath xPath = XPathFactory.newInstance().newXPath();
Element documentElement = parsedXml.getDocumentElement();
xPath.setNamespaceContext(new DOMNamespaceContext(documentElement));
try {
return xPath.evaluate(path, parsedXml.getDocumentElement());
return xPath.evaluate(path, documentElement);
}
catch (XPathExpressionException exception) {
LOG.error("Incorrect xpath provided: " + path, exception);
@@ -99,9 +104,10 @@ public final class ContractVerifierUtil {
*/
public static Node nodeFromXPath(Document parsedXml, String path) {
XPath xPath = XPathFactory.newInstance().newXPath();
Element documentElement = parsedXml.getDocumentElement();
xPath.setNamespaceContext(new DOMNamespaceContext(documentElement));
try {
return (Node) xPath.evaluate(path, parsedXml.getDocumentElement(),
XPathConstants.NODE);
return (Node) xPath.evaluate(path, documentElement, XPathConstants.NODE);
}
catch (XPathExpressionException exception) {
LOG.error("Incorrect xpath provided: " + path, exception);

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.util.xml;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.xml.namespace.NamespaceContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
public class DOMNamespaceContext implements NamespaceContext {
private final Map<String, String> namespaceMap = new HashMap<>();
public DOMNamespaceContext(Node contextNode) {
addNamespaces(contextNode);
}
public String getNamespaceURI(String arg0) {
return namespaceMap.get(arg0);
}
public String getPrefix(String arg0) {
for (Map.Entry<String, String> entry : namespaceMap.entrySet()) {
if (entry.getValue().equals(arg0)) {
return entry.getKey();
}
}
return null;
}
public Iterator<String> getPrefixes(String arg0) {
return namespaceMap.keySet().iterator();
}
private void addNamespaces(Node element) {
if (element.getParentNode() != null) {
addNamespaces(element.getParentNode());
}
if (element instanceof Element) {
Element el = (Element) element;
NamedNodeMap map = el.getAttributes();
for (int x = 0; x < map.getLength(); x++) {
Attr attr = (Attr) map.item(x);
if ("xmlns".equals(attr.getPrefix())) {
namespaceMap.put(attr.getLocalName(), attr.getValue());
}
}
}
}
}

View File

@@ -89,6 +89,9 @@ class YamlContractConverterSpec extends Specification {
URL ymlRestXmlFile = YamlContractConverterSpec.
getResource("/yml/contract_rest_xml.yml")
File ymlRestXml = new File(ymlRestXmlFile.toURI())
URL ymlRestNamedXmlFile = YamlContractConverterSpec.
getResource("/yml/contract_rest_named_xml.yml")
File ymlRestNamedXml = new File(ymlRestNamedXmlFile.toURI())
URL oa3SpecUrl = YamlContractConverterSpec.getResource('/yml/oa3/openapi_petstore.yml')
File oa3File = new File(oa3SpecUrl.toURI())
YamlContractConverter converter = new YamlContractConverter()
@@ -110,6 +113,11 @@ class YamlContractConverterSpec extends Specification {
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>
'''
String xmlContractBodyWithNamespaces = '''
<ns1:customer xmlns:ns1="http://demo.com/testns">
<email>customer@test.com</email>
</ns1:customer>
'''
def "should convert YAML with Cookies to DSL"() {
@@ -1331,6 +1339,34 @@ metadata: null
.replaceAll("\n", "").replaceAll(' ', '')
}
def "should convert REST YAML with XML with namespace in request and response to DSL"() {
given:
assert converter.isAccepted(ymlRestNamedXml)
when:
Collection<Contract> contracts = converter.convertFrom(ymlRestNamedXml)
then:
contracts.size() == 1
Contract contract = contracts.first()
contract.request.headers.entries.find({
it.name == 'Content-Type' && it.clientValue == "application/xml" && it.serverValue == "application/xml"
})
contract.request.bodyMatchers.matchers.isEmpty()
contract.request.body.clientValue.replaceAll("\n", "").
replaceAll(' ', '') == xmlContractBodyWithNamespaces.replaceAll("\n", "").
replaceAll(' ', '')
contract.request.body.serverValue.replaceAll("\n", "").
replaceAll(' ', '') == xmlContractBodyWithNamespaces.replaceAll("\n", "").
replaceAll(' ', '')
and:
contract.response.bodyMatchers.matchers.isEmpty()
contract.response.body.clientValue.replaceAll("\n", "")
.replaceAll(' ', '') == xmlContractBodyWithNamespaces
.replaceAll("\n", "").replaceAll(' ', '')
contract.response.body.serverValue.replaceAll("\n", "")
.replaceAll(' ', '') == xmlContractBodyWithNamespaces
.replaceAll("\n", "").replaceAll(' ', '')
}
def "should accept a yaml file that is a proper scc YAML contract"() {
when:
def accepted = converter.isAccepted(ymlWithRest3)

View File

@@ -1,5 +1,6 @@
package org.springframework.cloud.contract.verifier.util.xml
import org.springframework.cloud.contract.spec.internal.BodyMatchers
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
@@ -23,18 +24,32 @@ class XmlToXPathsConverterSpec extends Specification {
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'''
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 inexistent name space"() {
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 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>'''
}
}

View File

@@ -0,0 +1,34 @@
package org.springframework.cloud.contract.verifier.builder;
import org.junit.Test;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
import java.util.Optional;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThat;
public class XmlBodyVerificationBuilderTest {
private static final String xml = "<customer>\r\n"
+ " <email>customer@test.com</email>\r\n" + " </customer>";
@Test
public void shouldAddXmlProcessingLines() {
// Given
XmlBodyVerificationBuilder builder = new XmlBodyVerificationBuilder(
new Contract(), Optional.of(";"));
BlockBuilder blockBuilder = new BlockBuilder(" ");
BodyMatchers matchers = new BodyMatchers();
// When
builder.addXmlResponseBodyCheck(blockBuilder, xml, matchers, xml, true);
// Then
String test = blockBuilder.toString();
assertThat(test).contains("DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();")
.contains("builderFactory.setNamespaceAware(true);")
.contains("DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();")
.contains("Document parsedXml = documentBuilder.parse(new InputSource(new StringReader(")
.contains(xml);
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2020-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.util;
import java.io.IOException;
import java.io.StringReader;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import static org.assertj.core.api.Assertions.assertThat;
public class ContractVerifierUtilTest {
private final String unnamedXml = "<customer>\r\n"
+ " <email>customer@test.com</email>\r\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>";
@Test
public void shouldGetValueFromXPath()
throws ParserConfigurationException, IOException, SAXException {
// Given
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setNamespaceAware(true);
DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
Document parsedXml = documentBuilder
.parse(new InputSource(new StringReader(unnamedXml)));
// When
String value = ContractVerifierUtil.valueFromXPath(parsedXml,
"/customer/email/text()");
// Then
assertThat(value).isEqualTo("customer@test.com");
}
@Test(expected = IllegalArgumentException.class)
public void shouldThrowExceptionOnIllegalValueFromXPath()
throws ParserConfigurationException, IOException, SAXException {
// Given
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setNamespaceAware(true);
DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
Document parsedXml = documentBuilder
.parse(new InputSource(new StringReader(unnamedXml)));
// When
ContractVerifierUtil.valueFromXPath(parsedXml, "/ns1:customer/email/text()");
}
@Test
public void shouldGetValueFromXPathWithNamespace()
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)));
// 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)));
// When
String value = ContractVerifierUtil.valueFromXPath(parsedXml,
"/customer/email/text()");
// Then
assertThat(value).isEqualTo("");
}
@Test
public void shouldGetNodeFromXPath()
throws ParserConfigurationException, IOException, SAXException {
// Given
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setNamespaceAware(true);
DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
Document parsedXml = documentBuilder
.parse(new InputSource(new StringReader(unnamedXml)));
// When
Node node = ContractVerifierUtil.nodeFromXPath(parsedXml,
"/customer/email/text()");
// Then
assertThat(node.getTextContent()).isEqualTo("customer@test.com");
}
@Test(expected = IllegalArgumentException.class)
public void shouldThrowExceptionOnIllegalNodeFromXPath()
throws ParserConfigurationException, IOException, SAXException {
// Given
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setNamespaceAware(true);
DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
Document parsedXml = documentBuilder
.parse(new InputSource(new StringReader(unnamedXml)));
// When
ContractVerifierUtil.nodeFromXPath(parsedXml, "/ns1:customer/email/text()");
}
@Test
public void shouldGetNodeFromXPathWithNamespace()
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)));
// When
Node node = ContractVerifierUtil.nodeFromXPath(parsedXml,
"/ns1:customer/email/text()");
// Then
assertThat(node.getTextContent()).isEqualTo("customer@test.com");
}
@Test
public void shouldGetEmptyNodeFromXPathWithNamespace()
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)));
// When
Node node = ContractVerifierUtil.nodeFromXPath(parsedXml,
"/customer/email/text()");
// Then
assertThat(node).isNull();
}
}

View File

@@ -0,0 +1,17 @@
request:
method: GET
url: /getymlResponse
headers:
Content-Type: application/xml
body: |
<ns1:customer xmlns:ns1="http://demo.com/testns">
<email>customer@test.com</email>
</ns1:customer>
response:
status: 200
headers:
Content-Type: application/xml
body: |
<ns1:customer xmlns:ns1="http://demo.com/testns">
<email>customer@test.com</email>
</ns1:customer>