Skip to content

Commit 44bd33b

Browse files
author
Ronald Holshausen
committedApr 10, 2020
feat: Prototype of a XML DSL for consumer tests #243
1 parent aead5fa commit 44bd33b

File tree

11 files changed

+504
-0
lines changed

11 files changed

+504
-0
lines changed
 

‎consumer/pact-jvm-consumer-junit/build.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ dependencies {
3434

3535
testCompile 'org.clojure:clojure:1.10.0',
3636
'http-kit:http-kit:2.3.0'
37+
testCompile 'javax.xml.bind:jaxb-api:2.3.0'
38+
testCompile 'javax.activation:activation:1.1'
39+
testCompile 'org.glassfish.jaxb:jaxb-runtime:2.3.0'
3740
}
3841

3942
clojureTest {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package au.com.dius.pact.consumer.junit.xml;
2+
3+
import javax.xml.bind.annotation.XmlAccessType;
4+
import javax.xml.bind.annotation.XmlAccessorType;
5+
import javax.xml.bind.annotation.XmlAttribute;
6+
import javax.xml.bind.annotation.XmlElement;
7+
import javax.xml.bind.annotation.XmlRootElement;
8+
9+
@XmlRootElement(name = "project")
10+
@XmlAccessorType(XmlAccessType.FIELD)
11+
public class Project {
12+
@XmlAttribute(name = "id")
13+
private int id;
14+
@XmlAttribute(name = "type")
15+
private String type;
16+
@XmlAttribute(name = "name")
17+
private String name;
18+
@XmlAttribute(name = "due")
19+
private String due;
20+
21+
@XmlElement(name = "tasks")
22+
private Tasks tasks;
23+
24+
public int getId() {
25+
return id;
26+
}
27+
28+
public void setId(int id) {
29+
this.id = id;
30+
}
31+
32+
public String getType() {
33+
return type;
34+
}
35+
36+
public void setType(String type) {
37+
this.type = type;
38+
}
39+
40+
public String getName() {
41+
return name;
42+
}
43+
44+
public void setName(String name) {
45+
this.name = name;
46+
}
47+
48+
public String getDue() {
49+
return due;
50+
}
51+
52+
public void setDue(String due) {
53+
this.due = due;
54+
}
55+
56+
public Tasks getTasks() {
57+
return tasks;
58+
}
59+
60+
public void setTasks(Tasks tasks) {
61+
this.tasks = tasks;
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package au.com.dius.pact.consumer.junit.xml;
2+
3+
import javax.xml.bind.annotation.XmlAccessType;
4+
import javax.xml.bind.annotation.XmlAccessorType;
5+
import javax.xml.bind.annotation.XmlAttribute;
6+
import javax.xml.bind.annotation.XmlElement;
7+
import javax.xml.bind.annotation.XmlRootElement;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
11+
@XmlRootElement(name = "projects", namespace = "http://some.namespace/and/more/stuff")
12+
@XmlAccessorType(XmlAccessType.FIELD)
13+
public class Projects {
14+
@XmlElement(name = "project", type = Project.class)
15+
private List<Project> projects = new ArrayList<>();
16+
17+
@XmlAttribute(name = "id")
18+
private String id;
19+
20+
public List<Project> getProjects() {
21+
return projects;
22+
}
23+
24+
public void setProjects(List<Project> projects) {
25+
this.projects = projects;
26+
}
27+
28+
public String getId() {
29+
return id;
30+
}
31+
32+
public void setId(String id) {
33+
this.id = id;
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package au.com.dius.pact.consumer.junit.xml;
2+
3+
import javax.xml.bind.annotation.XmlAccessType;
4+
import javax.xml.bind.annotation.XmlAccessorType;
5+
import javax.xml.bind.annotation.XmlAttribute;
6+
import javax.xml.bind.annotation.XmlRootElement;
7+
8+
@XmlRootElement(name = "task")
9+
@XmlAccessorType(XmlAccessType.FIELD)
10+
public class Task {
11+
@XmlAttribute(name = "id")
12+
private int id;
13+
@XmlAttribute(name = "name")
14+
private String name;
15+
@XmlAttribute(name = "done")
16+
private Boolean done;
17+
18+
public int getId() {
19+
return id;
20+
}
21+
22+
public void setId(int id) {
23+
this.id = id;
24+
}
25+
26+
public String getName() {
27+
return name;
28+
}
29+
30+
public void setName(String name) {
31+
this.name = name;
32+
}
33+
34+
public Boolean getDone() {
35+
return done;
36+
}
37+
38+
public void setDone(Boolean done) {
39+
this.done = done;
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package au.com.dius.pact.consumer.junit.xml;
2+
3+
import javax.xml.bind.annotation.XmlAccessType;
4+
import javax.xml.bind.annotation.XmlAccessorType;
5+
import javax.xml.bind.annotation.XmlElement;
6+
import javax.xml.bind.annotation.XmlRootElement;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
10+
@XmlRootElement(name = "tasks")
11+
@XmlAccessorType(XmlAccessType.FIELD)
12+
public class Tasks {
13+
@XmlElement(name = "task", type = Task.class)
14+
private List<Task> tasks = new ArrayList<>();
15+
16+
public List<Task> getTasks() {
17+
return tasks;
18+
}
19+
20+
public void setTasks(List<Task> tasks) {
21+
this.tasks = tasks;
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package au.com.dius.pact.consumer.junit.xml;
2+
3+
import org.apache.http.client.fluent.Request;
4+
5+
import javax.xml.bind.JAXBContext;
6+
import javax.xml.bind.JAXBException;
7+
import javax.xml.bind.Unmarshaller;
8+
import java.io.IOException;
9+
10+
public class TodoApp {
11+
private String url;
12+
13+
public TodoApp setUrl(String url) {
14+
this.url = url;
15+
return this;
16+
}
17+
18+
public Projects getProjects(String format) throws IOException {
19+
String contentType = "application/json";
20+
if (format.equalsIgnoreCase("xml")) {
21+
contentType = "application/xml";
22+
}
23+
return (Projects) Request.Get(this.url + "/projects?from=today")
24+
.addHeader("Accept", contentType)
25+
.execute().handleResponse(httpResponse -> {
26+
try {
27+
JAXBContext jaxbContext = JAXBContext.newInstance(Projects.class);
28+
Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
29+
return jaxbUnmarshaller.unmarshal(httpResponse.getEntity().getContent());
30+
} catch (JAXBException e) {
31+
throw new IOException(e);
32+
}
33+
});
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package au.com.dius.pact.consumer.junit.xml;
2+
3+
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
4+
import au.com.dius.pact.consumer.junit.PactProviderRule;
5+
import au.com.dius.pact.consumer.junit.PactVerification;
6+
import au.com.dius.pact.consumer.xml.PactXmlBuilder;
7+
import au.com.dius.pact.core.model.RequestResponsePact;
8+
import au.com.dius.pact.core.model.annotations.Pact;
9+
import org.apache.commons.collections4.MapUtils;
10+
import org.junit.Rule;
11+
import org.junit.Test;
12+
13+
import java.io.IOException;
14+
import java.util.Collections;
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
18+
import static au.com.dius.pact.consumer.dsl.Matchers.bool;
19+
import static au.com.dius.pact.consumer.dsl.Matchers.integer;
20+
import static au.com.dius.pact.consumer.dsl.Matchers.string;
21+
import static au.com.dius.pact.consumer.dsl.Matchers.timestamp;
22+
import static org.hamcrest.MatcherAssert.assertThat;
23+
import static org.hamcrest.Matchers.greaterThan;
24+
import static org.hamcrest.Matchers.hasSize;
25+
import static org.hamcrest.Matchers.is;
26+
import static org.hamcrest.Matchers.isEmptyString;
27+
import static org.hamcrest.Matchers.not;
28+
29+
public class TodoXmlTest {
30+
@Rule
31+
public PactProviderRule provider = new PactProviderRule("TodoProvider", "localhost", 1234, this);
32+
33+
// body: <?xml version="1.0" encoding="UTF-8"?>
34+
// <projects foo="bar">
35+
// <project id="1" name="Project 1" due="2016-02-11T09:46:56.023Z">
36+
// <tasks>
37+
// <task id="1" name="Do the laundry" done="true"/>
38+
// <task id="2" name="Do the dishes" done="false"/>
39+
// <task id="3" name="Do the backyard" done="false"/>
40+
// <task id="4" name="Do nothing" done="false"/>
41+
// </tasks>
42+
// </project>
43+
// <project/>
44+
// </projects>
45+
46+
@Pact(provider = "TodoProvider", consumer = "TodoConsumer")
47+
public RequestResponsePact projects(PactDslWithProvider builder) {
48+
return builder
49+
.given("i have a list of projects")
50+
.uponReceiving("a request for projects in XML")
51+
.path("/projects")
52+
.query("from=today")
53+
.headers(mapOf("Accept", "application/xml"))
54+
.willRespondWith()
55+
.headers(mapOf("Content-Type", "application/xml"))
56+
.status(200)
57+
.body(
58+
new PactXmlBuilder("projects", "http://some.namespace/and/more/stuff").build(root -> {
59+
root.setAttributes(mapOf("id", "1234"));
60+
root.eachLike("project", 2, mapOf(
61+
"id", integer(),
62+
"type", "activity",
63+
"name", string("Project 1"),
64+
"due", timestamp("yyyy-MM-dd'T'HH:mm:ss.SSSX", "2016-02-11T09:46:56.023Z")
65+
), project -> {
66+
project.appendElement("tasks", Collections.emptyMap(), task -> {
67+
task.eachLike("task", 5, mapOf(
68+
"id", integer(),
69+
"name", string("Task 1"),
70+
"done", bool(true)
71+
));
72+
});
73+
});
74+
})
75+
)
76+
.toPact();
77+
}
78+
79+
@PactVerification("TodoProvider")
80+
@Test
81+
public void testGeneratesAListOfTODOsForTheMainScreen() throws IOException {
82+
Projects projects = new TodoApp()
83+
.setUrl(provider.getMockServer().getUrl())
84+
.getProjects("xml");
85+
assertThat(projects.getId(), is("1234"));
86+
assertThat(projects.getProjects(), hasSize(2));
87+
projects.getProjects().forEach(project -> {
88+
assertThat(project.getId(), is(greaterThan(0)));
89+
assertThat(project.getType(), is("activity"));
90+
assertThat(project.getName(), is("Project 1"));
91+
assertThat(project.getDue(), not(isEmptyString()));
92+
assertThat(project.getTasks().getTasks(), hasSize(5));
93+
});
94+
}
95+
96+
private <T> Map<String, T> mapOf(String key, T value) {
97+
return MapUtils.putAll(new HashMap<>(), new Object[] { key, value });
98+
}
99+
100+
private Map<String, Object> mapOf(String key1, Object value1, String key2, Object value2) {
101+
return MapUtils.putAll(new HashMap<>(), new Object[] { key1, value1, key2, value2 });
102+
}
103+
104+
private Map<String, Object> mapOf(String key1, Object value1, String key2, Object value2, String key3, Object value3) {
105+
return MapUtils.putAll(new HashMap<>(), new Object[] { key1, value1, key2, value2, key3, value3 });
106+
}
107+
108+
private Map<String, Object> mapOf(String key1, Object value1, String key2, Object value2, String key3, Object value3,
109+
String key4, Object value4) {
110+
return MapUtils.putAll(new HashMap<>(), new Object[] { key1, value1, key2, value2, key3, value3, key4, value4 });
111+
}
112+
}

‎consumer/pact-jvm-consumer/src/main/java/au/com/dius/pact/consumer/dsl/PactDslRequestWithPath.java

+24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package au.com.dius.pact.consumer.dsl;
22

33
import au.com.dius.pact.consumer.ConsumerPactBuilder;
4+
import au.com.dius.pact.consumer.xml.PactXmlBuilder;
45
import au.com.dius.pact.core.model.Consumer;
56
import au.com.dius.pact.core.model.OptionalBody;
67
import au.com.dius.pact.core.model.PactReaderKt;
@@ -335,6 +336,29 @@ public PactDslRequestWithPath body(Document body) throws TransformerException {
335336
return this;
336337
}
337338

339+
/**
340+
* XML body to return
341+
*
342+
* @param xmlBuilder XML Builder used to construct the XML document
343+
*/
344+
public PactDslRequestWithPath body(PactXmlBuilder xmlBuilder) {
345+
requestMatchers.addCategory(xmlBuilder.getMatchingRules());
346+
requestGenerators.addGenerators(xmlBuilder.getGenerators());
347+
348+
if (!requestHeaders.containsKey(CONTENT_TYPE)) {
349+
requestHeaders.put(CONTENT_TYPE, Collections.singletonList(ContentType.APPLICATION_XML.toString()));
350+
requestBody = OptionalBody.body(xmlBuilder.asBytes());
351+
} else {
352+
String contentType = requestHeaders.get(CONTENT_TYPE).get(0);
353+
ContentType ct = ContentType.parse(contentType);
354+
Charset charset = ct.getCharset() != null ? ct.getCharset() : Charset.defaultCharset();
355+
requestBody = OptionalBody.body(xmlBuilder.asBytes(charset),
356+
new au.com.dius.pact.core.model.ContentType(contentType));
357+
}
358+
359+
return this;
360+
}
361+
338362
/**
339363
* The path of the request
340364
*

‎consumer/pact-jvm-consumer/src/main/java/au/com/dius/pact/consumer/dsl/PactDslRequestWithoutPath.java

+24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package au.com.dius.pact.consumer.dsl;
22

33
import au.com.dius.pact.consumer.ConsumerPactBuilder;
4+
import au.com.dius.pact.consumer.xml.PactXmlBuilder;
45
import au.com.dius.pact.core.model.OptionalBody;
56
import au.com.dius.pact.core.model.PactReaderKt;
67
import au.com.dius.pact.core.model.generators.Category;
@@ -264,6 +265,29 @@ public PactDslRequestWithoutPath body(Document body) throws TransformerException
264265
return this;
265266
}
266267

268+
/**
269+
* XML Response body to return
270+
*
271+
* @param xmlBuilder XML Builder used to construct the XML document
272+
*/
273+
public PactDslRequestWithoutPath body(PactXmlBuilder xmlBuilder) {
274+
requestMatchers.addCategory(xmlBuilder.getMatchingRules());
275+
requestGenerators.addGenerators(xmlBuilder.getGenerators());
276+
277+
if (!requestHeaders.containsKey(CONTENT_TYPE)) {
278+
requestHeaders.put(CONTENT_TYPE, Collections.singletonList(ContentType.APPLICATION_XML.toString()));
279+
requestBody = OptionalBody.body(xmlBuilder.asBytes());
280+
} else {
281+
String contentType = requestHeaders.get(CONTENT_TYPE).get(0);
282+
ContentType ct = ContentType.parse(contentType);
283+
Charset charset = ct.getCharset() != null ? ct.getCharset() : Charset.defaultCharset();
284+
requestBody = OptionalBody.body(xmlBuilder.asBytes(charset),
285+
new au.com.dius.pact.core.model.ContentType(contentType));
286+
}
287+
288+
return this;
289+
}
290+
267291
/**
268292
* The path of the request
269293
*

‎consumer/pact-jvm-consumer/src/main/java/au/com/dius/pact/consumer/dsl/PactDslResponse.java

+25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package au.com.dius.pact.consumer.dsl;
22

33
import au.com.dius.pact.consumer.ConsumerPactBuilder;
4+
import au.com.dius.pact.consumer.MessagePactBuilder;
5+
import au.com.dius.pact.consumer.xml.PactXmlBuilder;
46
import au.com.dius.pact.core.model.OptionalBody;
57
import au.com.dius.pact.core.model.ProviderState;
68
import au.com.dius.pact.core.model.Request;
@@ -379,4 +381,27 @@ public PactDslResponse matchSetCookie(String cookie, String regex, String exampl
379381
}
380382
return this;
381383
}
384+
385+
/**
386+
* XML Response body to return
387+
*
388+
* @param xmlBuilder XML Builder used to construct the XML document
389+
*/
390+
public PactDslResponse body(PactXmlBuilder xmlBuilder) {
391+
responseMatchers.addCategory(xmlBuilder.getMatchingRules());
392+
responseGenerators.addGenerators(xmlBuilder.getGenerators());
393+
394+
if (!responseHeaders.containsKey(CONTENT_TYPE)) {
395+
responseHeaders.put(CONTENT_TYPE, Collections.singletonList(ContentType.APPLICATION_XML.toString()));
396+
responseBody = OptionalBody.body(xmlBuilder.asBytes());
397+
} else {
398+
String contentType = responseHeaders.get(CONTENT_TYPE).get(0);
399+
ContentType ct = ContentType.parse(contentType);
400+
Charset charset = ct.getCharset() != null ? ct.getCharset() : Charset.defaultCharset();
401+
responseBody = OptionalBody.body(xmlBuilder.asBytes(charset),
402+
new au.com.dius.pact.core.model.ContentType(contentType));
403+
}
404+
405+
return this;
406+
}
382407
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package au.com.dius.pact.consumer.xml
2+
3+
import au.com.dius.pact.consumer.dsl.Matcher
4+
import au.com.dius.pact.core.model.generators.Category.BODY
5+
import au.com.dius.pact.core.model.generators.Generators
6+
import au.com.dius.pact.core.model.matchingrules.Category
7+
import org.w3c.dom.DOMImplementation
8+
import org.w3c.dom.Document
9+
import org.w3c.dom.Element
10+
import java.io.ByteArrayOutputStream
11+
import java.io.OutputStreamWriter
12+
import java.nio.charset.Charset
13+
import java.util.function.Consumer
14+
import javax.xml.parsers.DocumentBuilderFactory
15+
import javax.xml.transform.OutputKeys
16+
import javax.xml.transform.TransformerFactory
17+
import javax.xml.transform.dom.DOMSource
18+
import javax.xml.transform.stream.StreamResult
19+
20+
class PactXmlBuilder @JvmOverloads constructor (
21+
val rootName: String,
22+
val rootNameSpace: String? = null,
23+
val namespaces: Map<String, String> = emptyMap(),
24+
val version: String? = null,
25+
val charset: String? = null
26+
) {
27+
val generators: Generators = Generators()
28+
val matchingRules: Category = Category("body")
29+
30+
lateinit var doc: Document
31+
private lateinit var dom: DOMImplementation
32+
33+
fun build(cl: Consumer<XmlNode>): PactXmlBuilder {
34+
val factory = DocumentBuilderFactory.newInstance()
35+
val builder = factory.newDocumentBuilder()
36+
this.dom = builder.domImplementation
37+
this.doc = if (rootNameSpace != null) {
38+
dom.createDocument(rootNameSpace, "ns:$rootName", null)
39+
} else {
40+
builder.newDocument()
41+
}
42+
if (version != null) {
43+
doc.xmlVersion = version
44+
}
45+
val xmlNode = XmlNode(this, doc.documentElement, listOf("$", rootName))
46+
cl.accept(xmlNode)
47+
return this
48+
}
49+
50+
@JvmOverloads
51+
fun asBytes(charset: Charset? = null): ByteArray {
52+
val transformer = TransformerFactory.newInstance().newTransformer()
53+
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
54+
val source = DOMSource(doc)
55+
val outputStream = ByteArrayOutputStream()
56+
val result = if (charset != null) {
57+
StreamResult(OutputStreamWriter(outputStream, charset))
58+
} else {
59+
StreamResult(ByteArrayOutputStream())
60+
}
61+
transformer.transform(source, result)
62+
return outputStream.toByteArray()
63+
}
64+
}
65+
66+
class XmlNode(private val builder: PactXmlBuilder, private val element: Element, private val path: List<String>) {
67+
fun setAttributes(attributes: Map<String, String>) {
68+
attributes.forEach {
69+
element.setAttribute(it.key, it.value)
70+
}
71+
}
72+
73+
@JvmOverloads
74+
fun eachLike(name: String, examples: Int = 1, attributes: Map<String, Any?> = emptyMap(), cl: Consumer<XmlNode>? = null) {
75+
val element = builder.doc.createElement(name)
76+
attributes.forEach {
77+
if (it.value is Matcher) {
78+
val matcherDef = it.value as Matcher
79+
builder.matchingRules.addRule(matcherKey(path, name, "['@" + it.key + "']"), matcherDef.matcher!!)
80+
if (matcherDef.generator != null) {
81+
builder.generators.addGenerator(BODY, matcherKey(path, name, "['@" + it.key + "']"), matcherDef.generator!!)
82+
}
83+
element.setAttribute(it.key, matcherDef.value.toString())
84+
} else {
85+
element.setAttribute(it.key, it.value.toString())
86+
}
87+
}
88+
89+
val node = XmlNode(builder, element, this.path + element.tagName)
90+
cl?.accept(node)
91+
this.element.appendChild(element)
92+
(2..examples).forEach { _ ->
93+
this.element.appendChild(element.cloneNode(true))
94+
}
95+
}
96+
97+
private fun matcherKey(path: List<String>, vararg key: String) = (path + key).joinToString(".")
98+
99+
@JvmOverloads
100+
fun appendElement(name: String, attributes: Map<String, Any?> = emptyMap(), cl: Consumer<XmlNode>? = null) {
101+
val element = builder.doc.createElement(name)
102+
attributes.forEach {
103+
if (it.value is Matcher) {
104+
val matcherDef = it.value as Matcher
105+
builder.matchingRules.addRule(matcherKey(path, "@" + it.key), matcherDef.matcher!!)
106+
if (matcherDef.generator != null) {
107+
builder.generators.addGenerator(BODY, matcherKey(path, "@" + it.key), matcherDef.generator!!)
108+
}
109+
element.setAttribute(it.key, matcherDef.value.toString())
110+
} else {
111+
element.setAttribute(it.key, it.value.toString())
112+
}
113+
}
114+
115+
val node = XmlNode(builder, element, this.path + element.tagName)
116+
cl?.accept(node)
117+
this.element.appendChild(element)
118+
}
119+
}

0 commit comments

Comments
 (0)
Please sign in to comment.